rig-play 1.3.0 → 1.5.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/dist/index.js CHANGED
@@ -377,7 +377,7 @@ function timelineIdentity(slot, shown) {
377
377
  return `${shown.skin}/${slot}/${shown.placeholder}`;
378
378
  }
379
379
  const nameOf = (a) => `${a.skin}/${a.slot}/${a.attachment}`;
380
- function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {}) {
380
+ function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {}, mixed) {
381
381
  const evalDeform = plant.deform ?? deformAt;
382
382
  const blendOf = plant.deformBlend ?? blendDeform;
383
383
  const evalFrame = plant.sequence ?? sequenceFrameAt;
@@ -388,9 +388,10 @@ function attachmentStates(doc, resolve, placeholders, sample, sliders, plant = {
388
388
  for (const slot of doc.slots) {
389
389
  const live = gate(doc, slot, active);
390
390
  let placeholder = placeholders.get(slot.name) ?? null;
391
- let deform = null;
391
+ const base = live ? mixed?.get(slot.name) : void 0;
392
+ let deform = base?.deform ?? null;
392
393
  let frame2 = null;
393
- const seen = [];
394
+ const seen = base === void 0 ? [] : [...base.seen];
394
395
  const state = () => {
395
396
  if (placeholder === null) return null;
396
397
  const s = resolve(doc, { ...slot, setup: placeholder });
@@ -2450,7 +2451,7 @@ function applySliderPhysics(r, at, alpha, target) {
2450
2451
  }
2451
2452
  }
2452
2453
  }
2453
- const clamp01$1 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
2454
+ const clamp01$2 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
2454
2455
  const COLOUR_CHANNELS = { rgba: [0, 1, 2, 3], rgb: [0, 1, 2], alpha: [3], rgba2: [0, 1, 2, 3, 4, 5, 6], rgb2: [0, 1, 2, 4, 5, 6] };
2455
2456
  function applySliderSlots(slot, pose, applications) {
2456
2457
  for (const app of applications) {
@@ -2465,8 +2466,8 @@ function applySliderSlots(slot, pose, applications) {
2465
2466
  const v = valuesAt$1(tl.keys, app.at);
2466
2467
  if (v === null) continue;
2467
2468
  COLOUR_CHANNELS[tl.kind].forEach((at, i) => {
2468
- if (at < 4) pose.light[at] = app.alpha === 1 ? clamp01$1(v[i]) : clamp01$1(pose.light[at] + (v[i] - pose.light[at]) * app.alpha);
2469
- else if (pose.dark !== null) pose.dark[at - 4] = app.alpha === 1 ? clamp01$1(v[i]) : clamp01$1(pose.dark[at - 4] + (v[i] - pose.dark[at - 4]) * app.alpha);
2469
+ if (at < 4) pose.light[at] = app.alpha === 1 ? clamp01$2(v[i]) : clamp01$2(pose.light[at] + (v[i] - pose.light[at]) * app.alpha);
2470
+ else if (pose.dark !== null) pose.dark[at - 4] = app.alpha === 1 ? clamp01$2(v[i]) : clamp01$2(pose.dark[at - 4] + (v[i] - pose.dark[at - 4]) * app.alpha);
2470
2471
  });
2471
2472
  }
2472
2473
  }
@@ -3922,23 +3923,15 @@ function adjustLocals(doc, bones, adjust) {
3922
3923
  }
3923
3924
  });
3924
3925
  }
3925
- const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
3926
+ const clamp01$1 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
3926
3927
  function posedSlots(doc, timelines, t, plant = {}, sliders = [], placeholders) {
3927
- const resolve = plant.shown ?? shownAttachment;
3928
3928
  const colour = plant.colour ?? readColour;
3929
- const blend = plant.blend ?? readBlend;
3930
- const round = plant.round ?? gridRound;
3931
3929
  const byName = new Map(timelines.slots.map((s) => [s.name, s]));
3932
- const rows = [];
3933
- const conflicts = [];
3934
- const active = activeBones(doc);
3935
- const gate = plant.slotTimelines ?? slotTimelinesApply;
3936
- for (const slot of doc.slots) {
3930
+ return slotRowsFrom(doc, plant, sliders, placeholders, (slot, live) => {
3937
3931
  let placeholder = slot.setup;
3938
3932
  const light = slot.color === void 0 ? [1, 1, 1, 1] : colour(slot.color);
3939
3933
  const setupDark = slot.dark === void 0 ? null : readColour(slot.dark);
3940
3934
  const dark = setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]];
3941
- const live = gate(doc, slot, active);
3942
3935
  for (const tl of live ? byName.get(slot.name)?.timelines ?? [] : []) {
3943
3936
  if (tl.kind === "attachment") {
3944
3937
  const key = keyAt(tl.keys, t, plant);
@@ -3950,10 +3943,26 @@ function posedSlots(doc, timelines, t, plant = {}, sliders = [], placeholders) {
3950
3943
  const channels = SLOT_COLOUR_TIMELINES[tl.kind].channels;
3951
3944
  channels.forEach((name, i) => {
3952
3945
  const at = { r: 0, g: 1, b: 2, a: 3, r2: 4, g2: 5, b2: 6 }[name];
3953
- if (at < 4) light[at] = v === null ? setupLight[at] : clamp01(v[i]);
3954
- else if (dark !== null && setupDark !== null) dark[at - 4] = v === null ? setupDark[at - 4] : clamp01(v[i]);
3946
+ if (at < 4) light[at] = v === null ? setupLight[at] : clamp01$1(v[i]);
3947
+ else if (dark !== null && setupDark !== null) dark[at - 4] = v === null ? setupDark[at - 4] : clamp01$1(v[i]);
3955
3948
  });
3956
3949
  }
3950
+ return { placeholder, light, dark };
3951
+ });
3952
+ }
3953
+ function slotRowsFrom(doc, plant, sliders, placeholders, baseOf) {
3954
+ const resolve = plant.shown ?? shownAttachment;
3955
+ const blend = plant.blend ?? readBlend;
3956
+ const round = plant.round ?? gridRound;
3957
+ const rows = [];
3958
+ const conflicts = [];
3959
+ const active = activeBones(doc);
3960
+ const gate = plant.slotTimelines ?? slotTimelinesApply;
3961
+ for (const slot of doc.slots) {
3962
+ const live = gate(doc, slot, active);
3963
+ const base = baseOf(slot, live);
3964
+ let placeholder = base.placeholder;
3965
+ const { light, dark } = base;
3957
3966
  placeholders?.set(slot.name, placeholder);
3958
3967
  const pose = { placeholder, light, dark };
3959
3968
  if (live) applySliderSlots(slot.name, pose, sliders);
@@ -3996,14 +4005,12 @@ function stepRows(posed) {
3996
4005
  return posed.rows;
3997
4006
  }
3998
4007
  function posedBoneStep(doc, timelines, t, plant, constraints, sliders, step) {
3999
- const active = activeBones(doc);
4000
4008
  const bones = posedBones(doc, timelines, t, plant);
4009
+ if (constraints !== void 0 && step !== void 0) return steppedLocals(doc, bones, plant, () => pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, sliders, step);
4010
+ const active = activeBones(doc);
4001
4011
  if (step?.adjust !== void 0) adjustLocals(doc, bones, step.adjust);
4002
4012
  let world = (plant.evaluate ?? worldTransforms)(bones, active);
4003
- if (constraints !== void 0 && step !== void 0) {
4004
- const records = stepPhysicsRecords(pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, active, step.ctx, step.before);
4005
- world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(records) : records, null, sliders, step.ctx, void 0, solverRules(plant.solver));
4006
- } else if (constraints !== void 0) {
4013
+ if (constraints !== void 0) {
4007
4014
  const setupRecords = constraintRecords(doc);
4008
4015
  const records = pathDeformed(doc, posedRecords(setupRecords, constraints, t), timelines, t, plant);
4009
4016
  const previous = setupRecords.some((r) => r.kind === "path") ? applyConstraints(doc.bones, (plant.evaluate ?? worldTransforms)(doc.bones, active), active, plant.constraints ? plant.constraints(setupRecords) : setupRecords, null, void 0, void 0, void 0, solverRules(plant.solver)) : null;
@@ -4012,6 +4019,20 @@ function posedBoneStep(doc, timelines, t, plant, constraints, sliders, step) {
4012
4019
  for (const b of bones) if (world.get(b.name) === void 0) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
4013
4020
  return { bones, world, active, round: plant.round ?? gridRound };
4014
4021
  }
4022
+ function steppedLocals(doc, bones, plant, records, physicsKeyed, t, sliders, step) {
4023
+ const active = activeBones(doc);
4024
+ if (step.adjust !== void 0) adjustLocals(doc, bones, step.adjust);
4025
+ let world = (plant.evaluate ?? worldTransforms)(bones, active);
4026
+ const stepped = stepPhysicsRecords(records(), physicsKeyed, t, active, step.ctx, step.before);
4027
+ world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(stepped) : stepped, null, sliders, step.ctx, void 0, solverRules(plant.solver));
4028
+ for (const b of bones) if (world.get(b.name) === void 0) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
4029
+ return { bones, world, active, round: plant.round ?? gridRound };
4030
+ }
4031
+ function steppedLocalsWorld(doc, bones, plant, sliders, step, rows) {
4032
+ const posed = steppedLocals(doc, bones, plant, () => constraintRecords(doc), [], 0, sliders, step);
4033
+ stepRows(posed);
4034
+ return posed.world;
4035
+ }
4015
4036
  function pathDeformed(doc, records, timelines, t, plant) {
4016
4037
  if (timelines.attachments.every((a) => a.deform === null)) return records;
4017
4038
  return records.map((r) => {
@@ -4929,6 +4950,14 @@ function openWalk(doc, anim, plant, reset, mode, shape, adjust) {
4929
4950
  const ctx = freshStepContext(plant.physicsStep);
4930
4951
  if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
4931
4952
  const track = { doc, anim, duration: anim.timelines.duration, mode, plant, raw: { ...plant, ...roundPlant(mode) }, resolve: plant.shown ?? shownAttachment, shape, ctx, adjust, trackTime: 0, last: -1 };
4953
+ if (reset === "setup") {
4954
+ const setup = poseSetup(doc, { ...plant, ...roundPlant(mode) }, ctx);
4955
+ const absent = setup.absent.filter(([block]) => block !== "setup.clipped");
4956
+ if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join("; ")}`);
4957
+ if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError("the raw walk's reset pose was not posed");
4958
+ ctx.phase = "update";
4959
+ return { track, first: shape.setup(doc, setup.world, setup.setup.slots, setup.setup.drawOrder, setup.shown, plant, mode) };
4960
+ }
4932
4961
  return { track, first: stepWalk(track, 0, true) };
4933
4962
  }
4934
4963
  function stepWalk(track, dt, first = false) {
@@ -4939,26 +4968,56 @@ function stepWalk(track, dt, first = false) {
4939
4968
  const t = mode.loop ? (mode.time ?? loopedTime)(trackTime, duration, trackTime - dt) : trackTime < duration ? trackTime : duration;
4940
4969
  const before = ctx.time;
4941
4970
  ctx.time += mode.clock === void 0 ? dt : mode.clock(dt, t - (first ? 0 : Math.max(last, 0)));
4971
+ const fire = plant.events ?? eventsFired;
4972
+ const keys = anim.timelines.events;
4973
+ const pose = poseStepFrom(doc, plant, resolve, shape, mode, ctx, first, trackTime, t, {
4974
+ world: (sliders) => shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust }),
4975
+ slots: (sliders, placeholders) => posedSlots(doc, anim.timelines, t, raw, sliders, placeholders),
4976
+ drawOrder: anim.timelines.drawOrder,
4977
+ at: t,
4978
+ sample: { timelines: anim.timelines, t },
4979
+ events: () => mode.loop && mode.live !== true ? [] : mode.loop ? fire(keys, last, t, rawNumber, t < last) : fire(keys, last, t, rawNumber)
4980
+ });
4981
+ track.last = t;
4982
+ return pose;
4983
+ }
4984
+ function poseStepFrom(doc, plant, resolve, shape, mode, ctx, first, trackTime, animationTime, posers) {
4942
4985
  const sliders = [];
4943
- const world = shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust });
4986
+ const world = posers.world(sliders);
4944
4987
  if (first) ctx.phase = "update";
4945
4988
  const placeholders = /* @__PURE__ */ new Map();
4946
- const slots = posedSlots(doc, anim.timelines, t, raw, sliders, placeholders);
4989
+ const slots = posers.slots(sliders, placeholders);
4947
4990
  if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join("; ")}`);
4948
4991
  const evalOrder = plant.drawOrder ?? drawOrderAt;
4949
- let order = evalOrder(doc.slots.length, anim.timelines.drawOrder, t) ?? doc.slots.map((_s, k) => k);
4992
+ let order = evalOrder(doc.slots.length, posers.drawOrder, posers.at) ?? doc.slots.map((_s, k) => k);
4950
4993
  for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
4951
4994
  const drawOrder = order.map((k) => doc.slots[k].name);
4952
- const states = attachmentStates(doc, resolve, placeholders, { timelines: anim.timelines, t }, sliders, plant);
4995
+ const states = attachmentStates(doc, resolve, placeholders, posers.sample, sliders, plant, posers.mixed);
4953
4996
  if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join("; ")}`);
4954
4997
  const rank = new Map(drawOrder.map((n, r) => [n, r]));
4955
4998
  const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
4956
- const fire = plant.events ?? eventsFired;
4957
- const keys = anim.timelines.events;
4958
- const events = () => mode.loop && mode.live !== true ? [] : (mode.loop ? fire(keys, last, t, rawNumber, t < last) : fire(keys, last, t, rawNumber)).map((e) => [e[0], finite(e[1], "event time"), e[2], finite(e[3], "event float"), e[4]]);
4959
- const pose = shape.step(doc, { trackTime, animationTime: t, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
4960
- track.last = t;
4961
- return pose;
4999
+ const events = () => posers.events().map((e) => [e[0], finite(e[1], "event time"), e[2], finite(e[3], "event float"), e[4]]);
5000
+ return shape.step(doc, { trackTime, animationTime, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
5001
+ }
5002
+ function openRawTracksPath(doc, plant) {
5003
+ const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
5004
+ if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
5005
+ const ctx = freshStepContext(plant.physicsStep);
5006
+ const raw = { ...plant, ...roundPlant(RAW_MODE) };
5007
+ const resolve = plant.shown ?? shownAttachment;
5008
+ return {
5009
+ ctx,
5010
+ raw,
5011
+ setupPose: () => {
5012
+ const setup = poseSetup(doc, raw, ctx);
5013
+ const absent = setup.absent.filter(([block]) => block !== "setup.clipped");
5014
+ if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join("; ")}`);
5015
+ if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError("the raw walk's reset pose was not posed");
5016
+ ctx.phase = "update";
5017
+ return RAW_SHAPE.setup(doc, setup.world, setup.setup.slots, setup.setup.drawOrder, setup.shown, plant, RAW_MODE);
5018
+ },
5019
+ step: (first, trackTime, animationTime, posers) => poseStepFrom(doc, plant, resolve, RAW_SHAPE, RAW_MODE, ctx, first, trackTime, animationTime, posers)
5020
+ };
4962
5021
  }
4963
5022
  function openRawTrack(doc, animation, plant, reset, mode, adjust) {
4964
5023
  return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE, adjust);
@@ -4970,6 +5029,7 @@ const HELD = { loop: false, keep: false };
4970
5029
  const LOOPED = { loop: true, keep: false, live: true };
4971
5030
  function openTrack(doc, animation, options, plant = {}) {
4972
5031
  if (typeof options !== "object" || options === null || typeof options.loop !== "boolean") throw new CoreInputError(`the live track's options.loop is ${JSON.stringify(options?.loop)}, not true or false`);
5032
+ if (options.reset !== "animation" && options.reset !== "setup") throw new CoreInputError(`the live track's options.reset is ${JSON.stringify(options.reset)}, not "animation" or "setup"`);
4973
5033
  if (options.adjust !== void 0 && typeof options.adjust !== "function") throw new CoreInputError(`the live track's options.adjust is ${typeof options.adjust}, not a function`);
4974
5034
  const { track, first } = openRawTrack(doc, animation, plant, options.reset, options.loop ? LOOPED : HELD, options.adjust);
4975
5035
  return new Track(track, first);
@@ -5006,6 +5066,940 @@ class Track {
5006
5066
  }
5007
5067
  }
5008
5068
  }
5069
+ const CURRENT = 0;
5070
+ const SETUP = 1;
5071
+ const FIRST = 2;
5072
+ const MODE = 3;
5073
+ const HOLD = 4;
5074
+ const EMPTY = { name: null, duration: 0, timelines: [], ids: /* @__PURE__ */ new Set() };
5075
+ const BONE_PROPERTIES = {
5076
+ rotate: ["rotate"],
5077
+ translate: ["x", "y"],
5078
+ translatex: ["x"],
5079
+ translatey: ["y"],
5080
+ scale: ["scaleX", "scaleY"],
5081
+ scalex: ["scaleX"],
5082
+ scaley: ["scaleY"],
5083
+ shear: ["shearX", "shearY"],
5084
+ shearx: ["shearX"],
5085
+ sheary: ["shearY"],
5086
+ inherit: ["inherit"]
5087
+ };
5088
+ const COLOUR_PROPERTIES = { rgba: ["rgb", "alpha"], rgb: ["rgb"], alpha: ["alpha"], rgba2: ["rgb", "alpha", "rgb2"], rgb2: ["rgb", "rgb2"] };
5089
+ function mixAnimationOf(doc, name) {
5090
+ const anim = doc.animations.find((a) => a.name === name);
5091
+ if (anim === void 0) throw new CoreInputError(`animation "${name}" is not one of this document's [${doc.animations.map((a) => a.name).join(", ")}]`);
5092
+ const t = anim.timelines;
5093
+ const c = anim.constraints;
5094
+ const left = [];
5095
+ for (const [kind, n] of [["ik", c.ik.length], ["transform", c.transform.length], ["path", c.path.length], ["physics", c.physics + (c.physicsKeyed?.length ?? 0)], ["slider", c.slider.length]]) if (n > 0) left.push(`${kind} constraint timelines`);
5096
+ if (t.attachments.some((a) => a.sequence !== null)) left.push("a sequence timeline");
5097
+ if (t.drawOrder.length > 0) left.push("the draw order");
5098
+ for (const a of t.attachments) {
5099
+ if (a.deform === null) continue;
5100
+ const g = doc.skins.find((k) => k.name === a.skin)?.attachments[a.slot]?.[a.attachment]?.geometry;
5101
+ if (g?.kind === "path") left.push(`a deform of path attachment "${a.skin}/${a.slot}/${a.attachment}"`);
5102
+ }
5103
+ if (left.length > 0) throw new CoreInputError(`animation "${name}" keys ${left.join(", ")}, which the live tracks do not mix — they mix bone, slot colour, attachment and deform timelines, and fire events (issue #1336)`);
5104
+ const boneIndex2 = new Map(doc.bones.map((b, i) => [b.name, i]));
5105
+ const slotIndex = new Map(doc.slots.map((s, i) => [s.name, i]));
5106
+ const timelines = [];
5107
+ for (const target of t.slots) {
5108
+ const slot = slotIndex.get(target.name);
5109
+ for (const tl of target.timelines) {
5110
+ if (tl.kind === "attachment") timelines.push({ type: "attachment", slot, keys: tl.keys, ids: [`attachment|${target.name}`], instant: true });
5111
+ else timelines.push({ type: "colour", slot, kind: tl.kind, keys: tl.keys, ids: COLOUR_PROPERTIES[tl.kind].map((p) => `${p}|${target.name}`), instant: false });
5112
+ }
5113
+ }
5114
+ for (const target of t.bones) {
5115
+ const bone2 = boneIndex2.get(target.name);
5116
+ for (const tl of target.timelines) timelines.push({ type: "bone", bone: bone2, kind: tl.kind, keys: tl.keys, ids: BONE_PROPERTIES[tl.kind].map((p) => `${p}|${target.name}`), instant: tl.kind === "inherit" });
5117
+ }
5118
+ for (const a of t.attachments) {
5119
+ if (a.deform === null) continue;
5120
+ const identity = `${a.skin}/${a.slot}/${a.attachment}`;
5121
+ timelines.push({ type: "deform", identity, timeline: a, ids: [`deform|${a.slot}|${identity}`], instant: false });
5122
+ }
5123
+ if (t.events.length > 0) timelines.push({ type: "events", keys: t.events, ids: ["event"], instant: true });
5124
+ return { name, duration: t.duration, timelines, ids: new Set(timelines.flatMap((x) => x.ids)) };
5125
+ }
5126
+ function newEntry(anim, loop) {
5127
+ return {
5128
+ anim,
5129
+ loop,
5130
+ previous: null,
5131
+ next: null,
5132
+ mixingFrom: null,
5133
+ mixingTo: null,
5134
+ delay: 0,
5135
+ trackTime: 0,
5136
+ trackLast: -1,
5137
+ nextTrackLast: -1,
5138
+ trackEnd: Number.MAX_VALUE,
5139
+ animationLast: -1,
5140
+ nextAnimationLast: -1,
5141
+ mixTime: 0,
5142
+ mixDuration: 0,
5143
+ totalAlpha: 0,
5144
+ keepHold: false,
5145
+ timelineMode: [],
5146
+ timelineHoldMix: [],
5147
+ timelinesRotation: []
5148
+ };
5149
+ }
5150
+ function animationTimeOf(e) {
5151
+ if (!e.loop) return Math.min(e.trackTime + 0, e.anim.duration);
5152
+ const d = e.anim.duration;
5153
+ if (d === 0) return 0;
5154
+ return e.trackTime % d + 0;
5155
+ }
5156
+ function trackComplete(e) {
5157
+ const d = e.anim.duration;
5158
+ if (d !== 0) {
5159
+ if (e.loop) return d * (1 + (e.trackTime / d | 0));
5160
+ if (e.trackTime < d) return d;
5161
+ }
5162
+ return e.trackTime;
5163
+ }
5164
+ const signum = (v) => v > 0 ? 1 : v < 0 ? -1 : 0;
5165
+ const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
5166
+ const ATTACH_NONE = 0;
5167
+ const ATTACH_SETUP = 1;
5168
+ const ATTACH_RETAIN = 2;
5169
+ class Pose {
5170
+ constructor(doc, plant) {
5171
+ this.doc = doc;
5172
+ this.plant = plant;
5173
+ this.events = [];
5174
+ this.resolved = /* @__PURE__ */ new Map();
5175
+ this.bones = [...doc.bones];
5176
+ this.active = activeBones(doc);
5177
+ const colour = plant.colour ?? readColour;
5178
+ const gate = plant.slotTimelines ?? slotTimelinesApply;
5179
+ this.live = doc.slots.map((s) => gate(doc, s, this.active));
5180
+ this.slots = doc.slots.map((slot) => {
5181
+ const setupDark = slot.dark === void 0 ? null : readColour(slot.dark);
5182
+ return { placeholder: slot.setup, light: slot.color === void 0 ? [1, 1, 1, 1] : colour(slot.color), dark: setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]], deform: null, seen: [], attachmentState: ATTACH_NONE };
5183
+ });
5184
+ }
5185
+ /** Bone `i`'s locals to write: the setup bone copied on its first write, as `posedBones` copies a keyed one. */
5186
+ bone(i) {
5187
+ const b = this.bones[i];
5188
+ if (!this.active.has(b.name)) return null;
5189
+ if (b === this.doc.bones[i]) this.bones[i] = { ...b };
5190
+ return this.bones[i];
5191
+ }
5192
+ slotLive(i) {
5193
+ return this.live[i];
5194
+ }
5195
+ /** What `placeholder` shows in slot `i` (`Resolved`). */
5196
+ resolve(i, placeholder) {
5197
+ if (placeholder === null) return { key: null, vertex: false, timeline: null, vertices: null };
5198
+ const slot = this.doc.slots[i];
5199
+ const memo = `${i}\0${placeholder}`;
5200
+ const known = this.resolved.get(memo);
5201
+ if (known !== void 0) return known;
5202
+ const s = (this.plant.shown ?? shownAttachment)(this.doc, { ...slot, setup: placeholder });
5203
+ let out;
5204
+ if (s === null) out = { key: null, vertex: false, timeline: null, vertices: null };
5205
+ else if ("conflict" in s) out = { key: `conflict\0${placeholder}`, vertex: false, timeline: null, vertices: null };
5206
+ else out = resolvedOf(this.doc, slot, s);
5207
+ this.resolved.set(memo, out);
5208
+ return out;
5209
+ }
5210
+ /** `SlotPose.setAttachment`: a different attachment clears the deform unless both play the same record's timelines. */
5211
+ setAttachment(i, placeholder) {
5212
+ const s = this.slots[i];
5213
+ const was = this.resolve(i, s.placeholder);
5214
+ const now = this.resolve(i, placeholder);
5215
+ s.placeholder = placeholder;
5216
+ if (was.key === now.key) return;
5217
+ if (!now.vertex || !was.vertex || now.timeline !== was.timeline) s.deform = null;
5218
+ }
5219
+ }
5220
+ function resolvedOf(doc, slot, s) {
5221
+ const g = s.record.geometry;
5222
+ const key = `${s.skin}\0${s.placeholder}`;
5223
+ if (g === void 0 || g.kind === "region") return { key, vertex: false, timeline: null, vertices: null };
5224
+ if (g.kind === "linkedmesh") {
5225
+ const source = doc.skins.find((k) => k.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
5226
+ const vertices = source !== void 0 && source.kind !== "region" && source.kind !== "linkedmesh" ? source.vertices : null;
5227
+ return { key, vertex: true, timeline: s.record.timelines === true ? `${g.skin}/${g.slot}/${g.source}` : `${s.skin}/${slot.name}/${s.placeholder}`, vertices };
5228
+ }
5229
+ return { key, vertex: true, timeline: `${s.skin}/${slot.name}/${s.placeholder}`, vertices: g.vertices };
5230
+ }
5231
+ class Mixer {
5232
+ constructor(doc, plant) {
5233
+ this.doc = doc;
5234
+ this.plant = plant;
5235
+ this.tracks = [];
5236
+ this.animationsChanged = false;
5237
+ this.propertyIds = /* @__PURE__ */ new Map();
5238
+ this.anims = /* @__PURE__ */ new Map();
5239
+ }
5240
+ animation(name) {
5241
+ let a = this.anims.get(name);
5242
+ if (a === void 0) {
5243
+ a = mixAnimationOf(this.doc, name);
5244
+ this.anims.set(name, a);
5245
+ }
5246
+ return a;
5247
+ }
5248
+ changed() {
5249
+ this.animationsChanged = true;
5250
+ }
5251
+ expandToIndex(index) {
5252
+ while (this.tracks.length <= index) this.tracks.push(null);
5253
+ return this.tracks[index];
5254
+ }
5255
+ clearNext(entry) {
5256
+ entry.next = null;
5257
+ }
5258
+ setTrack(index, current) {
5259
+ const from = this.expandToIndex(index);
5260
+ this.tracks[index] = current;
5261
+ current.previous = null;
5262
+ if (from !== null) {
5263
+ from.next = null;
5264
+ current.mixingFrom = from;
5265
+ from.mixingTo = current;
5266
+ current.mixTime = 0;
5267
+ from.timelinesRotation.length = 0;
5268
+ }
5269
+ this.changed();
5270
+ }
5271
+ setAnimation(track, anim, loop) {
5272
+ let current = this.expandToIndex(track);
5273
+ if (current !== null) {
5274
+ if (current.nextTrackLast === -1 && current.anim === anim) {
5275
+ this.tracks[track] = current.mixingFrom;
5276
+ this.changed();
5277
+ this.clearNext(current);
5278
+ current = current.mixingFrom;
5279
+ } else this.clearNext(current);
5280
+ }
5281
+ const entry = newEntry(anim, loop);
5282
+ this.setTrack(track, entry);
5283
+ return entry;
5284
+ }
5285
+ addAnimation(track, anim, loop, delay) {
5286
+ let last = this.expandToIndex(track);
5287
+ if (last !== null) while (last.next !== null) last = last.next;
5288
+ const entry = newEntry(anim, loop);
5289
+ if (last === null) {
5290
+ this.setTrack(track, entry);
5291
+ if (delay < 0) delay = 0;
5292
+ } else {
5293
+ last.next = entry;
5294
+ entry.previous = last;
5295
+ if (delay <= 0) delay = Math.max(delay + this.complete(last) - entry.mixDuration, 0);
5296
+ }
5297
+ entry.delay = delay;
5298
+ return entry;
5299
+ }
5300
+ /** `TrackEntry.setMixDuration(mixDuration, delay)`. */
5301
+ setMixDuration(entry, mixDuration, delay) {
5302
+ entry.mixDuration = mixDuration;
5303
+ if (delay <= 0) delay = entry.previous === null ? 0 : Math.max(delay + this.complete(entry.previous) - mixDuration, 0);
5304
+ entry.delay = delay;
5305
+ }
5306
+ /** `getTrackComplete`, or 0 under the plant that reads a queued delay as "now". */
5307
+ complete(e) {
5308
+ return this.plant.queueDelayNow === true ? e.trackTime : trackComplete(e);
5309
+ }
5310
+ /** `TrackEntry.mix()`: `mixTime / mixDuration` clamped to 1, 1 over a mix of 0 — reversed under the plant. */
5311
+ mixOf(e) {
5312
+ let m;
5313
+ if (e.mixDuration === 0) m = 1;
5314
+ else {
5315
+ m = e.mixTime / e.mixDuration;
5316
+ if (m >= 1) m = 1;
5317
+ }
5318
+ return this.plant.mixReversed === true ? 1 - m : m;
5319
+ }
5320
+ update(delta) {
5321
+ const tracks = this.tracks;
5322
+ for (let i = 0, n = tracks.length; i < n; i++) {
5323
+ const current = tracks[i];
5324
+ if (current === null) continue;
5325
+ current.animationLast = current.nextAnimationLast;
5326
+ current.trackLast = current.nextTrackLast;
5327
+ let currentDelta = delta;
5328
+ if (current.delay > 0) {
5329
+ current.delay -= currentDelta;
5330
+ if (current.delay > 0) continue;
5331
+ currentDelta = -current.delay;
5332
+ current.delay = 0;
5333
+ }
5334
+ let next = current.next;
5335
+ if (next !== null) {
5336
+ const nextTime = current.trackLast - next.delay;
5337
+ if (nextTime >= 0) {
5338
+ next.delay = 0;
5339
+ next.trackTime += nextTime + delta;
5340
+ current.trackTime += currentDelta;
5341
+ this.setTrack(i, next);
5342
+ while (next.mixingFrom !== null) {
5343
+ next.mixTime += delta;
5344
+ next = next.mixingFrom;
5345
+ }
5346
+ continue;
5347
+ }
5348
+ } else if (current.trackLast >= current.trackEnd && current.mixingFrom === null) {
5349
+ tracks[i] = null;
5350
+ this.changed();
5351
+ this.clearNext(current);
5352
+ continue;
5353
+ }
5354
+ if (current.mixingFrom !== null && this.updateMixingFrom(current, delta)) {
5355
+ const from = current.mixingFrom;
5356
+ current.mixingFrom = null;
5357
+ if (from !== null) from.mixingTo = null;
5358
+ this.changed();
5359
+ }
5360
+ current.trackTime += currentDelta;
5361
+ }
5362
+ }
5363
+ updateMixingFrom(to, delta) {
5364
+ const from = to.mixingFrom;
5365
+ if (from === null) return true;
5366
+ const finished = this.updateMixingFrom(from, delta);
5367
+ from.animationLast = from.nextAnimationLast;
5368
+ from.trackLast = from.nextTrackLast;
5369
+ if (to.nextTrackLast !== -1 && to.mixTime >= to.mixDuration) {
5370
+ if (from.totalAlpha === 0 || to.mixDuration === 0) {
5371
+ to.mixingFrom = from.mixingFrom;
5372
+ if (from.mixingFrom !== null) from.mixingFrom.mixingTo = to;
5373
+ if (from.totalAlpha === 0) for (let next = to; next.mixingTo !== null; next = next.mixingTo) next.keepHold = true;
5374
+ this.changed();
5375
+ }
5376
+ return finished;
5377
+ }
5378
+ from.trackTime += delta;
5379
+ to.mixTime += delta;
5380
+ return false;
5381
+ }
5382
+ /** `AnimationState.apply` onto one step's pose, every track in order; returns track 0's current entry's animation time (0 where it applied none). */
5383
+ apply(pose) {
5384
+ if (this.animationsChanged) this.computeModes();
5385
+ let base = 0;
5386
+ const tracks = this.tracks;
5387
+ for (let i = 0, n = tracks.length; i < n; i++) {
5388
+ const current = tracks[i];
5389
+ if (current === null || current.delay > 0) continue;
5390
+ let alpha = 1;
5391
+ if (current.mixingFrom !== null) alpha *= this.applyMixingFrom(current, pose, i);
5392
+ else if (current.trackTime >= current.trackEnd && current.next === null) alpha = 0;
5393
+ const animationLast = current.animationLast;
5394
+ const animationTime = animationTimeOf(current);
5395
+ const timelines = current.anim.timelines;
5396
+ const count = timelines.length;
5397
+ if (i === 0 && alpha === 1) {
5398
+ for (const tl of timelines) {
5399
+ if (tl.type === "attachment") this.applyAttachment(pose, tl, animationTime, SETUP, true);
5400
+ else this.applyTimeline(pose, tl, animationLast, animationTime, true, 1, SETUP, false);
5401
+ }
5402
+ } else {
5403
+ const modes = current.timelineMode;
5404
+ const retain = alpha >= 0;
5405
+ const firstFrame = current.timelinesRotation.length !== count << 1;
5406
+ if (firstFrame) current.timelinesRotation.length = count << 1;
5407
+ timelines.forEach((tl, ii) => {
5408
+ const from = this.fromOf(modes[ii] & MODE, i);
5409
+ if (tl.type === "bone" && tl.kind === "rotate") this.applyRotate(pose, tl, animationTime, alpha, from, current.timelinesRotation, ii << 1, firstFrame);
5410
+ else if (tl.type === "attachment") this.applyAttachment(pose, tl, animationTime, from, retain);
5411
+ else this.applyTimeline(pose, tl, animationLast, animationTime, true, alpha, from, false);
5412
+ });
5413
+ }
5414
+ if (i === 0) base = animationTime;
5415
+ current.nextAnimationLast = animationTime;
5416
+ current.nextTrackLast = current.trackTime;
5417
+ }
5418
+ pose.slots.forEach((s, k) => {
5419
+ if (s.attachmentState === ATTACH_SETUP) pose.setAttachment(k, this.doc.slots[k].setup);
5420
+ });
5421
+ return base;
5422
+ }
5423
+ /** Where a timeline mixes from — `CURRENT` read as `SETUP` above track 0 under the plant. */
5424
+ fromOf(from, track) {
5425
+ return this.plant.higherTracksFromSetup === true && track > 0 && from === CURRENT ? SETUP : from;
5426
+ }
5427
+ applyMixingFrom(to, pose, track) {
5428
+ const from = to.mixingFrom;
5429
+ const fromMix = from.mixingFrom !== null ? this.applyMixingFrom(from, pose, track) : 1;
5430
+ const mix = this.mixOf(to);
5431
+ const a = 1 * fromMix;
5432
+ const keep = 1 - mix * 1;
5433
+ const alphaMix = a * (1 - mix);
5434
+ const alphaHold = keep > 0 ? alphaMix / keep : a;
5435
+ const timelines = from.anim.timelines;
5436
+ const count = timelines.length;
5437
+ const modes = from.timelineMode;
5438
+ const holdMix = from.timelineHoldMix;
5439
+ const firstFrame = from.timelinesRotation.length !== count << 1;
5440
+ if (firstFrame) from.timelinesRotation.length = count << 1;
5441
+ const animationLast = from.animationLast;
5442
+ const animationTime = animationTimeOf(from);
5443
+ const fire = this.plant.eventsUntilHalfMix === true && mix < 0.5;
5444
+ from.totalAlpha = 0;
5445
+ timelines.forEach((tl, i) => {
5446
+ const mode = modes[i];
5447
+ const mixFrom = this.fromOf(mode & MODE, track);
5448
+ let alpha;
5449
+ if ((mode & HOLD) !== 0 && this.plant.holdFaded !== true) {
5450
+ const h = holdMix[i];
5451
+ alpha = h === null || h === void 0 ? alphaHold : alphaHold * (1 - this.mixOf(h));
5452
+ } else alpha = alphaMix;
5453
+ from.totalAlpha += alpha;
5454
+ if (tl.type === "bone" && tl.kind === "rotate") this.applyRotate(pose, tl, animationTime, alpha, mixFrom, from.timelinesRotation, i << 1, firstFrame);
5455
+ else if (tl.type === "attachment") this.applyAttachment(pose, tl, animationTime, mixFrom, false);
5456
+ else this.applyTimeline(pose, tl, animationLast, animationTime, fire, alpha, mixFrom, true);
5457
+ });
5458
+ from.nextAnimationLast = animationTime;
5459
+ from.nextTrackLast = from.trackTime;
5460
+ return mix;
5461
+ }
5462
+ /** `_animationsChanged`: every track's entries' modes, oldest first, the property owners cleared after. */
5463
+ computeModes() {
5464
+ this.animationsChanged = false;
5465
+ this.tracks.forEach((top, i) => {
5466
+ if (top === null) return;
5467
+ let entry = top;
5468
+ while (entry.mixingFrom !== null) entry = entry.mixingFrom;
5469
+ do {
5470
+ this.computeHold(entry, i);
5471
+ entry = entry.mixingTo;
5472
+ } while (entry !== null);
5473
+ });
5474
+ this.propertyIds.clear();
5475
+ }
5476
+ computeHold(entry, track) {
5477
+ const timelines = entry.anim.timelines;
5478
+ const modes = entry.timelineMode;
5479
+ modes.length = timelines.length;
5480
+ const holdMix = entry.timelineHoldMix;
5481
+ holdMix.length = 0;
5482
+ const to = entry.mixingTo;
5483
+ timelines.forEach((tl, i) => {
5484
+ const from = this.propertyFrom(track, tl.ids);
5485
+ let mode;
5486
+ if (to === null || tl.instant || !hasAny(to.anim, tl.ids)) mode = from;
5487
+ else {
5488
+ mode = from | HOLD;
5489
+ for (let next = to.mixingTo; next !== null; next = next.mixingTo) {
5490
+ if (!hasAny(next.anim, tl.ids)) {
5491
+ if (next.mixDuration > 0) holdMix[i] = next;
5492
+ break;
5493
+ }
5494
+ }
5495
+ }
5496
+ if (entry.keepHold) mode = mode & ~HOLD | (modes[i] ?? 0) & HOLD;
5497
+ modes[i] = mode;
5498
+ });
5499
+ }
5500
+ /** `AnimationState.from`: `SETUP` for a property no one keyed yet, `FIRST` for one this track keyed first, `CURRENT` for one a lower track keyed. */
5501
+ propertyFrom(track, ids) {
5502
+ const owners = this.propertyIds;
5503
+ let from = SETUP;
5504
+ for (let i = 0, n = ids.length; i < n; i++) {
5505
+ const owner = owners.get(ids[i]);
5506
+ if (owner === void 0) owners.set(ids[i], track);
5507
+ else {
5508
+ if (owner !== track) {
5509
+ while (++i < n) if (!owners.has(ids[i])) owners.set(ids[i], track);
5510
+ return CURRENT;
5511
+ }
5512
+ from = FIRST;
5513
+ }
5514
+ }
5515
+ return from;
5516
+ }
5517
+ // -------------------------------------------------------------------------
5518
+ // the timelines' `apply`
5519
+ // -------------------------------------------------------------------------
5520
+ values(keys, t) {
5521
+ const index = (this.plant.search ?? keyIndexAt)(keys, t);
5522
+ if (index < 0) return null;
5523
+ const channel2 = this.plant.channel ?? channelAt;
5524
+ return keys[index].values.map((_v, c) => channel2(keys, index, c, t));
5525
+ }
5526
+ /** `Timeline.apply` for every timeline but an attachment's and a rotation mixed below full weight. */
5527
+ applyTimeline(pose, tl, lastTime, time, fire, alpha, from, out) {
5528
+ switch (tl.type) {
5529
+ case "bone":
5530
+ return this.applyBone(pose, tl, time, alpha, from, out);
5531
+ case "colour":
5532
+ return this.applyColour(pose, tl, time, alpha, from);
5533
+ case "deform":
5534
+ return this.applyDeform(pose, tl, time, alpha, from);
5535
+ case "events":
5536
+ if (fire) pose.events.push(...eventsFired(tl.keys, lastTime, time, rawNumber, time < lastTime));
5537
+ return;
5538
+ case "attachment":
5539
+ return;
5540
+ }
5541
+ }
5542
+ applyBone(pose, tl, time, alpha, from, out) {
5543
+ const bone2 = pose.bone(tl.bone);
5544
+ if (bone2 === null) return;
5545
+ const setup = this.doc.bones[tl.bone];
5546
+ if (tl.kind === "inherit") {
5547
+ const restore = () => {
5548
+ if (setup.inheritMode === void 0) delete bone2.inheritMode;
5549
+ else bone2.inheritMode = setup.inheritMode;
5550
+ };
5551
+ if (out) {
5552
+ if (from !== CURRENT) restore();
5553
+ return;
5554
+ }
5555
+ const index = (this.plant.search ?? keyIndexAt)(tl.keys, time);
5556
+ if (index < 0) {
5557
+ if (from !== CURRENT) restore();
5558
+ } else bone2.inheritMode = tl.keys[index].mode;
5559
+ return;
5560
+ }
5561
+ const v = this.values(tl.keys, time);
5562
+ const relative = (field, i) => {
5563
+ const s = setup[field] ?? 0;
5564
+ const c = bone2[field] ?? 0;
5565
+ bone2[field] = v === null ? beforeFirstKey(from, alpha, c, s) : from === SETUP ? s + v[i] * alpha : c + (v[i] + s - c) * alpha;
5566
+ };
5567
+ const pair = (fx, fy) => {
5568
+ const sx = setup[fx] ?? 0;
5569
+ const sy = setup[fy] ?? 0;
5570
+ if (v === null) {
5571
+ if (from === SETUP) {
5572
+ bone2[fx] = sx;
5573
+ bone2[fy] = sy;
5574
+ } else if (from === FIRST) {
5575
+ bone2[fx] = (bone2[fx] ?? 0) + (sx - (bone2[fx] ?? 0)) * alpha;
5576
+ bone2[fy] = (bone2[fy] ?? 0) + (sy - (bone2[fy] ?? 0)) * alpha;
5577
+ }
5578
+ return;
5579
+ }
5580
+ if (from === SETUP) {
5581
+ bone2[fx] = sx + v[0] * alpha;
5582
+ bone2[fy] = sy + v[1] * alpha;
5583
+ } else {
5584
+ bone2[fx] = (bone2[fx] ?? 0) + (sx + v[0] - (bone2[fx] ?? 0)) * alpha;
5585
+ bone2[fy] = (bone2[fy] ?? 0) + (sy + v[1] - (bone2[fy] ?? 0)) * alpha;
5586
+ }
5587
+ };
5588
+ const scale = (field, i) => {
5589
+ const s = setup[field] ?? 1;
5590
+ const c = bone2[field] ?? 1;
5591
+ bone2[field] = v === null ? beforeFirstKey(from, alpha, c, s) : scaleValue(v[i] * s, alpha, from, out, c, s);
5592
+ };
5593
+ switch (tl.kind) {
5594
+ case "rotate":
5595
+ relative("rotation", 0);
5596
+ break;
5597
+ case "translate":
5598
+ pair("x", "y");
5599
+ break;
5600
+ case "translatex":
5601
+ relative("x", 0);
5602
+ break;
5603
+ case "translatey":
5604
+ relative("y", 0);
5605
+ break;
5606
+ case "shear":
5607
+ pair("shearX", "shearY");
5608
+ break;
5609
+ case "shearx":
5610
+ relative("shearX", 0);
5611
+ break;
5612
+ case "sheary":
5613
+ relative("shearY", 0);
5614
+ break;
5615
+ case "scale":
5616
+ if (v === null) {
5617
+ const sx = setup.scaleX ?? 1;
5618
+ const sy = setup.scaleY ?? 1;
5619
+ if (from === SETUP) {
5620
+ bone2.scaleX = sx;
5621
+ bone2.scaleY = sy;
5622
+ } else if (from === FIRST) {
5623
+ bone2.scaleX = (bone2.scaleX ?? 1) + (sx - (bone2.scaleX ?? 1)) * alpha;
5624
+ bone2.scaleY = (bone2.scaleY ?? 1) + (sy - (bone2.scaleY ?? 1)) * alpha;
5625
+ }
5626
+ } else {
5627
+ bone2.scaleX = scaleValue(v[0] * (setup.scaleX ?? 1), alpha, from, out, bone2.scaleX ?? 1, setup.scaleX ?? 1);
5628
+ bone2.scaleY = scaleValue(v[1] * (setup.scaleY ?? 1), alpha, from, out, bone2.scaleY ?? 1, setup.scaleY ?? 1);
5629
+ }
5630
+ break;
5631
+ case "scalex":
5632
+ scale("scaleX", 0);
5633
+ break;
5634
+ case "scaley":
5635
+ scale("scaleY", 0);
5636
+ break;
5637
+ }
5638
+ }
5639
+ /** `applyRotateTimeline`: a rotation mixed below full weight turns the way it first turned. */
5640
+ applyRotate(pose, tl, time, alpha, from, rotations, i, firstFrame) {
5641
+ if (firstFrame) rotations[i] = 0;
5642
+ if (alpha === 1) {
5643
+ this.applyBone(pose, tl, time, 1, from, false);
5644
+ return;
5645
+ }
5646
+ const bone2 = pose.bone(tl.bone);
5647
+ if (bone2 === null) return;
5648
+ const setup = this.doc.bones[tl.bone].rotation ?? 0;
5649
+ const v = this.values(tl.keys, time);
5650
+ let r1;
5651
+ let r2;
5652
+ if (v === null) {
5653
+ if (from === SETUP) {
5654
+ bone2.rotation = setup;
5655
+ return;
5656
+ }
5657
+ if (from === CURRENT) return;
5658
+ r1 = bone2.rotation ?? 0;
5659
+ r2 = setup;
5660
+ } else {
5661
+ r1 = from === SETUP ? setup : bone2.rotation ?? 0;
5662
+ r2 = setup + v[0];
5663
+ }
5664
+ let total;
5665
+ let diff = r2 - r1;
5666
+ diff -= Math.ceil(diff / 360 - 0.5) * 360;
5667
+ if (diff === 0) total = rotations[i];
5668
+ else {
5669
+ let lastTotal;
5670
+ let lastDiff;
5671
+ if (firstFrame) {
5672
+ lastTotal = 0;
5673
+ lastDiff = diff;
5674
+ } else {
5675
+ lastTotal = rotations[i];
5676
+ lastDiff = rotations[i + 1] ?? Number.NaN;
5677
+ }
5678
+ const loops = lastTotal - lastTotal % 360;
5679
+ total = diff + loops;
5680
+ const current = diff >= 0;
5681
+ let dir = lastTotal >= 0;
5682
+ if (Math.abs(lastDiff) <= 90 && signum(lastDiff) !== signum(diff)) {
5683
+ if (Math.abs(lastTotal - loops) > 180) {
5684
+ total += 360 * signum(lastTotal);
5685
+ dir = current;
5686
+ } else if (loops !== 0) total -= 360 * signum(lastTotal);
5687
+ else dir = current;
5688
+ }
5689
+ if (dir !== current) total += 360 * signum(lastTotal);
5690
+ rotations[i] = total;
5691
+ }
5692
+ rotations[i + 1] = diff;
5693
+ bone2.rotation = r1 + total * alpha;
5694
+ }
5695
+ /** `applyAttachmentTimeline`: shown for good when retained, else for the deform timelines only and set back at the end of the apply. */
5696
+ applyAttachment(pose, tl, time, from, retain) {
5697
+ if (!pose.slotLive(tl.slot)) return;
5698
+ const s = pose.slots[tl.slot];
5699
+ if (!retain && s.attachmentState === ATTACH_RETAIN) return;
5700
+ const index = (this.plant.search ?? keyIndexAt)(tl.keys, time);
5701
+ let setup = index < 0;
5702
+ let name = null;
5703
+ if (!setup) {
5704
+ name = tl.keys[index].name ?? null;
5705
+ setup = !retain && name === null;
5706
+ }
5707
+ if (setup) {
5708
+ if (from === CURRENT) return;
5709
+ name = this.doc.slots[tl.slot].setup;
5710
+ }
5711
+ pose.setAttachment(tl.slot, name);
5712
+ if (retain) s.attachmentState = ATTACH_RETAIN;
5713
+ else if (!setup) s.attachmentState = ATTACH_SETUP;
5714
+ }
5715
+ applyColour(pose, tl, time, alpha, from) {
5716
+ if (!pose.slotLive(tl.slot)) return;
5717
+ const slot = this.doc.slots[tl.slot];
5718
+ const s = pose.slots[tl.slot];
5719
+ const l = s.light;
5720
+ const d = s.dark;
5721
+ const sl = slot.color === void 0 ? [1, 1, 1, 1] : (this.plant.colour ?? readColour)(slot.color);
5722
+ const sdFull = slot.dark === void 0 ? null : readColour(slot.dark);
5723
+ const sd = sdFull === null ? [0, 0, 0] : [sdFull[0], sdFull[1], sdFull[2]];
5724
+ const clampLight = () => {
5725
+ for (let k = 0; k < 4; k++) l[k] = clamp01(l[k]);
5726
+ };
5727
+ const v = this.values(tl.keys, time);
5728
+ const channels = SLOT_COLOUR_TIMELINES[tl.kind].channels.length;
5729
+ switch (tl.kind) {
5730
+ case "rgba": {
5731
+ if (v === null) {
5732
+ if (from === SETUP) for (let k = 0; k < 4; k++) l[k] = sl[k];
5733
+ else if (from === FIRST) {
5734
+ for (let k = 0; k < 4; k++) l[k] += (sl[k] - l[k]) * alpha;
5735
+ clampLight();
5736
+ }
5737
+ return;
5738
+ }
5739
+ if (alpha === 1) for (let k = 0; k < 4; k++) l[k] = v[k];
5740
+ else if (from === SETUP) for (let k = 0; k < 4; k++) l[k] = sl[k] + (v[k] - sl[k]) * alpha;
5741
+ else for (let k = 0; k < 4; k++) l[k] += (v[k] - l[k]) * alpha;
5742
+ clampLight();
5743
+ return;
5744
+ }
5745
+ case "rgb": {
5746
+ if (v === null) {
5747
+ if (from === SETUP) for (let k = 0; k < 3; k++) l[k] = sl[k];
5748
+ else if (from === FIRST) for (let k = 0; k < 3; k++) l[k] += (sl[k] - l[k]) * alpha;
5749
+ return;
5750
+ }
5751
+ const rgb = [v[0], v[1], v[2]];
5752
+ if (alpha !== 1) for (let k = 0; k < 3; k++) rgb[k] = from === SETUP ? sl[k] + (rgb[k] - sl[k]) * alpha : l[k] + (rgb[k] - l[k]) * alpha;
5753
+ for (let k = 0; k < 3; k++) l[k] = clamp01(rgb[k]);
5754
+ return;
5755
+ }
5756
+ case "alpha": {
5757
+ if (v === null) {
5758
+ if (from === SETUP) l[3] = sl[3];
5759
+ else if (from === FIRST) l[3] += (sl[3] - l[3]) * alpha;
5760
+ return;
5761
+ }
5762
+ let a = v[0];
5763
+ if (alpha !== 1) a = from === SETUP ? sl[3] + (a - sl[3]) * alpha : l[3] + (a - l[3]) * alpha;
5764
+ l[3] = clamp01(a);
5765
+ return;
5766
+ }
5767
+ case "rgba2":
5768
+ case "rgb2": {
5769
+ if (d === null) return;
5770
+ const withAlpha = tl.kind === "rgba2";
5771
+ const lightN = withAlpha ? 4 : 3;
5772
+ if (v === null) {
5773
+ if (from === SETUP) {
5774
+ for (let k = 0; k < lightN; k++) l[k] = sl[k];
5775
+ for (let k = 0; k < 3; k++) d[k] = sd[k];
5776
+ } else if (from === FIRST) {
5777
+ for (let k = 0; k < lightN; k++) l[k] += (sl[k] - l[k]) * alpha;
5778
+ if (withAlpha) clampLight();
5779
+ for (let k = 0; k < 3; k++) d[k] += (sd[k] - d[k]) * alpha;
5780
+ }
5781
+ return;
5782
+ }
5783
+ const light = v.slice(0, lightN);
5784
+ const dark = v.slice(lightN, channels);
5785
+ if (withAlpha) {
5786
+ if (alpha === 1) for (let k = 0; k < 4; k++) l[k] = light[k];
5787
+ else if (from === SETUP) {
5788
+ for (let k = 0; k < 4; k++) l[k] = sl[k] + (light[k] - sl[k]) * alpha;
5789
+ for (let k = 0; k < 3; k++) dark[k] = sd[k] + (dark[k] - sd[k]) * alpha;
5790
+ } else {
5791
+ for (let k = 0; k < 4; k++) l[k] += (light[k] - l[k]) * alpha;
5792
+ for (let k = 0; k < 3; k++) dark[k] = d[k] + (dark[k] - d[k]) * alpha;
5793
+ }
5794
+ clampLight();
5795
+ } else {
5796
+ if (alpha !== 1) {
5797
+ for (let k = 0; k < 3; k++) {
5798
+ light[k] = from === SETUP ? sl[k] + (light[k] - sl[k]) * alpha : l[k] + (light[k] - l[k]) * alpha;
5799
+ dark[k] = from === SETUP ? sd[k] + (dark[k] - sd[k]) * alpha : d[k] + (dark[k] - d[k]) * alpha;
5800
+ }
5801
+ }
5802
+ for (let k = 0; k < 3; k++) l[k] = clamp01(light[k]);
5803
+ }
5804
+ for (let k = 0; k < 3; k++) d[k] = clamp01(dark[k]);
5805
+ return;
5806
+ }
5807
+ }
5808
+ }
5809
+ /** `DeformTimeline.apply`: every live slot showing the record the timeline keys, its deform array mixed. */
5810
+ applyDeform(pose, tl, time, alpha, from) {
5811
+ const keys = tl.timeline.deform;
5812
+ if (keys === null) return;
5813
+ pose.slots.forEach((s, k) => {
5814
+ if (!pose.slotLive(k)) return;
5815
+ const shown = pose.resolve(k, s.placeholder);
5816
+ if (!shown.vertex || shown.timeline !== tl.identity || shown.vertices === null) return;
5817
+ const vertices = shown.vertices;
5818
+ if (!s.seen.includes(tl.identity)) s.seen.push(tl.identity);
5819
+ const target = (this.plant.deform ?? deformAt)(vertices, keys, time);
5820
+ let mixFrom = from;
5821
+ if (s.deform === null) mixFrom = SETUP;
5822
+ if (target === null) {
5823
+ if (mixFrom === SETUP) s.deform = null;
5824
+ else if (mixFrom === FIRST) {
5825
+ if (alpha === 1) {
5826
+ s.deform = null;
5827
+ return;
5828
+ }
5829
+ const current = s.deform;
5830
+ if (!vertices.weighted) {
5831
+ const setup = setupArray(vertices);
5832
+ s.deform = current.map((c, i) => c + (setup[i] - c) * alpha);
5833
+ } else {
5834
+ const keep = 1 - alpha;
5835
+ s.deform = current.map((c) => c * keep);
5836
+ }
5837
+ }
5838
+ return;
5839
+ }
5840
+ if (alpha === 1) s.deform = [...target];
5841
+ else if (mixFrom === SETUP) {
5842
+ if (!vertices.weighted) {
5843
+ const setup = setupArray(vertices);
5844
+ s.deform = target.map((v, i) => setup[i] + (v - setup[i]) * alpha);
5845
+ } else s.deform = target.map((v) => v * alpha);
5846
+ } else {
5847
+ const current = s.deform;
5848
+ s.deform = current.map((c, i) => c + (target[i] - c) * alpha);
5849
+ }
5850
+ });
5851
+ }
5852
+ }
5853
+ function beforeFirstKey(from, alpha, current, setup) {
5854
+ if (from === SETUP) return setup;
5855
+ if (from === FIRST) return current + (setup - current) * alpha;
5856
+ return current;
5857
+ }
5858
+ function scaleValue(value, alpha, from, out, current, setup) {
5859
+ if (alpha === 1) return value;
5860
+ let base = from === SETUP ? setup : current;
5861
+ if (out) return base + (Math.abs(value) * Math.sign(base) - base) * alpha;
5862
+ base = Math.abs(base) * Math.sign(value);
5863
+ return base + (value - base) * alpha;
5864
+ }
5865
+ function hasAny(anim, ids) {
5866
+ for (const id of ids) if (anim.ids.has(id)) return true;
5867
+ return false;
5868
+ }
5869
+ function trackIndex(track, call) {
5870
+ if (typeof track !== "number" || !Number.isInteger(track) || track < 0) throw new CoreInputError(`the live tracks' ${call} names track ${String(track)}, not a whole number at or above 0`);
5871
+ return track;
5872
+ }
5873
+ function finiteAtOrAbove0(v, what, call) {
5874
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) throw new CoreInputError(`the live tracks' ${call} ${what} is ${String(v)}, not a finite time at or above 0`);
5875
+ return v;
5876
+ }
5877
+ function loopOf(options, call) {
5878
+ if (typeof options !== "object" || options === null || typeof options.loop !== "boolean") throw new CoreInputError(`the live tracks' ${call} options.loop is ${JSON.stringify(options?.loop)}, not true or false`);
5879
+ return options.loop;
5880
+ }
5881
+ function delayOf(v, call) {
5882
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new CoreInputError(`the live tracks' ${call} delay is ${String(v)}, not a finite number`);
5883
+ return v;
5884
+ }
5885
+ function openTracks(doc, options, plant = {}) {
5886
+ if (typeof options !== "object" || options === null || options.reset !== "animation" && options.reset !== "setup") throw new CoreInputError(`the live tracks' options.reset is ${JSON.stringify(options?.reset)}, not "animation" or "setup"`);
5887
+ if (options.adjust !== void 0 && typeof options.adjust !== "function") throw new CoreInputError(`the live tracks' options.adjust is ${typeof options.adjust}, not a function`);
5888
+ return new Tracks(doc, options.reset, options.adjust, plant, openRawTracksPath(doc, plant));
5889
+ }
5890
+ class Tracks {
5891
+ constructor(doc, reset, adjust, plant, path) {
5892
+ this.doc = doc;
5893
+ this.reset = reset;
5894
+ this.adjust = adjust;
5895
+ this.plant = plant;
5896
+ this.path = path;
5897
+ this.started = false;
5898
+ this.steps = 0;
5899
+ this.time = 0;
5900
+ this.refused = null;
5901
+ this.mixer = new Mixer(doc, plant);
5902
+ }
5903
+ get trackTime() {
5904
+ return this.time;
5905
+ }
5906
+ live(call) {
5907
+ if (this.refused !== null) throw new CoreInputError(`the live tracks were refused at ${this.refused}, after their entries and physics had moved; open them again (${call} not made)`);
5908
+ }
5909
+ setAnimation(track, animation, options) {
5910
+ this.live("setAnimation");
5911
+ const i = trackIndex(track, "setAnimation");
5912
+ const loop = loopOf(options, "setAnimation");
5913
+ const mix = finiteAtOrAbove0(options.mixDuration, "mixDuration", "setAnimation");
5914
+ const entry = this.mixer.setAnimation(i, this.mixer.animation(animation), loop);
5915
+ entry.mixDuration = mix;
5916
+ }
5917
+ setEmptyAnimation(track, mixDuration) {
5918
+ this.live("setEmptyAnimation");
5919
+ const i = trackIndex(track, "setEmptyAnimation");
5920
+ const mix = finiteAtOrAbove0(mixDuration, "mixDuration", "setEmptyAnimation");
5921
+ const entry = this.mixer.setAnimation(i, EMPTY, false);
5922
+ entry.mixDuration = mix;
5923
+ entry.trackEnd = mix;
5924
+ }
5925
+ addAnimation(track, animation, options) {
5926
+ this.live("addAnimation");
5927
+ const i = trackIndex(track, "addAnimation");
5928
+ const loop = loopOf(options, "addAnimation");
5929
+ const mix = finiteAtOrAbove0(options.mixDuration, "mixDuration", "addAnimation");
5930
+ const delay = delayOf(options.delay, "addAnimation");
5931
+ const entry = this.mixer.addAnimation(i, this.mixer.animation(animation), loop, delay);
5932
+ this.mixer.setMixDuration(entry, mix, delay);
5933
+ }
5934
+ addEmptyAnimation(track, mixDuration, delay) {
5935
+ this.live("addEmptyAnimation");
5936
+ const i = trackIndex(track, "addEmptyAnimation");
5937
+ const mix = finiteAtOrAbove0(mixDuration, "mixDuration", "addEmptyAnimation");
5938
+ const d = delayOf(delay, "addEmptyAnimation");
5939
+ const entry = this.mixer.addAnimation(i, EMPTY, false, d);
5940
+ if (d <= 0) entry.delay = Math.max(entry.delay + entry.mixDuration - mix, 0);
5941
+ entry.mixDuration = mix;
5942
+ entry.trackEnd = mix;
5943
+ }
5944
+ current(track) {
5945
+ const e = this.mixer.tracks[trackIndex(track, "current")] ?? null;
5946
+ if (e === null) return null;
5947
+ return { animation: e.anim.name, loop: e.loop, trackTime: e.trackTime, animationTime: animationTimeOf(e), duration: e.anim.duration };
5948
+ }
5949
+ start() {
5950
+ this.live("start");
5951
+ if (this.started) throw new CoreInputError("the live tracks were started already; pose 0 is posed once");
5952
+ this.started = true;
5953
+ return this.guard("start", () => {
5954
+ if (this.reset === "setup") return this.path.setupPose();
5955
+ return this.pose(0, true);
5956
+ });
5957
+ }
5958
+ step(dt) {
5959
+ const n = this.steps + 1;
5960
+ this.live(`step ${n}`);
5961
+ if (!this.started) throw new CoreInputError(`the live tracks' step ${n} comes before start(), which poses pose 0`);
5962
+ if (typeof dt !== "number" || !Number.isFinite(dt) || dt < 0) throw new CoreInputError(`the live tracks' step ${n} is ${String(dt)}, not a finite time at or above 0`);
5963
+ return this.guard(`step ${n}`, () => {
5964
+ this.time += dt;
5965
+ this.mixer.update(dt);
5966
+ const pose = this.pose(dt, false);
5967
+ this.steps = n;
5968
+ return pose;
5969
+ });
5970
+ }
5971
+ /** `f`, the state refused for good if it throws once anything has moved. */
5972
+ guard(call, f) {
5973
+ try {
5974
+ return f();
5975
+ } catch (err) {
5976
+ this.refused = `${call} (${err instanceof Error ? err.message : String(err)})`;
5977
+ throw err;
5978
+ }
5979
+ }
5980
+ /** One pose: the entries applied over the setup pose, then the raw pose path (`openRawTracksPath`). */
5981
+ pose(dt, first) {
5982
+ const { doc, path } = this;
5983
+ const ctx = path.ctx;
5984
+ const before = ctx.time;
5985
+ ctx.time += dt;
5986
+ const pose = new Pose(doc, this.plant);
5987
+ const animationTime = this.mixer.apply(pose);
5988
+ const mixed = new Map(doc.slots.map((s, k) => [s.name, { deform: pose.slots[k].deform, seen: pose.slots[k].seen }]));
5989
+ return path.step(first, this.time, animationTime, {
5990
+ world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjust: this.adjust }),
5991
+ slots: (sliders, placeholders) => slotRowsFrom(doc, path.raw, sliders, placeholders, (_slot, _live) => {
5992
+ const s = pose.slots[doc.slots.indexOf(_slot)];
5993
+ return { placeholder: s.placeholder, light: s.light, dark: s.dark };
5994
+ }),
5995
+ drawOrder: [],
5996
+ at: 0,
5997
+ sample: null,
5998
+ mixed,
5999
+ events: () => pose.events
6000
+ });
6001
+ }
6002
+ }
5009
6003
  function documentPageLookup(pages) {
5010
6004
  const first = /* @__PURE__ */ new Map();
5011
6005
  for (const page of pages) {
@@ -5236,7 +6230,7 @@ function cutBackingPixels(placed, pageImages) {
5236
6230
  const plan = planCut(placed, image);
5237
6231
  return plan.cw * plan.ch;
5238
6232
  }
5239
- async function cutRegion(placed, pageImages) {
6233
+ async function cutRegion(placed, pageImages, encoding) {
5240
6234
  const { region, page } = placed;
5241
6235
  const image = pageImages.get(page.name);
5242
6236
  if (!image) throw new Error(`Missing page image: ${page.name}`);
@@ -5255,6 +6249,7 @@ async function cutRegion(placed, pageImages) {
5255
6249
  }
5256
6250
  const source = straightAlphaSource(image, page.pma === true);
5257
6251
  ctx.drawImage(source, plan.sx, plan.sy, plan.sw, plan.sh, 0, 0, plan.sw, plan.sh);
6252
+ if (encoding === "data") return { url: canvas.toDataURL("image/png"), width: w, height: h };
5258
6253
  const blob = await new Promise((resolve, reject) => {
5259
6254
  canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), "image/png");
5260
6255
  });
@@ -5262,7 +6257,9 @@ async function cutRegion(placed, pageImages) {
5262
6257
  ownedUrls.add(url);
5263
6258
  return { url, width: w, height: h };
5264
6259
  }
5265
- async function unpackRegions(pages, pageImages) {
6260
+ async function unpackRegions(pages, pageImages, options = {}) {
6261
+ const encoding = options.encoding ?? "blob";
6262
+ if (encoding !== "blob" && encoding !== "data") throw new Error(`encoding must be 'blob' or 'data', got ${String(options.encoding)}`);
5266
6263
  const regions = pages.flatMap((page) => page.regions.map((region) => ({ region, page })));
5267
6264
  const cuts = new Array(regions.length);
5268
6265
  let failedIndex = -1;
@@ -5283,7 +6280,7 @@ async function unpackRegions(pages, pageImages) {
5283
6280
  const job = { done: false, promise: Promise.resolve() };
5284
6281
  job.promise = (async () => {
5285
6282
  try {
5286
- cuts[index] = await cutRegion(region, pageImages);
6283
+ cuts[index] = await cutRegion(region, pageImages, encoding);
5287
6284
  } catch (error) {
5288
6285
  if (failedIndex === -1 || index < failedIndex) {
5289
6286
  failedIndex = index;
@@ -7128,6 +8125,10 @@ async function loadModel(options) {
7128
8125
  `${modelUrl} is a Spine export; this player reads a compiled model document (${CORE_DOCUMENT_SPECS[0]}, skeleton.model.json) — build it with \`rigc build\``
7129
8126
  );
7130
8127
  }
8128
+ const regions = options.regions ?? "blob";
8129
+ if (regions !== "blob" && regions !== "data" && regions !== "none") {
8130
+ throw new Error(`regions must be 'blob', 'data' or 'none', got ${String(options.regions)}`);
8131
+ }
7131
8132
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
7132
8133
  const resolvePage = options.resolvePage ?? resolveAgainstModel;
7133
8134
  const read = readModelText(await fetchText(fetchImpl, modelUrl), modelUrl);
@@ -7138,13 +8139,14 @@ async function loadModel(options) {
7138
8139
  pageImages.set(page.name, await loadImage(resolvePage(page.name, modelUrl), crossOrigin));
7139
8140
  })
7140
8141
  );
7141
- const regionImages = await unpackRegions(pages, pageImages);
8142
+ const regionImages = regions === "none" ? /* @__PURE__ */ new Map() : await unpackRegions(pages, pageImages, { encoding: regions });
7142
8143
  let disposed = false;
7143
8144
  return {
7144
8145
  spec: read.spec,
7145
8146
  animations: read.doc.animations.map((a) => ({ name: a.name, duration: a.timelines.duration })),
7146
8147
  skins: read.doc.skins.map((s) => s.name),
7147
8148
  stage: stageOf(read.doc),
8149
+ regions,
7148
8150
  regionImages,
7149
8151
  pageImages,
7150
8152
  document: read,
@@ -7311,10 +8313,30 @@ class RigPlayer {
7311
8313
  meshBackend;
7312
8314
  view;
7313
8315
  source;
8316
+ read;
8317
+ skinName;
7314
8318
  seekStep;
8319
+ /**
8320
+ * The single live track (`openTrack`): what a plain `play` on track 0 runs
8321
+ * on, as every 1.x player did — one animation, every construct the core
8322
+ * poses, `seek` and a skin change by reopening. Null once the player is
8323
+ * composed.
8324
+ */
7315
8325
  track = null;
8326
+ /**
8327
+ * The live tracks (`openTracks`): opened by the first composing call — a
8328
+ * `play` with a mix or on another track, a `queue`, a `clear` — from the
8329
+ * single track's animation stepped to its time. The mixing is the Spine
8330
+ * runtime's, which does not mix a constraint, sequence, draw-order or
8331
+ * path-deform timeline, so such an animation is refused here and plays on
8332
+ * the single track only.
8333
+ */
8334
+ tracks = null;
8335
+ /** Track 0's animation and loop as `play` last set them — what `seek` and a skin change reopen. */
7316
8336
  animation = null;
7317
8337
  loop = true;
8338
+ /** One past the highest track touched — how many `current(track)` reads a step takes. */
8339
+ trackCount = 0;
7318
8340
  paused = false;
7319
8341
  pose = null;
7320
8342
  setup = null;
@@ -7329,6 +8351,8 @@ class RigPlayer {
7329
8351
  };
7330
8352
  constructor(root, assets, options = {}) {
7331
8353
  const read = assets.document;
8354
+ this.read = read;
8355
+ this.skinName = options.skin;
7332
8356
  this.view = options.skin === void 0 ? underNoSkin(read.doc) : underSkin(read.doc, options.skin);
7333
8357
  this.source = frameSource(this.view, read.parsed);
7334
8358
  this.seekStep = options.seekStep ?? 1 / 60;
@@ -7367,27 +8391,127 @@ class RigPlayer {
7367
8391
  return renderer2;
7368
8392
  }
7369
8393
  if (mode !== "dom") throw new Error(`mode must be 'dom' or 'canvas', got ${String(mode)}`);
8394
+ if (this.assets.regions === "none") {
8395
+ throw new Error(
8396
+ "these assets were loaded with regions: 'none' (no region bitmaps, for the canvas mode only); the DOM mode needs a load with regions 'blob' or 'data'"
8397
+ );
8398
+ }
7370
8399
  const renderer = new DomRenderer(this.root, this.assets.regionImages, this.assets.pageImages);
7371
8400
  if (this.meshBackend) renderer.meshBackend = this.meshBackend;
7372
8401
  return renderer;
7373
8402
  }
7374
- /** The animation the track is playing, or null before `play`. */
8403
+ /** What track 0 plays, or null before `play` and after a `clear` of track 0 has mixed out. */
7375
8404
  get current() {
7376
- return this.animation;
8405
+ return this.tracks !== null ? this.tracks.current(0)?.animation ?? null : this.animation;
7377
8406
  }
7378
- /** The track time — the running sum of the steps since `play` (or the last `seek`). */
8407
+ /** The player's time — the running sum of the steps since `play` (or the last `seek`). */
7379
8408
  get time() {
7380
- return this.track?.trackTime ?? 0;
8409
+ return this.tracks?.trackTime ?? this.track?.trackTime ?? 0;
7381
8410
  }
7382
8411
  /**
7383
- * Opens `name` on the track at its first pose, the physics reset there. The
7384
- * pose is drawn on the next `frame`.
8412
+ * Plays `name` on a track — track 0 by default, which is what `play(name)`
8413
+ * has always meant: the first `play` opens the tracks at their first pose
8414
+ * with the physics reset there, and the pose is drawn on the next `frame`.
8415
+ * Later calls replace what the track plays: the entry it replaces mixes out
8416
+ * over `mix` seconds (0 cuts), and every entry queued on that track is
8417
+ * dropped. The pose the player holds is re-posed at once, so a held player
8418
+ * shows the change on its next `draw`. The mixing is the Spine runtime's,
8419
+ * which rig-c holds at tolerance 0; an animation keying what the tracks do
8420
+ * not mix — a constraint, a sequence, the draw order, a path's deform — is
8421
+ * refused here, by name, in the core's words.
7385
8422
  */
7386
8423
  play(name, options = {}) {
7387
- this.loop = options.loop ?? true;
7388
- this.animation = name;
8424
+ const track = options.track ?? 0;
8425
+ const loop = options.loop ?? true;
8426
+ const mix = options.mix ?? 0;
7389
8427
  this.paused = false;
7390
- this.open();
8428
+ if (this.tracks === null && track === 0 && mix === 0) {
8429
+ this.animation = name;
8430
+ this.loop = loop;
8431
+ this.track = openTrack(this.view, name, { loop, reset: "animation", adjust: this.adjustHook });
8432
+ this.pose = this.track.first;
8433
+ return;
8434
+ }
8435
+ const tracks = this.compose();
8436
+ tracks.setAnimation(track, name, { loop, mixDuration: mix });
8437
+ this.trackCount = Math.max(this.trackCount, track + 1);
8438
+ if (track === 0) {
8439
+ this.animation = name;
8440
+ this.loop = loop;
8441
+ }
8442
+ this.pose = tracks.step(0);
8443
+ }
8444
+ /**
8445
+ * Queues `name` after the last entry on `track` (`QueueOptions`): with
8446
+ * `delay` at or below 0 it starts when that entry completes less its own
8447
+ * `mix`, so a one-shot hands over to it across a mix; on an empty track it
8448
+ * is set. Before any `play`, the tracks are opened for it.
8449
+ */
8450
+ queue(name, options = {}) {
8451
+ const track = options.track ?? 0;
8452
+ const tracks = this.compose();
8453
+ tracks.addAnimation(track, name, { loop: options.loop ?? true, mixDuration: options.mix ?? 0, delay: options.delay ?? 0 });
8454
+ this.trackCount = Math.max(this.trackCount, track + 1);
8455
+ this.pose = tracks.step(0);
8456
+ }
8457
+ /**
8458
+ * Mixes `track` out to nothing over `mix` seconds — the layer or one-shot
8459
+ * gone and the tracks below showing through. Track 0 cleared shows the
8460
+ * setup pose. The pose the player holds is re-posed at once.
8461
+ */
8462
+ clear(track = 0, options = {}) {
8463
+ const tracks = this.compose();
8464
+ tracks.setEmptyAnimation(track, options.mix ?? 0);
8465
+ this.trackCount = Math.max(this.trackCount, track + 1);
8466
+ this.pose = tracks.step(0);
8467
+ }
8468
+ /**
8469
+ * Queues a mix out to nothing after the last entry on `track`, over `mix`
8470
+ * seconds; with `delay` 0 (the default) the mix ends as that entry
8471
+ * completes, so a one-shot queued this way plays once and fades out over
8472
+ * its last `mix` seconds.
8473
+ */
8474
+ queueClear(track = 0, options = {}) {
8475
+ const tracks = this.compose();
8476
+ tracks.addEmptyAnimation(track, options.mix ?? 0, options.delay ?? 0);
8477
+ this.trackCount = Math.max(this.trackCount, track + 1);
8478
+ this.pose = tracks.step(0);
8479
+ }
8480
+ /** `track`'s current entry, or null where it plays nothing (and before `play`). */
8481
+ entry(track = 0) {
8482
+ if (this.tracks !== null) {
8483
+ const view = this.tracks.current(track);
8484
+ return view === null ? null : { animation: view.animation, loop: view.loop, trackTime: view.trackTime, animationTime: view.animationTime, duration: view.duration };
8485
+ }
8486
+ const single = this.track;
8487
+ if (track !== 0 || single === null) return null;
8488
+ return { animation: this.animation, loop: this.loop, trackTime: single.trackTime, animationTime: single.animationTime, duration: single.duration };
8489
+ }
8490
+ /** Whether the player is composed: on the live tracks rather than the single track. */
8491
+ get composed() {
8492
+ return this.tracks !== null;
8493
+ }
8494
+ /**
8495
+ * The live tracks, opened by the first composing call. A single track in
8496
+ * play is carried over by replay: track 0 set to its animation, pose 0 taken
8497
+ * by `start()` (the player's — nothing steps before it) and the tracks
8498
+ * stepped to the single track's time in `seekStep` increments, so a physics
8499
+ * rig's history restarts there and nothing is fired on the way.
8500
+ */
8501
+ compose() {
8502
+ if (this.tracks !== null) return this.tracks;
8503
+ const tracks = openTracks(this.view, { reset: "animation", adjust: this.adjustHook });
8504
+ const single = this.track;
8505
+ const time = single?.trackTime ?? 0;
8506
+ if (single !== null && this.animation !== null) {
8507
+ tracks.setAnimation(0, this.animation, { loop: this.loop, mixDuration: 0 });
8508
+ this.trackCount = 1;
8509
+ }
8510
+ this.tracks = tracks;
8511
+ this.track = null;
8512
+ this.pose = tracks.start();
8513
+ if (time > 0) this.stepTo(time);
8514
+ return tracks;
7391
8515
  }
7392
8516
  /** Stops stepping; `frame` keeps drawing the pose it holds. */
7393
8517
  pause() {
@@ -7398,19 +8522,33 @@ class RigPlayer {
7398
8522
  this.paused = false;
7399
8523
  }
7400
8524
  /**
7401
- * Moves the track to `time`: the animation is reopened and stepped forward
7402
- * in `seekStep` increments (the last one shorter) until the track time
7403
- * reaches `time`. Events and completes crossed on the way are not fired.
8525
+ * Moves a single animation on track 0 to `time`: the tracks are reopened
8526
+ * with that animation and stepped forward in `seekStep` increments (the
8527
+ * last one shorter) until the time is reached. Events and completes crossed
8528
+ * on the way are not fired. It has a meaning for one animation on one
8529
+ * track only — the player cannot replay when a mix, a second track or a
8530
+ * queue was asked for — so after any of those it refuses by name; step the
8531
+ * player forward instead.
7404
8532
  */
7405
8533
  seek(time) {
7406
8534
  if (this.animation === null) throw new Error("seek before play: no animation is open");
7407
8535
  if (!(time >= 0) || !Number.isFinite(time)) throw new Error(`seek time must be a finite time at or above 0, got ${String(time)}`);
7408
- this.open();
7409
- const track = this.track;
8536
+ if (this.tracks !== null) throw new Error("seek is defined for a single animation on track 0; after a mix, another track, a queue or a clear the player cannot replay to a time — step it forward with frame/advance instead");
8537
+ this.reopen();
8538
+ this.stepTo(time);
8539
+ }
8540
+ /** The single track opened again with track 0's animation as `play` last set it, at pose 0. */
8541
+ reopen() {
8542
+ this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook });
8543
+ this.pose = this.track.first;
8544
+ }
8545
+ /** Steps whatever is open forward to `time` in `seekStep` increments, the last one shorter. */
8546
+ stepTo(time) {
8547
+ const stepper = this.tracks ?? this.track;
7410
8548
  const steps = Math.floor(time / this.seekStep);
7411
- for (let i = 0; i < steps; i++) this.pose = track.step(this.seekStep);
7412
- const rest = time - track.trackTime;
7413
- if (rest > 0) this.pose = track.step(rest);
8549
+ for (let i = 0; i < steps; i++) this.pose = stepper.step(this.seekStep);
8550
+ const rest = time - stepper.trackTime;
8551
+ if (rest > 0) this.pose = stepper.step(rest);
7414
8552
  }
7415
8553
  on(type, listener) {
7416
8554
  const set = this.listeners[type];
@@ -7432,19 +8570,55 @@ class RigPlayer {
7432
8570
  * checks and nothing else.
7433
8571
  */
7434
8572
  advance(dt) {
7435
- const track = this.track;
7436
- if (track === null || this.pose === null) return;
8573
+ if (this.pose === null) return;
7437
8574
  const step = dt * this.timeScale;
7438
8575
  if (this.paused || !(step > 0)) return;
8576
+ const tracks = this.tracks;
8577
+ if (tracks !== null) {
8578
+ const before2 = [];
8579
+ for (let t = 0; t < this.trackCount; t++) before2.push(tracks.current(t));
8580
+ this.pose = tracks.step(step);
8581
+ this.dispatch(this.pose, before2, tracks);
8582
+ return;
8583
+ }
8584
+ const track = this.track;
8585
+ if (track === null) return;
7439
8586
  const before = track.trackTime;
7440
8587
  this.pose = track.step(step);
7441
- this.dispatch(this.pose, before, track);
8588
+ this.dispatchSingle(this.pose, before, track);
7442
8589
  }
7443
8590
  /** Draws the pose the player holds. */
7444
8591
  draw() {
7445
8592
  if (this.pose === null) return;
7446
8593
  this.renderer.render(this.posedFrame());
7447
8594
  }
8595
+ /**
8596
+ * The skin the rig is posed under — `RigPlayerOptions.skin`, or undefined
8597
+ * for no skin set. Setting it re-resolves the pose under the new skin
8598
+ * (which attachment each slot shows, which bones and constraints are
8599
+ * active) and keeps the animation, its loop and its track time: the track
8600
+ * is reopened under the new skin and stepped to the same time the way
8601
+ * `seek` does, so a physics rig's history restarts there and nothing is
8602
+ * fired on the way. The pose is drawn on the next `draw`/`frame`. An
8603
+ * unknown skin is refused by name, and the player is left as it was.
8604
+ */
8605
+ get skin() {
8606
+ return this.skinName;
8607
+ }
8608
+ set skin(name) {
8609
+ if (name === this.skinName) return;
8610
+ if (this.tracks !== null) throw new Error("skin cannot change after a mix, another track, a queue or a clear: the player cannot replay those to the current time — set it before composing, or play again");
8611
+ const view = name === void 0 ? underNoSkin(this.read.doc) : underSkin(this.read.doc, name);
8612
+ this.view = view;
8613
+ this.source = frameSource(view, this.read.parsed);
8614
+ this.skinName = name;
8615
+ this.setup = null;
8616
+ if (this.animation !== null && this.track !== null) {
8617
+ const time = this.track.trackTime;
8618
+ this.reopen();
8619
+ this.stepTo(time);
8620
+ }
8621
+ }
7448
8622
  /**
7449
8623
  * The per-pose adjustment of bone locals (`BoneAdjust`), or null for none.
7450
8624
  * It is applied by every track the player opens — `play`, the reopen inside
@@ -7469,8 +8643,9 @@ class RigPlayer {
7469
8643
  * changed, then `draw`. Nothing before `play`.
7470
8644
  */
7471
8645
  repose() {
7472
- if (this.track === null || this.pose === null) return;
7473
- this.pose = this.track.step(0);
8646
+ const stepper = this.tracks ?? this.track;
8647
+ if (stepper === null || this.pose === null) return;
8648
+ this.pose = stepper.step(0);
7474
8649
  }
7475
8650
  /**
7476
8651
  * Where `name` is in the pose the player holds — the pose of the last
@@ -7553,27 +8728,15 @@ class RigPlayer {
7553
8728
  /** Removes the renderer's elements and hands its GPU pages back. The assets stay the caller's. */
7554
8729
  dispose() {
7555
8730
  this.renderer.dispose();
8731
+ this.tracks = null;
7556
8732
  this.track = null;
7557
8733
  this.pose = null;
7558
8734
  }
7559
- open() {
7560
- this.track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook });
7561
- this.pose = this.track.first;
7562
- }
7563
- /**
7564
- * The step's events, then its complete. The core fires the keys
7565
- * (`pose.events`, in the runtime's order across a wrap) and leaves
7566
- * `complete` to the player, which derives it from the track time and the
7567
- * duration: on a looping track, once on a step whose whole cycles rose past
7568
- * 0 (rig-c `src/core/track.ts`, *Across the wrap*); on a held one, on the
7569
- * step that reaches the duration. The runtime queues a looping complete
7570
- * between the keys before the wrap and the keys after it; this player
7571
- * dispatches it after all of the step's keys.
7572
- */
7573
- dispatch(pose, before, track) {
8735
+ /** The single track's step: its events (track 0) and its complete, as every 1.x player fired them. */
8736
+ dispatchSingle(pose, before, track) {
7574
8737
  if (this.listeners.event.size > 0) {
7575
8738
  for (const [name, time, int, float, string] of pose.events) {
7576
- const event = { name, time, int, float, string };
8739
+ const event = { name, time, int, float, string, track: 0 };
7577
8740
  for (const listener of this.listeners.event) listener(event);
7578
8741
  }
7579
8742
  }
@@ -7583,7 +8746,69 @@ class RigPlayer {
7583
8746
  const cycles = d > 0 ? Math.floor(after / d) : Infinity;
7584
8747
  const was = d > 0 ? Math.floor(before / d) : -Infinity;
7585
8748
  if (this.loop ? cycles > 0 && cycles > was : before < d && after >= d) {
7586
- const payload2 = { animation: this.animation };
8749
+ const payload2 = { animation: this.animation, track: 0 };
8750
+ for (const listener of this.listeners.complete) listener(payload2);
8751
+ }
8752
+ }
8753
+ }
8754
+ /**
8755
+ * The step's events, then its completes, each with its track. The core
8756
+ * fires a track's keys from its current entry (`pose.events`, in track
8757
+ * order and in the runtime's order across a wrap) — an entry mixing out
8758
+ * fires none — and leaves `complete` to the player, which derives it per
8759
+ * track from that track's current entry: on a looping entry, once on a step
8760
+ * whose whole cycles rose; on a held one, on the step that reaches its
8761
+ * duration. An entry replaced inside the step — by `play`, or by a queued
8762
+ * entry starting at its completion less the mix — is no longer current and
8763
+ * completes no more than it fires, which is the runtime's rule too.
8764
+ */
8765
+ dispatch(pose, before, tracks) {
8766
+ const after = [];
8767
+ for (let t = 0; t < this.trackCount; t++) after.push(tracks.current(t));
8768
+ const same = (b, a) => b !== null && a !== null && b.animation === a.animation && b.loop === a.loop && a.trackTime >= b.trackTime;
8769
+ if (this.listeners.event.size > 0 && pose.events.length > 0) {
8770
+ const counts = [];
8771
+ let total = 0;
8772
+ for (let t = 0; t < this.trackCount; t++) {
8773
+ const a = after[t];
8774
+ const b = before[t];
8775
+ let n = 0;
8776
+ if (a !== null && a.animation !== null) {
8777
+ const keys = this.view.animations.find((anim) => anim.name === a.animation)?.timelines.events ?? [];
8778
+ if (keys.length > 0) {
8779
+ const last = same(b, a) ? b.animationTime : -1;
8780
+ n = eventsFired(keys, last, a.animationTime, void 0, a.animationTime < last).length;
8781
+ }
8782
+ }
8783
+ counts.push(n);
8784
+ total += n;
8785
+ }
8786
+ const attributed = total === pose.events.length;
8787
+ let track = 0;
8788
+ let left = counts[0] ?? 0;
8789
+ for (const [name, time, int, float, string] of pose.events) {
8790
+ if (attributed) {
8791
+ while (left === 0 && track < counts.length - 1) {
8792
+ track++;
8793
+ left = counts[track];
8794
+ }
8795
+ left--;
8796
+ }
8797
+ const event = attributed ? { name, time, int, float, string, track } : { name, time, int, float, string };
8798
+ for (const listener of this.listeners.event) listener(event);
8799
+ }
8800
+ }
8801
+ if (this.listeners.complete.size > 0) {
8802
+ for (let t = 0; t < this.trackCount; t++) {
8803
+ const a = after[t];
8804
+ const b = before[t];
8805
+ if (!same(b, a) || a === null || a.animation === null) continue;
8806
+ const d = a.duration;
8807
+ const cycles = d > 0 ? Math.floor(a.trackTime / d) : Infinity;
8808
+ const was = d > 0 ? Math.floor(b.trackTime / d) : -Infinity;
8809
+ const fired = a.loop ? cycles > 0 && cycles > was : b.trackTime < d && a.trackTime >= d;
8810
+ if (!fired) continue;
8811
+ const payload2 = { animation: a.animation, track: t };
7587
8812
  for (const listener of this.listeners.complete) listener(payload2);
7588
8813
  }
7589
8814
  }