Skip to content

Commit dca316a

Browse files
jkuriclaude
andcommitted
fix(editor): snap smoothed cursor to click positions during spring simulation
Spring physics smoothing caused the cursor to lag behind click highlight positions, especially at slower speeds. Add output correction that blends toward upcoming click positions using smoothstep interpolation, and snaps the spring state at click time to ensure alignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1647c9a commit dca316a

2 files changed

Lines changed: 51 additions & 3 deletions

File tree

Reframed/Editor/CursorSmoothing.swift

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,31 @@ enum CursorMovementSpeed: String, Codable, Sendable, CaseIterable, Identifiable
4444
case .rapid: 0.6
4545
}
4646
}
47+
48+
var convergenceDuration: Double {
49+
switch self {
50+
case .slow: 0.3
51+
case .medium: 0.2
52+
case .fast: 0.15
53+
case .rapid: 0.1
54+
}
55+
}
4756
}
4857

4958
enum CursorSmoothing {
5059
static func smooth(
5160
samples: [CursorSample],
52-
speed: CursorMovementSpeed
61+
speed: CursorMovementSpeed,
62+
clicks: [CursorClickEvent] = []
5363
) -> [CursorSample] {
5464
guard samples.count >= 2 else { return samples }
5565

5666
let tension = speed.tension
5767
let friction = speed.friction
5868
let mass = speed.mass
69+
let convergence = speed.convergenceDuration
70+
let sortedClicks = clicks.sorted { $0.t < $1.t }
71+
var clickIdx = 0
5972

6073
var result: [CursorSample] = []
6174
result.reserveCapacity(samples.count)
@@ -77,6 +90,9 @@ enum CursorSmoothing {
7790
velX = 0
7891
velY = 0
7992
result.append(CursorSample(t: target.t, x: posX, y: posY, p: target.p))
93+
while clickIdx < sortedClicks.count && sortedClicks[clickIdx].t <= target.t {
94+
clickIdx += 1
95+
}
8096
continue
8197
}
8298

@@ -92,7 +108,38 @@ enum CursorSmoothing {
92108
posY += velY * stepDt
93109
}
94110

95-
result.append(CursorSample(t: target.t, x: posX, y: posY, p: target.p))
111+
while clickIdx < sortedClicks.count && sortedClicks[clickIdx].t <= prev.t {
112+
clickIdx += 1
113+
}
114+
115+
if clickIdx < sortedClicks.count {
116+
let click = sortedClicks[clickIdx]
117+
if click.t > prev.t && click.t <= target.t {
118+
posX = click.x
119+
posY = click.y
120+
velX = 0
121+
velY = 0
122+
result.append(CursorSample(t: target.t, x: posX, y: posY, p: target.p))
123+
clickIdx += 1
124+
continue
125+
}
126+
}
127+
128+
var outX = posX
129+
var outY = posY
130+
131+
if clickIdx < sortedClicks.count {
132+
let click = sortedClicks[clickIdx]
133+
let timeToClick = click.t - target.t
134+
if timeToClick > 0 && timeToClick <= convergence {
135+
let raw = 1.0 - timeToClick / convergence
136+
let blend = raw * raw * (3.0 - 2.0 * raw)
137+
outX = posX + (click.x - posX) * blend
138+
outY = posY + (click.y - posY) * blend
139+
}
140+
}
141+
142+
result.append(CursorSample(t: target.t, x: outX, y: outY, p: target.p))
96143
}
97144

98145
return result

Reframed/Editor/EditorState.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1369,7 +1369,8 @@ final class EditorState {
13691369
}
13701370
let smoothedSamples = CursorSmoothing.smooth(
13711371
samples: provider.metadata.samples,
1372-
speed: cursorMovementSpeed
1372+
speed: cursorMovementSpeed,
1373+
clicks: provider.metadata.clicks
13731374
)
13741375
var smoothedMetadata = provider.metadata
13751376
smoothedMetadata.samples = smoothedSamples

0 commit comments

Comments
 (0)