diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 258bf1f..5a98a51 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; }; EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */; }; + EC5E83902BFDB69C00261D9C /* MovingAverage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */; }; EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; }; EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; }; EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; }; @@ -100,6 +101,7 @@ EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDropDelegate.swift; sourceTree = ""; }; EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = ""; }; EC50500E2BF670EA00B4D86E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingAverage.swift; sourceTree = ""; }; EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; }; EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -268,6 +270,14 @@ path = Config; sourceTree = ""; }; + EC5E838E2BFDB69000261D9C /* Algorithms */ = { + isa = PBXGroup; + children = ( + EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */, + ); + path = Algorithms; + sourceTree = ""; + }; EC7F6BDF2BE5E6E300A34A7B = { isa = PBXGroup; children = ( @@ -529,6 +539,7 @@ ECA738CE2BE60F5000A4542E /* Stroke */ = { isa = PBXGroup; children = ( + EC5E838E2BFDB69000261D9C /* Algorithms */, ECA738D52BE60FA200A4542E /* Generators */, ECA738D12BE60F7B00A4542E /* Stroke.swift */, ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */, @@ -713,6 +724,7 @@ ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */, ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */, ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */, + EC5E83902BFDB69C00261D9C /* MovingAverage.swift in Sources */, ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */, ECA738E82BE6120F00A4542E /* Color++.swift in Sources */, ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */, diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index fa6b910..19b0a5f 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -153,10 +153,7 @@ extension GraphicContext { func endStroke(at point: CGPoint) { guard currentPoint != nil, let currentStroke else { return } currentStroke.finish(at: point) - let batchIndex = currentStroke.batchIndex - let quads = Array(currentStroke.quads[batchIndex.. CGPoint { + points.append(point) + sum.x += point.x + sum.y += point.y + + if points.count > windowSize { + let removedValue = points.remove(at: 0) + sum.x -= removedValue.x + sum.y -= removedValue.y + } + + return currentAverage() + } + + func currentAverage() -> CGPoint { + guard !points.isEmpty else { return CGPoint.zero } + let count = CGFloat(points.count) + return CGPoint(x: sum.x / count, y: sum.y / count) + } +} diff --git a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift index 6b728d3..d00c498 100644 --- a/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift +++ b/Memola/Canvas/Geometries/Stroke/Generators/SolidPointStrokeGenerator.swift @@ -11,6 +11,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { var configuration: Configuration func begin(at point: CGPoint, on stroke: Stroke) { + let point = stroke.movingAverage.addPoint(point) stroke.keyPoints.append(point) addPoint(point, on: stroke) } @@ -19,6 +20,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { guard stroke.keyPoints.endIndex > 0 else { return } + let point = stroke.movingAverage.addPoint(point) stroke.keyPoints.append(point) switch stroke.keyPoints.endIndex { case 2: @@ -27,7 +29,6 @@ struct SolidPointStrokeGenerator: StrokeGenerator { let control = CGPoint.middle(p1: start, p2: end) addCurve(from: start, to: end, by: control, on: stroke) case 3: - stroke.removeQuads(from: stroke.quadIndex + 1) let index = stroke.keyPoints.endIndex - 1 var start = stroke.keyPoints[index - 2] var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) @@ -38,7 +39,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index]) addCurve(from: start, to: end, by: control, on: stroke) default: - smoothOutPath(on: stroke) + adjustKeyPoint(on: stroke) let index = stroke.keyPoints.endIndex - 1 let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) let control = stroke.keyPoints[index - 1] @@ -53,45 +54,17 @@ struct SolidPointStrokeGenerator: StrokeGenerator { break default: append(to: point, on: stroke) - let index = stroke.keyPoints.endIndex - 1 - let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) - let end = stroke.keyPoints[index] - let control = CGPoint.middle(p1: start, p2: end) - addCurve(from: start, to: end, by: control, on: stroke) } } - private func smoothOutPath(on stroke: Stroke) { - stroke.removeQuads(from: stroke.quadIndex + 1) - adjustPreviousKeyPoint(on: stroke) - switch stroke.keyPoints.endIndex { - case 4: - let index = stroke.keyPoints.endIndex - 2 - let start = stroke.keyPoints[index - 2] - let end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) - let control = CGPoint.middle(p1: start, p2: end) - addCurve(from: start, to: end, by: control, on: stroke) - fallthrough - default: - let index = stroke.keyPoints.endIndex - 2 - let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1]) - let control = stroke.keyPoints[index - 1] - let end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index]) - addCurve(from: start, to: end, by: control, on: stroke) - } - stroke.quadIndex = stroke.quads.endIndex - 1 - } - - private func adjustPreviousKeyPoint(on stroke: Stroke) { + private func adjustKeyPoint(on stroke: Stroke) { let index = stroke.keyPoints.endIndex - 1 - let prev = stroke.keyPoints[index - 2] - let mid = stroke.keyPoints[index - 1] + let prev = stroke.keyPoints[index - 1] let current = stroke.keyPoints[index] - let averageX = (prev.x + current.x + mid.x) / 3 - let averageY = (prev.y + current.y + mid.y) / 3 + let averageX = (prev.x + current.x) / 2 + let averageY = (prev.y + current.y) / 2 let point = CGPoint(x: averageX, y: averageY) stroke.keyPoints[index] = point - stroke.keyPoints[index - 1] = point } private func addPoint(_ point: CGPoint, on stroke: Stroke) { @@ -102,8 +75,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator { case .random: rotation = CGFloat.random(in: 0...360) * .pi / 180 } - let quad = stroke.addQuad(at: point, rotation: rotation, shape: .rounded) - stroke.quads.append(quad) + stroke.addQuad(at: point, rotation: rotation, shape: .rounded) } private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) { diff --git a/Memola/Canvas/Geometries/Stroke/Stroke.swift b/Memola/Canvas/Geometries/Stroke/Stroke.swift index 0a13a11..f5bbcbd 100644 --- a/Memola/Canvas/Geometries/Stroke/Stroke.swift +++ b/Memola/Canvas/Geometries/Stroke/Stroke.swift @@ -54,6 +54,8 @@ final class Stroke: @unchecked Sendable { var keyPoints: [CGPoint] = [] var thicknessFactor: CGFloat = 0.7 + let movingAverage = MovingAverage(windowSize: 3) + var vertexBuffer: MTLBuffer? var texture: MTLTexture? @@ -105,7 +107,7 @@ extension Stroke { } } - func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) -> Quad { + func addQuad(at point: CGPoint, rotation: CGFloat, shape: QuadShape) { let quad = Quad( origin: point, size: thickness, @@ -114,23 +116,7 @@ extension Stroke { color: color ) quads.append(quad) - return quad - } - - func removeQuads(from index: Int) { - let dropCount = quads.endIndex - max(1, index) - quads.removeLast(dropCount) - let quads = Array(quads[batchIndex..