@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.
- package/LICENSE +201 -0
- package/audio/audio.cues.js +27 -0
- package/audio/audio.js +259 -0
- package/bands/bands.js +392 -0
- package/bundle/bundle.fonts.js +55 -0
- package/bundle/bundle.js +131 -0
- package/focus/focus.direction.js +46 -0
- package/focus/focus.dom.js +158 -0
- package/focus/focus.js +277 -0
- package/focus/focus.tree.js +146 -0
- package/hit/hit.geometry.js +113 -0
- package/hit/hit.js +129 -0
- package/list/list.flex.js +181 -0
- package/list/list.items.js +80 -0
- package/list/list.js +518 -0
- package/lottie/lottie.js +124 -0
- package/lottie/lottie.layers.js +416 -0
- package/lottie/lottie.paint.js +238 -0
- package/lottie/lottie.paths.js +474 -0
- package/lottie/lottie.props.js +265 -0
- package/lottie/lottie.shapes.js +259 -0
- package/mixer/mixer.js +307 -0
- package/mixer/mixer.mix.js +107 -0
- package/mixer/mixer.stage.js +431 -0
- package/motion/motion.convert.js +404 -0
- package/motion/motion.flow.js +421 -0
- package/motion/motion.keys.js +150 -0
- package/motion/motion.machine.js +382 -0
- package/motion/motion.nested.js +345 -0
- package/motion/motion.values.js +368 -0
- package/motion.js +1155 -0
- package/package.json +47 -4
- package/player/overlay/overlay.js +325 -0
- package/player/overlay/overlay.layout.js +208 -0
- package/player/player.fit.js +101 -0
- package/player/player.js +421 -0
- package/player/player.raster.js +127 -0
- package/pointer/pointer.js +292 -0
- package/readme.md +345 -0
- package/script/luau/lib/base/base.js +202 -0
- package/script/luau/lib/base/base.table.js +189 -0
- package/script/luau/lib/lib.args.js +152 -0
- package/script/luau/lib/lib.js +23 -0
- package/script/luau/lib/math/math.bit32.js +144 -0
- package/script/luau/lib/math/math.js +288 -0
- package/script/luau/lib/math/math.vector.js +125 -0
- package/script/luau/lib/string/string.format.js +143 -0
- package/script/luau/lib/string/string.js +160 -0
- package/script/luau/lib/string/string.pattern.js +505 -0
- package/script/luau/load/load.js +362 -0
- package/script/luau/load/load.ops.js +79 -0
- package/script/luau/luau.convert.js +259 -0
- package/script/luau/luau.js +132 -0
- package/script/luau/luau.md +111 -0
- package/script/luau/vm/vm.js +167 -0
- package/script/luau/vm/vm.meta.js +401 -0
- package/script/luau/vm/vm.num.js +236 -0
- package/script/luau/vm/vm.run.js +842 -0
- package/script/luau/vm/vm.value.js +406 -0
- package/script/play/play.js +199 -0
- package/script/play/play.objects.js +295 -0
- package/script/play/play.scope.js +91 -0
- package/script/rive/api/api.statics.js +31 -0
- package/script/rive/api/api.type.js +188 -0
- package/script/rive/api/artboard/artboard.js +89 -0
- package/script/rive/api/assets/assets.js +72 -0
- package/script/rive/api/data/data.value.js +99 -0
- package/script/rive/api/draw/draw.contours.js +163 -0
- package/script/rive/api/draw/draw.measure.js +196 -0
- package/script/rive/api/draw/draw.paint.js +235 -0
- package/script/rive/api/draw/draw.path.js +153 -0
- package/script/rive/api/draw/draw.renderer.js +117 -0
- package/script/rive/api/math/math.color.js +96 -0
- package/script/rive/api/math/math.mat2d.js +180 -0
- package/script/rive/api/math/math.vector.js +121 -0
- package/script/rive/api/media/media.audio.js +113 -0
- package/script/rive/api/media/media.draw.js +73 -0
- package/script/rive/api/media/media.image.js +61 -0
- package/script/rive/api/media/media.mesh.js +50 -0
- package/script/rive/api/pointer/pointer.event.js +64 -0
- package/script/rive/api/pointer/pointer.route.js +69 -0
- package/script/rive/api/text/text.font.js +129 -0
- package/script/rive/api/text/text.js +278 -0
- package/script/rive/api/text/text.layout.js +259 -0
- package/script/rive/context/context.js +76 -0
- package/script/rive/context/context.memory.js +196 -0
- package/script/rive/context/context.viewmodel.js +302 -0
- package/script/rive/context/lists/lists.js +160 -0
- package/script/rive/context/lists/lists.values.js +281 -0
- package/script/rive/host/host.globals.js +44 -0
- package/script/rive/host/host.js +533 -0
- package/script/rive/host/host.node.js +45 -0
- package/script/rive/rive.js +23 -0
- package/script/rive/rive.md +180 -0
- package/script/script.js +11 -0
- package/skin/skin.deform.js +514 -0
- package/skin/skin.ik.js +260 -0
- package/skin/skin.js +297 -0
- 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
|
+
}
|