@tldraw/editor 4.3.0 → 4.4.0-canary.09e80a09d230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/dist-cjs/index.d.ts +180 -11
  3. package/dist-cjs/index.js +3 -1
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/LiveCollaborators.js +14 -24
  6. package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +201 -0
  8. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +7 -0
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +30 -16
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  11. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +3 -1
  12. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  13. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +13 -1
  14. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +2 -2
  15. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  16. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  17. package/dist-cjs/lib/editor/Editor.js +58 -6
  18. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  19. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +13 -21
  20. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +378 -89
  22. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
  24. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
  25. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +180 -0
  26. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
  27. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +8 -3
  28. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +29 -0
  30. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  31. package/dist-cjs/lib/hooks/usePeerIds.js +29 -0
  32. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  33. package/dist-cjs/lib/options.js +1 -0
  34. package/dist-cjs/lib/options.js.map +2 -2
  35. package/dist-cjs/lib/utils/collaboratorState.js +42 -0
  36. package/dist-cjs/lib/utils/collaboratorState.js.map +7 -0
  37. package/dist-cjs/version.js +3 -3
  38. package/dist-cjs/version.js.map +1 -1
  39. package/dist-esm/index.d.mts +180 -11
  40. package/dist-esm/index.mjs +3 -1
  41. package/dist-esm/index.mjs.map +2 -2
  42. package/dist-esm/lib/components/LiveCollaborators.mjs +17 -24
  43. package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
  44. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +181 -0
  45. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +7 -0
  46. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +30 -16
  47. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  48. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +3 -1
  49. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  50. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +13 -1
  51. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +2 -2
  52. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  53. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  54. package/dist-esm/lib/editor/Editor.mjs +58 -6
  55. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  56. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +13 -21
  57. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  58. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +378 -89
  59. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  60. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
  61. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
  62. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +160 -0
  63. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
  64. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +8 -3
  65. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  66. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +29 -0
  67. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  68. package/dist-esm/lib/hooks/usePeerIds.mjs +33 -1
  69. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  70. package/dist-esm/lib/options.mjs +1 -0
  71. package/dist-esm/lib/options.mjs.map +2 -2
  72. package/dist-esm/lib/utils/collaboratorState.mjs +22 -0
  73. package/dist-esm/lib/utils/collaboratorState.mjs.map +7 -0
  74. package/dist-esm/version.mjs +3 -3
  75. package/dist-esm/version.mjs.map +1 -1
  76. package/editor.css +6 -0
  77. package/package.json +10 -8
  78. package/src/index.ts +3 -0
  79. package/src/lib/components/LiveCollaborators.tsx +26 -37
  80. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +244 -0
  81. package/src/lib/components/default-components/DefaultCanvas.tsx +16 -6
  82. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +6 -1
  83. package/src/lib/components/default-components/DefaultShapeIndicators.tsx +16 -1
  84. package/src/lib/config/TLUserPreferences.test.ts +1 -0
  85. package/src/lib/config/TLUserPreferences.ts +8 -0
  86. package/src/lib/editor/Editor.ts +84 -6
  87. package/src/lib/editor/derivations/notVisibleShapes.ts +15 -41
  88. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +491 -106
  89. package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
  90. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +214 -0
  91. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +24 -0
  92. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  93. package/src/lib/editor/shapes/ShapeUtil.ts +44 -0
  94. package/src/lib/hooks/usePeerIds.ts +46 -1
  95. package/src/lib/options.ts +7 -0
  96. package/src/lib/utils/collaboratorState.ts +54 -0
  97. package/src/version.ts +3 -3
  98. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -621
@@ -13,18 +13,111 @@ export interface ScribbleItem {
13
13
  next: null | VecModel
14
14
  }
15
15
 
16
+ /** @public */
17
+ export interface ScribbleSessionOptions {
18
+ /** Session id. Auto-generated if not provided. */
19
+ id?: string
20
+ /**
21
+ * Whether scribbles self-consume (shrink from start) while drawing.
22
+ * - true: scribbles eat their own tail as you draw (default, used for eraser/select)
23
+ * - false: scribbles persist until session stops (used for laser)
24
+ */
25
+ selfConsume?: boolean
26
+ /**
27
+ * How long to wait after last activity before auto-stopping the session.
28
+ * Only applies when selfConsume is false.
29
+ */
30
+ idleTimeoutMs?: number
31
+ /**
32
+ * How scribbles fade when stopping.
33
+ * - 'individual': each scribble fades on its own (default)
34
+ * - 'grouped': all scribbles fade together as one sequence
35
+ */
36
+ fadeMode?: 'individual' | 'grouped'
37
+ /**
38
+ * Easing for grouped fade.
39
+ */
40
+ fadeEasing?: 'linear' | 'ease-in'
41
+ /**
42
+ * Duration of the fade in milliseconds.
43
+ */
44
+ fadeDurationMs?: number
45
+ }
46
+
47
+ // Internal session state (not exported)
48
+ interface Session {
49
+ id: string
50
+ items: ScribbleItem[]
51
+ state: 'active' | 'stopping' | 'complete'
52
+ options: Required<Omit<ScribbleSessionOptions, 'id'>>
53
+ idleTimeoutHandle?: number
54
+ fadeElapsed: number
55
+ totalPointsAtFadeStart: number
56
+ }
57
+
16
58
  /** @public */
17
59
  export class ScribbleManager {
18
- scribbleItems = new Map<string, ScribbleItem>()
19
- state = 'paused' as 'paused' | 'running'
60
+ private sessions = new Map<string, Session>()
20
61
 
21
62
  constructor(private editor: Editor) {}
22
63
 
23
- addScribble(scribble: Partial<TLScribble>, id = uniqueId()) {
24
- const item: ScribbleItem = {
64
+ // ==================== SESSION API ====================
65
+
66
+ /**
67
+ * Start a new session for grouping scribbles.
68
+ * Returns a session ID that can be used with other session methods.
69
+ *
70
+ * @param options - Session configuration
71
+ * @returns Session ID
72
+ * @public
73
+ */
74
+ startSession(options: ScribbleSessionOptions = {}): string {
75
+ const id = options.id ?? uniqueId()
76
+ const session: Session = {
25
77
  id,
78
+ items: [],
79
+ state: 'active',
80
+ options: {
81
+ selfConsume: options.selfConsume ?? true,
82
+ idleTimeoutMs: options.idleTimeoutMs ?? 0,
83
+ fadeMode: options.fadeMode ?? 'individual',
84
+ fadeEasing: options.fadeEasing ?? (options.fadeMode === 'grouped' ? 'ease-in' : 'linear'),
85
+ fadeDurationMs: options.fadeDurationMs ?? this.editor.options.laserFadeoutMs,
86
+ },
87
+ fadeElapsed: 0,
88
+ totalPointsAtFadeStart: 0,
89
+ }
90
+
91
+ this.sessions.set(id, session)
92
+
93
+ // Set up idle timeout if configured
94
+ if (session.options.idleTimeoutMs > 0) {
95
+ this.resetIdleTimeout(session)
96
+ }
97
+
98
+ return id
99
+ }
100
+
101
+ /**
102
+ * Add a scribble to a session.
103
+ *
104
+ * @param sessionId - The session ID
105
+ * @param scribble - Partial scribble properties
106
+ * @param scribbleId - Optional scribble ID
107
+ * @public
108
+ */
109
+ addScribbleToSession(
110
+ sessionId: string,
111
+ scribble: Partial<TLScribble>,
112
+ scribbleId = uniqueId()
113
+ ): ScribbleItem {
114
+ const session = this.sessions.get(sessionId)
115
+ if (!session) throw Error(`Session ${sessionId} not found`)
116
+
117
+ const item: ScribbleItem = {
118
+ id: scribbleId,
26
119
  scribble: {
27
- id,
120
+ id: scribbleId,
28
121
  size: 20,
29
122
  color: 'accent',
30
123
  opacity: 0.8,
@@ -40,46 +133,217 @@ export class ScribbleManager {
40
133
  prev: null,
41
134
  next: null,
42
135
  }
43
- this.scribbleItems.set(id, item)
136
+
137
+ session.items.push(item)
138
+
139
+ // Reset idle timeout on activity
140
+ if (session.options.idleTimeoutMs > 0) {
141
+ this.resetIdleTimeout(session)
142
+ }
143
+
44
144
  return item
45
145
  }
46
146
 
47
- reset() {
48
- this.editor.updateInstanceState({ scribbles: [] })
49
- this.scribbleItems.clear()
147
+ /**
148
+ * Add a point to a scribble in a session.
149
+ *
150
+ * @param sessionId - The session ID
151
+ * @param scribbleId - The scribble ID
152
+ * @param x - X coordinate
153
+ * @param y - Y coordinate
154
+ * @param z - Z coordinate (pressure)
155
+ * @public
156
+ */
157
+ addPointToSession(
158
+ sessionId: string,
159
+ scribbleId: string,
160
+ x: number,
161
+ y: number,
162
+ z = 0.5
163
+ ): ScribbleItem {
164
+ const session = this.sessions.get(sessionId)
165
+ if (!session) throw Error(`Session ${sessionId} not found`)
166
+
167
+ const item = session.items.find((i) => i.id === scribbleId)
168
+ if (!item) throw Error(`Scribble ${scribbleId} not found in session ${sessionId}`)
169
+
170
+ const point = { x, y, z }
171
+ if (!item.prev || Vec.Dist(item.prev, point) >= 1) {
172
+ item.next = point
173
+ }
174
+
175
+ // Reset idle timeout on activity
176
+ if (session.options.idleTimeoutMs > 0) {
177
+ this.resetIdleTimeout(session)
178
+ }
179
+
180
+ return item
50
181
  }
51
182
 
52
183
  /**
53
- * Start stopping the scribble. The scribble won't be removed until its last point is cleared.
184
+ * Extend a session, resetting its idle timeout.
54
185
  *
186
+ * @param sessionId - The session ID
55
187
  * @public
56
188
  */
57
- stop(id: ScribbleItem['id']) {
58
- const item = this.scribbleItems.get(id)
59
- if (!item) throw Error(`Scribble with id ${id} not found`)
60
- item.delayRemaining = Math.min(item.delayRemaining, 200)
61
- item.scribble.state = 'stopping'
62
- return item
189
+ extendSession(sessionId: string): void {
190
+ const session = this.sessions.get(sessionId)
191
+ if (!session) return
192
+
193
+ if (session.options.idleTimeoutMs > 0) {
194
+ this.resetIdleTimeout(session)
195
+ }
63
196
  }
64
197
 
65
198
  /**
66
- * Set the scribble's next point.
199
+ * Stop a session, triggering fade-out.
67
200
  *
68
- * @param id - The id of the scribble to add a point to.
69
- * @param x - The x coordinate of the point.
70
- * @param y - The y coordinate of the point.
71
- * @param z - The z coordinate of the point.
201
+ * @param sessionId - The session ID
72
202
  * @public
73
203
  */
74
- addPoint(id: ScribbleItem['id'], x: number, y: number, z = 0.5) {
75
- const item = this.scribbleItems.get(id)
76
- if (!item) throw Error(`Scribble with id ${id} not found`)
77
- const { prev } = item
78
- const point = { x, y, z }
79
- if (!prev || Vec.Dist(prev, point) >= 1) {
80
- item.next = point
204
+ stopSession(sessionId: string): void {
205
+ const session = this.sessions.get(sessionId)
206
+ if (!session || session.state !== 'active') return
207
+
208
+ this.clearIdleTimeout(session)
209
+ session.state = 'stopping'
210
+
211
+ if (session.options.fadeMode === 'grouped') {
212
+ session.totalPointsAtFadeStart = session.items.reduce(
213
+ (sum, item) => sum + item.scribble.points.length,
214
+ 0
215
+ )
216
+ session.fadeElapsed = 0
217
+ for (const item of session.items) {
218
+ item.scribble.state = 'stopping'
219
+ }
220
+ } else {
221
+ for (const item of session.items) {
222
+ item.delayRemaining = Math.min(item.delayRemaining, 200)
223
+ item.scribble.state = 'stopping'
224
+ }
81
225
  }
82
- return item
226
+ }
227
+
228
+ /**
229
+ * Clear all scribbles in a session immediately.
230
+ *
231
+ * @param sessionId - The session ID
232
+ * @public
233
+ */
234
+ clearSession(sessionId: string): void {
235
+ const session = this.sessions.get(sessionId)
236
+ if (!session) return
237
+
238
+ this.clearIdleTimeout(session)
239
+ for (const item of session.items) {
240
+ item.scribble.points.length = 0
241
+ }
242
+ session.state = 'complete'
243
+ }
244
+
245
+ /**
246
+ * Check if a session is active.
247
+ *
248
+ * @param sessionId - The session ID
249
+ * @public
250
+ */
251
+ isSessionActive(sessionId: string): boolean {
252
+ const session = this.sessions.get(sessionId)
253
+ return session?.state === 'active'
254
+ }
255
+
256
+ // ==================== SIMPLE API (for eraser, select, etc.) ====================
257
+
258
+ /**
259
+ * Add a scribble using the default self-consuming behavior.
260
+ * Creates an implicit session for the scribble.
261
+ *
262
+ * @param scribble - Partial scribble properties
263
+ * @param id - Optional scribble id
264
+ * @returns The created scribble item
265
+ * @public
266
+ */
267
+ addScribble(scribble: Partial<TLScribble>, id = uniqueId()): ScribbleItem {
268
+ const sessionId = this.startSession()
269
+ return this.addScribbleToSession(sessionId, scribble, id)
270
+ }
271
+
272
+ /**
273
+ * Add a point to a scribble. Searches all sessions.
274
+ *
275
+ * @param id - The scribble id
276
+ * @param x - X coordinate
277
+ * @param y - Y coordinate
278
+ * @param z - Z coordinate (pressure)
279
+ * @public
280
+ */
281
+ addPoint(id: string, x: number, y: number, z = 0.5): ScribbleItem {
282
+ for (const session of this.sessions.values()) {
283
+ const item = session.items.find((i) => i.id === id)
284
+ if (item) {
285
+ const point = { x, y, z }
286
+ if (!item.prev || Vec.Dist(item.prev, point) >= 1) {
287
+ item.next = point
288
+ }
289
+ if (session.options.idleTimeoutMs > 0) {
290
+ this.resetIdleTimeout(session)
291
+ }
292
+ return item
293
+ }
294
+ }
295
+ throw Error(`Scribble with id ${id} not found`)
296
+ }
297
+
298
+ /**
299
+ * Mark a scribble as complete (done being drawn but not yet fading).
300
+ * Searches all sessions.
301
+ *
302
+ * @param id - The scribble id
303
+ * @public
304
+ */
305
+ complete(id: string): ScribbleItem {
306
+ for (const session of this.sessions.values()) {
307
+ const item = session.items.find((i) => i.id === id)
308
+ if (item) {
309
+ if (item.scribble.state === 'starting' || item.scribble.state === 'active') {
310
+ item.scribble.state = 'complete'
311
+ }
312
+ return item
313
+ }
314
+ }
315
+ throw Error(`Scribble with id ${id} not found`)
316
+ }
317
+
318
+ /**
319
+ * Stop a scribble. Searches all sessions.
320
+ *
321
+ * @param id - The scribble id
322
+ * @public
323
+ */
324
+ stop(id: string): ScribbleItem {
325
+ for (const session of this.sessions.values()) {
326
+ const item = session.items.find((i) => i.id === id)
327
+ if (item) {
328
+ item.delayRemaining = Math.min(item.delayRemaining, 200)
329
+ item.scribble.state = 'stopping'
330
+ return item
331
+ }
332
+ }
333
+ throw Error(`Scribble with id ${id} not found`)
334
+ }
335
+
336
+ /**
337
+ * Stop and remove all sessions.
338
+ *
339
+ * @public
340
+ */
341
+ reset(): void {
342
+ for (const session of this.sessions.values()) {
343
+ this.clearIdleTimeout(session)
344
+ }
345
+ this.sessions.clear()
346
+ this.editor.updateInstanceState({ scribbles: [] })
83
347
  }
84
348
 
85
349
  /**
@@ -88,98 +352,219 @@ export class ScribbleManager {
88
352
  * @param elapsed - The number of milliseconds since the last tick.
89
353
  * @public
90
354
  */
91
- tick(elapsed: number) {
92
- if (this.scribbleItems.size === 0) return
355
+ tick(elapsed: number): void {
356
+ const currentScribbles = this.editor.getInstanceState().scribbles
357
+ if (this.sessions.size === 0 && currentScribbles.length === 0) return
358
+
93
359
  this.editor.run(() => {
94
- this.scribbleItems.forEach((item) => {
95
- // let the item get at least eight points before
96
- // switching from starting to active
97
- if (item.scribble.state === 'starting') {
98
- const { next, prev } = item
99
- if (next && next !== prev) {
100
- item.prev = next
101
- item.scribble.points.push(next)
102
- }
360
+ // Tick all sessions
361
+ for (const session of this.sessions.values()) {
362
+ this.tickSession(session, elapsed)
363
+ }
103
364
 
104
- if (item.scribble.points.length > 8) {
105
- item.scribble.state = 'active'
106
- }
107
- return
365
+ // Remove completed sessions
366
+ for (const [id, session] of this.sessions) {
367
+ if (session.state === 'complete') {
368
+ this.clearIdleTimeout(session)
369
+ this.sessions.delete(id)
108
370
  }
371
+ }
109
372
 
110
- if (item.delayRemaining > 0) {
111
- item.delayRemaining = Math.max(0, item.delayRemaining - elapsed)
373
+ // Collect scribbles from all sessions
374
+ const scribbles: TLScribble[] = []
375
+ for (const session of this.sessions.values()) {
376
+ for (const item of session.items) {
377
+ if (item.scribble.points.length > 0) {
378
+ scribbles.push({
379
+ ...item.scribble,
380
+ points: [...item.scribble.points],
381
+ })
382
+ }
112
383
  }
384
+ }
385
+
386
+ this.editor.updateInstanceState({ scribbles })
387
+ })
388
+ }
389
+
390
+ // ==================== PRIVATE HELPERS ====================
113
391
 
114
- item.timeoutMs += elapsed
115
- if (item.timeoutMs >= 16) {
116
- item.timeoutMs = 0
392
+ private resetIdleTimeout(session: Session): void {
393
+ this.clearIdleTimeout(session)
394
+ session.idleTimeoutHandle = this.editor.timers.setTimeout(() => {
395
+ this.stopSession(session.id)
396
+ }, session.options.idleTimeoutMs)
397
+ }
398
+
399
+ private clearIdleTimeout(session: Session): void {
400
+ if (session.idleTimeoutHandle !== undefined) {
401
+ clearTimeout(session.idleTimeoutHandle)
402
+ session.idleTimeoutHandle = undefined
403
+ }
404
+ }
405
+
406
+ private tickSession(session: Session, elapsed: number): void {
407
+ if (session.state === 'complete') return
408
+
409
+ if (session.state === 'stopping' && session.options.fadeMode === 'grouped') {
410
+ this.tickGroupedFade(session, elapsed)
411
+ } else {
412
+ this.tickSessionItems(session, elapsed)
413
+ }
414
+
415
+ // Check if session is complete
416
+ const hasContent = session.items.some((item) => item.scribble.points.length > 0)
417
+ if (!hasContent && (session.state === 'stopping' || session.items.length === 0)) {
418
+ session.state = 'complete'
419
+ }
420
+ }
421
+
422
+ private tickSessionItems(session: Session, elapsed: number): void {
423
+ for (const item of session.items) {
424
+ const shouldSelfConsume =
425
+ session.options.selfConsume ||
426
+ session.state === 'stopping' ||
427
+ item.scribble.state === 'stopping'
428
+
429
+ if (shouldSelfConsume) {
430
+ this.tickSelfConsumingItem(item, elapsed)
431
+ } else {
432
+ this.tickPersistentItem(item)
433
+ }
434
+ }
435
+
436
+ // Remove completed items in individual fade mode
437
+ if (session.options.fadeMode === 'individual') {
438
+ for (let i = session.items.length - 1; i >= 0; i--) {
439
+ if (session.items[i].scribble.points.length === 0) {
440
+ session.items.splice(i, 1)
117
441
  }
442
+ }
443
+ }
444
+ }
445
+
446
+ private tickPersistentItem(item: ScribbleItem): void {
447
+ const { scribble } = item
448
+
449
+ if (scribble.state === 'starting') {
450
+ const { next, prev } = item
451
+ if (next && next !== prev) {
452
+ item.prev = next
453
+ scribble.points.push(next)
454
+ }
455
+ if (scribble.points.length > 8) {
456
+ scribble.state = 'active'
457
+ }
458
+ return
459
+ }
118
460
 
119
- const { delayRemaining, timeoutMs, prev, next, scribble } = item
461
+ if (scribble.state === 'active') {
462
+ const { next, prev } = item
463
+ if (next && next !== prev) {
464
+ item.prev = next
465
+ scribble.points.push(next)
466
+ }
467
+ }
468
+ }
120
469
 
121
- switch (scribble.state) {
122
- case 'active': {
123
- if (next && next !== prev) {
124
- item.prev = next
125
- scribble.points.push(next)
470
+ private tickSelfConsumingItem(item: ScribbleItem, elapsed: number): void {
471
+ const { scribble } = item
126
472
 
127
- // If we've run out of delay, then shrink the scribble from the start
128
- if (delayRemaining === 0) {
129
- if (scribble.points.length > 8) {
130
- scribble.points.shift()
131
- }
132
- }
473
+ if (scribble.state === 'starting') {
474
+ const { next, prev } = item
475
+ if (next && next !== prev) {
476
+ item.prev = next
477
+ scribble.points.push(next)
478
+ }
479
+ if (scribble.points.length > 8) {
480
+ scribble.state = 'active'
481
+ }
482
+ return
483
+ }
484
+
485
+ if (item.delayRemaining > 0) {
486
+ item.delayRemaining = Math.max(0, item.delayRemaining - elapsed)
487
+ }
488
+
489
+ item.timeoutMs += elapsed
490
+ if (item.timeoutMs >= 16) {
491
+ item.timeoutMs = 0
492
+ }
493
+
494
+ const { delayRemaining, timeoutMs, prev, next } = item
495
+
496
+ switch (scribble.state) {
497
+ case 'active': {
498
+ if (next && next !== prev) {
499
+ item.prev = next
500
+ scribble.points.push(next)
501
+ if (delayRemaining === 0 && scribble.points.length > 8) {
502
+ scribble.points.shift()
503
+ }
504
+ } else {
505
+ if (timeoutMs === 0) {
506
+ if (scribble.points.length > 1) {
507
+ scribble.points.shift()
133
508
  } else {
134
- // While not moving, shrink the scribble from the start
135
- if (timeoutMs === 0) {
136
- if (scribble.points.length > 1) {
137
- scribble.points.shift()
138
- } else {
139
- // Reset the item's delay
140
- item.delayRemaining = scribble.delay
141
- }
142
- }
509
+ item.delayRemaining = scribble.delay
143
510
  }
144
- break
145
511
  }
146
- case 'stopping': {
147
- if (item.delayRemaining === 0) {
148
- if (timeoutMs === 0) {
149
- // If the scribble is down to one point, we're done!
150
- if (scribble.points.length === 1) {
151
- this.scribbleItems.delete(item.id) // Remove the scribble
152
- return
153
- }
154
-
155
- if (scribble.shrink) {
156
- // Drop the scribble's size as it shrinks
157
- scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink))
158
- }
159
-
160
- // Drop the scribble's first point (its tail)
161
- scribble.points.shift()
162
- }
163
- }
164
- break
512
+ }
513
+ break
514
+ }
515
+ case 'stopping': {
516
+ if (delayRemaining === 0 && timeoutMs === 0) {
517
+ if (scribble.points.length <= 1) {
518
+ scribble.points.length = 0
519
+ return
165
520
  }
166
- case 'paused': {
167
- // Nothing to do while paused.
168
- break
521
+ if (scribble.shrink) {
522
+ scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink))
169
523
  }
524
+ scribble.points.shift()
170
525
  }
171
- })
172
-
173
- // The object here will get frozen into the record, so we need to
174
- // create a copies of the parts that what we'll be mutating later.
175
- this.editor.updateInstanceState({
176
- scribbles: Array.from(this.scribbleItems.values())
177
- .map(({ scribble }) => ({
178
- ...scribble,
179
- points: [...scribble.points],
180
- }))
181
- .slice(-5), // limit to three as a minor sanity check
182
- })
183
- })
526
+ break
527
+ }
528
+ case 'paused': {
529
+ break
530
+ }
531
+ }
532
+ }
533
+
534
+ private tickGroupedFade(session: Session, elapsed: number): void {
535
+ session.fadeElapsed += elapsed
536
+
537
+ let remainingPoints = 0
538
+ for (const item of session.items) {
539
+ remainingPoints += item.scribble.points.length
540
+ }
541
+
542
+ if (remainingPoints === 0) return
543
+
544
+ if (session.fadeElapsed >= session.options.fadeDurationMs) {
545
+ for (const item of session.items) {
546
+ item.scribble.points.length = 0
547
+ }
548
+ return
549
+ }
550
+
551
+ const progress = session.fadeElapsed / session.options.fadeDurationMs
552
+ const easedProgress = session.options.fadeEasing === 'ease-in' ? progress * progress : progress
553
+
554
+ const targetRemoved = Math.floor(easedProgress * session.totalPointsAtFadeStart)
555
+ const actuallyRemoved = session.totalPointsAtFadeStart - remainingPoints
556
+ const pointsToRemove = Math.max(1, targetRemoved - actuallyRemoved)
557
+
558
+ let removed = 0
559
+ let itemIndex = 0
560
+ while (removed < pointsToRemove && itemIndex < session.items.length) {
561
+ const item = session.items[itemIndex]
562
+ if (item.scribble.points.length > 0) {
563
+ item.scribble.points.shift()
564
+ removed++
565
+ } else {
566
+ itemIndex++
567
+ }
568
+ }
184
569
  }
185
570
  }