@px-petals/roku.motion-web 0.0.0-stage → 1.1.0

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 (99) hide show
  1. package/LICENSE +201 -0
  2. package/audio/audio.cues.js +27 -0
  3. package/audio/audio.js +259 -0
  4. package/bands/bands.js +392 -0
  5. package/bundle/bundle.fonts.js +55 -0
  6. package/bundle/bundle.js +131 -0
  7. package/focus/focus.direction.js +46 -0
  8. package/focus/focus.dom.js +158 -0
  9. package/focus/focus.js +277 -0
  10. package/focus/focus.tree.js +146 -0
  11. package/hit/hit.geometry.js +113 -0
  12. package/hit/hit.js +129 -0
  13. package/list/list.flex.js +181 -0
  14. package/list/list.items.js +80 -0
  15. package/list/list.js +518 -0
  16. package/lottie/lottie.js +124 -0
  17. package/lottie/lottie.layers.js +416 -0
  18. package/lottie/lottie.paint.js +238 -0
  19. package/lottie/lottie.paths.js +474 -0
  20. package/lottie/lottie.props.js +265 -0
  21. package/lottie/lottie.shapes.js +259 -0
  22. package/mixer/mixer.js +307 -0
  23. package/mixer/mixer.mix.js +107 -0
  24. package/mixer/mixer.stage.js +431 -0
  25. package/motion/motion.convert.js +404 -0
  26. package/motion/motion.flow.js +421 -0
  27. package/motion/motion.keys.js +150 -0
  28. package/motion/motion.machine.js +382 -0
  29. package/motion/motion.nested.js +345 -0
  30. package/motion/motion.values.js +368 -0
  31. package/motion.js +1155 -0
  32. package/package.json +47 -4
  33. package/player/overlay/overlay.js +325 -0
  34. package/player/overlay/overlay.layout.js +208 -0
  35. package/player/player.fit.js +101 -0
  36. package/player/player.js +421 -0
  37. package/player/player.raster.js +127 -0
  38. package/pointer/pointer.js +292 -0
  39. package/readme.md +345 -0
  40. package/script/luau/lib/base/base.js +202 -0
  41. package/script/luau/lib/base/base.table.js +189 -0
  42. package/script/luau/lib/lib.args.js +152 -0
  43. package/script/luau/lib/lib.js +23 -0
  44. package/script/luau/lib/math/math.bit32.js +144 -0
  45. package/script/luau/lib/math/math.js +288 -0
  46. package/script/luau/lib/math/math.vector.js +125 -0
  47. package/script/luau/lib/string/string.format.js +143 -0
  48. package/script/luau/lib/string/string.js +160 -0
  49. package/script/luau/lib/string/string.pattern.js +505 -0
  50. package/script/luau/load/load.js +362 -0
  51. package/script/luau/load/load.ops.js +79 -0
  52. package/script/luau/luau.convert.js +259 -0
  53. package/script/luau/luau.js +132 -0
  54. package/script/luau/luau.md +111 -0
  55. package/script/luau/vm/vm.js +167 -0
  56. package/script/luau/vm/vm.meta.js +401 -0
  57. package/script/luau/vm/vm.num.js +236 -0
  58. package/script/luau/vm/vm.run.js +842 -0
  59. package/script/luau/vm/vm.value.js +406 -0
  60. package/script/play/play.js +199 -0
  61. package/script/play/play.objects.js +295 -0
  62. package/script/play/play.scope.js +91 -0
  63. package/script/rive/api/api.statics.js +31 -0
  64. package/script/rive/api/api.type.js +188 -0
  65. package/script/rive/api/artboard/artboard.js +89 -0
  66. package/script/rive/api/assets/assets.js +72 -0
  67. package/script/rive/api/data/data.value.js +99 -0
  68. package/script/rive/api/draw/draw.contours.js +163 -0
  69. package/script/rive/api/draw/draw.measure.js +196 -0
  70. package/script/rive/api/draw/draw.paint.js +235 -0
  71. package/script/rive/api/draw/draw.path.js +153 -0
  72. package/script/rive/api/draw/draw.renderer.js +117 -0
  73. package/script/rive/api/math/math.color.js +96 -0
  74. package/script/rive/api/math/math.mat2d.js +180 -0
  75. package/script/rive/api/math/math.vector.js +121 -0
  76. package/script/rive/api/media/media.audio.js +113 -0
  77. package/script/rive/api/media/media.draw.js +73 -0
  78. package/script/rive/api/media/media.image.js +61 -0
  79. package/script/rive/api/media/media.mesh.js +50 -0
  80. package/script/rive/api/pointer/pointer.event.js +64 -0
  81. package/script/rive/api/pointer/pointer.route.js +69 -0
  82. package/script/rive/api/text/text.font.js +129 -0
  83. package/script/rive/api/text/text.js +278 -0
  84. package/script/rive/api/text/text.layout.js +259 -0
  85. package/script/rive/context/context.js +76 -0
  86. package/script/rive/context/context.memory.js +196 -0
  87. package/script/rive/context/context.viewmodel.js +302 -0
  88. package/script/rive/context/lists/lists.js +160 -0
  89. package/script/rive/context/lists/lists.values.js +281 -0
  90. package/script/rive/host/host.globals.js +44 -0
  91. package/script/rive/host/host.js +533 -0
  92. package/script/rive/host/host.node.js +45 -0
  93. package/script/rive/rive.js +23 -0
  94. package/script/rive/rive.md +180 -0
  95. package/script/script.js +11 -0
  96. package/skin/skin.deform.js +514 -0
  97. package/skin/skin.ik.js +260 -0
  98. package/skin/skin.js +297 -0
  99. package/README.md +0 -3
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Lottie layers onto a 2D canvas: parenting, precomps with time remapping,
3
+ * solids, images, shapes, masks, track mattes and blend modes. Text layers
4
+ * are left out: lottie-roku turns them into overlays, which Motion draws.
5
+ *
6
+ * A layer's properties run on its own clock, the composition frame less its
7
+ * start time (`st`); its in and out points (`ip`, `op`) are composition
8
+ * frames, as lottie-web reads them.
9
+ */
10
+
11
+ import { drawDocument } from '../player/overlay/overlay.js'
12
+ import { clipToOperations, drawOperations } from './lottie.paint.js'
13
+ import { fromLottie, IDENTITY, multiply, trace, transformPath } from './lottie.paths.js'
14
+ import { evaluator } from './lottie.props.js'
15
+ import { compileItems, compileTransform, runGroup, scalar } from './lottie.shapes.js'
16
+
17
+ /** @import { Matrix } from './lottie.paths.js' */
18
+ /** @import { Slots } from './lottie.props.js' */
19
+
20
+ /** Lottie blend modes, by `bm`, as canvas composite operations */
21
+ const BLENDS = [ 'source-over', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity' ]
22
+
23
+ /**
24
+ * @typedef {object} Context
25
+ * @property {Record<string, any>} assets by id: precomp layer lists and image entries
26
+ * @property {Record<string, HTMLImageElement>} images by asset id
27
+ * @property {number} fps
28
+ * @property {Slots} slots
29
+ * @property {import('../hit/hit.js').Hits} [hits] records the geometry of the groups pointer listeners target
30
+ * @property {(() => Record<string, string>) | null} [families] bundle font name → registered family, for text a precomp holds
31
+ * @property {string} [scope] while a nested machine's layer draws, `<its nid>/`, which names the groups it records
32
+ */
33
+
34
+ /**
35
+ * @param {any[]} layers
36
+ * @param {Context} context
37
+ * @returns {any[]} compiled layers, in the file's order (top first)
38
+ */
39
+ export function compileLayers(layers, context) {
40
+ const compiled = []
41
+ for (let n = 0; n < (layers || []).length; n++) {
42
+ const layer = layers[n]
43
+ compiled.push(compileLayer(layer, context))
44
+ }
45
+ return compiled
46
+ }
47
+
48
+ /**
49
+ * @param {any} layer
50
+ * @param {Context} context
51
+ */
52
+ function compileLayer(layer, context) {
53
+ const slots = context.slots
54
+ const asset = layer.refId ? context.assets[layer.refId] : null
55
+ return {
56
+ ty: layer.ty,
57
+ ind: layer.ind,
58
+ parent: layer.parent,
59
+ hidden: !!layer.hd,
60
+ ip: layer.ip || 0,
61
+ op: layer.op === undefined ? Infinity : layer.op,
62
+ st: layer.st || 0,
63
+ sr: layer.sr || 1,
64
+ isMatte: layer.td === 1,
65
+ matte: layer.tt || 0,
66
+ blend: BLENDS[layer.bm || 0] || 'source-over',
67
+ transform: compileTransform(layer.ks, slots),
68
+ shapes: layer.ty === 4 ? compileItems(layer.shapes, slots) : null,
69
+ precomp: layer.ty === 0 && asset && asset.layers ? compileLayers(asset.layers, context) : null,
70
+ // a nested artboard's machine moves its precomp's time through the slot its `nid` names
71
+ // a text layer inside a precomp: a nested artboard's text a live value fills
72
+ text: layer.ty === 5 && layer.t && layer.t.d && layer.t.d.k && layer.t.d.k[0] ? { sid: typeof layer.t.d.sid === 'string' ? layer.t.d.sid : null, doc: layer.t.d.k[0].s } : null,
73
+ nid: typeof layer.nid === 'string' ? layer.nid : null,
74
+ timeRemap: layer.tm ? evaluator(typeof layer.nid === 'string' ? mergeSid(layer.tm, layer.nid) : layer.tm, slots, 0) : null,
75
+ // an artboard laid out to its place: its size is a property, which a layout band mixes
76
+ // a nested artboard that does not clip draws past its own edge
77
+ unclipped: layer.nc === 1,
78
+ size: layer.wh ? evaluator(layer.wh, slots, [ 0, 0 ]) : null,
79
+ width: layer.w || (asset && asset.w) || 0,
80
+ height: layer.h || (asset && asset.h) || 0,
81
+ image: layer.ty === 2 && layer.refId ? layer.refId : null,
82
+ // a data-bound image: the slot table says which asset to draw, anchored at the same fraction of it
83
+ imageSlot: layer.ty === 2 && typeof layer.isid === 'string' ? layer.isid : null,
84
+ origin: Array.isArray(layer.origin) ? layer.origin : null,
85
+ anchor: layer.ks && layer.ks.a && layer.ks.a.a !== 1 && Array.isArray(layer.ks.a.k) ? layer.ks.a.k : null,
86
+ solid: layer.ty === 1 ? { color: layer.sc || '#000000', width: layer.sw || 0, height: layer.sh || 0 } : null,
87
+ masks: compileMasks(layer, slots),
88
+ }
89
+ }
90
+
91
+ /**
92
+ * A copy of a property that reads a slot first.
93
+ *
94
+ * @param {any} property
95
+ * @param {string} sid
96
+ * @returns {any}
97
+ */
98
+ function mergeSid(property, sid) {
99
+ /** @type {Record<string, any>} */
100
+ const copy = {}
101
+ for (const key in property) copy[key] = property[key]
102
+ copy.sid = sid
103
+ return copy
104
+ }
105
+
106
+ /**
107
+ * @param {any} layer
108
+ * @param {Slots} slots
109
+ */
110
+ function compileMasks(layer, slots) {
111
+ if (!layer.hasMask || !Array.isArray(layer.masksProperties)) return []
112
+
113
+ const masks = []
114
+ for (let n = 0; n < layer.masksProperties.length; n++) {
115
+ const mask = layer.masksProperties[n]
116
+ if (mask.mode === 'n') continue
117
+ masks.push({ mode: mask.mode || 'a', inverted: !!mask.inv, path: evaluator(mask.pt, slots, { v: [], i: [], o: [], c: true }) })
118
+ }
119
+ return masks
120
+ }
121
+
122
+ // Drawing
123
+ // ■■■■■■■
124
+
125
+ /**
126
+ * Draws a list of layers at a composition frame.
127
+ *
128
+ * @param {CanvasRenderingContext2D} ctx
129
+ * @param {any[]} layers compiled, top first
130
+ * @param {Context} context
131
+ * @param {number} frame
132
+ * @param {Matrix} matrix the composition's space onto the canvas
133
+ * @param {number} opacity
134
+ */
135
+ export function drawLayers(ctx, layers, context, frame, matrix, opacity) {
136
+ const worlds = layerMatrices(layers, frame)
137
+ for (let n = layers.length - 1; n >= 0; n--) {
138
+ const layer = layers[n]
139
+ const isVisible = !layer.hidden && !layer.isMatte && frame >= layer.ip && frame < layer.op
140
+ if (!isVisible) continue
141
+
142
+ const matte = layer.matte && n > 0 && layers[n - 1].isMatte ? layers[n - 1] : null
143
+ if (matte) {
144
+ drawMatted(ctx, layer, matte, context, frame, multiply(matrix, worlds[n].matrix), multiply(matrix, worlds[n - 1].matrix), opacity * worlds[n].opacity)
145
+ } else {
146
+ drawLayer(ctx, layer, context, frame, multiply(matrix, worlds[n].matrix), opacity * worlds[n].opacity)
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Each layer's matrix within the composition, through its parents. A parent
153
+ * passes on its transform, not its opacity.
154
+ *
155
+ * @param {any[]} layers
156
+ * @param {number} frame
157
+ * @returns {{ matrix: Matrix, opacity: number }[]}
158
+ */
159
+ function layerMatrices(layers, frame) {
160
+ /** @type {Record<string, number>} a layer's `ind` → its position */
161
+ const position = {}
162
+ for (let n = 0; n < layers.length; n++) if (layers[n].ind !== undefined) position[layers[n].ind] = n
163
+
164
+ // each layer's own transform, once: parents are shared
165
+ const own = layers.map((layer) => layer.transform(frame - layer.st))
166
+ const parentOf = (/** @type {any} */ layer) => (layer.parent !== undefined && position[layer.parent] !== undefined ? position[layer.parent] : -1)
167
+
168
+ return layers.map((layer, n) => {
169
+ let matrix = own[n].matrix
170
+ let parent = parentOf(layer)
171
+ // a parent chain is short; the guard only stops a cycle
172
+ for (let depth = 0; parent >= 0 && depth < 32; depth++) {
173
+ matrix = multiply(own[parent].matrix, matrix)
174
+ parent = parentOf(layers[parent])
175
+ }
176
+ return { matrix, opacity: own[n].opacity }
177
+ })
178
+ }
179
+
180
+ /**
181
+ * @param {CanvasRenderingContext2D} ctx
182
+ * @param {any} layer
183
+ * @param {Context} context
184
+ * @param {number} frame the composition's
185
+ * @param {Matrix} matrix the layer's space onto the canvas
186
+ * @param {number} opacity
187
+ */
188
+ function drawLayer(ctx, layer, context, frame, matrix, opacity) {
189
+ if (opacity <= 0) return
190
+ const local = frame - layer.st
191
+
192
+ ctx.save()
193
+ ctx.globalCompositeOperation = layer.blend
194
+ if (layer.masks.length) clipToMasks(ctx, layer.masks, local, matrix)
195
+
196
+ if (layer.shapes) {
197
+ const sink = context.hits && context.hits.active ? { hits: context.hits, matrix, prefix: context.scope || '' } : null
198
+ const run = runGroup(layer.shapes, local, IDENTITY, 1, sink)
199
+ drawOperations(ctx, run.operations, matrix, opacity, local)
200
+ } else if (layer.text) {
201
+ const live = layer.text.sid && typeof context.slots[layer.text.sid] === 'string' ? context.slots[layer.text.sid] : null
202
+ ctx.globalAlpha = opacity
203
+ drawDocument(ctx, layer.text.doc, live, context.families ? context.families() : {}, matrix)
204
+ } else if (layer.precomp) {
205
+ drawPrecomp(ctx, layer, context, local, matrix, opacity)
206
+ } else if (layer.solid) {
207
+ setTransform(ctx, matrix)
208
+ ctx.globalAlpha = opacity
209
+ ctx.fillStyle = layer.solid.color
210
+ ctx.fillRect(0, 0, layer.solid.width, layer.solid.height)
211
+ } else if (layer.image && context.images[imageOf(layer, context)]) {
212
+ const id = imageOf(layer, context)
213
+ const image = context.images[id]
214
+ setTransform(ctx, matrix)
215
+ ctx.globalAlpha = opacity
216
+ const asset = context.assets[id] || {}
217
+ const width = asset.w || image.width
218
+ const height = asset.h || image.height
219
+ // the layer's anchor is a fraction of the image it was authored with, so an image of another size moves to keep it
220
+ const shiftX = layer.origin && layer.anchor ? layer.anchor[0] - layer.origin[0] * width : 0
221
+ const shiftY = layer.origin && layer.anchor ? layer.anchor[1] - layer.origin[1] * height : 0
222
+ ctx.drawImage(image, shiftX, shiftY, width, height)
223
+ }
224
+ ctx.restore()
225
+ }
226
+
227
+ /**
228
+ * The asset an image layer draws: its slot's, when the slot table names one this animation holds.
229
+ *
230
+ * @param {any} layer
231
+ * @param {Context} context
232
+ */
233
+ function imageOf(layer, context) {
234
+ const live = layer.imageSlot ? context.slots[layer.imageSlot] : undefined
235
+ return typeof live === 'string' && context.images[live] ? live : layer.image
236
+ }
237
+
238
+ /**
239
+ * A precomp's own layers, on its own clock: the layer's time remap (seconds)
240
+ * when it has one, else the layer's time slowed by its stretch. Its
241
+ * width and height clip it.
242
+ *
243
+ * @param {CanvasRenderingContext2D} ctx
244
+ * @param {any} layer
245
+ * @param {Context} context
246
+ * @param {number} local the layer's own frame
247
+ * @param {Matrix} matrix
248
+ * @param {number} opacity
249
+ */
250
+ function drawPrecomp(ctx, layer, context, local, matrix, opacity) {
251
+ const inner = layer.timeRemap ? scalar(layer.timeRemap(local)) * context.fps : local / layer.sr
252
+ const size = layer.size ? layer.size(local) : null
253
+ const width = size ? size[0] : layer.width
254
+ const height = size ? size[1] : layer.height
255
+ if (width && height && !layer.unclipped) {
256
+ setTransform(ctx, matrix)
257
+ ctx.beginPath()
258
+ ctx.rect(0, 0, width, height)
259
+ ctx.clip()
260
+ }
261
+ // a nested machine's shapes are named under the layer's `nid`, since its artboard numbers its objects from 0 too
262
+ const outer = context.scope
263
+ if (layer.nid) context.scope = layer.nid + '/'
264
+ drawLayers(ctx, layer.precomp, context, inner, matrix, opacity)
265
+ context.scope = outer
266
+ }
267
+
268
+ /**
269
+ * Clips to a layer's masks: added masks union, subtracted ones cut out
270
+ * (an even-odd clip with a frame around everything), intersected ones each
271
+ * clip again.
272
+ *
273
+ * @param {CanvasRenderingContext2D} ctx
274
+ * @param {any[]} masks
275
+ * @param {number} local
276
+ * @param {Matrix} matrix
277
+ */
278
+ function clipToMasks(ctx, masks, local, matrix) {
279
+ setTransform(ctx, matrix)
280
+ const path = (/** @type {any} */ mask) => {
281
+ const value = mask.path(local)
282
+ return fromLottie(Array.isArray(value) ? value[0] : value)
283
+ }
284
+ const huge = () => {
285
+ ctx.moveTo(-1e5, -1e5)
286
+ ctx.lineTo(1e5, -1e5)
287
+ ctx.lineTo(1e5, 1e5)
288
+ ctx.lineTo(-1e5, 1e5)
289
+ ctx.closePath()
290
+ }
291
+
292
+ const added = masks.filter((mask) => mask.mode === 'a' && !mask.inverted)
293
+ if (added.length) {
294
+ ctx.beginPath()
295
+ for (let n = 0; n < added.length; n++) trace(ctx, transformPath(path(added[n]), IDENTITY))
296
+ ctx.clip()
297
+ }
298
+ for (let n = 0; n < masks.length; n++) {
299
+ const mask = masks[n]
300
+ const cutsOut = mask.mode === 's' || (mask.mode === 'a' && mask.inverted)
301
+ if (!cutsOut && mask.mode !== 'i') continue
302
+ ctx.beginPath()
303
+ if (cutsOut) huge()
304
+ trace(ctx, path(mask))
305
+ ctx.clip(cutsOut ? 'evenodd' : 'nonzero')
306
+ }
307
+ }
308
+
309
+ /**
310
+ * A layer through its track matte. An alpha matte that is one opaque fill
311
+ * clips the layer to its path; any other is drawn with the layer on scratch
312
+ * canvases the size of the target, the content kept where the matte is
313
+ * opaque (alpha) or clear (inverted). Luma mattes are read as alpha mattes.
314
+ *
315
+ * @param {CanvasRenderingContext2D} ctx
316
+ * @param {any} layer
317
+ * @param {any} matte
318
+ * @param {Context} context
319
+ * @param {number} frame
320
+ * @param {Matrix} matrix
321
+ * @param {Matrix} matteMatrix
322
+ * @param {number} opacity
323
+ */
324
+ function drawMatted(ctx, layer, matte, context, frame, matrix, matteMatrix, opacity) {
325
+ if (clipsByPath(ctx, layer, matte, frame, matteMatrix)) {
326
+ drawLayer(ctx, layer, context, frame, matrix, opacity)
327
+ ctx.restore()
328
+ return
329
+ }
330
+
331
+ const width = ctx.canvas.width
332
+ const height = ctx.canvas.height
333
+ const content = takeScratch(width, height)
334
+ const mask = takeScratch(width, height)
335
+ if (!content || !mask) {
336
+ drawLayer(ctx, layer, context, frame, matrix, opacity)
337
+ return
338
+ }
339
+
340
+ drawLayer(content, layer, context, frame, matrix, opacity)
341
+ drawLayer(mask, matte, context, frame, matteMatrix, 1)
342
+ content.globalCompositeOperation = layer.matte === 2 || layer.matte === 4 ? 'destination-out' : 'destination-in'
343
+ content.setTransform(1, 0, 0, 1, 0, 0)
344
+ content.drawImage(mask.canvas, 0, 0)
345
+
346
+ ctx.save()
347
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
348
+ ctx.drawImage(content.canvas, 0, 0)
349
+ ctx.restore()
350
+ giveScratch(mask)
351
+ giveScratch(content)
352
+ }
353
+
354
+ /**
355
+ * Clips to the matte's path when an alpha matte is one opaque fill, leaving
356
+ * the clip saved for the caller to restore.
357
+ *
358
+ * @param {CanvasRenderingContext2D} ctx
359
+ * @param {any} layer
360
+ * @param {any} matte
361
+ * @param {number} frame
362
+ * @param {Matrix} matteMatrix
363
+ * @returns {boolean} whether it clipped
364
+ */
365
+ function clipsByPath(ctx, layer, matte, frame, matteMatrix) {
366
+ const isPlainShapes = layer.matte === 1 && matte.shapes && !matte.masks.length && matte.blend === 'source-over'
367
+ if (!isPlainShapes) return false
368
+
369
+ const local = frame - matte.st
370
+ const run = runGroup(matte.shapes, local, IDENTITY, 1, null)
371
+ ctx.save()
372
+ if (clipToOperations(ctx, run.operations, matteMatrix, local)) return true
373
+ ctx.restore()
374
+ return false
375
+ }
376
+
377
+ /** scratch canvases free for a matte to draw on; a matte inside a matte takes others */
378
+ const SCRATCH = /** @type {CanvasRenderingContext2D[]} */ ([])
379
+
380
+ /**
381
+ * A cleared scratch canvas of the size, reused across frames: a matte drawn
382
+ * every frame would otherwise make two canvases a frame.
383
+ *
384
+ * @param {number} width
385
+ * @param {number} height
386
+ * @returns {CanvasRenderingContext2D | null}
387
+ */
388
+ function takeScratch(width, height) {
389
+ const ctx = SCRATCH.pop() || document.createElement('canvas').getContext('2d')
390
+ if (!ctx) return null
391
+ const canvas = ctx.canvas
392
+ // setting the size clears it; a canvas already that size is cleared by hand
393
+ if (canvas.width !== width || canvas.height !== height) {
394
+ canvas.width = width
395
+ canvas.height = height
396
+ } else {
397
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
398
+ ctx.globalCompositeOperation = 'source-over'
399
+ ctx.globalAlpha = 1
400
+ ctx.clearRect(0, 0, width, height)
401
+ }
402
+ return ctx
403
+ }
404
+
405
+ /** @param {CanvasRenderingContext2D} ctx */
406
+ function giveScratch(ctx) {
407
+ SCRATCH.push(ctx)
408
+ }
409
+
410
+ /**
411
+ * @param {CanvasRenderingContext2D} ctx
412
+ * @param {Matrix} m
413
+ */
414
+ function setTransform(ctx, m) {
415
+ ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
416
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Draw operations onto a 2D canvas: fills, strokes, gradients and dashes,
3
+ * each in its own group's space so stroke widths and gradient points come
4
+ * out as authored. Merged paths in intersect mode (`mm` 4, how rive2lottie
5
+ * writes clipping) draw as their first part clipped by the rest.
6
+ */
7
+
8
+ import { multiply, trace, transformPath } from './lottie.paths.js'
9
+ import { scalar } from './lottie.shapes.js'
10
+
11
+ /** @import { Matrix } from './lottie.paths.js' */
12
+ /** @import { Entry, Operation } from './lottie.shapes.js' */
13
+
14
+ /**
15
+ * Draws a layer's operations, bottom first.
16
+ *
17
+ * @param {CanvasRenderingContext2D} ctx
18
+ * @param {Operation[]} operations top first, as runGroup returns them
19
+ * @param {Matrix} layer the layer's space onto the canvas
20
+ * @param {number} opacity the layer's
21
+ * @param {number} frame
22
+ */
23
+ export function drawOperations(ctx, operations, layer, opacity, frame) {
24
+ for (let n = operations.length - 1; n >= 0; n--) {
25
+ const operation = operations[n]
26
+ const alpha = opacity * operation.opacity * (scalar(operation.paint.opacity(frame)) / 100)
27
+ if (alpha <= 0 || !operation.entries.length) continue
28
+
29
+ const m = multiply(layer, operation.matrix)
30
+ ctx.save()
31
+ ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
32
+ ctx.globalAlpha = Math.min(1, alpha)
33
+ paint(ctx, operation, frame)
34
+ ctx.restore()
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Clips to what a layer's operations cover, when that is exactly one opaque
40
+ * solid fill: an alpha matte of it keeps the same pixels as a clip to its
41
+ * path, at none of a matte's offscreen cost. Anything else (strokes,
42
+ * gradients, transparency, several paints, merged paths) returns false and
43
+ * clips nothing.
44
+ *
45
+ * @param {CanvasRenderingContext2D} ctx
46
+ * @param {Operation[]} operations as runGroup returns them
47
+ * @param {Matrix} layer the layer's space onto the canvas
48
+ * @param {number} frame
49
+ * @returns {boolean} whether it clipped
50
+ */
51
+ export function clipToOperations(ctx, operations, layer, frame) {
52
+ if (operations.length !== 1) return false
53
+ const operation = operations[0]
54
+ const style = operation.paint
55
+ const isOpaque = operation.opacity >= 1 && scalar(style.opacity(frame)) >= 100
56
+ const isSolidFill = !style.isStroke && !style.isGradient && (style.color(frame).length < 4 || style.color(frame)[3] >= 1)
57
+ if (!isOpaque || !isSolidFill || !operation.entries.length) return false
58
+ for (let n = 0; n < operation.entries.length; n++) if (operation.entries[n].holder.merge) return false
59
+
60
+ const m = multiply(layer, operation.matrix)
61
+ ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
62
+ ctx.beginPath()
63
+ for (let n = 0; n < operation.entries.length; n++) traceEntry(ctx, operation.entries[n])
64
+ ctx.clip(style.rule)
65
+ return true
66
+ }
67
+
68
+ /**
69
+ * @param {CanvasRenderingContext2D} ctx
70
+ * @param {Operation} operation
71
+ * @param {number} frame
72
+ */
73
+ function paint(ctx, operation, frame) {
74
+ const style = operation.paint
75
+ const fill = style.isGradient ? gradient(ctx, style.gradient, frame) : color(style.color(frame))
76
+ if (style.isStroke) strokeStyle(ctx, style, fill, frame)
77
+ else ctx.fillStyle = fill
78
+
79
+ const plain = []
80
+ for (let n = 0; n < operation.entries.length; n++) {
81
+ const entry = operation.entries[n]
82
+ if (entry.holder.merge) drawMerged(ctx, entry, style)
83
+ else plain.push(entry)
84
+ }
85
+ if (!plain.length) return
86
+
87
+ ctx.beginPath()
88
+ for (let n = 0; n < plain.length; n++) traceEntry(ctx, plain[n])
89
+ if (style.isStroke) ctx.stroke()
90
+ else ctx.fill(style.rule)
91
+ }
92
+
93
+ /**
94
+ * @param {CanvasRenderingContext2D} ctx
95
+ * @param {Entry} entry
96
+ */
97
+ function traceEntry(ctx, entry) {
98
+ const paths = entry.holder.paths
99
+ for (let k = 0; k < paths.length; k++) trace(ctx, transformPath(paths[k], entry.matrix))
100
+ }
101
+
102
+ /**
103
+ * A merge: intersect clips the first part by each of the rest; the other
104
+ * modes draw every part as one path, which is exactly `mm` 1 (merge) and
105
+ * close to add, and what lottie-web's canvas renderer does for them too.
106
+ *
107
+ * @param {CanvasRenderingContext2D} ctx
108
+ * @param {Entry} entry
109
+ * @param {any} style
110
+ */
111
+ function drawMerged(ctx, entry, style) {
112
+ const merge = /** @type {{ mode: number, parts: Entry[] }} */ (entry.holder.merge)
113
+ const parts = merge.parts.map((part) => ({ holder: part.holder, matrix: multiply(entry.matrix, part.matrix) }))
114
+
115
+ ctx.save()
116
+ if (merge.mode === 4) {
117
+ for (let n = 1; n < parts.length; n++) {
118
+ ctx.beginPath()
119
+ traceEntry(ctx, parts[n])
120
+ ctx.clip()
121
+ }
122
+ ctx.beginPath()
123
+ traceEntry(ctx, parts[0])
124
+ } else {
125
+ ctx.beginPath()
126
+ for (let n = 0; n < parts.length; n++) traceEntry(ctx, parts[n])
127
+ }
128
+ if (style.isStroke) ctx.stroke()
129
+ else ctx.fill(merge.mode === 5 || merge.mode === 3 ? 'evenodd' : style.rule)
130
+ ctx.restore()
131
+ }
132
+
133
+ /**
134
+ * @param {CanvasRenderingContext2D} ctx
135
+ * @param {any} style
136
+ * @param {any} fill
137
+ * @param {number} frame
138
+ */
139
+ function strokeStyle(ctx, style, fill, frame) {
140
+ ctx.strokeStyle = fill
141
+ ctx.lineWidth = scalar(style.width(frame))
142
+ ctx.lineCap = style.cap
143
+ ctx.lineJoin = style.join
144
+ ctx.miterLimit = style.miter
145
+
146
+ // older browsers have no line dashes; their strokes stay solid
147
+ if (!style.dashes.length || typeof ctx.setLineDash !== 'function') return
148
+ const lengths = []
149
+ let offset = 0
150
+ for (let n = 0; n < style.dashes.length; n++) {
151
+ const dash = style.dashes[n]
152
+ const value = scalar(dash.value(frame))
153
+ if (dash.kind === 'o') offset = value
154
+ else lengths.push(Math.max(0, value))
155
+ }
156
+ ctx.setLineDash(lengths)
157
+ ctx.lineDashOffset = offset
158
+ }
159
+
160
+ /**
161
+ * A Lottie colour, `[r, g, b]` or `[r, g, b, a]` in 0-1, as CSS.
162
+ *
163
+ * @param {number[]} rgba
164
+ * @returns {string}
165
+ */
166
+ export function color(rgba) {
167
+ const channel = (/** @type {number} */ value) => Math.round(Math.max(0, Math.min(1, value)) * 255)
168
+ const alpha = rgba.length > 3 ? Math.max(0, Math.min(1, rgba[3])) : 1
169
+ return 'rgba(' + channel(rgba[0]) + ',' + channel(rgba[1]) + ',' + channel(rgba[2]) + ',' + alpha + ')'
170
+ }
171
+
172
+ /**
173
+ * A linear or radial gradient in the paint's group space. A radial
174
+ * gradient's highlight moves its focal point, as lottie-web's does.
175
+ *
176
+ * @param {CanvasRenderingContext2D} ctx
177
+ * @param {any} spec
178
+ * @param {number} frame
179
+ */
180
+ function gradient(ctx, spec, frame) {
181
+ const s = spec.start(frame)
182
+ const e = spec.end(frame)
183
+ let canvasGradient
184
+ if (spec.type === 2) {
185
+ const dx = e[0] - s[0]
186
+ const dy = e[1] - s[1]
187
+ const radius = Math.sqrt(dx * dx + dy * dy)
188
+ const highlight = Math.max(-0.99, Math.min(0.99, scalar(spec.highlight(frame)) / 100))
189
+ const angle = Math.atan2(dy, dx) + (scalar(spec.angle(frame)) * Math.PI) / 180
190
+ const fx = s[0] + Math.cos(angle) * radius * highlight
191
+ const fy = s[1] + Math.sin(angle) * radius * highlight
192
+ canvasGradient = ctx.createRadialGradient(fx, fy, 0, s[0], s[1], radius)
193
+ } else {
194
+ canvasGradient = ctx.createLinearGradient(s[0], s[1], e[0], e[1])
195
+ }
196
+
197
+ const stops = colorStops(spec.stops(frame), spec.count)
198
+ for (let n = 0; n < stops.length; n++) canvasGradient.addColorStop(stops[n].offset, stops[n].color)
199
+ return canvasGradient
200
+ }
201
+
202
+ /**
203
+ * Lottie's gradient data: `count` colour stops `[offset, r, g, b]`, then
204
+ * optional opacity stops `[offset, alpha]`. Each colour stop takes the
205
+ * opacity interpolated at its offset.
206
+ *
207
+ * @param {number[]} data
208
+ * @param {number} count
209
+ * @returns {{ offset: number, color: string }[]}
210
+ */
211
+ export function colorStops(data, count) {
212
+ const alphas = []
213
+ for (let n = count * 4; n + 1 < data.length; n += 2) alphas.push([ data[n], data[n + 1] ])
214
+
215
+ const stops = []
216
+ for (let n = 0; n < count; n++) {
217
+ const offset = Math.max(0, Math.min(1, data[n * 4]))
218
+ const alpha = alphas.length ? alphaAt(alphas, offset) : 1
219
+ stops.push({ offset, color: color([ data[n * 4 + 1], data[n * 4 + 2], data[n * 4 + 3], alpha ]) })
220
+ }
221
+ return stops
222
+ }
223
+
224
+ /**
225
+ * @param {number[][]} alphas `[offset, alpha]`, in offset order
226
+ * @param {number} offset
227
+ */
228
+ function alphaAt(alphas, offset) {
229
+ if (offset <= alphas[0][0]) return alphas[0][1]
230
+ for (let n = 1; n < alphas.length; n++) {
231
+ if (offset <= alphas[n][0]) {
232
+ const [ a, va ] = alphas[n - 1]
233
+ const [ b, vb ] = alphas[n]
234
+ return b > a ? va + ((vb - va) * (offset - a)) / (b - a) : vb
235
+ }
236
+ }
237
+ return alphas[alphas.length - 1][1]
238
+ }