Skip to content

Instantly share code, notes, and snippets.

@mmuszynski
Created January 30, 2018 20:36
Show Gist options
  • Save mmuszynski/b5e79a9f97141aee1568d289c6a6df0b to your computer and use it in GitHub Desktop.
Save mmuszynski/b5e79a9f97141aee1568d289c6a6df0b to your computer and use it in GitHub Desktop.
extension MusicNote: MusicStaffViewElement {
public func direction(in clef: MusicClef) -> MusicStaffViewElementDirection {
let offset = clef.offsetForPitch(named: self.pitch.name, octave: self.pitch.octave)
return offset < 0 ? .up : .down
}
public func offset(in clef: MusicClef) -> Int {
return clef.offsetForPitch(named: self.pitch.name, octave: self.pitch.octave)
}
public func requiredLedgerLines(in clef: MusicClef) -> Int {
var theOffset = offset(in: clef)
guard theOffset < -5 || theOffset > 5 else { return 0 }
if theOffset < 0 {
theOffset += 4
} else {
theOffset -= 4
}
return theOffset / 2
}
/// MusicStaffViewElement Conformance
///
/// Generalized to be as DRY as possible, this version of the function does introspection on the rhythm enum value and calls the appropriate method to draw a notehead for a whole note, half note, or quarter note followed by the appropriate stem and number of flags and dots to complete the full shape of the note.
///
/// The most complicated logic happens currently in the dotted case, however, this complication could be removed if the function were made to be recursive. This would require forwarding to a separate method, as the current method is required for conformance to `MusicStaffViewElement.`
///
/// - Parameters:
/// - frame: The frame
/// - direction: The direction of the note
/// - Returns: A path suitable for drawing in the CAShapeLayer
public func path(in frame: CGRect, for direction: MusicStaffViewElementDirection) throws -> CGPath {
switch self.rhythm {
case .sixtyfourth, .thirtysecond, .sixteenth, .eighth, .quarter:
return filledNotePath(in: frame, for: direction, withFlagCount: self.rhythm.flagCount)
case .half:
return halfNotePath(in: frame, for: direction)
case .whole:
return wholeNotePath(in: frame)
case .dotted(let base, let dots):
if base.hasStem {
if base.isFilled {
return filledNotePath(in: frame, for: direction, withFlagCount: base.flagCount, withDots: dots)
} else {
return halfNotePath(in: frame, for: direction, withDots: dots)
}
}
return wholeNotePath(in: frame, withDots: dots)
default:
return filledNotePath(in: frame, for: direction)
}
}
public var aspectRatio: CGFloat {
return self.aspectRatio(for: self.rhythm)
//return 39.0 / 90.0
}
private func aspectRatio(for rhythm: MusicRhythm) -> CGFloat {
switch rhythm {
case .quarter, .half, .eighth, .sixteenth, .thirtysecond, .sixtyfourth:
return 493.0 / 321.0
case .whole:
return 40.0 / 20.0
case .dotted(let base, _):
return self.aspectRatio(for: base)
default:
fatalError("Can't draw this rhythm yet")
}
}
public var heightInStaffSpace: CGFloat {
return 1.0
//previously
//return 4.0
}
public var anchorPoint: CGPoint {
return CGPoint(x: 0.5, y: 0.5)
//previously
//return CGPoint(x: 0.5, y: 0.865)
}
public var accessoryElements: [MusicStaffViewAccessory]? {
var accessories: [MusicStaffViewAccessory] = []
if self.accidental != .none {
let noteAccidental = MusicNoteAccidental(name: self.name, accidental: self.accidental, octave: self.octave)
accessories.append(noteAccidental)
}
if case let .dotted(_, dots) = self.rhythm {
for i in 0..<dots {
let dot = MusicStaffViewDotAccessory(spacing: i == 0 ? .zero : .minimalNonzero, placement: .trailing, offset: self.offset)
accessories.append(dot)
}
}
guard accessories.count > 0 else { return nil }
return accessories
}
func path(in frame: CGRect, forResource resourceName: String, withExtension extensionName: String = "svg") throws -> UIBezierPath {
let url = MusicStaffView.bundle.url(forResource: resourceName, withExtension: extensionName)
let parser = SVGParser(url: url!)
parser.parse()
guard let path = parser.path else {
throw MusicStaffViewFontError.couldNotUnpackPath
}
let pathsize = parser.frame?.size ?? path.bounds.size
let scaling = CGAffineTransform(scaleX: frame.size.width / pathsize.width, y: frame.size.height / pathsize.height)
let translate = CGAffineTransform(translationX: frame.origin.x, y: frame.origin.y)
path.apply(scaling)
path.apply(translate)
return path
}
func wholeNotePath(in frame: CGRect, withDots dots: Int = 0) -> CGPath {
do {
let wholeNotePath = try path(in: frame, forResource: "opusWholeNote")
return wholeNotePath.cgPath
} catch {
fatalError("Something went wrong unpacking a path")
}
}
func halfNotePath(in frame: CGRect, for direction: MusicStaffViewElementDirection, withDots dots: Int = 0) -> CGPath {
let path = try! self.path(in: frame, forResource: "opusHalfNoteHead")
let stemAttachmentPoint: CGPoint
var stemEndpoint: CGPoint
let stemLength = frame.height * 3.0
let stemThickness: CGFloat = frame.height / 12.5
if direction == .up {
stemAttachmentPoint = CGPoint(x: 34.801 / 38.395 * frame.width, y: 9.196 / 25.0 * frame.height)
stemEndpoint = stemAttachmentPoint
stemEndpoint.y = stemAttachmentPoint.y + stemLength
} else {
stemAttachmentPoint = CGPoint(x: 9.24 / 98.73 * frame.width, y: 40.88/68.28 * frame.height)
stemEndpoint = stemAttachmentPoint
stemEndpoint.y = stemAttachmentPoint.y - stemLength
}
let stem = UIBezierPath(rect: CGRect(x: stemAttachmentPoint.x - stemThickness, y: stemAttachmentPoint.y, width: stemThickness, height: -stemEndpoint.y + stemAttachmentPoint.y))
path.append(stem)
return path.cgPath
}
func filledNotePath(in frame: CGRect, for direction: MusicStaffViewElementDirection, withFlagCount flagCount: Int = 0, withDots dots: Int = 0) -> CGPath {
let path = try! self.path(in: frame, forResource: "opusQuarterNoteHead")
//quarter note also needs a stem
//where should the stem attach?
//sort of depends on whether it's upward or downward
//hardcoding the stem attachment points based on where they exist in illustrator
//the path for the half/quarter notes are exactly the same with respect to the size/shape of the notes
//the vertical tangent point on the left side is at roughly x=3, y=11 points
//while the right side is roughly x=29,y=7
//those would probably need to be scaled
//could probably also find those anchor points using the path itself
//that's harder than trivial, but possible
//these are really shitty answers, and it's really fragile.
//i'd like to go back and change this to the harder solution.
//right side
//29/32
//7/19
//left side
//3/32
//11/19
let stemAttachmentPoint: CGPoint
var stemEndpoint: CGPoint
let stemLength = frame.height * 3.0
let stemThickness: CGFloat = frame.height / 12.5
if direction == .up {
stemAttachmentPoint = CGPoint(x: 34.801 / 38.395 * frame.width - stemThickness, y: 9.196 / 25.0 * frame.height)
stemEndpoint = stemAttachmentPoint
stemEndpoint.y = stemAttachmentPoint.y + stemLength
} else {
stemAttachmentPoint = CGPoint(x: 9.24 / 98.73 * frame.width, y: 40.88/68.28 * frame.height)
stemEndpoint = stemAttachmentPoint
stemEndpoint.y = stemAttachmentPoint.y - stemLength
}
let stemOrigin = CGPoint(x: stemAttachmentPoint.x, y: stemAttachmentPoint.y)
let stemSize = CGSize(width: stemThickness, height: -stemEndpoint.y + stemAttachmentPoint.y)
let stem = UIBezierPath(rect: CGRect(origin: stemOrigin, size: stemSize))
path.append(stem)
for flagNumber in 0..<flagCount {
var origin: CGPoint = direction == .up ? stemEndpoint : stemOrigin
var flagOffset: CGFloat = 0
if flagNumber > 0 {
flagOffset = frame.size.height + frame.size.height / 1.5 * CGFloat(flagNumber - 1)
}
if direction == .up {
origin.y -= flagOffset
} else {
origin.y += flagOffset
}
let flagFrame = CGRect(origin: .zero, size: CGSize(width: frame.size.width / 1.5, height: stemLength))
var flagPath: UIBezierPath
if flagNumber == 0 {
flagPath = try! self.path(in: flagFrame, forResource: "opusNoteFlagFinal")
} else {
flagPath = try! self.path(in: flagFrame, forResource: "opusNoteFlagIntermediate").reversing()
}
if direction == .down {
flagPath.apply(CGAffineTransform(scaleX: 1.0, y: -1.0))
flagPath.apply(CGAffineTransform(translationX: 0.0, y: flagPath.bounds.height))
flagPath.apply(CGAffineTransform(translationX: origin.x, y: origin.y))
} else {
flagPath.apply(CGAffineTransform(translationX: origin.x, y: -origin.y))
}
path.append(flagPath)
}
path.usesEvenOddFillRule = false
return path.cgPath
}
public func layer(in clef: MusicClef, withSpaceWidth spaceWidth: CGFloat, color: UIColor?) throws -> CALayer {
let frame = self.bounds(withSpaceWidth: spaceWidth)
let layer = CAShapeLayer()
let objectPath = try self.path(in: frame, for: self.direction(in: clef))
layer.bounds = frame
layer.path = objectPath
layer.anchorPoint = self.anchorPoint
layer.fillColor = color?.cgColor ?? self.color.cgColor
let offset = self.offset(in: clef)
layer.position.y -= CGFloat(offset) * spaceWidth / 2.0
return layer
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment