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