Created
January 30, 2018 20:36
-
-
Save mmuszynski/b5e79a9f97141aee1568d289c6a6df0b to your computer and use it in GitHub Desktop.
This file contains hidden or 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
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