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/README.md +54 -8
- package/dist/DomTexture.d.ts +11 -1
- package/dist/DomTexture.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1301 -76
- package/dist/index.js.map +1 -1
- package/dist/loadModel.d.ts +24 -4
- package/dist/loadModel.d.ts.map +1 -1
- package/dist/player.d.ts +149 -24
- package/dist/player.d.ts.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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$
|
|
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$
|
|
2469
|
-
else if (pose.dark !== null) pose.dark[at - 4] = app.alpha === 1 ? clamp01$
|
|
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
|
-
|
|
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
|
|
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 =
|
|
4986
|
+
const world = posers.world(sliders);
|
|
4944
4987
|
if (first) ctx.phase = "update";
|
|
4945
4988
|
const placeholders = /* @__PURE__ */ new Map();
|
|
4946
|
-
const slots =
|
|
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,
|
|
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,
|
|
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
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
7384
|
-
*
|
|
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
|
-
|
|
7388
|
-
|
|
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.
|
|
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
|
|
7402
|
-
* in `seekStep` increments (the
|
|
7403
|
-
*
|
|
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.
|
|
7409
|
-
|
|
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 =
|
|
7412
|
-
const rest = time -
|
|
7413
|
-
if (rest > 0) this.pose =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
7473
|
-
this.pose
|
|
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
|
-
|
|
7560
|
-
|
|
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
|
}
|