@flighthq/scene2d-formats 0.3.0 → 0.3.1-next.320.fe56fc2

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 +1 @@
1
- {"version":3,"file":"lottieDocument.d.ts","sourceRoot":"","sources":["../src/lottieDocument.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAEV,aAAa,EAMb,gBAAgB,EAKhB,cAAc,EACd,2BAA2B,EAC3B,0BAA0B,EAsB3B,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAQpG;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,EACzC,WAAW,CAAC,EAAE,gBAAgB,EAAE,EAChC,OAAO,CAAC,EAAE,QAAQ,CAAC,2BAA2B,CAAC,GAC9C,0BAA0B,CAsC5B"}
1
+ {"version":3,"file":"lottieDocument.d.ts","sourceRoot":"","sources":["../src/lottieDocument.ts"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EAEV,aAAa,EAOb,gBAAgB,EAKhB,cAAc,EACd,2BAA2B,EAC3B,0BAA0B,EAsB3B,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAQpG;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,EACzC,WAAW,CAAC,EAAE,gBAAgB,EAAE,EAChC,OAAO,CAAC,EAAE,QAAQ,CAAC,2BAA2B,CAAC,GAC9C,0BAA0B,CAsC5B"}
@@ -4,8 +4,8 @@ import { packColor } from '@flighthq/color/contract';
4
4
  import { easeCubicBezier } from '@flighthq/easing/contract';
5
5
  import { createGradientTransformMatrix } from '@flighthq/geometry/contract';
6
6
  import { reportImportDiagnostic } from '@flighthq/importdiagnostics/contract';
7
- import { addNodeChild } from '@flighthq/node/contract';
8
- import { appendPathCubicCurveTo, appendPathEllipse, appendPathLineTo, appendPathMoveTo, appendPathPolygon, appendPathRectangle, appendPathRoundRectangle, createPath, dashPath, getPathLength, } from '@flighthq/path/contract';
7
+ import { addNodeChild, invalidateNodeLocalTransform } from '@flighthq/node/contract';
8
+ import { appendPathCubicCurveTo, appendPathEllipse, appendPathLineTo, appendPathMoveTo, appendPathPolygon, appendPathRectangle, appendPathRoundRectangle, createPath, dashPath, getPathLength, reversePath, } from '@flighthq/path/contract';
9
9
  import { applyAnimationClipToNode2D, createSprite, createDisplayObject } from '@flighthq/scene2d/contract';
10
10
  import { appendShapeBeginFill, appendShapeBeginGradientFill, appendShapeEndFill, appendShapeLineGradientStyle, appendShapeLineStyle, appendShapePath, clearShapeCommands, createShape, } from '@flighthq/shape/contract';
11
11
  import { createTextLabel } from '@flighthq/text/contract';
@@ -20,7 +20,7 @@ export function applyAnimationClipToLottieDocument(clip, time) {
20
20
  if (target === null || typeof target !== 'object' || target.lottieApply === undefined)
21
21
  continue;
22
22
  sampleAnimationTrack(_sampleScratch, channel.track, time);
23
- target.lottieApply(_sampleScratch);
23
+ target.lottieApply(_sampleScratch, time);
24
24
  }
25
25
  }
26
26
  /**
@@ -77,10 +77,16 @@ function appendLottieLayers(root, layers, context) {
77
77
  }
78
78
  function createLottieLayerNode(layer, context) {
79
79
  const container = createDisplayObject({ name: layer.nm ?? null });
80
- applyLottieTransform(container, layer.ks, context);
80
+ const hidden = layer.hd === true;
81
+ // A hidden layer still contributes its spatial transform when another layer parents to it. Its
82
+ // opacity, visibility window, paint, and masks affect only its own content, so none may leak onto
83
+ // the referencing child through Flight's hierarchy.
84
+ applyLottieTransform(container, layer.ks, context, !hidden, layer.ao === 1);
85
+ reportLottieExpression(layer.ks, context);
86
+ if (hidden)
87
+ return container;
81
88
  applyLottieLayerVisibility(container, layer, context);
82
89
  applyLottieBlendMode(container, layer, context);
83
- reportLottieExpression(layer.ks, context);
84
90
  if (layer.ty === 0)
85
91
  appendLottiePrecomposition(container, layer, context);
86
92
  else if (layer.ty === 1)
@@ -102,7 +108,7 @@ function createLottieLayerNode(layer, context) {
102
108
  applyLottieMasks(container, layer.masksProperties ?? [], context);
103
109
  return container;
104
110
  }
105
- function applyLottieTransform(target, transform, context) {
111
+ function applyLottieTransform(target, transform, context, includeOpacity = true, autoOrient = false) {
106
112
  if (transform === undefined)
107
113
  return;
108
114
  if (isSeparatedPosition(transform.p)) {
@@ -118,10 +124,13 @@ function applyLottieTransform(target, transform, context) {
118
124
  // pass through unconverted. The radians live below the seam, where nodeTransform2d applies
119
125
  // DEG_TO_RAD.
120
126
  applyScalarProperty(target, transform.r ?? transform.rz, 'Rotation', (value) => value, context);
121
- applyScalarProperty(target, transform.o, 'Alpha', (value) => value / 100, context);
127
+ if (includeOpacity)
128
+ applyScalarProperty(target, transform.o, 'Alpha', (value) => value / 100, context);
122
129
  if (transform.sk !== undefined) {
123
130
  applyScalarProperty(target, transform.sk, 'SkewX', (value) => value, context);
124
131
  }
132
+ if (autoOrient)
133
+ appendLottieAutoOrientation(target, transform.p, transform.r ?? transform.rz, context);
125
134
  }
126
135
  function applyVectorProperty(target, property, vectorPath, scalarPaths, components, convert, context) {
127
136
  if (property === undefined)
@@ -133,7 +142,7 @@ function applyVectorProperty(target, property, vectorPath, scalarPaths, componen
133
142
  appendNumericPropertyChannels(property, components, (component) => ({
134
143
  node: target,
135
144
  path: component === null ? vectorPath : scalarPaths[component],
136
- }), convert, context);
145
+ }), convert, context, vectorPath === 'Position');
137
146
  }
138
147
  function applyScalarProperty(target, property, path, convert, context) {
139
148
  if (property === undefined)
@@ -143,20 +152,112 @@ function applyScalarProperty(target, property, path, convert, context) {
143
152
  return;
144
153
  appendNumericPropertyChannels(property, 1, () => ({ node: target, path }), convert, context);
145
154
  }
146
- function appendNumericPropertyChannels(property, components, target, convert, context) {
155
+ function appendNumericPropertyChannels(property, components, target, convert, context, spatialTangents = false) {
147
156
  if (!isAnimatedProperty(property))
148
157
  return;
149
158
  const keyframes = property.k;
150
159
  if (keyframes.length === 0)
151
160
  return;
152
- const componentSpecific = hasComponentSpecificEasing(keyframes, components);
161
+ const spatial = spatialTangents && hasSpatialTangents(keyframes);
162
+ const componentSpecific = !spatial && hasComponentSpecificEasing(keyframes, components);
153
163
  if (componentSpecific && components > 1) {
154
164
  for (let component = 0; component < components; component++) {
155
165
  context.channels.push(createAnimationChannel(createLottieTrack(keyframes, 1, context, (value) => [convert(numericValue(value, components)[component], component)], component), target(component)));
156
166
  }
157
167
  return;
158
168
  }
159
- context.channels.push(createAnimationChannel(createLottieTrack(keyframes, components, context, (value) => numericValue(value, components).map(convert), 0), target(null)));
169
+ context.channels.push(createAnimationChannel(createLottieTrack(keyframes, components, context, (value) => numericValue(value, components).map(convert), 0, spatial, (value, component) => convert(value, component) - convert(0, component)), target(null)));
170
+ }
171
+ function appendLottieAutoOrientation(target, property, rotation, context) {
172
+ const sampler = createLottiePositionSampler(property, context);
173
+ if (sampler === null)
174
+ return;
175
+ const baseRotation = target.rotation;
176
+ const rotationTrack = rotation !== undefined && isAnimatedProperty(rotation)
177
+ ? createLottieTrack(rotation.k, 1, context, (value) => numericValue(value, 1), 0)
178
+ : null;
179
+ const previous = [0, 0];
180
+ const current = [0, 0];
181
+ const frameStep = Math.abs(context.frameScale) / context.document.fr;
182
+ const applyOrientation = (time) => {
183
+ let previousTime;
184
+ let currentTime;
185
+ if (time <= sampler.firstTime) {
186
+ previousTime = sampler.firstTime;
187
+ currentTime = sampler.firstTime + frameStep * 0.01;
188
+ }
189
+ else if (time >= sampler.lastTime) {
190
+ previousTime = sampler.lastTime - frameStep * (sampler.separated ? 0.01 : 0.05);
191
+ currentTime = sampler.lastTime;
192
+ }
193
+ else {
194
+ previousTime = time - frameStep * 0.01;
195
+ currentTime = time;
196
+ }
197
+ sampler.sample(previous, previousTime);
198
+ sampler.sample(current, currentTime);
199
+ const orientation = (Math.atan2(current[1] - previous[1], current[0] - previous[0]) * 180) / Math.PI;
200
+ if (rotationTrack !== null)
201
+ sampleAnimationTrack(_autoOrientRotationScratch, rotationTrack, time);
202
+ target.rotation = (rotationTrack === null ? baseRotation : _autoOrientRotationScratch[0]) + orientation;
203
+ invalidateNodeLocalTransform(target);
204
+ };
205
+ applyOrientation(sampler.firstTime);
206
+ context.channels.push(createAnimationChannel(sampler.carrier, {
207
+ lottieApply(_sample, time) {
208
+ applyOrientation(time);
209
+ },
210
+ }));
211
+ }
212
+ function createLottiePositionSampler(property, context) {
213
+ if (property === undefined)
214
+ return null;
215
+ if (isSeparatedPosition(property)) {
216
+ const xTrack = isAnimatedProperty(property.x)
217
+ ? createLottieTrack(property.x.k, 1, context, (value) => numericValue(value, 1), 0)
218
+ : null;
219
+ const yTrack = isAnimatedProperty(property.y)
220
+ ? createLottieTrack(property.y.k, 1, context, (value) => numericValue(value, 1), 0)
221
+ : null;
222
+ const carrier = xTrack ?? yTrack;
223
+ if (carrier === null)
224
+ return null;
225
+ const x = numericValue(initialLottieValue(property.x), 1)[0];
226
+ const y = numericValue(initialLottieValue(property.y), 1)[0];
227
+ const tracks = [xTrack, yTrack].filter((track) => track !== null && track.times.length > 0);
228
+ if (tracks.length === 0)
229
+ return null;
230
+ return {
231
+ carrier,
232
+ firstTime: Math.min(...tracks.map((track) => track.times[0])),
233
+ lastTime: Math.max(...tracks.map((track) => track.times[track.times.length - 1])),
234
+ sample(out, time) {
235
+ out[0] = x;
236
+ out[1] = y;
237
+ if (xTrack !== null)
238
+ sampleAnimationTrack(out, xTrack, time);
239
+ if (yTrack !== null) {
240
+ sampleAnimationTrack(_autoOrientScalarScratch, yTrack, time);
241
+ out[1] = _autoOrientScalarScratch[0];
242
+ }
243
+ },
244
+ separated: true,
245
+ };
246
+ }
247
+ if (!isAnimatedProperty(property))
248
+ return null;
249
+ const track = createLottieTrack(property.k, 2, context, (value) => numericValue(value, 2), 0, hasSpatialTangents(property.k));
250
+ if (track.times.length === 0)
251
+ return null;
252
+ return {
253
+ carrier: track,
254
+ firstTime: track.times[0],
255
+ lastTime: track.times[track.times.length - 1],
256
+ sample(out, time) {
257
+ sampleAnimationTrack(out, track, time);
258
+ },
259
+ separated: false,
260
+ };
160
261
  }
161
262
  function bindMutableNumericProperty(property, current, convert, onChange, context) {
162
263
  if (!isAnimatedProperty(property))
@@ -174,9 +275,9 @@ function bindMutableNumericProperty(property, current, convert, onChange, contex
174
275
  },
175
276
  }), convert, context);
176
277
  }
177
- function createLottieTrack(keyframes, components, context, valueOf, easingComponent) {
278
+ function createLottieTrack(keyframes, components, context, valueOf, easingComponent, spatialTangents = false, tangentOf = (value) => value) {
178
279
  const times = [];
179
- const values = [];
280
+ const samples = [];
180
281
  const retained = [];
181
282
  for (let index = 0; index < keyframes.length; index++) {
182
283
  const keyframe = keyframes[index];
@@ -186,26 +287,102 @@ function createLottieTrack(keyframes, components, context, valueOf, easingCompon
186
287
  times.push(time);
187
288
  retained.push(keyframe);
188
289
  const source = keyframe.s ?? keyframes[index - 1]?.e;
189
- values.push(...valueOf(source, index));
290
+ samples.push(valueOf(source, index));
190
291
  }
292
+ const spatial = spatialTangents && hasSpatialTangents(retained);
293
+ const values = spatial
294
+ ? createLottieSpatialTrackValues(retained, samples, times, components, tangentOf)
295
+ : samples.flat();
191
296
  const segmentEasings = [];
192
297
  for (let index = 0; index < retained.length - 1; index++) {
193
298
  const keyframe = retained[index];
194
- if (keyframe.h === 1) {
195
- segmentEasings.push(_holdEasing);
196
- }
197
- else {
198
- segmentEasings.push(createLottieSegmentEasing(keyframe.o, retained[index + 1].i, easingComponent));
199
- }
299
+ const temporal = keyframe.h === 1 ? _holdEasing : createLottieSegmentEasing(keyframe.o, keyframe.i, easingComponent);
300
+ const outgoing = spatialTangent(keyframe.to, components, tangentOf);
301
+ const incoming = spatialTangent(keyframe.ti, components, tangentOf);
302
+ segmentEasings.push(spatial && outgoing !== null && incoming !== null
303
+ ? createLottieSpatialSegmentEasing(temporal, samples[index], samples[index + 1], outgoing, incoming)
304
+ : temporal);
200
305
  }
201
306
  return createAnimationTrack({
202
307
  components,
203
- interpolation: 'Linear',
308
+ interpolation: spatial ? 'Cubic' : 'Linear',
204
309
  segmentEasings,
205
310
  times,
206
311
  values,
207
312
  });
208
313
  }
314
+ function createLottieSpatialTrackValues(keyframes, samples, times, components, tangentOf) {
315
+ const incoming = samples.map(() => new Array(components).fill(0));
316
+ const outgoing = samples.map(() => new Array(components).fill(0));
317
+ for (let index = 0; index < samples.length - 1; index++) {
318
+ const dt = times[index + 1] - times[index];
319
+ const spatialOut = spatialTangent(keyframes[index].to, components, tangentOf);
320
+ const spatialIn = spatialTangent(keyframes[index].ti, components, tangentOf);
321
+ for (let component = 0; component < components; component++) {
322
+ if (spatialOut !== null && spatialIn !== null) {
323
+ outgoing[index][component] = (3 * spatialOut[component]) / dt;
324
+ incoming[index + 1][component] = (-3 * spatialIn[component]) / dt;
325
+ }
326
+ else {
327
+ const slope = (samples[index + 1][component] - samples[index][component]) / dt;
328
+ outgoing[index][component] = slope;
329
+ incoming[index + 1][component] = slope;
330
+ }
331
+ }
332
+ }
333
+ const values = [];
334
+ for (let index = 0; index < samples.length; index++) {
335
+ values.push(...incoming[index], ...samples[index], ...outgoing[index]);
336
+ }
337
+ return values;
338
+ }
339
+ function spatialTangent(value, components, tangentOf) {
340
+ if (!Array.isArray(value))
341
+ return null;
342
+ return numericValue(value, components).map(tangentOf);
343
+ }
344
+ const LOTTIE_SPATIAL_CURVE_SAMPLES = 150;
345
+ function createLottieSpatialSegmentEasing(temporal, start, end, outgoing, incoming) {
346
+ const lengths = new Array(LOTTIE_SPATIAL_CURVE_SAMPLES).fill(0);
347
+ let previous = start;
348
+ for (let index = 1; index < LOTTIE_SPATIAL_CURVE_SAMPLES; index++) {
349
+ const point = sampleLottieSpatialBezier(start, end, outgoing, incoming, index / (LOTTIE_SPATIAL_CURVE_SAMPLES - 1));
350
+ let distanceSquared = 0;
351
+ for (let component = 0; component < start.length; component++) {
352
+ distanceSquared += (point[component] - previous[component]) ** 2;
353
+ }
354
+ lengths[index] = lengths[index - 1] + Math.sqrt(distanceSquared);
355
+ previous = point;
356
+ }
357
+ const total = lengths[lengths.length - 1];
358
+ return (alpha) => {
359
+ const distanceFraction = temporal?.(alpha) ?? alpha;
360
+ if (distanceFraction <= 0 || total === 0)
361
+ return 0;
362
+ if (distanceFraction >= 1)
363
+ return 1;
364
+ const distance = total * distanceFraction;
365
+ let low = 0;
366
+ let high = lengths.length - 1;
367
+ while (low + 1 < high) {
368
+ const middle = (low + high) >> 1;
369
+ if (lengths[middle] <= distance)
370
+ low = middle;
371
+ else
372
+ high = middle;
373
+ }
374
+ const span = lengths[high] - lengths[low];
375
+ const fraction = span > 0 ? (distance - lengths[low]) / span : 0;
376
+ return (low + fraction) / (LOTTIE_SPATIAL_CURVE_SAMPLES - 1);
377
+ };
378
+ }
379
+ function sampleLottieSpatialBezier(start, end, outgoing, incoming, time) {
380
+ const inverse = 1 - time;
381
+ return start.map((value, component) => inverse ** 3 * value +
382
+ 3 * inverse ** 2 * time * (value + outgoing[component]) +
383
+ 3 * inverse * time ** 2 * (end[component] + incoming[component]) +
384
+ time ** 3 * end[component]);
385
+ }
209
386
  function createLottieSegmentEasing(outgoing, incoming, component) {
210
387
  if (outgoing === undefined || incoming === undefined)
211
388
  return null;
@@ -318,6 +495,9 @@ function appendLottieShapeItems(parent, items, context, name = null) {
318
495
  const color = numericValue(initialLottieValue(stroke.c), 3);
319
496
  const opacity = [numericValue(initialLottieValue(stroke.o), 1)[0] / 100];
320
497
  const width = [numericValue(initialLottieValue(stroke.w), 1)[0]];
498
+ const miterLimit = [
499
+ stroke.ml2 === undefined ? (stroke.ml ?? 4) : numericValue(initialLottieValue(stroke.ml2), 1)[0],
500
+ ];
321
501
  const dashEntries = stroke.d ?? [];
322
502
  const hasAnimatedDash = dashEntries.some((entry) => isAnimatedProperty(entry.v));
323
503
  const dash = hasAnimatedDash
@@ -338,7 +518,7 @@ function appendLottieShapeItems(parent, items, context, name = null) {
338
518
  dashOffset,
339
519
  joints: mapLottieLineJoin(stroke.lj),
340
520
  kind: 'stroke',
341
- miterLimit: stroke.ml ?? 4,
521
+ miterLimit: miterLimit[0],
342
522
  opacity: opacity[0],
343
523
  width: width[0],
344
524
  };
@@ -352,6 +532,12 @@ function appendLottieShapeItems(parent, items, context, name = null) {
352
532
  paint.width = width[0];
353
533
  rerender();
354
534
  }, context);
535
+ if (stroke.ml2 !== undefined) {
536
+ bindMutableNumericProperty(stroke.ml2, miterLimit, (value) => value, () => {
537
+ paint.miterLimit = miterLimit[0];
538
+ rerender();
539
+ }, context);
540
+ }
355
541
  }
356
542
  else if (item.ty === 'gf' || item.ty === 'gs') {
357
543
  const gradient = item;
@@ -361,6 +547,9 @@ function appendLottieShapeItems(parent, items, context, name = null) {
361
547
  const end = numericValue(initialLottieValue(gradient.e), 2);
362
548
  const opacity = [gradient.o === undefined ? 100 : numericValue(initialLottieValue(gradient.o), 1)[0]];
363
549
  const width = [gradient.w === undefined ? 1 : numericValue(initialLottieValue(gradient.w), 1)[0]];
550
+ const miterLimit = [
551
+ gradient.ml2 === undefined ? (gradient.ml ?? 4) : numericValue(initialLottieValue(gradient.ml2), 1)[0],
552
+ ];
364
553
  const dashEntries = gradient.d ?? [];
365
554
  const hasAnimatedDash = dashEntries.some((entry) => isAnimatedProperty(entry.v));
366
555
  const dash = hasAnimatedDash
@@ -382,13 +571,14 @@ function appendLottieShapeItems(parent, items, context, name = null) {
382
571
  end,
383
572
  joints: mapLottieLineJoin(gradient.lj),
384
573
  kind: 'gradient',
385
- miterLimit: gradient.ml ?? 4,
574
+ miterLimit: miterLimit[0],
386
575
  opacity: opacity[0] / 100,
387
576
  shape: gradient.t,
388
577
  start,
389
578
  type: gradient.ty,
390
579
  values,
391
580
  width: width[0],
581
+ winding: gradient.r === 2 ? 'evenOdd' : 'nonZero',
392
582
  };
393
583
  state.paints.push(paint);
394
584
  bindMutableNumericProperty(gradient.g.k, values, (value) => value, rerender, context);
@@ -406,6 +596,12 @@ function appendLottieShapeItems(parent, items, context, name = null) {
406
596
  rerender();
407
597
  }, context);
408
598
  }
599
+ if (gradient.ml2 !== undefined) {
600
+ bindMutableNumericProperty(gradient.ml2, miterLimit, (value) => value, () => {
601
+ paint.miterLimit = miterLimit[0];
602
+ rerender();
603
+ }, context);
604
+ }
409
605
  }
410
606
  else if (item.ty === 'tm') {
411
607
  const trim = item;
@@ -433,7 +629,7 @@ function createLottieShapeItemPath(item) {
433
629
  const value = toLottieShapePath(initialLottieValue(shapePath.ks));
434
630
  if (value === undefined)
435
631
  return null;
436
- return createLottieBezierPath(value);
632
+ return applyLottieShapeDirection(createLottieBezierPath(value), shapePath.d);
437
633
  }
438
634
  const path = createPath();
439
635
  if (item.ty === 'rc') {
@@ -447,14 +643,14 @@ function createLottieShapeItemPath(item) {
447
643
  else {
448
644
  appendPathRectangle(path, position[0] - size[0] / 2, position[1] - size[1] / 2, size[0], size[1]);
449
645
  }
450
- return path;
646
+ return applyLottieShapeDirection(path, rectangle.d);
451
647
  }
452
648
  if (item.ty === 'el') {
453
649
  const ellipse = item;
454
650
  const position = numericValue(initialLottieValue(ellipse.p), 2);
455
651
  const size = numericValue(initialLottieValue(ellipse.s), 2);
456
652
  appendPathEllipse(path, position[0], position[1], size[0] / 2, size[1] / 2);
457
- return path;
653
+ return applyLottieShapeDirection(path, ellipse.d);
458
654
  }
459
655
  if (item.ty === 'sr') {
460
656
  const polystar = item;
@@ -465,7 +661,7 @@ function createLottieShapeItemPath(item) {
465
661
  const rotation = numericValue(initialLottieValue(polystar.r), 1)[0];
466
662
  const outerRoundness = polystar.os === undefined ? 0 : numericValue(initialLottieValue(polystar.os), 1)[0];
467
663
  const innerRoundness = polystar.sy === 1 && polystar.is !== undefined ? numericValue(initialLottieValue(polystar.is), 1)[0] : 0;
468
- return createLottiePolystarPath(polystar.sy, center, points, outer, inner, rotation, outerRoundness, innerRoundness);
664
+ return applyLottieShapeDirection(createLottiePolystarPath(polystar.sy, center, points, outer, inner, rotation, outerRoundness, innerRoundness), polystar.d);
469
665
  }
470
666
  return null;
471
667
  }
@@ -513,7 +709,7 @@ function createLottiePolystarPath(kind, center, pointCount, outer, inner, rotati
513
709
  }
514
710
  function bindLottieGeometryItem(item, state, pathIndex, rerender, context) {
515
711
  const rebuild = (path) => {
516
- state.paths[pathIndex] = path;
712
+ state.paths[pathIndex] = applyLottieShapeDirection(path, item.d);
517
713
  rerender();
518
714
  };
519
715
  if (item.ty === 'sh') {
@@ -705,9 +901,10 @@ function createLottieBezierPath(value) {
705
901
  }
706
902
  return path;
707
903
  }
708
- // Each paint restates the group's whole path set, because that is what a Bodymovin paint means: it
709
- // applies to every path in its group. Paints are emitted in the order the file lists them, so a
710
- // second fill lands on top of the first rather than replacing it.
904
+ // The current representation restates every local path for every local paint. This preserves
905
+ // multiple paints when all paths precede all styles, but it does not yet implement Lottie's general
906
+ // render stack: styles scope only over preceding shapes (including shapes in nested groups), and
907
+ // repeated styles render in reverse order. That needs a scoped stack rather than another field here.
711
908
  function renderLottieShapeState(state) {
712
909
  clearShapeCommands(state.shape);
713
910
  if (state.paths.length === 0)
@@ -728,7 +925,7 @@ function renderLottieShapeState(state) {
728
925
  }
729
926
  else if (paint.type === 'gf') {
730
927
  appendLottieGradientFill(state.shape, paint);
731
- appendLottieShapePaths(state, null);
928
+ appendLottieShapePaths(state, paint.winding);
732
929
  appendShapeEndFill(state.shape);
733
930
  }
734
931
  else {
@@ -756,6 +953,13 @@ function appendLottieGradientStroke(shape, paint) {
756
953
  appendShapeLineStyle(shape, paint.width, 0, 1, false, 'normal', paint.caps, paint.joints, paint.miterLimit);
757
954
  appendShapeLineGradientStyle(shape, paint.shape === 2 ? 'radial' : 'linear', gradient.colors, gradient.alphas, gradient.ratios, createLottieGradientMatrix(paint.start, paint.end));
758
955
  }
956
+ function applyLottieShapeDirection(path, direction) {
957
+ if (direction !== 3)
958
+ return path;
959
+ const reversed = createPath(path.winding);
960
+ reversePath(path, reversed);
961
+ return reversed;
962
+ }
759
963
  function applyLottieMasks(target, masks, context) {
760
964
  const active = masks.filter((mask) => mask.mode !== 'n');
761
965
  if (active.length === 0)
@@ -915,8 +1119,7 @@ function hasComponentSpecificEasing(keyframes, components) {
915
1119
  return false;
916
1120
  for (let index = 0; index < keyframes.length - 1; index++) {
917
1121
  const current = keyframes[index];
918
- const next = keyframes[index + 1];
919
- for (const handle of [current.o, next.i]) {
1122
+ for (const handle of [current.o, current.i]) {
920
1123
  if (handle === undefined)
921
1124
  continue;
922
1125
  if (handleVaries(handle.x, components) || handleVaries(handle.y, components))
@@ -925,6 +1128,11 @@ function hasComponentSpecificEasing(keyframes, components) {
925
1128
  }
926
1129
  return false;
927
1130
  }
1131
+ function hasSpatialTangents(keyframes) {
1132
+ return keyframes.some((keyframe, index) => {
1133
+ return index < keyframes.length - 1 && Array.isArray(keyframe.to) && Array.isArray(keyframe.ti);
1134
+ });
1135
+ }
928
1136
  function handleVaries(value, components) {
929
1137
  if (!Array.isArray(value) || value.length < 2)
930
1138
  return false;
@@ -1029,6 +1237,8 @@ function reportLottieDrop(context, kind, detail) {
1029
1237
  reportImportDiagnostic(context.diagnostics, ImportDiagnosticSeverity.Drop, kind, 'lottieDocument', detail);
1030
1238
  }
1031
1239
  const _sampleScratch = new Array(256).fill(0);
1240
+ const _autoOrientRotationScratch = [0];
1241
+ const _autoOrientScalarScratch = [0];
1032
1242
  const _holdEasing = () => 0;
1033
1243
  // Bodymovin's own numbering. The five that fold into blend state:
1034
1244
  const _lottieFixedBlendModes = new Map([