mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-04-25 01:58:52 +02:00
feat: smooth out stroke using moving average algorithm
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; };
|
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; };
|
||||||
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; };
|
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; };
|
||||||
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.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 */; };
|
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; };
|
||||||
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
|
EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; };
|
||||||
EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview 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 = "<group>"; };
|
EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDropDelegate.swift; sourceTree = "<group>"; };
|
||||||
EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = "<group>"; };
|
EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = "<group>"; };
|
||||||
EC50500E2BF670EA00B4D86E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
EC50500E2BF670EA00B4D86E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingAverage.swift; sourceTree = "<group>"; };
|
||||||
EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 = "<group>"; };
|
EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = "<group>"; };
|
||||||
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
@@ -268,6 +270,14 @@
|
|||||||
path = Config;
|
path = Config;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
EC5E838E2BFDB69000261D9C /* Algorithms */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */,
|
||||||
|
);
|
||||||
|
path = Algorithms;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
EC7F6BDF2BE5E6E300A34A7B = {
|
EC7F6BDF2BE5E6E300A34A7B = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -529,6 +539,7 @@
|
|||||||
ECA738CE2BE60F5000A4542E /* Stroke */ = {
|
ECA738CE2BE60F5000A4542E /* Stroke */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
EC5E838E2BFDB69000261D9C /* Algorithms */,
|
||||||
ECA738D52BE60FA200A4542E /* Generators */,
|
ECA738D52BE60FA200A4542E /* Generators */,
|
||||||
ECA738D12BE60F7B00A4542E /* Stroke.swift */,
|
ECA738D12BE60F7B00A4542E /* Stroke.swift */,
|
||||||
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */,
|
ECA738D32BE60F9100A4542E /* StrokeGenerator.swift */,
|
||||||
@@ -713,6 +724,7 @@
|
|||||||
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
|
ECA7389C2BE601AF00A4542E /* GridVertex.swift in Sources */,
|
||||||
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
|
ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */,
|
||||||
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
|
ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */,
|
||||||
|
EC5E83902BFDB69C00261D9C /* MovingAverage.swift in Sources */,
|
||||||
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */,
|
ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */,
|
||||||
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
|
ECA738E82BE6120F00A4542E /* Color++.swift in Sources */,
|
||||||
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */,
|
ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */,
|
||||||
|
|||||||
@@ -153,10 +153,7 @@ extension GraphicContext {
|
|||||||
func endStroke(at point: CGPoint) {
|
func endStroke(at point: CGPoint) {
|
||||||
guard currentPoint != nil, let currentStroke else { return }
|
guard currentPoint != nil, let currentStroke else { return }
|
||||||
currentStroke.finish(at: point)
|
currentStroke.finish(at: point)
|
||||||
let batchIndex = currentStroke.batchIndex
|
withPersistence(\.backgroundContext) { [currentStroke] context in
|
||||||
let quads = Array(currentStroke.quads[batchIndex..<currentStroke.quads.count])
|
|
||||||
withPersistence(\.backgroundContext) { [currentStroke, quads] context in
|
|
||||||
currentStroke.saveQuads(for: quads)
|
|
||||||
currentStroke.object?.bounds = currentStroke.bounds
|
currentStroke.object?.bounds = currentStroke.bounds
|
||||||
try context.saveIfNeeded()
|
try context.saveIfNeeded()
|
||||||
if let stroke = currentStroke.object {
|
if let stroke = currentStroke.object {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// MovingAverage.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 5/22/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class MovingAverage {
|
||||||
|
private var sum: CGPoint
|
||||||
|
private var points: [CGPoint]
|
||||||
|
private var windowSize: Int
|
||||||
|
|
||||||
|
init(windowSize: Int) {
|
||||||
|
self.windowSize = windowSize
|
||||||
|
self.points = []
|
||||||
|
self.sum = CGPoint.zero
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPoint(_ point: CGPoint) -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
var configuration: Configuration
|
var configuration: Configuration
|
||||||
|
|
||||||
func begin(at point: CGPoint, on stroke: Stroke) {
|
func begin(at point: CGPoint, on stroke: Stroke) {
|
||||||
|
let point = stroke.movingAverage.addPoint(point)
|
||||||
stroke.keyPoints.append(point)
|
stroke.keyPoints.append(point)
|
||||||
addPoint(point, on: stroke)
|
addPoint(point, on: stroke)
|
||||||
}
|
}
|
||||||
@@ -19,6 +20,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
guard stroke.keyPoints.endIndex > 0 else {
|
guard stroke.keyPoints.endIndex > 0 else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let point = stroke.movingAverage.addPoint(point)
|
||||||
stroke.keyPoints.append(point)
|
stroke.keyPoints.append(point)
|
||||||
switch stroke.keyPoints.endIndex {
|
switch stroke.keyPoints.endIndex {
|
||||||
case 2:
|
case 2:
|
||||||
@@ -27,7 +29,6 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
let control = CGPoint.middle(p1: start, p2: end)
|
let control = CGPoint.middle(p1: start, p2: end)
|
||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
case 3:
|
case 3:
|
||||||
stroke.removeQuads(from: stroke.quadIndex + 1)
|
|
||||||
let index = stroke.keyPoints.endIndex - 1
|
let index = stroke.keyPoints.endIndex - 1
|
||||||
var start = stroke.keyPoints[index - 2]
|
var start = stroke.keyPoints[index - 2]
|
||||||
var end = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
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])
|
end = CGPoint.middle(p1: stroke.keyPoints[index - 1], p2: stroke.keyPoints[index])
|
||||||
addCurve(from: start, to: end, by: control, on: stroke)
|
addCurve(from: start, to: end, by: control, on: stroke)
|
||||||
default:
|
default:
|
||||||
smoothOutPath(on: stroke)
|
adjustKeyPoint(on: stroke)
|
||||||
let index = stroke.keyPoints.endIndex - 1
|
let index = stroke.keyPoints.endIndex - 1
|
||||||
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
let start = CGPoint.middle(p1: stroke.keyPoints[index - 2], p2: stroke.keyPoints[index - 1])
|
||||||
let control = stroke.keyPoints[index - 1]
|
let control = stroke.keyPoints[index - 1]
|
||||||
@@ -53,45 +54,17 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
append(to: point, on: stroke)
|
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) {
|
private func adjustKeyPoint(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) {
|
|
||||||
let index = stroke.keyPoints.endIndex - 1
|
let index = stroke.keyPoints.endIndex - 1
|
||||||
let prev = stroke.keyPoints[index - 2]
|
let prev = stroke.keyPoints[index - 1]
|
||||||
let mid = stroke.keyPoints[index - 1]
|
|
||||||
let current = stroke.keyPoints[index]
|
let current = stroke.keyPoints[index]
|
||||||
let averageX = (prev.x + current.x + mid.x) / 3
|
let averageX = (prev.x + current.x) / 2
|
||||||
let averageY = (prev.y + current.y + mid.y) / 3
|
let averageY = (prev.y + current.y) / 2
|
||||||
let point = CGPoint(x: averageX, y: averageY)
|
let point = CGPoint(x: averageX, y: averageY)
|
||||||
stroke.keyPoints[index] = point
|
stroke.keyPoints[index] = point
|
||||||
stroke.keyPoints[index - 1] = point
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addPoint(_ point: CGPoint, on stroke: Stroke) {
|
private func addPoint(_ point: CGPoint, on stroke: Stroke) {
|
||||||
@@ -102,8 +75,7 @@ struct SolidPointStrokeGenerator: StrokeGenerator {
|
|||||||
case .random:
|
case .random:
|
||||||
rotation = CGFloat.random(in: 0...360) * .pi / 180
|
rotation = CGFloat.random(in: 0...360) * .pi / 180
|
||||||
}
|
}
|
||||||
let quad = stroke.addQuad(at: point, rotation: rotation, shape: .rounded)
|
stroke.addQuad(at: point, rotation: rotation, shape: .rounded)
|
||||||
stroke.quads.append(quad)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) {
|
private func addCurve(from start: CGPoint, to end: CGPoint, by control: CGPoint, on stroke: Stroke) {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ final class Stroke: @unchecked Sendable {
|
|||||||
var keyPoints: [CGPoint] = []
|
var keyPoints: [CGPoint] = []
|
||||||
var thicknessFactor: CGFloat = 0.7
|
var thicknessFactor: CGFloat = 0.7
|
||||||
|
|
||||||
|
let movingAverage = MovingAverage(windowSize: 3)
|
||||||
|
|
||||||
var vertexBuffer: MTLBuffer?
|
var vertexBuffer: MTLBuffer?
|
||||||
var texture: MTLTexture?
|
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(
|
let quad = Quad(
|
||||||
origin: point,
|
origin: point,
|
||||||
size: thickness,
|
size: thickness,
|
||||||
@@ -114,23 +116,7 @@ extension Stroke {
|
|||||||
color: color
|
color: color
|
||||||
)
|
)
|
||||||
quads.append(quad)
|
quads.append(quad)
|
||||||
return quad
|
withPersistence(\.backgroundContext) { [weak self, _quad = quad, object, bounds] context in
|
||||||
}
|
|
||||||
|
|
||||||
func removeQuads(from index: Int) {
|
|
||||||
let dropCount = quads.endIndex - max(1, index)
|
|
||||||
quads.removeLast(dropCount)
|
|
||||||
let quads = Array(quads[batchIndex..<index])
|
|
||||||
batchIndex = index
|
|
||||||
withPersistence(\.backgroundContext) { [weak self, quads] context in
|
|
||||||
self?.saveQuads(for: quads)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveQuads(for quads: [Quad]) {
|
|
||||||
var topLeft: CGPoint = CGPoint(x: bounds[0], y: bounds[1])
|
|
||||||
var bottomRight: CGPoint = CGPoint(x: bounds[2], y: bounds[3])
|
|
||||||
for _quad in quads {
|
|
||||||
let quad = QuadObject(\.backgroundContext)
|
let quad = QuadObject(\.backgroundContext)
|
||||||
quad.originX = _quad.originX.cgFloat
|
quad.originX = _quad.originX.cgFloat
|
||||||
quad.originY = _quad.originY.cgFloat
|
quad.originY = _quad.originY.cgFloat
|
||||||
@@ -140,12 +126,11 @@ extension Stroke {
|
|||||||
quad.color = _quad.getColor()
|
quad.color = _quad.getColor()
|
||||||
quad.stroke = object
|
quad.stroke = object
|
||||||
object?.quads.add(quad)
|
object?.quads.add(quad)
|
||||||
topLeft.x = min(quad.originX, topLeft.x)
|
self?.bounds[0] = min(_quad.originX.cgFloat, bounds[0])
|
||||||
topLeft.y = min(quad.originY, topLeft.y)
|
self?.bounds[1] = min(_quad.originY.cgFloat, bounds[1])
|
||||||
bottomRight.x = max(quad.originX, bottomRight.x)
|
self?.bounds[2] = max(_quad.originX.cgFloat, bounds[2])
|
||||||
bottomRight.y = max(quad.originY, bottomRight.y)
|
self?.bounds[3] = max(_quad.originY.cgFloat, bounds[3])
|
||||||
}
|
}
|
||||||
bounds = [topLeft.x, topLeft.y, bottomRight.x, bottomRight.y]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user