@babylonjs/lottie-player 9.2.0 → 9.2.2

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.
@@ -1,16 +1,18 @@
1
1
  import "@babylonjs/core/Engines/Extensions/engine.dynamicTexture.js";
2
2
  import { ThinTexture } from "@babylonjs/core/Materials/Textures/thinTexture.js";
3
+ import { GetInitialScalarValue, GetInitialVectorValues, GetInitialBezierData } from "./rawPropertyHelpers.js";
3
4
  import { GetShapesBoundingBox, GetTextBoundingBox } from "../maths/boundingBox.js";
4
5
  /**
5
6
  * SpritePacker is a class that handles the packing of sprites into a texture atlas.
7
+ * If sprites exceed the capacity of a single atlas texture, additional atlas pages are created.
6
8
  */
7
9
  export class SpritePacker {
8
10
  /**
9
- * Gets the texture atlas that contains all the sprites packed by this SpritePacker.
10
- * @returns The texture atlas containing the sprites.
11
+ * Gets the textures for all atlas pages.
12
+ * @returns An array of textures, one per atlas page.
11
13
  */
12
- get texture() {
13
- return this._spritesTexture;
14
+ get textures() {
15
+ return this._pages.map((p) => p.texture);
14
16
  }
15
17
  /**
16
18
  * Sets the fonts that will be used to render text in the sprite atlas.
@@ -29,28 +31,11 @@ export class SpritePacker {
29
31
  */
30
32
  constructor(engine, isHtmlCanvas, atlasScale, variables, configuration) {
31
33
  this._engine = engine;
34
+ this._isHtmlCanvas = isHtmlCanvas;
32
35
  this._atlasScale = atlasScale;
33
36
  this._variables = variables;
34
37
  this._configuration = configuration;
35
- this._isDirty = false;
36
- this._currentX = this._configuration.gapSize;
37
- this._currentY = this._configuration.gapSize;
38
- this._maxRowHeight = 0;
39
- if (isHtmlCanvas) {
40
- this._spritesCanvas = document.createElement("canvas");
41
- this._spritesCanvas.width = this._configuration.spriteAtlasWidth;
42
- this._spritesCanvas.height = this._configuration.spriteAtlasHeight;
43
- this._spritesCanvasContext = this._spritesCanvas.getContext("2d");
44
- }
45
- else {
46
- this._spritesCanvas = new OffscreenCanvas(this._configuration.spriteAtlasWidth, this._configuration.spriteAtlasHeight);
47
- this._spritesCanvasContext = this._spritesCanvas.getContext("2d");
48
- }
49
- this._spritesInternalTexture = this._engine.createDynamicTexture(this._configuration.spriteAtlasWidth, this._configuration.spriteAtlasHeight, false, 2); // Linear filtering
50
- this._engine.updateDynamicTexture(this._spritesInternalTexture, this._spritesCanvas, false);
51
- this._spritesTexture = new ThinTexture(this._spritesInternalTexture);
52
- this._spritesTexture.wrapU = 0; // Disable wrapping
53
- this._spritesTexture.wrapV = 0; // Disable wrapping
38
+ this._pages = [this._createPage()];
54
39
  this._spriteAtlasInfo = {
55
40
  uOffset: 0,
56
41
  vOffset: 0,
@@ -60,6 +45,7 @@ export class SpritePacker {
60
45
  heightPx: 0,
61
46
  centerX: 0,
62
47
  centerY: 0,
48
+ atlasIndex: 0,
63
49
  };
64
50
  }
65
51
  /**
@@ -74,28 +60,25 @@ export class SpritePacker {
74
60
  scalingFactor.y = scalingFactor.y * this._atlasScale * this._configuration.devicePixelRatio;
75
61
  // Calculate the size of the sprite in the atlas in pixels
76
62
  // This takes into account the scaling factor so in the call to _drawVectorShape the canvas will be scaled when rendering
77
- this._spriteAtlasInfo.cellWidth = boundingBox.width * scalingFactor.x;
78
- this._spriteAtlasInfo.cellHeight = boundingBox.height * scalingFactor.y;
79
- // Check if the sprite fits in the current row
80
- if (this._currentX + this._spriteAtlasInfo.cellWidth > this._configuration.spriteAtlasWidth) {
81
- // Add a gap between sprites to avoid bleeding issues
82
- this._currentX = this._configuration.gapSize;
83
- this._currentY += this._maxRowHeight + this._configuration.gapSize;
84
- this._maxRowHeight = 0;
85
- }
63
+ this._spriteAtlasInfo.cellWidth = this._getAtlasCellDimension(boundingBox.width * scalingFactor.x);
64
+ this._spriteAtlasInfo.cellHeight = this._getAtlasCellDimension(boundingBox.height * scalingFactor.y);
65
+ // Get (or create) the page that has room for this sprite
66
+ const page = this._getPageWithRoom(this._spriteAtlasInfo.cellWidth, this._spriteAtlasInfo.cellHeight);
86
67
  // Draw the shape in the canvas
87
- this._drawVectorShape(rawElements, boundingBox, scalingFactor);
88
- this._isDirty = true;
68
+ this._drawVectorShape(rawElements, boundingBox, scalingFactor, page);
69
+ this._extrudeSpriteEdges(page, page.currentX, page.currentY, this._spriteAtlasInfo.cellWidth, this._spriteAtlasInfo.cellHeight);
70
+ page.isDirty = true;
89
71
  // Get the rest of the sprite information required to render the shape
90
- this._spriteAtlasInfo.uOffset = this._currentX / this._configuration.spriteAtlasWidth;
91
- this._spriteAtlasInfo.vOffset = this._currentY / this._configuration.spriteAtlasHeight;
72
+ this._spriteAtlasInfo.uOffset = page.currentX / this._configuration.spriteAtlasWidth;
73
+ this._spriteAtlasInfo.vOffset = page.currentY / this._configuration.spriteAtlasHeight;
92
74
  this._spriteAtlasInfo.widthPx = boundingBox.width;
93
75
  this._spriteAtlasInfo.heightPx = boundingBox.height;
94
76
  this._spriteAtlasInfo.centerX = boundingBox.offsetX;
95
77
  this._spriteAtlasInfo.centerY = boundingBox.offsetY;
78
+ this._spriteAtlasInfo.atlasIndex = this._pages.indexOf(page);
96
79
  // Advance the current position for the next sprite
97
- this._currentX += this._spriteAtlasInfo.cellWidth + this._configuration.gapSize; // Add a gap between sprites to avoid bleeding
98
- this._maxRowHeight = Math.max(this._maxRowHeight, this._spriteAtlasInfo.cellHeight);
80
+ page.currentX += this._spriteAtlasInfo.cellWidth + this._configuration.gapSize; // Add a gap between sprites to avoid bleeding
81
+ page.maxRowHeight = Math.max(page.maxRowHeight, this._spriteAtlasInfo.cellHeight);
99
82
  return this._spriteAtlasInfo;
100
83
  }
101
84
  /**
@@ -109,7 +92,7 @@ export class SpritePacker {
109
92
  return undefined;
110
93
  }
111
94
  // If the text information is malformed and we can't get the bounding box, then just return
112
- const boundingBox = GetTextBoundingBox(this._spritesCanvasContext, textData, this._rawFonts, this._variables);
95
+ const boundingBox = GetTextBoundingBox(this._pages[this._pages.length - 1].context, textData, this._rawFonts, this._variables);
113
96
  if (boundingBox === undefined) {
114
97
  return undefined;
115
98
  }
@@ -117,106 +100,234 @@ export class SpritePacker {
117
100
  scalingFactor.y = scalingFactor.y * this._atlasScale * this._configuration.devicePixelRatio;
118
101
  // Calculate the size of the sprite in the atlas in pixels
119
102
  // This takes into account the scaling factor so in the call to _drawText the canvas will be scaled when rendering
120
- this._spriteAtlasInfo.cellWidth = boundingBox.width * scalingFactor.x;
121
- this._spriteAtlasInfo.cellHeight = boundingBox.height * scalingFactor.y;
122
- // Find the position to draw the text
123
- // If the text doesn't fit in the current row, move to the next row
124
- if (this._currentX + this._spriteAtlasInfo.cellWidth > this._configuration.spriteAtlasWidth) {
125
- // Add a gap between sprites to avoid bleeding issues
126
- this._currentX = this._configuration.gapSize;
127
- this._currentY += this._maxRowHeight + this._configuration.gapSize;
128
- this._maxRowHeight = 0;
129
- }
103
+ this._spriteAtlasInfo.cellWidth = this._getAtlasCellDimension(boundingBox.width * scalingFactor.x);
104
+ this._spriteAtlasInfo.cellHeight = this._getAtlasCellDimension(boundingBox.height * scalingFactor.y);
105
+ // Get (or create) the page that has room for this sprite
106
+ const page = this._getPageWithRoom(this._spriteAtlasInfo.cellWidth, this._spriteAtlasInfo.cellHeight);
130
107
  // Draw the text in the canvas
131
- this._drawText(textData, boundingBox, scalingFactor);
132
- this._isDirty = true;
108
+ this._drawText(textData, boundingBox, scalingFactor, page);
109
+ this._extrudeSpriteEdges(page, page.currentX, page.currentY, this._spriteAtlasInfo.cellWidth, this._spriteAtlasInfo.cellHeight);
110
+ page.isDirty = true;
133
111
  // Get the rest of the sprite information required to render the text
134
- this._spriteAtlasInfo.uOffset = this._currentX / this._configuration.spriteAtlasWidth;
135
- this._spriteAtlasInfo.vOffset = this._currentY / this._configuration.spriteAtlasHeight;
112
+ this._spriteAtlasInfo.uOffset = page.currentX / this._configuration.spriteAtlasWidth;
113
+ this._spriteAtlasInfo.vOffset = page.currentY / this._configuration.spriteAtlasHeight;
136
114
  this._spriteAtlasInfo.widthPx = boundingBox.width;
137
115
  this._spriteAtlasInfo.heightPx = boundingBox.height;
138
116
  this._spriteAtlasInfo.centerX = boundingBox.offsetX;
139
117
  this._spriteAtlasInfo.centerY = boundingBox.offsetY;
118
+ this._spriteAtlasInfo.atlasIndex = this._pages.indexOf(page);
140
119
  // Advance the current position for the next sprite
141
- this._currentX += this._spriteAtlasInfo.cellWidth + this._configuration.gapSize; // Add a gap between sprites to avoid bleeding
142
- this._maxRowHeight = Math.max(this._maxRowHeight, this._spriteAtlasInfo.cellHeight);
120
+ page.currentX += this._spriteAtlasInfo.cellWidth + this._configuration.gapSize; // Add a gap between sprites to avoid bleeding
121
+ page.maxRowHeight = Math.max(page.maxRowHeight, this._spriteAtlasInfo.cellHeight);
143
122
  return this._spriteAtlasInfo;
144
123
  }
145
124
  /**
146
- * Updates the internal atlas texture with the information that has been added to the SpritePacker.
125
+ * Updates all dirty atlas page textures with the latest canvas content.
147
126
  */
148
127
  updateAtlasTexture() {
149
- if (!this._isDirty) {
150
- return; // No need to update if nothing has changed
128
+ for (const page of this._pages) {
129
+ if (!page.isDirty) {
130
+ continue;
131
+ }
132
+ this._engine.updateDynamicTexture(page.internalTexture, page.canvas, false);
133
+ page.isDirty = false;
151
134
  }
152
- // Update the internal texture with the new canvas content
153
- this._engine.updateDynamicTexture(this._spritesInternalTexture, this._spritesCanvas, false);
154
- this._isDirty = false;
155
135
  }
156
136
  /**
157
- * Releases the canvas and its context to allow garbage collection.
137
+ * Releases the canvases and their contexts to allow garbage collection.
158
138
  */
159
139
  releaseCanvas() {
160
- this._spritesCanvasContext = undefined; // Clear the context to allow garbage collection
161
- this._spritesCanvas = undefined; // Clear the canvas to allow garbage collection
140
+ for (const page of this._pages) {
141
+ page.context = undefined;
142
+ page.canvas = undefined;
143
+ }
144
+ }
145
+ _createPage() {
146
+ let canvas;
147
+ let context;
148
+ if (this._isHtmlCanvas) {
149
+ canvas = document.createElement("canvas");
150
+ canvas.width = this._configuration.spriteAtlasWidth;
151
+ canvas.height = this._configuration.spriteAtlasHeight;
152
+ context = canvas.getContext("2d");
153
+ }
154
+ else {
155
+ canvas = new OffscreenCanvas(this._configuration.spriteAtlasWidth, this._configuration.spriteAtlasHeight);
156
+ context = canvas.getContext("2d");
157
+ }
158
+ const internalTexture = this._engine.createDynamicTexture(this._configuration.spriteAtlasWidth, this._configuration.spriteAtlasHeight, false, 2);
159
+ this._engine.updateDynamicTexture(internalTexture, canvas, false);
160
+ const texture = new ThinTexture(internalTexture);
161
+ texture.wrapU = 0;
162
+ texture.wrapV = 0;
163
+ return {
164
+ canvas,
165
+ context,
166
+ internalTexture,
167
+ texture,
168
+ isDirty: false,
169
+ currentX: this._configuration.gapSize,
170
+ currentY: this._configuration.gapSize,
171
+ maxRowHeight: 0,
172
+ };
173
+ }
174
+ /**
175
+ * Returns a page with room for a sprite of the given size. Wraps to the next row if needed,
176
+ * and creates a new page if the current page is full.
177
+ * @param cellWidth The width of the sprite cell in pixels.
178
+ * @param cellHeight The height of the sprite cell in pixels.
179
+ * @returns An atlas page with enough room for the sprite.
180
+ */
181
+ _getPageWithRoom(cellWidth, cellHeight) {
182
+ let page = this._pages[this._pages.length - 1];
183
+ // Clamp oversized cells to fit within a single atlas page
184
+ const maxCellWidth = this._configuration.spriteAtlasWidth - 2 * this._configuration.gapSize;
185
+ const maxCellHeight = this._configuration.spriteAtlasHeight - 2 * this._configuration.gapSize;
186
+ if (cellWidth > maxCellWidth || cellHeight > maxCellHeight) {
187
+ // eslint-disable-next-line no-console
188
+ console.warn(`[SpritePacker] Sprite cell (${cellWidth}x${cellHeight}) exceeds atlas page (${this._configuration.spriteAtlasWidth}x${this._configuration.spriteAtlasHeight}). Clamping to fit.`);
189
+ this._spriteAtlasInfo.cellWidth = Math.min(cellWidth, maxCellWidth);
190
+ this._spriteAtlasInfo.cellHeight = Math.min(cellHeight, maxCellHeight);
191
+ cellWidth = this._spriteAtlasInfo.cellWidth;
192
+ cellHeight = this._spriteAtlasInfo.cellHeight;
193
+ }
194
+ // Check if the sprite fits in the current row
195
+ if (page.currentX + cellWidth > this._configuration.spriteAtlasWidth) {
196
+ // Move to the next row
197
+ page.currentX = this._configuration.gapSize;
198
+ page.currentY += page.maxRowHeight + this._configuration.gapSize;
199
+ page.maxRowHeight = 0;
200
+ }
201
+ // Check if the sprite fits vertically on this page
202
+ if (page.currentY + cellHeight > this._configuration.spriteAtlasHeight) {
203
+ // Current page is full — create a new one
204
+ page = this._createPage();
205
+ this._pages.push(page);
206
+ }
207
+ return page;
208
+ }
209
+ _getAtlasCellDimension(size) {
210
+ return Math.max(1, Math.ceil(size));
162
211
  }
163
- _drawVectorShape(rawElements, boundingBox, scalingFactor) {
164
- this._spritesCanvasContext.save();
165
- this._spritesCanvasContext.globalCompositeOperation = "destination-over";
166
- this._spritesCanvasContext.translate(this._currentX + Math.ceil(boundingBox.strokeInset / 2), this._currentY + Math.ceil(boundingBox.strokeInset / 2));
167
- this._spritesCanvasContext.scale(scalingFactor.x, scalingFactor.y);
168
- this._spritesCanvasContext.beginPath();
212
+ _extrudeSpriteEdges(page, x, y, width, height) {
213
+ const padding = Math.min(2, Math.floor(this._configuration.gapSize / 2));
214
+ const pixelX = Math.floor(x);
215
+ const pixelY = Math.floor(y);
216
+ const pixelWidth = Math.ceil(width);
217
+ const pixelHeight = Math.ceil(height);
218
+ if (padding <= 0 || pixelWidth <= 0 || pixelHeight <= 0) {
219
+ return;
220
+ }
221
+ for (let offset = 1; offset <= padding; offset++) {
222
+ // Left edge
223
+ if (pixelX - offset >= 0) {
224
+ page.context.drawImage(page.canvas, pixelX, pixelY, 1, pixelHeight, pixelX - offset, pixelY, 1, pixelHeight);
225
+ }
226
+ // Right edge
227
+ if (pixelX + pixelWidth - 1 + offset < this._configuration.spriteAtlasWidth) {
228
+ page.context.drawImage(page.canvas, pixelX + pixelWidth - 1, pixelY, 1, pixelHeight, pixelX + pixelWidth - 1 + offset, pixelY, 1, pixelHeight);
229
+ }
230
+ // Top edge
231
+ if (pixelY - offset >= 0) {
232
+ page.context.drawImage(page.canvas, pixelX, pixelY, pixelWidth, 1, pixelX, pixelY - offset, pixelWidth, 1);
233
+ }
234
+ // Bottom edge
235
+ if (pixelY + pixelHeight - 1 + offset < this._configuration.spriteAtlasHeight) {
236
+ page.context.drawImage(page.canvas, pixelX, pixelY + pixelHeight - 1, pixelWidth, 1, pixelX, pixelY + pixelHeight - 1 + offset, pixelWidth, 1);
237
+ }
238
+ // Top-left corner
239
+ if (pixelX - offset >= 0 && pixelY - offset >= 0) {
240
+ page.context.drawImage(page.canvas, pixelX, pixelY, 1, 1, pixelX - offset, pixelY - offset, 1, 1);
241
+ }
242
+ // Top-right corner
243
+ if (pixelX + pixelWidth - 1 + offset < this._configuration.spriteAtlasWidth && pixelY - offset >= 0) {
244
+ page.context.drawImage(page.canvas, pixelX + pixelWidth - 1, pixelY, 1, 1, pixelX + pixelWidth - 1 + offset, pixelY - offset, 1, 1);
245
+ }
246
+ // Bottom-left corner
247
+ if (pixelX - offset >= 0 && pixelY + pixelHeight - 1 + offset < this._configuration.spriteAtlasHeight) {
248
+ page.context.drawImage(page.canvas, pixelX, pixelY + pixelHeight - 1, 1, 1, pixelX - offset, pixelY + pixelHeight - 1 + offset, 1, 1);
249
+ }
250
+ // Bottom-right corner
251
+ if (pixelX + pixelWidth - 1 + offset < this._configuration.spriteAtlasWidth && pixelY + pixelHeight - 1 + offset < this._configuration.spriteAtlasHeight) {
252
+ page.context.drawImage(page.canvas, pixelX + pixelWidth - 1, pixelY + pixelHeight - 1, 1, 1, pixelX + pixelWidth - 1 + offset, pixelY + pixelHeight - 1 + offset, 1, 1);
253
+ }
254
+ }
255
+ }
256
+ _drawVectorShape(rawElements, boundingBox, scalingFactor, page) {
257
+ page.context.save();
258
+ page.context.globalCompositeOperation = "destination-over";
259
+ page.context.translate(page.currentX + Math.ceil(boundingBox.strokeInset / 2), page.currentY + Math.ceil(boundingBox.strokeInset / 2));
260
+ page.context.scale(scalingFactor.x, scalingFactor.y);
261
+ page.context.beginPath();
262
+ page.context.rect(0, 0, boundingBox.width, boundingBox.height);
263
+ page.context.clip();
264
+ page.context.beginPath();
169
265
  for (let i = 0; i < rawElements.length; i++) {
170
266
  const shape = rawElements[i];
171
267
  switch (shape.ty) {
172
268
  case "rc":
173
- this._drawRectangle(shape, boundingBox);
269
+ this._drawRectangle(shape, boundingBox, page.context);
270
+ break;
271
+ case "el":
272
+ this._drawEllipse(shape, boundingBox, page.context);
174
273
  break;
175
274
  case "sh":
176
- this._drawPath(shape, boundingBox);
275
+ this._drawPath(shape, boundingBox, page.context);
177
276
  break;
178
277
  case "fl":
179
- this._drawFill(shape);
278
+ this._drawFill(shape, page.context);
180
279
  break;
181
280
  case "st":
182
- this._drawStroke(shape);
281
+ this._drawStroke(shape, page.context);
183
282
  break;
184
283
  case "gf":
185
- this._drawGradientFill(shape, boundingBox);
284
+ this._drawGradientFill(shape, boundingBox, page.context);
186
285
  break;
187
286
  case "tr":
188
287
  break; // Nothing needed with transforms
189
288
  }
190
289
  }
191
- this._spritesCanvasContext.restore();
290
+ page.context.restore();
192
291
  }
193
- // This function assumes that GetTextBoundingBox has already been called as to measure the text
194
- // we need to setup the canvas context with the correct font and styles, so we don't set them up here
195
- // again, but we still need to make sure to restore the context when we are done
196
- _drawText(textData, boundingBox, scalingFactor) {
292
+ _drawText(textData, boundingBox, scalingFactor, page) {
197
293
  if (this._rawFonts === undefined) {
198
- this._spritesCanvasContext.restore();
199
294
  return;
200
295
  }
201
- this._spritesCanvasContext.translate(this._currentX, this._currentY);
202
- this._spritesCanvasContext.scale(scalingFactor.x, scalingFactor.y);
203
296
  const textInfo = textData.d.k[0].s;
297
+ const fontFamily = textInfo.f;
298
+ const finalFont = this._rawFonts.get(fontFamily);
299
+ if (!finalFont) {
300
+ return;
301
+ }
302
+ page.context.save();
303
+ page.context.translate(page.currentX, page.currentY);
304
+ page.context.scale(scalingFactor.x, scalingFactor.y);
305
+ // Set up font (same setup as GetTextBoundingBox for measurement consistency)
306
+ const weight = finalFont.fWeight || "400";
307
+ page.context.font = `${weight} ${textInfo.s}px ${finalFont.fFamily}`;
308
+ if (textInfo.sc !== undefined && textInfo.sc.length >= 3 && textInfo.sw !== undefined && textInfo.sw > 0) {
309
+ page.context.lineWidth = textInfo.sw;
310
+ }
311
+ // Clip to cell bounds to prevent text overdraw into adjacent cells
312
+ page.context.beginPath();
313
+ page.context.rect(0, 0, boundingBox.width, boundingBox.height);
314
+ page.context.clip();
204
315
  if (textInfo.fc !== undefined && textInfo.fc.length >= 3) {
205
316
  const rawFillStyle = textInfo.fc;
206
317
  if (Array.isArray(rawFillStyle)) {
207
318
  // If the fill style is an array, we assume it's a color array
208
- this._spritesCanvasContext.fillStyle = this._lottieColorToCSSColor(rawFillStyle, 1);
319
+ page.context.fillStyle = this._lottieColorToCSSColor(rawFillStyle, 1);
209
320
  }
210
321
  else {
211
322
  // If it's a string, we need to get the value from the variables map
212
323
  const variableFillStyle = this._variables.get(rawFillStyle);
213
324
  if (variableFillStyle !== undefined) {
214
- this._spritesCanvasContext.fillStyle = variableFillStyle;
325
+ page.context.fillStyle = variableFillStyle;
215
326
  }
216
327
  }
217
328
  }
218
329
  if (textInfo.sc !== undefined && textInfo.sc.length >= 3 && textInfo.sw !== undefined && textInfo.sw > 0) {
219
- this._spritesCanvasContext.strokeStyle = this._lottieColorToCSSColor(textInfo.sc, 1);
330
+ page.context.strokeStyle = this._lottieColorToCSSColor(textInfo.sc, 1);
220
331
  }
221
332
  // Text is supported as a possible variable (for localization for example)
222
333
  // Check if the text is a variable and replace it if it is
@@ -225,43 +336,56 @@ export class SpritePacker {
225
336
  if (variableText !== undefined) {
226
337
  text = variableText;
227
338
  }
228
- this._spritesCanvasContext.fillText(text, 0, boundingBox.actualBoundingBoxAscent);
339
+ page.context.fillText(text, 0, boundingBox.actualBoundingBoxAscent);
229
340
  if (textInfo.sc !== undefined && textInfo.sc.length >= 3 && textInfo.sw !== undefined && textInfo.sw > 0 && textInfo.of === true) {
230
- this._spritesCanvasContext.strokeText(text, 0, boundingBox.actualBoundingBoxAscent);
341
+ page.context.strokeText(text, 0, boundingBox.actualBoundingBoxAscent);
231
342
  }
232
- this._spritesCanvasContext.restore();
343
+ page.context.restore();
233
344
  }
234
- _drawRectangle(shape, boundingBox) {
235
- const size = shape.s.k;
236
- const position = shape.p.k;
237
- const radius = shape.r.k;
345
+ _drawRectangle(shape, boundingBox, ctx) {
346
+ const size = GetInitialVectorValues(shape.s);
347
+ const position = GetInitialVectorValues(shape.p);
348
+ const radius = GetInitialScalarValue(shape.r);
238
349
  // Translate to the correct position within the atlas cell, same as paths use centerX/centerY
239
350
  const x = position[0] - size[0] / 2 + boundingBox.centerX - Math.ceil(boundingBox.strokeInset);
240
351
  const y = position[1] - size[1] / 2 + boundingBox.centerY - Math.ceil(boundingBox.strokeInset);
241
352
  if (radius <= 0) {
242
- this._spritesCanvasContext.rect(x, y, size[0], size[1]);
353
+ ctx.rect(x, y, size[0], size[1]);
243
354
  }
244
355
  else {
245
- this._spritesCanvasContext.roundRect(x, y, size[0], size[1], radius);
356
+ ctx.roundRect(x, y, size[0], size[1], radius);
246
357
  }
247
358
  }
248
- _drawPath(shape, boundingBox) {
359
+ _drawEllipse(shape, boundingBox, ctx) {
360
+ const size = GetInitialVectorValues(shape.s);
361
+ const position = GetInitialVectorValues(shape.p);
362
+ const centerX = position[0] + boundingBox.centerX - Math.ceil(boundingBox.strokeInset);
363
+ const centerY = position[1] + boundingBox.centerY - Math.ceil(boundingBox.strokeInset);
364
+ const radiusX = size[0] / 2;
365
+ const radiusY = size[1] / 2;
366
+ ctx.moveTo(centerX + radiusX, centerY);
367
+ ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
368
+ }
369
+ _drawPath(shape, boundingBox, ctx) {
249
370
  // The path data has to be translated to the center of the bounding box
250
371
  // If the paths have stroke, we need to account for the stroke width
251
- const pathData = shape.ks.k;
372
+ const pathData = GetInitialBezierData(shape.ks);
373
+ if (!pathData) {
374
+ return;
375
+ }
252
376
  const xTranslate = boundingBox.centerX - Math.ceil(boundingBox.strokeInset);
253
377
  const yTranslate = boundingBox.centerY - Math.ceil(boundingBox.strokeInset);
254
378
  const vertices = pathData.v;
255
379
  const inTangents = pathData.i;
256
380
  const outTangents = pathData.o;
257
381
  if (vertices.length > 0) {
258
- this._spritesCanvasContext.moveTo(vertices[0][0] + xTranslate, vertices[0][1] + yTranslate);
382
+ ctx.moveTo(vertices[0][0] + xTranslate, vertices[0][1] + yTranslate);
259
383
  for (let i = 0; i < vertices.length - 1; i++) {
260
384
  const start = vertices[i];
261
385
  const end = vertices[i + 1];
262
386
  const outTangent = outTangents[i];
263
387
  const inTangent = inTangents[i + 1];
264
- this._spritesCanvasContext.bezierCurveTo(start[0] + xTranslate + outTangent[0], start[1] + yTranslate + outTangent[1], end[0] + xTranslate + inTangent[0], end[1] + yTranslate + inTangent[1], end[0] + xTranslate, end[1] + yTranslate);
388
+ ctx.bezierCurveTo(start[0] + xTranslate + outTangent[0], start[1] + yTranslate + outTangent[1], end[0] + xTranslate + inTangent[0], end[1] + yTranslate + inTangent[1], end[0] + xTranslate, end[1] + yTranslate);
265
389
  }
266
390
  if (pathData.c) {
267
391
  // Close path with curve from last to first point
@@ -269,34 +393,34 @@ export class SpritePacker {
269
393
  const end = vertices[0];
270
394
  const outTangent = outTangents[vertices.length - 1];
271
395
  const inTangent = inTangents[0];
272
- this._spritesCanvasContext.bezierCurveTo(start[0] + xTranslate + outTangent[0], start[1] + yTranslate + outTangent[1], end[0] + xTranslate + inTangent[0], end[1] + yTranslate + inTangent[1], end[0] + xTranslate, end[1] + yTranslate);
273
- this._spritesCanvasContext.closePath();
396
+ ctx.bezierCurveTo(start[0] + xTranslate + outTangent[0], start[1] + yTranslate + outTangent[1], end[0] + xTranslate + inTangent[0], end[1] + yTranslate + inTangent[1], end[0] + xTranslate, end[1] + yTranslate);
397
+ ctx.closePath();
274
398
  }
275
399
  }
276
400
  }
277
- _drawFill(fill) {
401
+ _drawFill(fill, ctx) {
278
402
  const color = this._lottieColorToCSSColor(fill.c.k, fill.o.k / 100);
279
- this._spritesCanvasContext.fillStyle = color;
280
- this._spritesCanvasContext.fill();
403
+ ctx.fillStyle = color;
404
+ ctx.fill();
281
405
  }
282
- _drawStroke(stroke) {
406
+ _drawStroke(stroke, ctx) {
283
407
  // Color and opacity
284
408
  const opacity = stroke.o?.k ?? 100;
285
409
  const color = this._lottieColorToCSSColor(stroke.c?.k ?? [0, 0, 0], opacity / 100);
286
- this._spritesCanvasContext.strokeStyle = color;
410
+ ctx.strokeStyle = color;
287
411
  // Width
288
412
  const width = stroke.w?.k ?? 1;
289
- this._spritesCanvasContext.lineWidth = width;
413
+ ctx.lineWidth = width;
290
414
  // Line cap
291
415
  switch (stroke.lc) {
292
416
  case 1:
293
- this._spritesCanvasContext.lineCap = "butt";
417
+ ctx.lineCap = "butt";
294
418
  break;
295
419
  case 2:
296
- this._spritesCanvasContext.lineCap = "round";
420
+ ctx.lineCap = "round";
297
421
  break;
298
422
  case 3:
299
- this._spritesCanvasContext.lineCap = "square";
423
+ ctx.lineCap = "square";
300
424
  break;
301
425
  default:
302
426
  // leave default
@@ -305,13 +429,13 @@ export class SpritePacker {
305
429
  // Line join
306
430
  switch (stroke.lj) {
307
431
  case 1:
308
- this._spritesCanvasContext.lineJoin = "miter";
432
+ ctx.lineJoin = "miter";
309
433
  break;
310
434
  case 2:
311
- this._spritesCanvasContext.lineJoin = "round";
435
+ ctx.lineJoin = "round";
312
436
  break;
313
437
  case 3:
314
- this._spritesCanvasContext.lineJoin = "bevel";
438
+ ctx.lineJoin = "bevel";
315
439
  break;
316
440
  default:
317
441
  // leave default
@@ -319,7 +443,7 @@ export class SpritePacker {
319
443
  }
320
444
  // Miter limit
321
445
  if (stroke.ml !== undefined) {
322
- this._spritesCanvasContext.miterLimit = stroke.ml;
446
+ ctx.miterLimit = stroke.ml;
323
447
  }
324
448
  // Dash pattern
325
449
  const dashes = stroke.d;
@@ -330,35 +454,35 @@ export class SpritePacker {
330
454
  lineDashes.push(dashes[i].v.k);
331
455
  }
332
456
  }
333
- this._spritesCanvasContext.setLineDash(lineDashes);
457
+ ctx.setLineDash(lineDashes);
334
458
  }
335
- this._spritesCanvasContext.stroke();
459
+ ctx.stroke();
336
460
  }
337
- _drawGradientFill(fill, boundingBox) {
461
+ _drawGradientFill(fill, boundingBox, ctx) {
338
462
  switch (fill.t) {
339
463
  case 1: {
340
- this._drawLinearGradientFill(fill, boundingBox);
464
+ this._drawLinearGradientFill(fill, boundingBox, ctx);
341
465
  break;
342
466
  }
343
467
  case 2: {
344
- this._drawRadialGradientFill(fill, boundingBox);
468
+ this._drawRadialGradientFill(fill, boundingBox, ctx);
345
469
  break;
346
470
  }
347
471
  }
348
472
  }
349
- _drawLinearGradientFill(fill, boundingBox) {
473
+ _drawLinearGradientFill(fill, boundingBox, ctx) {
350
474
  // We need to translate the gradient to the center of the bounding box
351
475
  const xTranslate = boundingBox.centerX;
352
476
  const yTranslate = boundingBox.centerY;
353
477
  // Create the gradient
354
478
  const startPoint = fill.s.k;
355
479
  const endPoint = fill.e.k;
356
- const gradient = this._spritesCanvasContext.createLinearGradient(startPoint[0] + xTranslate, startPoint[1] + yTranslate, endPoint[0] + xTranslate, endPoint[1] + yTranslate);
480
+ const gradient = ctx.createLinearGradient(startPoint[0] + xTranslate, startPoint[1] + yTranslate, endPoint[0] + xTranslate, endPoint[1] + yTranslate);
357
481
  this._addColorStops(gradient, fill);
358
- this._spritesCanvasContext.fillStyle = gradient;
359
- this._spritesCanvasContext.fill();
482
+ ctx.fillStyle = gradient;
483
+ ctx.fill();
360
484
  }
361
- _drawRadialGradientFill(fill, boundingBox) {
485
+ _drawRadialGradientFill(fill, boundingBox, ctx) {
362
486
  // We need to translate the gradient to the center of the bounding box
363
487
  const xTranslate = boundingBox.centerX;
364
488
  const yTranslate = boundingBox.centerY;
@@ -368,10 +492,10 @@ export class SpritePacker {
368
492
  const centerX = startPoint[0] + xTranslate;
369
493
  const centerY = startPoint[1] + yTranslate;
370
494
  const outerRadius = Math.hypot(endPoint[0] - startPoint[0], endPoint[1] - startPoint[1]);
371
- const gradient = this._spritesCanvasContext.createRadialGradient(centerX, centerY, 0, centerX, centerY, outerRadius);
495
+ const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, outerRadius);
372
496
  this._addColorStops(gradient, fill);
373
- this._spritesCanvasContext.fillStyle = gradient;
374
- this._spritesCanvasContext.fill();
497
+ ctx.fillStyle = gradient;
498
+ ctx.fill();
375
499
  }
376
500
  _addColorStops(gradient, fill) {
377
501
  const stops = fill.g.p;