forked from dmrschmidt/DSWaveformImage
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add new circular drawing style and introduce new public protocol for …
…extensible custom drawing
- Loading branch information
1 parent
eab9f88
commit 2a325f4
Showing
14 changed files
with
304 additions
and
112 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
Sources/DSWaveformImage/Renderers/CircularWaveformRenderer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import Foundation | ||
import CoreGraphics | ||
|
||
public struct CircularWaveformRenderer: WaveformRenderer { | ||
public enum Kind { | ||
case circle | ||
case ring(CGFloat) | ||
} | ||
|
||
private let kind: Kind | ||
|
||
public init(kind: Kind = .circle) { | ||
self.kind = kind | ||
} | ||
|
||
public func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int) { | ||
switch kind { | ||
case .circle: drawCircle(samples: samples, on: context, with: configuration, lastOffset: lastOffset) | ||
case .ring: drawRing(samples: samples, on: context, with: configuration, lastOffset: lastOffset) | ||
} | ||
} | ||
|
||
public func style(context: CGContext, with configuration: Waveform.Configuration) { | ||
if case let .gradient(colors) = configuration.style { | ||
context.clip() | ||
let colors = NSArray(array: colors.map { (color: DSColor) -> CGColor in color.cgColor }) as CFArray | ||
let colorSpace = CGColorSpaceCreateDeviceRGB() | ||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: nil)! | ||
context.drawLinearGradient(gradient, | ||
start: CGPoint(x: 0, y: 0), | ||
end: CGPoint(x: 0, y: configuration.size.height), | ||
options: .drawsAfterEndLocation) | ||
} else { | ||
defaultStyle(context: context, with: configuration) | ||
} | ||
} | ||
|
||
private func drawCircle(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int) { | ||
let graphRect = CGRect(origin: .zero, size: configuration.size) | ||
let maxRadius = CGFloat(min(graphRect.maxX, graphRect.maxY) / 2.0) * configuration.verticalScalingFactor | ||
let center = CGPoint(x: graphRect.maxX * configuration.position.value().x, y: graphRect.maxY * configuration.position.value().y) | ||
let path = CGMutablePath() | ||
|
||
path.move(to: center) | ||
|
||
for (y, sample) in samples.enumerated() { | ||
let angle = CGFloat.pi * 2 * (CGFloat(y) / CGFloat(samples.count)) | ||
let x = y + lastOffset | ||
|
||
if case .striped = configuration.style, x % Int(configuration.scale) != 0 || x % stripeBucket(configuration) != 0 { | ||
// skip sub-pixels - any x value not scale aligned | ||
// skip any point that is not a multiple of our bucket width (width + spacing) | ||
path.addLine(to: center) | ||
continue | ||
} | ||
|
||
let invertedDbSample = 1 - CGFloat(sample) // sample is in dB, linearly normalized to [0, 1] (1 -> -50 dB) | ||
let pointOnCircle = CGPoint( | ||
x: center.x + maxRadius * invertedDbSample * cos(angle), | ||
y: center.y + maxRadius * invertedDbSample * sin(angle) | ||
) | ||
|
||
path.addLine(to: pointOnCircle) | ||
} | ||
|
||
path.closeSubpath() | ||
context.addPath(path) | ||
} | ||
|
||
private func drawRing(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int) { | ||
guard case let .ring(config) = kind else { | ||
return | ||
} | ||
|
||
let graphRect = CGRect(origin: .zero, size: configuration.size) | ||
let maxRadius = CGFloat(min(graphRect.maxX, graphRect.maxY) / 2.0) * configuration.verticalScalingFactor | ||
let innerRadius: CGFloat = maxRadius * config | ||
let center = CGPoint(x: graphRect.maxX * configuration.position.value().x, y: graphRect.maxY * configuration.position.value().y) | ||
let path = CGMutablePath() | ||
|
||
path.move(to: CGPoint( | ||
x: center.x + innerRadius * cos(0), | ||
y: center.y + innerRadius * sin(0) | ||
)) | ||
|
||
for (y, sample) in samples.enumerated() { | ||
let x = y + lastOffset | ||
let angle = CGFloat.pi * 2 * (CGFloat(y) / CGFloat(samples.count)) | ||
|
||
if case .striped = configuration.style, x % Int(configuration.scale) != 0 || x % stripeBucket(configuration) != 0 { | ||
// skip sub-pixels - any x value not scale aligned | ||
// skip any point that is not a multiple of our bucket width (width + spacing) | ||
path.move(to: CGPoint( | ||
x: center.x + innerRadius * cos(angle), | ||
y: center.y + innerRadius * sin(angle) | ||
)) | ||
continue | ||
} | ||
|
||
let invertedDbSample = 1 - CGFloat(sample) // sample is in dB, linearly normalized to [0, 1] (1 -> -50 dB) | ||
let pointOnCircle = CGPoint( | ||
x: center.x + innerRadius * cos(angle) + (maxRadius - innerRadius) * invertedDbSample * cos(angle), | ||
y: center.y + innerRadius * sin(angle) + (maxRadius - innerRadius) * invertedDbSample * sin(angle) | ||
) | ||
|
||
path.addLine(to: pointOnCircle) | ||
} | ||
|
||
path.closeSubpath() | ||
context.addPath(path) | ||
} | ||
|
||
private func stripeBucket(_ configuration: Waveform.Configuration) -> Int { | ||
if case let .striped(stripeConfig) = configuration.style { | ||
return Int(stripeConfig.width + stripeConfig.spacing) * Int(configuration.scale) | ||
} else { | ||
return 0 | ||
} | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
Sources/DSWaveformImage/Renderers/LinearWaveformRenderer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import Foundation | ||
import CoreGraphics | ||
|
||
public struct LinearWaveformRenderer: WaveformRenderer { | ||
public init() {} | ||
|
||
public func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int) { | ||
let graphRect = CGRect(origin: CGPoint.zero, size: configuration.size) | ||
let positionAdjustedGraphCenter = CGFloat(configuration.position.value().y) * graphRect.size.height | ||
let drawMappingFactor = graphRect.size.height * configuration.verticalScalingFactor | ||
let minimumGraphAmplitude: CGFloat = 1 / configuration.scale // we want to see at least a 1px line for silence | ||
let path = CGMutablePath() | ||
var maxAmplitude: CGFloat = 0.0 // we know 1 is our max in normalized data, but we keep it 'generic' | ||
for (index, sample) in samples.enumerated() { | ||
var x = index + lastOffset | ||
if case .striped = configuration.style, x % Int(configuration.scale) != 0 || x % stripeBucket(configuration) != 0 { | ||
// skip sub-pixels - any x value not scale aligned | ||
// skip any point that is not a multiple of our bucket width (width + spacing) | ||
continue | ||
} else if case let .striped(config) = configuration.style { | ||
// ensure 1st stripe is drawn completely inside bounds and does not clip half way on the left side | ||
x += Int(config.width / 2 * configuration.scale) | ||
} | ||
|
||
let samplesNeeded = Int(configuration.size.width * configuration.scale) | ||
let xOffset = CGFloat(samplesNeeded - samples.count) / configuration.scale // When there's extra space, draw waveform on the right | ||
let xPos = (CGFloat(x - lastOffset) / configuration.scale) + xOffset | ||
let invertedDbSample = 1 - CGFloat(sample) // sample is in dB, linearly normalized to [0, 1] (1 -> -50 dB) | ||
let drawingAmplitude = max(minimumGraphAmplitude, invertedDbSample * drawMappingFactor) | ||
let drawingAmplitudeUp = positionAdjustedGraphCenter - drawingAmplitude | ||
let drawingAmplitudeDown = positionAdjustedGraphCenter + drawingAmplitude | ||
maxAmplitude = max(drawingAmplitude, maxAmplitude) | ||
|
||
path.move(to: CGPoint(x: xPos, y: drawingAmplitudeUp)) | ||
path.addLine(to: CGPoint(x: xPos, y: drawingAmplitudeDown)) | ||
} | ||
|
||
path.closeSubpath() | ||
context.addPath(path) | ||
} | ||
|
||
public func style(context: CGContext, with configuration: Waveform.Configuration) { | ||
defaultStyle(context: context, with: configuration) | ||
} | ||
|
||
private func stripeBucket(_ configuration: Waveform.Configuration) -> Int { | ||
if case let .striped(stripeConfig) = configuration.style { | ||
return Int(stripeConfig.width + stripeConfig.spacing) * Int(configuration.scale) | ||
} else { | ||
return 0 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import Foundation | ||
import CoreGraphics | ||
|
||
public extension WaveformRenderer { | ||
func defaultStyle(context: CGContext, with configuration: Waveform.Configuration) { | ||
// draw pixel-perfect by default | ||
context.setLineWidth(1.0 / configuration.scale) | ||
|
||
switch configuration.style { | ||
case let .filled(color): | ||
context.setStrokeColor(color.cgColor) | ||
context.strokePath() | ||
|
||
case let .outlined(color, lineWidth): | ||
context.setStrokeColor(color.cgColor) | ||
context.setLineWidth(lineWidth) | ||
context.setLineCap(.round) | ||
context.strokePath() | ||
|
||
case let .striped(config): | ||
context.setLineWidth(config.width) | ||
context.setLineCap(config.lineCap) | ||
context.setStrokeColor(config.color.cgColor) | ||
context.strokePath() | ||
|
||
case let .gradient(colors): | ||
context.replacePathWithStrokedPath() | ||
context.clip() | ||
let colors = NSArray(array: colors.map { (color: DSColor) -> CGColor in color.cgColor }) as CFArray | ||
let colorSpace = CGColorSpaceCreateDeviceRGB() | ||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: nil)! | ||
context.drawLinearGradient(gradient, | ||
start: CGPoint(x: 0, y: 0), | ||
end: CGPoint(x: 0, y: configuration.size.height), | ||
options: .drawsAfterEndLocation) | ||
|
||
case let .gradientOutlined(colors, lineWidth): | ||
context.setLineWidth(lineWidth) | ||
context.replacePathWithStrokedPath() | ||
context.setLineCap(.round) | ||
context.setLineJoin(.round) | ||
context.clip() | ||
let colors = NSArray(array: colors.map { (color: DSColor) -> CGColor in color.cgColor }) as CFArray | ||
let colorSpace = CGColorSpaceCreateDeviceRGB() | ||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: nil)! | ||
context.drawLinearGradient(gradient, | ||
start: CGPoint(x: 0, y: 0), | ||
end: CGPoint(x: 0, y: configuration.size.height), | ||
options: .drawsAfterEndLocation) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.