@energy8platform/golem 0.1.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 +376 -0
- package/bin/golem.js +2 -0
- package/dist/editor.css +701 -0
- package/dist/editor.js +59202 -0
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.js +5521 -0
- package/dist/lib/cli.js.map +7 -0
- package/dist/lib/editor/api.d.ts +42 -0
- package/dist/lib/editor/app.d.ts +13 -0
- package/dist/lib/editor/atlas-detect.d.ts +28 -0
- package/dist/lib/editor/atlas-panel.d.ts +1 -0
- package/dist/lib/editor/atlas.d.ts +39 -0
- package/dist/lib/editor/gizmo-math.d.ts +36 -0
- package/dist/lib/editor/gizmos.d.ts +185 -0
- package/dist/lib/editor/layers.d.ts +1 -0
- package/dist/lib/editor/library.d.ts +3 -0
- package/dist/lib/editor/panels.d.ts +26 -0
- package/dist/lib/editor/props.d.ts +19 -0
- package/dist/lib/editor/server.d.ts +11 -0
- package/dist/lib/editor/stage.d.ts +222 -0
- package/dist/lib/editor/store.d.ts +1053 -0
- package/dist/lib/editor/timeline-layout.d.ts +64 -0
- package/dist/lib/editor/timeline.d.ts +45 -0
- package/dist/lib/editor-entry.d.ts +2 -0
- package/dist/lib/editor-entry.js +5490 -0
- package/dist/lib/editor-entry.js.map +7 -0
- package/dist/lib/harness.js +51012 -0
- package/dist/lib/ktx2.d.ts +15 -0
- package/dist/lib/mcp-server.d.ts +2 -0
- package/dist/lib/preview/harness.d.ts +16 -0
- package/dist/lib/preview/render-preview.d.ts +27 -0
- package/dist/lib/preview/viewer.d.ts +1 -0
- package/dist/lib/rig-anim.d.ts +109 -0
- package/dist/lib/rig-api.d.ts +26 -0
- package/dist/lib/rig-atlas.d.ts +20 -0
- package/dist/lib/rig-check.d.ts +45 -0
- package/dist/lib/rig-constraints.d.ts +59 -0
- package/dist/lib/rig-deform.d.ts +27 -0
- package/dist/lib/rig-format.d.ts +5036 -0
- package/dist/lib/rig-history.d.ts +21 -0
- package/dist/lib/rig-import-layers.d.ts +32 -0
- package/dist/lib/rig-io.d.ts +9 -0
- package/dist/lib/rig-mesh-image.d.ts +2 -0
- package/dist/lib/rig-mesh.d.ts +143 -0
- package/dist/lib/rig-path.d.ts +101 -0
- package/dist/lib/rig-presets.d.ts +101 -0
- package/dist/lib/rig-queue.d.ts +2 -0
- package/dist/lib/rig-runtime.d.ts +70 -0
- package/dist/lib/rig-state.d.ts +64 -0
- package/dist/lib/rig-symbol.d.ts +60 -0
- package/dist/lib/rig-template-library.d.ts +3 -0
- package/dist/lib/rig-templates.d.ts +46 -0
- package/dist/lib/rig-tools.d.ts +376 -0
- package/dist/lib/runtime.d.ts +9 -0
- package/dist/lib/runtime.js +1584 -0
- package/dist/lib/runtime.js.map +7 -0
- package/dist/lib/spine-import.d.ts +68 -0
- package/dist/lib/tools.d.ts +2 -0
- package/dist/lib/tools.js +5242 -0
- package/dist/lib/tools.js.map +7 -0
- package/editor.html +3 -0
- package/package.json +96 -0
|
@@ -0,0 +1,1584 @@
|
|
|
1
|
+
// src/rig-runtime.ts
|
|
2
|
+
import { Container, Sprite, Mesh, MeshGeometry, MeshPlane, Texture, Matrix, Rectangle } from "pixi.js";
|
|
3
|
+
|
|
4
|
+
// src/rig-format.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
// src/rig-deform.ts
|
|
8
|
+
var DEFORM_BOUNDS = { bulge: 0.9, bend: 1, sway: 1, squash: 0.9 };
|
|
9
|
+
var T = [0, 0.5, 1];
|
|
10
|
+
var basis = (t) => [(1 - t) * (1 - 2 * t), 4 * t * (1 - t), t * (2 * t - 1)];
|
|
11
|
+
function controlOffsets(params, w, h, pivot, out = new Float64Array(18)) {
|
|
12
|
+
out.fill(0);
|
|
13
|
+
const bulge = params.bulge ?? 0, bend = params.bend ?? 0, sway = params.sway ?? 0, squash = params.squash ?? 0;
|
|
14
|
+
for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) {
|
|
15
|
+
const i = (r * 3 + c) * 2, u = T[c], v = T[r], mid = r === 1 ? 1 : 0;
|
|
16
|
+
out[i] = mid * (bulge * w * (u - 0.5) + bend * w) + sway * w * (v - pivot.y) + squash * w * (u - pivot.x) * 0.5 + 0;
|
|
17
|
+
out[i + 1] = -squash * h * (v - pivot.y) + 0;
|
|
18
|
+
}
|
|
19
|
+
for (const k in params) {
|
|
20
|
+
if (k.length !== 4 || k[0] !== "p") continue;
|
|
21
|
+
const r = +k[1], c = +k[2];
|
|
22
|
+
if (!(r >= 0 && r < 3 && c >= 0 && c < 3) || k[3] !== "x" && k[3] !== "y") continue;
|
|
23
|
+
out[(r * 3 + c) * 2 + (k[3] === "y" ? 1 : 0)] += params[k];
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function evalPatch(offsets, u, v) {
|
|
28
|
+
const bu = basis(u), bv = basis(v);
|
|
29
|
+
let x = 0, y = 0;
|
|
30
|
+
for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) {
|
|
31
|
+
const wgt = bv[r] * bu[c], i = (r * 3 + c) * 2;
|
|
32
|
+
x += wgt * offsets[i];
|
|
33
|
+
y += wgt * offsets[i + 1];
|
|
34
|
+
}
|
|
35
|
+
return [x, y];
|
|
36
|
+
}
|
|
37
|
+
function gridPositions(offsets, w, h, pivot, n, out = new Float32Array(2 * n * n)) {
|
|
38
|
+
const px = pivot.x * w, py = pivot.y * h;
|
|
39
|
+
for (let j = 0; j < n; j++) for (let i = 0; i < n; i++) {
|
|
40
|
+
const u = i / (n - 1), v = j / (n - 1), [ox, oy] = evalPatch(offsets, u, v), k = (j * n + i) * 2;
|
|
41
|
+
out[k] = u * w - px + ox;
|
|
42
|
+
out[k + 1] = v * h - py + oy;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/rig-format.ts
|
|
48
|
+
var ID_RE = /^[a-z][a-z0-9_]*$/;
|
|
49
|
+
var Id = z.string().regex(ID_RE, "snake_case id");
|
|
50
|
+
var Ease = z.union([
|
|
51
|
+
z.enum(["linear", "step", "ease_in", "ease_out", "ease_in_out", "back_out", "elastic_out"]),
|
|
52
|
+
// cubic-bezier control points, same semantics as CSS
|
|
53
|
+
z.object({ bezier: z.tuple([z.number(), z.number(), z.number(), z.number()]) })
|
|
54
|
+
]);
|
|
55
|
+
var Transform = z.object({
|
|
56
|
+
x: z.number().default(0),
|
|
57
|
+
y: z.number().default(0),
|
|
58
|
+
rotation: z.number().default(0),
|
|
59
|
+
scaleX: z.number().default(1),
|
|
60
|
+
scaleY: z.number().default(1),
|
|
61
|
+
shearX: z.number().default(0),
|
|
62
|
+
shearY: z.number().default(0)
|
|
63
|
+
});
|
|
64
|
+
var Color = z.string().regex(/^#[0-9a-fA-F]{8}$/, "color must be #rrggbbaa");
|
|
65
|
+
var Blend = z.enum(["normal", "add", "multiply", "screen"]);
|
|
66
|
+
var Inherit = z.enum(["normal", "onlyTranslation", "noRotationOrReflection", "noScale", "noScaleOrReflection"]);
|
|
67
|
+
var Asset = z.object({
|
|
68
|
+
id: Id,
|
|
69
|
+
/** relative path inside the rig package, e.g. "layers/arm_l.png" */
|
|
70
|
+
src: z.string(),
|
|
71
|
+
/** original (untrimmed, unrotated) image size — the space attachments and uvs refer to */
|
|
72
|
+
width: z.number().int().positive(),
|
|
73
|
+
height: z.number().int().positive(),
|
|
74
|
+
/** packed region [x, y, w, h] inside `src` when `src` is an atlas (w/h as packed: trimmed, rotated) */
|
|
75
|
+
frame: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(),
|
|
76
|
+
/** the packed region is stored rotated 90° clockwise */
|
|
77
|
+
rotate: z.union([z.literal(0), z.literal(90)]).default(0),
|
|
78
|
+
/** offset [x, y] of the packed (trimmed) region inside the original image */
|
|
79
|
+
trim: z.tuple([z.number(), z.number()]).optional(),
|
|
80
|
+
/** where this layer came from — provenance for agents */
|
|
81
|
+
origin: z.object({
|
|
82
|
+
sourceImage: z.string().optional(),
|
|
83
|
+
/** bbox in source image: [x, y, w, h] */
|
|
84
|
+
bbox: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(),
|
|
85
|
+
inpainted: z.boolean().default(false),
|
|
86
|
+
prompt: z.string().optional()
|
|
87
|
+
}).optional()
|
|
88
|
+
});
|
|
89
|
+
var Bone = z.object({
|
|
90
|
+
id: Id,
|
|
91
|
+
parent: Id.optional(),
|
|
92
|
+
/** visual length for the editor gizmo and for chains (ik/path), no effect on child transforms */
|
|
93
|
+
length: z.number().nonnegative().default(0),
|
|
94
|
+
transform: Transform.default({}),
|
|
95
|
+
/** which parts of the parent's world transform this bone inherits */
|
|
96
|
+
inherit: Inherit.default("normal"),
|
|
97
|
+
/** semantic hint: "head", "arm_l", "torso"… drives auto-rig & presets */
|
|
98
|
+
role: z.string().optional(),
|
|
99
|
+
/** editor colour, no runtime effect */
|
|
100
|
+
color: z.string().optional()
|
|
101
|
+
});
|
|
102
|
+
var Slot = z.object({
|
|
103
|
+
id: Id,
|
|
104
|
+
bone: Id,
|
|
105
|
+
/** active attachment id; undefined = hidden */
|
|
106
|
+
attachment: Id.optional(),
|
|
107
|
+
alpha: z.number().min(0).max(1).default(1),
|
|
108
|
+
/** draw order, higher = on top */
|
|
109
|
+
z: z.number().int().default(0),
|
|
110
|
+
/** blend mode: `add` for glows/fire on black, `multiply` for shadows */
|
|
111
|
+
blend: Blend.default("normal"),
|
|
112
|
+
/** tint, #rrggbbaa */
|
|
113
|
+
color: Color.default("#ffffffff")
|
|
114
|
+
});
|
|
115
|
+
var Influence = z.object({ bone: Id, x: z.number(), y: z.number(), w: z.number() });
|
|
116
|
+
var VertexData = {
|
|
117
|
+
/** unweighted: [x, y, …] in the slot bone's local space */
|
|
118
|
+
vertices: z.array(z.number()).optional(),
|
|
119
|
+
/** weighted: one influence list per vertex */
|
|
120
|
+
weights: z.array(z.array(Influence).min(1)).optional()
|
|
121
|
+
};
|
|
122
|
+
var RegionAttachment = z.object({
|
|
123
|
+
id: Id,
|
|
124
|
+
slot: Id,
|
|
125
|
+
type: z.literal("region"),
|
|
126
|
+
asset: Id,
|
|
127
|
+
/** placement of the image centre in the slot bone's local space */
|
|
128
|
+
x: z.number().default(0),
|
|
129
|
+
y: z.number().default(0),
|
|
130
|
+
rotation: z.number().default(0),
|
|
131
|
+
scaleX: z.number().default(1),
|
|
132
|
+
scaleY: z.number().default(1),
|
|
133
|
+
/** drawn size; default: the asset size */
|
|
134
|
+
width: z.number().positive().optional(),
|
|
135
|
+
height: z.number().positive().optional()
|
|
136
|
+
});
|
|
137
|
+
var MeshAttachment = z.object({
|
|
138
|
+
id: Id,
|
|
139
|
+
slot: Id,
|
|
140
|
+
type: z.literal("mesh"),
|
|
141
|
+
asset: Id,
|
|
142
|
+
/** texture coordinates 0..1 in the asset's original image, 2 per vertex */
|
|
143
|
+
uvs: z.array(z.number()),
|
|
144
|
+
triangles: z.array(z.number().int().nonnegative()),
|
|
145
|
+
...VertexData,
|
|
146
|
+
/** number of hull vertices (the outline is uvs[0..hull)) */
|
|
147
|
+
hull: z.number().int().nonnegative().default(0),
|
|
148
|
+
width: z.number().positive().optional(),
|
|
149
|
+
height: z.number().positive().optional()
|
|
150
|
+
});
|
|
151
|
+
var PathAttachment = z.object({
|
|
152
|
+
id: Id,
|
|
153
|
+
slot: Id,
|
|
154
|
+
type: z.literal("path"),
|
|
155
|
+
...VertexData,
|
|
156
|
+
/** curve lengths for constant-speed positioning */
|
|
157
|
+
lengths: z.array(z.number()).default([]),
|
|
158
|
+
closed: z.boolean().default(false),
|
|
159
|
+
constantSpeed: z.boolean().default(true)
|
|
160
|
+
});
|
|
161
|
+
var Attachment = z.discriminatedUnion("type", [RegionAttachment, MeshAttachment, PathAttachment]);
|
|
162
|
+
var IkConstraint = z.object({
|
|
163
|
+
id: Id,
|
|
164
|
+
order: z.number().int().default(0),
|
|
165
|
+
bones: z.array(Id).min(1),
|
|
166
|
+
target: Id,
|
|
167
|
+
mix: z.number().default(1),
|
|
168
|
+
softness: z.number().default(0),
|
|
169
|
+
bendPositive: z.boolean().default(true),
|
|
170
|
+
compress: z.boolean().default(false),
|
|
171
|
+
stretch: z.boolean().default(false),
|
|
172
|
+
uniform: z.boolean().default(false)
|
|
173
|
+
});
|
|
174
|
+
var TransformConstraint = z.object({
|
|
175
|
+
id: Id,
|
|
176
|
+
order: z.number().int().default(0),
|
|
177
|
+
bones: z.array(Id).min(1),
|
|
178
|
+
target: Id,
|
|
179
|
+
local: z.boolean().default(false),
|
|
180
|
+
relative: z.boolean().default(false),
|
|
181
|
+
rotation: z.number().default(0),
|
|
182
|
+
x: z.number().default(0),
|
|
183
|
+
y: z.number().default(0),
|
|
184
|
+
scaleX: z.number().default(0),
|
|
185
|
+
scaleY: z.number().default(0),
|
|
186
|
+
shearY: z.number().default(0),
|
|
187
|
+
mixRotate: z.number().default(1),
|
|
188
|
+
mixX: z.number().default(1),
|
|
189
|
+
mixY: z.number().default(1),
|
|
190
|
+
mixScaleX: z.number().default(1),
|
|
191
|
+
mixScaleY: z.number().default(1),
|
|
192
|
+
mixShearY: z.number().default(1)
|
|
193
|
+
});
|
|
194
|
+
var PathConstraint = z.object({
|
|
195
|
+
id: Id,
|
|
196
|
+
order: z.number().int().default(0),
|
|
197
|
+
bones: z.array(Id).min(1),
|
|
198
|
+
/** slot whose active attachment is a path */
|
|
199
|
+
target: Id,
|
|
200
|
+
positionMode: z.enum(["fixed", "percent"]).default("percent"),
|
|
201
|
+
spacingMode: z.enum(["length", "fixed", "percent", "proportional"]).default("length"),
|
|
202
|
+
rotateMode: z.enum(["tangent", "chain", "chainScale"]).default("tangent"),
|
|
203
|
+
rotation: z.number().default(0),
|
|
204
|
+
position: z.number().default(0),
|
|
205
|
+
spacing: z.number().default(0),
|
|
206
|
+
mixRotate: z.number().default(1),
|
|
207
|
+
mixX: z.number().default(1),
|
|
208
|
+
mixY: z.number().default(1)
|
|
209
|
+
});
|
|
210
|
+
var BoneProp = z.enum(["x", "y", "rotation", "scaleX", "scaleY", "shearX", "shearY"]);
|
|
211
|
+
var SlotProp = z.enum(["alpha", "attachment", "z", "color"]);
|
|
212
|
+
var DeformProp = z.string().regex(/^(bulge|bend|sway|squash|p[0-2][0-2][xy]|vertices)$/, "deform prop must be bulge|bend|sway|squash|p<row><col>x|y|vertices");
|
|
213
|
+
var IkProp = z.enum(["mix", "softness", "bendPositive", "compress", "stretch"]);
|
|
214
|
+
var TransformProp = z.enum(["mixRotate", "mixX", "mixY", "mixScaleX", "mixScaleY", "mixShearY"]);
|
|
215
|
+
var PathProp = z.enum(["position", "spacing", "mixRotate", "mixX", "mixY"]);
|
|
216
|
+
var KeyValue = z.union([z.number(), z.string(), z.null(), z.array(z.number()), z.array(z.string())]);
|
|
217
|
+
var Key = z.object({
|
|
218
|
+
t: z.number().nonnegative(),
|
|
219
|
+
v: KeyValue,
|
|
220
|
+
/** easing INTO the next key */
|
|
221
|
+
ease: Ease.default("ease_in_out")
|
|
222
|
+
});
|
|
223
|
+
var Track = z.discriminatedUnion("target", [
|
|
224
|
+
z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, keys: z.array(Key).min(1) }),
|
|
225
|
+
z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, keys: z.array(Key).min(1) }),
|
|
226
|
+
z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, keys: z.array(Key).min(1) }),
|
|
227
|
+
z.object({ target: z.literal("ik"), id: Id, prop: IkProp, keys: z.array(Key).min(1) }),
|
|
228
|
+
z.object({ target: z.literal("transform"), id: Id, prop: TransformProp, keys: z.array(Key).min(1) }),
|
|
229
|
+
z.object({ target: z.literal("path"), id: Id, prop: PathProp, keys: z.array(Key).min(1) }),
|
|
230
|
+
z.object({ target: z.literal("drawOrder"), id: Id.default("order"), prop: z.literal("order").default("order"), keys: z.array(Key).min(1) })
|
|
231
|
+
]);
|
|
232
|
+
var Event = z.object({
|
|
233
|
+
t: z.number().nonnegative(),
|
|
234
|
+
name: z.string(),
|
|
235
|
+
payload: z.record(z.unknown()).optional()
|
|
236
|
+
});
|
|
237
|
+
var Animation = z.object({
|
|
238
|
+
id: Id,
|
|
239
|
+
duration: z.number().positive(),
|
|
240
|
+
loop: z.boolean().default(false),
|
|
241
|
+
tracks: z.array(Track).default([]),
|
|
242
|
+
events: z.array(Event).default([]),
|
|
243
|
+
/** if generated from a preset — keep params so the agent can re-tune instead of re-keying */
|
|
244
|
+
preset: z.object({ name: z.string(), params: z.record(z.unknown()) }).optional()
|
|
245
|
+
});
|
|
246
|
+
var Reference = z.object({
|
|
247
|
+
src: z.string(),
|
|
248
|
+
opacity: z.number().min(0).max(1).default(0.5),
|
|
249
|
+
x: z.number().default(0),
|
|
250
|
+
y: z.number().default(0),
|
|
251
|
+
scale: z.number().positive().default(1)
|
|
252
|
+
});
|
|
253
|
+
var RigSchema = z.object({
|
|
254
|
+
format: z.literal("energy8.rig"),
|
|
255
|
+
version: z.literal(2),
|
|
256
|
+
meta: z.object({
|
|
257
|
+
name: z.string(),
|
|
258
|
+
width: z.number().int().positive(),
|
|
259
|
+
height: z.number().int().positive(),
|
|
260
|
+
/** free-form notes for agents & humans: what this rig is, what's intentionally missing */
|
|
261
|
+
notes: z.string().optional(),
|
|
262
|
+
reference: Reference.optional()
|
|
263
|
+
}),
|
|
264
|
+
assets: z.array(Asset),
|
|
265
|
+
bones: z.array(Bone).min(1),
|
|
266
|
+
slots: z.array(Slot),
|
|
267
|
+
attachments: z.array(Attachment).default([]),
|
|
268
|
+
ik: z.array(IkConstraint).default([]),
|
|
269
|
+
transform: z.array(TransformConstraint).default([]),
|
|
270
|
+
path: z.array(PathConstraint).default([]),
|
|
271
|
+
animations: z.array(Animation)
|
|
272
|
+
});
|
|
273
|
+
var RigDocument = z.preprocess((input) => {
|
|
274
|
+
const v = input?.version;
|
|
275
|
+
return v === 0 || v === 1 ? migrateV1(input) : input;
|
|
276
|
+
}, RigSchema);
|
|
277
|
+
var DEG = Math.PI / 180;
|
|
278
|
+
function migrateV1(input) {
|
|
279
|
+
const doc = input;
|
|
280
|
+
const assets = doc.assets ?? [], slots = doc.slots ?? [], anims = doc.animations ?? [];
|
|
281
|
+
const assetById = new Map(assets.map((a) => [a.id, a]));
|
|
282
|
+
const shows = /* @__PURE__ */ new Map();
|
|
283
|
+
const add = (slot, asset) => (shows.get(slot) ?? shows.set(slot, /* @__PURE__ */ new Set()).get(slot)).add(asset);
|
|
284
|
+
for (const s of slots) if (s.attachment) add(s.id, s.attachment);
|
|
285
|
+
for (const a of anims) for (const t of a.tracks ?? [])
|
|
286
|
+
if (t.target === "slot" && t.prop === "attachment") {
|
|
287
|
+
for (const k of t.keys ?? []) if (typeof k.v === "string") add(t.id, k.v);
|
|
288
|
+
}
|
|
289
|
+
const usage = /* @__PURE__ */ new Map();
|
|
290
|
+
for (const set of shows.values()) for (const asset of set) usage.set(asset, (usage.get(asset) ?? 0) + 1);
|
|
291
|
+
const attachmentId = (slot, asset) => (usage.get(asset) ?? 0) > 1 ? `${slot}_${asset}` : asset;
|
|
292
|
+
const attachments = [];
|
|
293
|
+
for (const s of slots) {
|
|
294
|
+
const pivot = s.pivot ?? { x: 0.5, y: 0.5 }, o = { x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1, ...s.offset ?? {} };
|
|
295
|
+
for (const asset of shows.get(s.id) ?? []) {
|
|
296
|
+
const a = assetById.get(asset);
|
|
297
|
+
if (!a) continue;
|
|
298
|
+
const cx = (0.5 - pivot.x) * a.width, cy = (0.5 - pivot.y) * a.height;
|
|
299
|
+
const r = o.rotation * DEG, cos = Math.cos(r), sin = Math.sin(r);
|
|
300
|
+
const x = o.x + cos * o.scaleX * cx - sin * o.scaleY * cy, y = o.y + sin * o.scaleX * cx + cos * o.scaleY * cy;
|
|
301
|
+
attachments.push({ id: attachmentId(s.id, asset), slot: s.id, type: "region", asset, x: round(x), y: round(y), rotation: o.rotation, scaleX: o.scaleX, scaleY: o.scaleY });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const restAttachment = (slot) => {
|
|
305
|
+
const s = slots.find((x) => x.id === slot);
|
|
306
|
+
const first = s?.attachment ?? [...shows.get(slot) ?? []][0];
|
|
307
|
+
return first ? attachmentId(slot, first) : void 0;
|
|
308
|
+
};
|
|
309
|
+
const newSlots = slots.map((s) => {
|
|
310
|
+
const { pivot, offset, attachment, ...rest } = s;
|
|
311
|
+
return { ...rest, ...attachment ? { attachment: attachmentId(s.id, attachment) } : {} };
|
|
312
|
+
});
|
|
313
|
+
const newAnims = anims.map((a) => ({
|
|
314
|
+
...a,
|
|
315
|
+
tracks: (a.tracks ?? []).flatMap((t) => {
|
|
316
|
+
if (t.target === "slot" && t.prop === "attachment")
|
|
317
|
+
return [{ ...t, keys: t.keys.map((k) => ({ ...k, v: typeof k.v === "string" ? attachmentId(t.id, k.v) : k.v })) }];
|
|
318
|
+
if (t.target === "deform") {
|
|
319
|
+
const id = restAttachment(t.id);
|
|
320
|
+
return id ? [{ ...t, id }] : [];
|
|
321
|
+
}
|
|
322
|
+
return [t];
|
|
323
|
+
})
|
|
324
|
+
}));
|
|
325
|
+
return { ...doc, version: 2, slots: newSlots, attachments: [...doc.attachments ?? [], ...attachments], animations: newAnims };
|
|
326
|
+
}
|
|
327
|
+
var round = (v) => Math.round(v * 1e6) / 1e6 + 0;
|
|
328
|
+
var trackId = (t) => `${t.target}.${t.id}.${t.prop}`;
|
|
329
|
+
function validate(doc) {
|
|
330
|
+
const errs = [];
|
|
331
|
+
const dup = (xs, kind) => {
|
|
332
|
+
const seen = /* @__PURE__ */ new Set();
|
|
333
|
+
for (const x of xs) {
|
|
334
|
+
if (seen.has(x.id)) errs.push(`${kind}: duplicate id "${x.id}"`);
|
|
335
|
+
seen.add(x.id);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
dup(doc.assets, "asset");
|
|
339
|
+
dup(doc.bones, "bone");
|
|
340
|
+
dup(doc.slots, "slot");
|
|
341
|
+
dup(doc.attachments, "attachment");
|
|
342
|
+
dup(doc.animations, "animation");
|
|
343
|
+
dup(doc.ik, "ik");
|
|
344
|
+
dup(doc.transform, "transform");
|
|
345
|
+
dup(doc.path, "path");
|
|
346
|
+
const bones = new Set(doc.bones.map((b) => b.id));
|
|
347
|
+
const assets = new Map(doc.assets.map((a) => [a.id, a]));
|
|
348
|
+
const slots = new Set(doc.slots.map((s) => s.id));
|
|
349
|
+
const attachments = new Map(doc.attachments.map((a) => [a.id, a]));
|
|
350
|
+
const iks = new Set(doc.ik.map((c) => c.id)), transforms = new Set(doc.transform.map((c) => c.id)), paths = new Set(doc.path.map((c) => c.id));
|
|
351
|
+
for (const b of doc.bones) {
|
|
352
|
+
if (b.parent && !bones.has(b.parent)) errs.push(`bone "${b.id}": unknown parent "${b.parent}"`);
|
|
353
|
+
}
|
|
354
|
+
for (const b of doc.bones) {
|
|
355
|
+
const seen = /* @__PURE__ */ new Set();
|
|
356
|
+
let cur = b;
|
|
357
|
+
while (cur?.parent) {
|
|
358
|
+
if (seen.has(cur.id)) {
|
|
359
|
+
errs.push(`bone "${b.id}": parent cycle`);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
seen.add(cur.id);
|
|
363
|
+
cur = doc.bones.find((x) => x.id === cur.parent);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (!doc.bones.some((b) => !b.parent)) errs.push("no root bone");
|
|
367
|
+
for (const a of doc.assets) {
|
|
368
|
+
if (a.frame && (a.frame[2] <= 0 || a.frame[3] <= 0)) errs.push(`asset "${a.id}": frame must have positive size`);
|
|
369
|
+
if (a.frame && !a.trim && !a.rotate && (a.frame[2] !== a.width || a.frame[3] !== a.height)) errs.push(`asset "${a.id}": frame size must equal width\xD7height (or declare trim/rotate)`);
|
|
370
|
+
}
|
|
371
|
+
for (const s of doc.slots) {
|
|
372
|
+
if (!bones.has(s.bone)) errs.push(`slot "${s.id}": unknown bone "${s.bone}"`);
|
|
373
|
+
if (s.attachment) {
|
|
374
|
+
const a = attachments.get(s.attachment);
|
|
375
|
+
if (!a) errs.push(`slot "${s.id}": unknown attachment "${s.attachment}"`);
|
|
376
|
+
else if (a.slot !== s.id) errs.push(`slot "${s.id}": attachment "${s.attachment}" belongs to slot "${a.slot}"`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const vertexCount = (a) => {
|
|
380
|
+
if (a.type === "region") return 4;
|
|
381
|
+
if (a.type === "mesh") return a.uvs.length / 2;
|
|
382
|
+
return a.vertices ? a.vertices.length / 2 : a.weights?.length ?? 0;
|
|
383
|
+
};
|
|
384
|
+
for (const a of doc.attachments) {
|
|
385
|
+
if (!slots.has(a.slot)) errs.push(`attachment "${a.id}": unknown slot "${a.slot}"`);
|
|
386
|
+
if (a.type !== "path" && !assets.has(a.asset)) errs.push(`attachment "${a.id}": unknown asset "${a.asset}"`);
|
|
387
|
+
if (a.type === "mesh" || a.type === "path") {
|
|
388
|
+
const n = a.type === "mesh" ? a.uvs.length / 2 : vertexCount(a);
|
|
389
|
+
if (a.type === "mesh") {
|
|
390
|
+
if (a.uvs.length % 2 || n < 3) errs.push(`attachment "${a.id}": uvs must have 2 numbers per vertex, at least 3 vertices`);
|
|
391
|
+
if (a.triangles.length % 3) errs.push(`attachment "${a.id}": triangles must have 3 indices each`);
|
|
392
|
+
for (const i of a.triangles) if (i >= n) errs.push(`attachment "${a.id}": triangle index ${i} out of range (${n} vertices)`);
|
|
393
|
+
}
|
|
394
|
+
if ((a.vertices ? 1 : 0) + (a.weights ? 1 : 0) !== 1) errs.push(`attachment "${a.id}": exactly one of vertices / weights`);
|
|
395
|
+
if (a.vertices && a.vertices.length !== 2 * n) errs.push(`attachment "${a.id}": vertices must have 2 numbers per vertex (${2 * n}, got ${a.vertices.length})`);
|
|
396
|
+
if (a.weights) {
|
|
397
|
+
if (a.weights.length !== n) errs.push(`attachment "${a.id}": weights must have one influence list per vertex (${n}, got ${a.weights.length})`);
|
|
398
|
+
for (const list of a.weights) for (const inf of list) if (!bones.has(inf.bone)) errs.push(`attachment "${a.id}": weights reference unknown bone "${inf.bone}"`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const c of doc.ik) {
|
|
403
|
+
if (c.bones.length > 2) errs.push(`ik "${c.id}": must have 1 or 2 bones`);
|
|
404
|
+
for (const b of c.bones) if (!bones.has(b)) errs.push(`ik "${c.id}": unknown bone "${b}"`);
|
|
405
|
+
if (!bones.has(c.target)) errs.push(`ik "${c.id}": unknown target bone "${c.target}"`);
|
|
406
|
+
}
|
|
407
|
+
for (const c of doc.transform) {
|
|
408
|
+
for (const b of c.bones) if (!bones.has(b)) errs.push(`transform "${c.id}": unknown bone "${b}"`);
|
|
409
|
+
if (!bones.has(c.target)) errs.push(`transform "${c.id}": unknown target bone "${c.target}"`);
|
|
410
|
+
}
|
|
411
|
+
for (const c of doc.path) {
|
|
412
|
+
for (const b of c.bones) if (!bones.has(b)) errs.push(`path "${c.id}": unknown bone "${b}"`);
|
|
413
|
+
if (!slots.has(c.target)) errs.push(`path "${c.id}": unknown target slot "${c.target}"`);
|
|
414
|
+
}
|
|
415
|
+
for (const a of doc.animations) {
|
|
416
|
+
const tracks = /* @__PURE__ */ new Set();
|
|
417
|
+
for (const t of a.tracks) {
|
|
418
|
+
const tid = trackId(t);
|
|
419
|
+
if (tracks.has(tid)) errs.push(`animation "${a.id}": duplicate track ${tid}`);
|
|
420
|
+
tracks.add(tid);
|
|
421
|
+
const known = t.target === "bone" ? bones.has(t.id) : t.target === "slot" ? slots.has(t.id) : t.target === "deform" ? attachments.has(t.id) : t.target === "ik" ? iks.has(t.id) : t.target === "transform" ? transforms.has(t.id) : t.target === "path" ? paths.has(t.id) : true;
|
|
422
|
+
if (!known) errs.push(`animation "${a.id}": unknown ${t.target} "${t.id}"`);
|
|
423
|
+
const bound = t.target === "deform" ? DEFORM_BOUNDS[t.prop] : void 0;
|
|
424
|
+
const att = t.target === "deform" ? attachments.get(t.id) : void 0;
|
|
425
|
+
let prev = -1;
|
|
426
|
+
for (const k of t.keys) {
|
|
427
|
+
if (k.t <= prev && prev >= 0) errs.push(`animation "${a.id}" ${tid}: keys not strictly increasing`);
|
|
428
|
+
if (k.t > a.duration) errs.push(`animation "${a.id}" ${tid}: key at ${k.t}s exceeds duration`);
|
|
429
|
+
if (t.target === "slot" && t.prop === "attachment") {
|
|
430
|
+
if (k.v !== null && typeof k.v !== "string") errs.push(`animation "${a.id}" ${tid}: attachment key must be an attachment id or null`);
|
|
431
|
+
else if (typeof k.v === "string") {
|
|
432
|
+
const target = attachments.get(k.v);
|
|
433
|
+
if (!target) errs.push(`animation "${a.id}": unknown attachment "${k.v}"`);
|
|
434
|
+
else if (target.slot !== t.id) errs.push(`animation "${a.id}" ${tid}: attachment "${k.v}" belongs to slot "${target.slot}"`);
|
|
435
|
+
}
|
|
436
|
+
} else if (t.target === "slot" && t.prop === "color") {
|
|
437
|
+
const ok = Array.isArray(k.v) && k.v.length === 4 && k.v.every((c) => typeof c === "number" && c >= 0 && c <= 1);
|
|
438
|
+
if (!ok) errs.push(`animation "${a.id}" ${tid}: color key must be [r, g, b, a] in 0..1`);
|
|
439
|
+
} else if (t.target === "drawOrder") {
|
|
440
|
+
const order = Array.isArray(k.v) ? k.v : [];
|
|
441
|
+
const ok = order.length === doc.slots.length && new Set(order).size === order.length && order.every((s) => typeof s === "string" && slots.has(s));
|
|
442
|
+
if (!ok) errs.push(`animation "${a.id}" drawOrder key at ${k.t}s must list every slot exactly once`);
|
|
443
|
+
} else if (t.target === "deform" && t.prop === "vertices") {
|
|
444
|
+
if (!Array.isArray(k.v) || !k.v.every((v) => typeof v === "number")) errs.push(`animation "${a.id}" ${tid}: vertices key must be a number array`);
|
|
445
|
+
else if (att && att.type !== "region") {
|
|
446
|
+
const expected = att.weights ? 2 * att.weights.reduce((s, list) => s + list.length, 0) : 2 * vertexCount(att);
|
|
447
|
+
if (k.v.length !== expected) errs.push(`animation "${a.id}" ${tid}: vertices key needs ${expected} numbers, got ${k.v.length}`);
|
|
448
|
+
} else if (att) errs.push(`animation "${a.id}" ${tid}: vertices deform needs a mesh or path attachment`);
|
|
449
|
+
} else if (typeof k.v !== "number") errs.push(`animation "${a.id}" ${tid}: key must be a number`);
|
|
450
|
+
else if (bound !== void 0 && Math.abs(k.v) > bound) errs.push(`animation "${a.id}" ${tid}: value ${k.v} out of range [-${bound}, ${bound}]`);
|
|
451
|
+
prev = k.t;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
for (const e of a.events) {
|
|
455
|
+
if (e.t > a.duration) errs.push(`animation "${a.id}": event "${e.name}" exceeds duration`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return errs;
|
|
459
|
+
}
|
|
460
|
+
function parse(input) {
|
|
461
|
+
const doc = RigDocument.parse(input);
|
|
462
|
+
const errs = validate(doc);
|
|
463
|
+
if (errs.length) throw new Error("rig validation failed:\n" + errs.join("\n"));
|
|
464
|
+
return doc;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/rig-anim.ts
|
|
468
|
+
var DEG2 = Math.PI / 180;
|
|
469
|
+
var named = {
|
|
470
|
+
linear: (t) => t,
|
|
471
|
+
step: () => 0,
|
|
472
|
+
ease_in: (t) => t * t,
|
|
473
|
+
ease_out: (t) => 1 - (1 - t) * (1 - t),
|
|
474
|
+
ease_in_out: (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2,
|
|
475
|
+
back_out: (t) => 1 + 2.70158 * (t - 1) ** 3 + 1.70158 * (t - 1) ** 2,
|
|
476
|
+
elastic_out: (t) => t === 0 ? 0 : t === 1 ? 1 : 2 ** (-10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI / 3)) + 1
|
|
477
|
+
};
|
|
478
|
+
var BEZIER_SEGMENTS = 10;
|
|
479
|
+
function bezier(x1, y1, x2, y2) {
|
|
480
|
+
const px = [0], py = [0];
|
|
481
|
+
for (let i = 1; i <= BEZIER_SEGMENTS; i++) {
|
|
482
|
+
const t = i / BEZIER_SEGMENTS, u = 1 - t;
|
|
483
|
+
px.push(3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t);
|
|
484
|
+
py.push(3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t);
|
|
485
|
+
}
|
|
486
|
+
return (x) => {
|
|
487
|
+
if (x <= 0) return 0;
|
|
488
|
+
for (let i = 1; i <= BEZIER_SEGMENTS; i++) {
|
|
489
|
+
if (px[i] >= x) {
|
|
490
|
+
const span = px[i] - px[i - 1];
|
|
491
|
+
return py[i - 1] + (span > 0 ? (x - px[i - 1]) / span * (py[i] - py[i - 1]) : 0);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return 1;
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
var easeCache = /* @__PURE__ */ new Map();
|
|
498
|
+
function ease(spec) {
|
|
499
|
+
if (typeof spec === "string") return named[spec];
|
|
500
|
+
const key = spec.bezier.join(",");
|
|
501
|
+
let f = easeCache.get(key);
|
|
502
|
+
if (!f) easeCache.set(key, f = bezier(...spec.bezier));
|
|
503
|
+
return f;
|
|
504
|
+
}
|
|
505
|
+
var pk = (target, id, prop) => `${target}:${id}:${prop}`;
|
|
506
|
+
function sample(track, t) {
|
|
507
|
+
const keys = track.keys;
|
|
508
|
+
if (t <= keys[0].t) return keys[0].v;
|
|
509
|
+
const last = keys[keys.length - 1];
|
|
510
|
+
if (t >= last.t) return last.v;
|
|
511
|
+
let i = 0;
|
|
512
|
+
while (keys[i + 1].t <= t) i++;
|
|
513
|
+
const a = keys[i], b = keys[i + 1];
|
|
514
|
+
if (typeof a.v === "number" && typeof b.v === "number") {
|
|
515
|
+
const u = ease(a.ease)((t - a.t) / (b.t - a.t));
|
|
516
|
+
return a.v + (b.v - a.v) * u;
|
|
517
|
+
}
|
|
518
|
+
if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
|
|
519
|
+
const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v, bv = b.v;
|
|
520
|
+
return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);
|
|
521
|
+
}
|
|
522
|
+
return a.v;
|
|
523
|
+
}
|
|
524
|
+
function poseOf(anim, t, out = /* @__PURE__ */ new Map()) {
|
|
525
|
+
for (const tr of anim.tracks) out.set(pk(tr.target, tr.id, tr.prop), sample(tr, t));
|
|
526
|
+
return out;
|
|
527
|
+
}
|
|
528
|
+
function blendPoses(from, to, w, restOf) {
|
|
529
|
+
const out = new Map(to);
|
|
530
|
+
for (const [k, f] of from) {
|
|
531
|
+
const cur = to.get(k);
|
|
532
|
+
if (typeof f === "number") {
|
|
533
|
+
const target = typeof cur === "number" ? cur : restOf(k);
|
|
534
|
+
out.set(k, f + (target - f) * w);
|
|
535
|
+
} else if (Array.isArray(f) && typeof f[0] === "number") {
|
|
536
|
+
const fv = f, tv = Array.isArray(cur) && typeof cur[0] === "number" ? cur : fv.map(() => 0);
|
|
537
|
+
out.set(k, fv.map((v, i) => v + ((tv[i] ?? 0) - v) * w));
|
|
538
|
+
} else if (cur === void 0 && w < 1) out.set(k, f);
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
function restValue(doc, key) {
|
|
543
|
+
const [target, id, prop] = key.split(":");
|
|
544
|
+
if (target === "bone") return doc.bones.find((b) => b.id === id).transform[prop];
|
|
545
|
+
if (target === "slot") {
|
|
546
|
+
const s = doc.slots.find((s2) => s2.id === id);
|
|
547
|
+
if (prop === "color") return colorToArray(s.color);
|
|
548
|
+
return s[prop];
|
|
549
|
+
}
|
|
550
|
+
if (target === "ik" || target === "transform" || target === "path") {
|
|
551
|
+
const c = doc[target].find((c2) => c2.id === id);
|
|
552
|
+
const v = c?.[prop];
|
|
553
|
+
return typeof v === "boolean" ? v ? 1 : 0 : v ?? 0;
|
|
554
|
+
}
|
|
555
|
+
if (target === "drawOrder") return doc.slots.map((s) => s.id);
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
var colorToArray = (c) => [0, 2, 4, 6].map((k) => parseInt(c.slice(1 + k, 3 + k), 16) / 255);
|
|
559
|
+
function deformParams(pose, attachmentId) {
|
|
560
|
+
let out;
|
|
561
|
+
const prefix = `deform:${attachmentId}:`;
|
|
562
|
+
for (const [k, v] of pose)
|
|
563
|
+
if (k.startsWith(prefix) && typeof v === "number") (out ??= {})[k.slice(prefix.length)] = v;
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
function affineOf(x, y, rotationDeg, sx, sy, shearX = 0, shearY = 0) {
|
|
567
|
+
const rx = (rotationDeg + shearX) * DEG2, ry = (rotationDeg + 90 + shearY) * DEG2;
|
|
568
|
+
return { a: Math.cos(rx) * sx, b: Math.sin(rx) * sx, c: Math.cos(ry) * sy, d: Math.sin(ry) * sy, tx: x, ty: y };
|
|
569
|
+
}
|
|
570
|
+
function mul(p, l) {
|
|
571
|
+
return {
|
|
572
|
+
a: p.a * l.a + p.c * l.b,
|
|
573
|
+
b: p.b * l.a + p.d * l.b,
|
|
574
|
+
c: p.a * l.c + p.c * l.d,
|
|
575
|
+
d: p.b * l.c + p.d * l.d,
|
|
576
|
+
tx: p.a * l.tx + p.c * l.ty + p.tx,
|
|
577
|
+
ty: p.b * l.tx + p.d * l.ty + p.ty
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
var apply = (m, x, y) => [m.a * x + m.c * y + m.tx, m.b * x + m.d * y + m.ty];
|
|
581
|
+
function invert(m) {
|
|
582
|
+
const det = m.a * m.d - m.b * m.c || 1e-12;
|
|
583
|
+
const a = m.d / det, b = -m.b / det, c = -m.c / det, d = m.a / det;
|
|
584
|
+
return { a, b, c, d, tx: -(a * m.tx + c * m.ty), ty: -(b * m.tx + d * m.ty) };
|
|
585
|
+
}
|
|
586
|
+
var num = (pose, key, rest) => {
|
|
587
|
+
const v = pose.get(key);
|
|
588
|
+
return typeof v === "number" ? v : rest;
|
|
589
|
+
};
|
|
590
|
+
var boneIndex = /* @__PURE__ */ new WeakMap();
|
|
591
|
+
function boneOf(doc, id) {
|
|
592
|
+
let idx = boneIndex.get(doc);
|
|
593
|
+
if (!idx) boneIndex.set(doc, idx = new Map(doc.bones.map((b2) => [b2.id, b2])));
|
|
594
|
+
const b = idx.get(id);
|
|
595
|
+
if (!b) throw new Error(`unknown bone "${id}"`);
|
|
596
|
+
return b;
|
|
597
|
+
}
|
|
598
|
+
function boneWorldOf(bone, s, parent) {
|
|
599
|
+
const local = affineOf(s.x, s.y, s.rotation, s.scaleX, s.scaleY, s.shearX, s.shearY);
|
|
600
|
+
if (!parent) return local;
|
|
601
|
+
if (bone.inherit === "normal") return mul(parent, local);
|
|
602
|
+
const [tx, ty] = apply(parent, s.x, s.y);
|
|
603
|
+
return { ...mul(inheritedParentLinear(bone, parent), { ...local, tx: 0, ty: 0 }), tx, ty };
|
|
604
|
+
}
|
|
605
|
+
var orderCache = /* @__PURE__ */ new WeakMap();
|
|
606
|
+
function boneOrder(doc) {
|
|
607
|
+
let order = orderCache.get(doc);
|
|
608
|
+
if (order) return order;
|
|
609
|
+
order = [];
|
|
610
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
611
|
+
while (order.length < doc.bones.length) {
|
|
612
|
+
let progressed = false;
|
|
613
|
+
for (const b of doc.bones) if (!emitted.has(b.id) && (!b.parent || emitted.has(b.parent))) {
|
|
614
|
+
emitted.add(b.id);
|
|
615
|
+
order.push(b);
|
|
616
|
+
progressed = true;
|
|
617
|
+
}
|
|
618
|
+
if (!progressed) throw new Error("bone hierarchy has a cycle");
|
|
619
|
+
}
|
|
620
|
+
orderCache.set(doc, order);
|
|
621
|
+
return order;
|
|
622
|
+
}
|
|
623
|
+
var localOf = (doc, pose, b) => {
|
|
624
|
+
const r = b.transform, id = b.id;
|
|
625
|
+
return {
|
|
626
|
+
x: num(pose, pk("bone", id, "x"), r.x),
|
|
627
|
+
y: num(pose, pk("bone", id, "y"), r.y),
|
|
628
|
+
rotation: num(pose, pk("bone", id, "rotation"), r.rotation),
|
|
629
|
+
scaleX: num(pose, pk("bone", id, "scaleX"), r.scaleX),
|
|
630
|
+
scaleY: num(pose, pk("bone", id, "scaleY"), r.scaleY),
|
|
631
|
+
shearX: num(pose, pk("bone", id, "shearX"), r.shearX),
|
|
632
|
+
shearY: num(pose, pk("bone", id, "shearY"), r.shearY)
|
|
633
|
+
};
|
|
634
|
+
};
|
|
635
|
+
function updateSubtree(skel, root) {
|
|
636
|
+
const doc = skel.doc;
|
|
637
|
+
const kids = /* @__PURE__ */ new Map();
|
|
638
|
+
for (const b of doc.bones) if (b.parent) (kids.get(b.parent) ?? kids.set(b.parent, []).get(b.parent)).push(b);
|
|
639
|
+
const stack = [root];
|
|
640
|
+
while (stack.length) {
|
|
641
|
+
const id = stack.pop(), b = boneOf(doc, id), st = skel.bones.get(id);
|
|
642
|
+
st.world = boneWorldOf(b, st, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
643
|
+
for (const k of kids.get(id) ?? []) stack.push(k.id);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function evaluate(doc, pose) {
|
|
647
|
+
const skel = { doc, pose, bones: /* @__PURE__ */ new Map(), attachment: /* @__PURE__ */ new Map() };
|
|
648
|
+
for (const b of boneOrder(doc)) {
|
|
649
|
+
const s = localOf(doc, pose, b);
|
|
650
|
+
const world = boneWorldOf(b, s, b.parent ? skel.bones.get(b.parent).world : void 0);
|
|
651
|
+
skel.bones.set(b.id, { ...s, world });
|
|
652
|
+
}
|
|
653
|
+
const attachments = new Map(doc.attachments.map((a) => [a.id, a]));
|
|
654
|
+
for (const s of doc.slots) {
|
|
655
|
+
const v = pose.get(pk("slot", s.id, "attachment"));
|
|
656
|
+
const id = v === void 0 ? s.attachment : v;
|
|
657
|
+
skel.attachment.set(s.id, id ? attachments.get(id) ?? null : null);
|
|
658
|
+
}
|
|
659
|
+
const all = [
|
|
660
|
+
...doc.ik.map((c) => ({ order: c.order, kind: "ik", c })),
|
|
661
|
+
...doc.transform.map((c) => ({ order: c.order, kind: "transform", c })),
|
|
662
|
+
...doc.path.map((c) => ({ order: c.order, kind: "path", c }))
|
|
663
|
+
].sort((a, b) => a.order - b.order);
|
|
664
|
+
for (const { kind, c } of all) {
|
|
665
|
+
if (kind === "ik") applyIk(skel, c);
|
|
666
|
+
else if (kind === "transform") applyTransformConstraint(skel, c);
|
|
667
|
+
else applyPathConstraint(skel, c);
|
|
668
|
+
}
|
|
669
|
+
return skel;
|
|
670
|
+
}
|
|
671
|
+
var cnum = (skel, kind, id, prop, rest) => num(skel.pose, pk(kind, id, prop), typeof rest === "boolean" ? rest ? 1 : 0 : rest);
|
|
672
|
+
var IDENTITY = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
673
|
+
var wrapRad = (r) => r > Math.PI ? r - 2 * Math.PI : r < -Math.PI ? r + 2 * Math.PI : r;
|
|
674
|
+
function aimAt(skel, boneId, tx, ty, mix, compress = false, stretch = false, uniform = false) {
|
|
675
|
+
const b = boneOf(skel.doc, boneId), st = skel.bones.get(boneId);
|
|
676
|
+
const parent = b.parent ? skel.bones.get(b.parent).world : void 0;
|
|
677
|
+
let dx, dy;
|
|
678
|
+
if (parent && b.inherit === "onlyTranslation") {
|
|
679
|
+
dx = tx - st.world.tx;
|
|
680
|
+
dy = ty - st.world.ty;
|
|
681
|
+
} else {
|
|
682
|
+
const [px, py] = parent ? apply(invert(parent), tx, ty) : [tx, ty];
|
|
683
|
+
dx = px - st.x;
|
|
684
|
+
dy = py - st.y;
|
|
685
|
+
}
|
|
686
|
+
let r = Math.atan2(dy, dx) / DEG2 - st.shearX - st.rotation;
|
|
687
|
+
if (st.scaleX < 0) r += 180;
|
|
688
|
+
r = shortest(r);
|
|
689
|
+
let sx = st.scaleX, sy = st.scaleY;
|
|
690
|
+
if (compress || stretch) {
|
|
691
|
+
const len = b.length * sx;
|
|
692
|
+
if (len > 1e-4) {
|
|
693
|
+
const dd = dx * dx + dy * dy;
|
|
694
|
+
if (compress && dd < len * len || stretch && dd > len * len) {
|
|
695
|
+
const f = (Math.sqrt(dd) / len - 1) * mix + 1;
|
|
696
|
+
sx *= f;
|
|
697
|
+
if (uniform) sy *= f;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
st.rotation += r * mix;
|
|
702
|
+
st.scaleX = sx;
|
|
703
|
+
st.scaleY = sy;
|
|
704
|
+
updateSubtree(skel, boneId);
|
|
705
|
+
}
|
|
706
|
+
var shortest = (deg) => ((deg + 180) % 360 + 360) % 360 - 180;
|
|
707
|
+
function applyIk(skel, c) {
|
|
708
|
+
const mix = cnum(skel, "ik", c.id, "mix", c.mix);
|
|
709
|
+
if (mix <= 0) return;
|
|
710
|
+
const target = skel.bones.get(c.target).world;
|
|
711
|
+
const bendDir = cnum(skel, "ik", c.id, "bendPositive", c.bendPositive) > 0.5 ? 1 : -1;
|
|
712
|
+
const compress = cnum(skel, "ik", c.id, "compress", c.compress) > 0.5, stretch = cnum(skel, "ik", c.id, "stretch", c.stretch) > 0.5;
|
|
713
|
+
let softness = cnum(skel, "ik", c.id, "softness", c.softness);
|
|
714
|
+
if (c.bones.length === 1) {
|
|
715
|
+
aimAt(skel, c.bones[0], target.tx, target.ty, mix, compress, stretch, c.uniform);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const [pId, cId] = c.bones;
|
|
719
|
+
const parentBone = boneOf(skel.doc, pId), childBone = boneOf(skel.doc, cId);
|
|
720
|
+
const p = skel.bones.get(pId), ch = skel.bones.get(cId);
|
|
721
|
+
const px = p.x, py = p.y;
|
|
722
|
+
let psx = p.scaleX, psy = p.scaleY, csx = ch.scaleX, sx = psx, sy = psy;
|
|
723
|
+
let os1 = 0, os2 = 0, s2 = 1;
|
|
724
|
+
if (psx < 0) {
|
|
725
|
+
psx = -psx;
|
|
726
|
+
os1 = 180;
|
|
727
|
+
s2 = -1;
|
|
728
|
+
}
|
|
729
|
+
if (psy < 0) {
|
|
730
|
+
psy = -psy;
|
|
731
|
+
s2 = -s2;
|
|
732
|
+
}
|
|
733
|
+
if (csx < 0) {
|
|
734
|
+
csx = -csx;
|
|
735
|
+
os2 = 180;
|
|
736
|
+
}
|
|
737
|
+
const uniform = Math.abs(psx - psy) <= 1e-4;
|
|
738
|
+
const cx = ch.x, cy = !uniform || stretch ? 0 : ch.y;
|
|
739
|
+
const [cwx, cwy] = apply(p.world, cx, cy);
|
|
740
|
+
const ppInv = invert(parentBone.parent ? skel.bones.get(parentBone.parent).world : IDENTITY);
|
|
741
|
+
const [clx, cly] = apply(ppInv, cwx, cwy);
|
|
742
|
+
const dx = clx - px, dy = cly - py, l1 = Math.hypot(dx, dy);
|
|
743
|
+
let l2 = childBone.length * csx;
|
|
744
|
+
if (l1 < 1e-4) {
|
|
745
|
+
aimAt(skel, pId, target.tx, target.ty, mix, false, stretch, false);
|
|
746
|
+
ch.x = cx;
|
|
747
|
+
ch.y = cy;
|
|
748
|
+
ch.rotation = 0;
|
|
749
|
+
updateSubtree(skel, pId);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const [tlx, tly] = apply(ppInv, target.tx, target.ty);
|
|
753
|
+
let tx = tlx - px, ty = tly - py, dd = tx * tx + ty * ty;
|
|
754
|
+
if (softness !== 0) {
|
|
755
|
+
softness *= psx * (csx + 1) * 0.5;
|
|
756
|
+
const td = Math.sqrt(dd), sd = td - l1 - l2 * psx + softness;
|
|
757
|
+
if (sd > 0) {
|
|
758
|
+
let q = Math.min(1, sd / (softness * 2)) - 1;
|
|
759
|
+
q = (sd - softness * (1 - q * q)) / td;
|
|
760
|
+
tx -= q * tx;
|
|
761
|
+
ty -= q * ty;
|
|
762
|
+
dd = tx * tx + ty * ty;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
let a1, a2;
|
|
766
|
+
if (uniform) {
|
|
767
|
+
l2 *= psx;
|
|
768
|
+
let cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2);
|
|
769
|
+
if (cos < -1) {
|
|
770
|
+
cos = -1;
|
|
771
|
+
a2 = Math.PI * bendDir;
|
|
772
|
+
} else if (cos > 1) {
|
|
773
|
+
cos = 1;
|
|
774
|
+
a2 = 0;
|
|
775
|
+
if (stretch) {
|
|
776
|
+
const f = (Math.sqrt(dd) / (l1 + l2) - 1) * mix + 1;
|
|
777
|
+
sx *= f;
|
|
778
|
+
if (c.uniform) sy *= f;
|
|
779
|
+
}
|
|
780
|
+
} else a2 = Math.acos(cos) * bendDir;
|
|
781
|
+
const a = l1 + l2 * cos, b = l2 * Math.sin(a2);
|
|
782
|
+
a1 = Math.atan2(ty * a - tx * b, tx * a + ty * b);
|
|
783
|
+
} else {
|
|
784
|
+
const a = psx * l2, b = psy * l2, aa = a * a, bb = b * b, ta = Math.atan2(ty, tx);
|
|
785
|
+
const c0 = bb * l1 * l1 + aa * dd - aa * bb, c1 = -2 * bb * l1, c2 = bb - aa;
|
|
786
|
+
const disc = c1 * c1 - 4 * c2 * c0;
|
|
787
|
+
let solved = false;
|
|
788
|
+
a1 = a2 = 0;
|
|
789
|
+
if (disc >= 0) {
|
|
790
|
+
let q = Math.sqrt(disc);
|
|
791
|
+
if (c1 < 0) q = -q;
|
|
792
|
+
q = -(c1 + q) * 0.5;
|
|
793
|
+
const r0 = q / c2, r1 = c0 / q, r = Math.abs(r0) < Math.abs(r1) ? r0 : r1, yy = dd - r * r;
|
|
794
|
+
if (yy >= 0) {
|
|
795
|
+
const y = Math.sqrt(yy) * bendDir;
|
|
796
|
+
a1 = ta - Math.atan2(y, r);
|
|
797
|
+
a2 = Math.atan2(y / psy, (r - l1) / psx);
|
|
798
|
+
solved = true;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (!solved) {
|
|
802
|
+
let minAngle = Math.PI, minX = l1 - a, minDist = minX * minX, minY = 0, maxAngle = 0, maxX = l1 + a, maxDist = maxX * maxX, maxY = 0;
|
|
803
|
+
const k = -a * l1 / (aa - bb);
|
|
804
|
+
if (k >= -1 && k <= 1) {
|
|
805
|
+
const ang = Math.acos(k), x = a * Math.cos(ang) + l1, y = b * Math.sin(ang), dist = x * x + y * y;
|
|
806
|
+
if (dist < minDist) {
|
|
807
|
+
minAngle = ang;
|
|
808
|
+
minDist = dist;
|
|
809
|
+
minX = x;
|
|
810
|
+
minY = y;
|
|
811
|
+
}
|
|
812
|
+
if (dist > maxDist) {
|
|
813
|
+
maxAngle = ang;
|
|
814
|
+
maxDist = dist;
|
|
815
|
+
maxX = x;
|
|
816
|
+
maxY = y;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (dd <= (minDist + maxDist) * 0.5) {
|
|
820
|
+
a1 = ta - Math.atan2(minY * bendDir, minX);
|
|
821
|
+
a2 = minAngle * bendDir;
|
|
822
|
+
} else {
|
|
823
|
+
a1 = ta - Math.atan2(maxY * bendDir, maxX);
|
|
824
|
+
a2 = maxAngle * bendDir;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const os = Math.atan2(cy, cx) * s2;
|
|
829
|
+
const d1 = shortest((a1 - os) / DEG2 + os1 - p.rotation);
|
|
830
|
+
const d2 = shortest(((a2 + os) / DEG2 - ch.shearX) * s2 + os2 - ch.rotation);
|
|
831
|
+
p.rotation += d1 * mix;
|
|
832
|
+
p.scaleX = sx;
|
|
833
|
+
p.scaleY = sy;
|
|
834
|
+
p.shearX = 0;
|
|
835
|
+
p.shearY = 0;
|
|
836
|
+
ch.x = cx;
|
|
837
|
+
ch.y = cy;
|
|
838
|
+
ch.rotation += d2 * mix;
|
|
839
|
+
updateSubtree(skel, pId);
|
|
840
|
+
}
|
|
841
|
+
function applyTransformConstraint(skel, c) {
|
|
842
|
+
const mixRotate = cnum(skel, "transform", c.id, "mixRotate", c.mixRotate), mixX = cnum(skel, "transform", c.id, "mixX", c.mixX), mixY = cnum(skel, "transform", c.id, "mixY", c.mixY);
|
|
843
|
+
const mixScaleX = cnum(skel, "transform", c.id, "mixScaleX", c.mixScaleX), mixScaleY = cnum(skel, "transform", c.id, "mixScaleY", c.mixScaleY), mixShearY = cnum(skel, "transform", c.id, "mixShearY", c.mixShearY);
|
|
844
|
+
if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
|
|
845
|
+
const target = skel.bones.get(c.target), tw = target.world;
|
|
846
|
+
const offsetRotation = c.rotation * DEG2, offsetShearY = c.shearY * DEG2;
|
|
847
|
+
for (const id of c.bones) {
|
|
848
|
+
const b = boneOf(skel.doc, id), st = skel.bones.get(id);
|
|
849
|
+
if (c.local) {
|
|
850
|
+
if (c.relative) {
|
|
851
|
+
st.rotation += (target.rotation + c.rotation) * mixRotate;
|
|
852
|
+
st.x += (target.x + c.x) * mixX;
|
|
853
|
+
st.y += (target.y + c.y) * mixY;
|
|
854
|
+
st.scaleX *= (target.scaleX - 1 + c.scaleX) * mixScaleX + 1;
|
|
855
|
+
st.scaleY *= (target.scaleY - 1 + c.scaleY) * mixScaleY + 1;
|
|
856
|
+
st.shearY += (target.shearY + c.shearY) * mixShearY;
|
|
857
|
+
} else {
|
|
858
|
+
st.rotation += (target.rotation - st.rotation + c.rotation) * mixRotate;
|
|
859
|
+
st.x += (target.x - st.x + c.x) * mixX;
|
|
860
|
+
st.y += (target.y - st.y + c.y) * mixY;
|
|
861
|
+
if (st.scaleX !== 0) st.scaleX += (target.scaleX - st.scaleX + c.scaleX) * mixScaleX;
|
|
862
|
+
if (st.scaleY !== 0) st.scaleY += (target.scaleY - st.scaleY + c.scaleY) * mixScaleY;
|
|
863
|
+
st.shearY += (target.shearY - st.shearY + c.shearY) * mixShearY;
|
|
864
|
+
}
|
|
865
|
+
updateSubtree(skel, id);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
let w = { ...st.world };
|
|
869
|
+
const targetRot = Math.atan2(tw.b, tw.a), rotate = (m, r) => {
|
|
870
|
+
const cos = Math.cos(r), sin = Math.sin(r);
|
|
871
|
+
return { a: cos * m.a - sin * m.b, b: sin * m.a + cos * m.b, c: cos * m.c - sin * m.d, d: sin * m.c + cos * m.d, tx: m.tx, ty: m.ty };
|
|
872
|
+
};
|
|
873
|
+
if (mixRotate !== 0) {
|
|
874
|
+
const r = wrapRad(c.relative ? targetRot + offsetRotation : targetRot - Math.atan2(w.b, w.a) + offsetRotation);
|
|
875
|
+
w = rotate(w, r * mixRotate);
|
|
876
|
+
}
|
|
877
|
+
if (mixX !== 0 || mixY !== 0) {
|
|
878
|
+
const [ox, oy] = apply(tw, c.x, c.y);
|
|
879
|
+
if (c.relative) {
|
|
880
|
+
w.tx += ox * mixX;
|
|
881
|
+
w.ty += oy * mixY;
|
|
882
|
+
} else {
|
|
883
|
+
w.tx += (ox - w.tx) * mixX;
|
|
884
|
+
w.ty += (oy - w.ty) * mixY;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (mixScaleX !== 0) {
|
|
888
|
+
const ts = Math.hypot(tw.a, tw.b);
|
|
889
|
+
let f;
|
|
890
|
+
if (c.relative) f = (ts - 1 + c.scaleX) * mixScaleX + 1;
|
|
891
|
+
else {
|
|
892
|
+
const s0 = Math.hypot(w.a, w.b);
|
|
893
|
+
f = s0 === 0 ? 1 : (s0 + (ts - s0 + c.scaleX) * mixScaleX) / s0;
|
|
894
|
+
}
|
|
895
|
+
w.a *= f;
|
|
896
|
+
w.b *= f;
|
|
897
|
+
}
|
|
898
|
+
if (mixScaleY !== 0) {
|
|
899
|
+
const ts = Math.hypot(tw.c, tw.d);
|
|
900
|
+
let f;
|
|
901
|
+
if (c.relative) f = (ts - 1 + c.scaleY) * mixScaleY + 1;
|
|
902
|
+
else {
|
|
903
|
+
const s0 = Math.hypot(w.c, w.d);
|
|
904
|
+
f = s0 === 0 ? 1 : (s0 + (ts - s0 + c.scaleY) * mixScaleY) / s0;
|
|
905
|
+
}
|
|
906
|
+
w.c *= f;
|
|
907
|
+
w.d *= f;
|
|
908
|
+
}
|
|
909
|
+
if (mixShearY > 0) {
|
|
910
|
+
const by = Math.atan2(w.d, w.c), targetShear = Math.atan2(tw.d, tw.c) - targetRot;
|
|
911
|
+
const r = c.relative ? by + (wrapRad(targetShear) - Math.PI / 2 + offsetShearY) * mixShearY : by + (wrapRad(targetShear - (by - Math.atan2(w.b, w.a))) + offsetShearY) * mixShearY;
|
|
912
|
+
const len = Math.hypot(w.c, w.d);
|
|
913
|
+
w.c = Math.cos(r) * len;
|
|
914
|
+
w.d = Math.sin(r) * len;
|
|
915
|
+
}
|
|
916
|
+
setEffectiveWorld(skel, b, st, w);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function pathWorldVertices(skel, slotId, att) {
|
|
920
|
+
return attachmentWorldVertices(skel, slotId, att);
|
|
921
|
+
}
|
|
922
|
+
function pathLength(vertexCount, closed, lengths) {
|
|
923
|
+
const curveCount = closed ? vertexCount / 3 : vertexCount / 3 - 1;
|
|
924
|
+
return curveCount >= 1 && lengths.length >= curveCount ? lengths[curveCount - 1] : lengths[lengths.length - 1] ?? 1;
|
|
925
|
+
}
|
|
926
|
+
function samplePath(points, closed, lengths, constantSpeed, positions, percent) {
|
|
927
|
+
const n = points.length / 2;
|
|
928
|
+
const curveCount = closed ? n / 3 : n / 3 - 1;
|
|
929
|
+
if (curveCount < 1) return positions.map(() => ({ x: points[2] ?? 0, y: points[3] ?? 0, angle: 0 }));
|
|
930
|
+
const curve = (i) => {
|
|
931
|
+
const a = (i * 3 + 1) % n * 2, c1 = (i * 3 + 2) % n * 2, c2 = (i * 3 + 3) % n * 2, b = (i * 3 + 4) % n * 2;
|
|
932
|
+
return [points[a], points[a + 1], points[c1], points[c1 + 1], points[c2], points[c2 + 1], points[b], points[b + 1]];
|
|
933
|
+
};
|
|
934
|
+
const pointAt = (i, t) => {
|
|
935
|
+
const [x0, y0, x1, y1, x2, y2, x3, y3] = curve(i), u = 1 - t;
|
|
936
|
+
return { x: u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3, y: u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3 };
|
|
937
|
+
};
|
|
938
|
+
const angleAt = (i, t) => {
|
|
939
|
+
const [x0, y0, x1, y1, x2, y2, x3, y3] = curve(i), u = 1 - t;
|
|
940
|
+
const dx = 3 * u * u * (x1 - x0) + 6 * u * t * (x2 - x1) + 3 * t * t * (x3 - x2);
|
|
941
|
+
const dy = 3 * u * u * (y1 - y0) + 6 * u * t * (y2 - y1) + 3 * t * t * (y3 - y2);
|
|
942
|
+
return Math.atan2(dy, dx) / DEG2;
|
|
943
|
+
};
|
|
944
|
+
const chords = (i, count) => {
|
|
945
|
+
const acc = [];
|
|
946
|
+
let sum = 0, prev = pointAt(i, 0);
|
|
947
|
+
for (let k = 1; k <= count; k++) {
|
|
948
|
+
const q = pointAt(i, k / count);
|
|
949
|
+
sum += Math.hypot(q.x - prev.x, q.y - prev.y);
|
|
950
|
+
acc.push(sum);
|
|
951
|
+
prev = q;
|
|
952
|
+
}
|
|
953
|
+
return acc;
|
|
954
|
+
};
|
|
955
|
+
const cum = [];
|
|
956
|
+
if (constantSpeed) {
|
|
957
|
+
let acc = 0;
|
|
958
|
+
for (let i = 0; i < curveCount; i++) {
|
|
959
|
+
acc += chords(i, 4)[3];
|
|
960
|
+
cum.push(acc);
|
|
961
|
+
}
|
|
962
|
+
} else for (let i = 0; i < curveCount; i++) cum.push(lengths[i] ?? (i ? cum[i - 1] : 0));
|
|
963
|
+
const total = cum[curveCount - 1];
|
|
964
|
+
const out = [];
|
|
965
|
+
for (let pos of positions) {
|
|
966
|
+
if (percent) pos *= total;
|
|
967
|
+
if (closed) {
|
|
968
|
+
if (total > 0) {
|
|
969
|
+
pos %= total;
|
|
970
|
+
if (pos < 0) pos += total;
|
|
971
|
+
} else pos = 0;
|
|
972
|
+
} else if (pos < 0) {
|
|
973
|
+
const r = Math.atan2(points[5] - points[3], points[4] - points[2]);
|
|
974
|
+
out.push({ x: points[2] + pos * Math.cos(r), y: points[3] + pos * Math.sin(r), angle: r / DEG2 });
|
|
975
|
+
continue;
|
|
976
|
+
} else if (pos > total) {
|
|
977
|
+
const a = (curveCount * 3 + 1) * 2, c = curveCount * 3 * 2, e = pos - total;
|
|
978
|
+
const r = Math.atan2(points[a + 1] - points[c + 1], points[a] - points[c]);
|
|
979
|
+
out.push({ x: points[a] + e * Math.cos(r), y: points[a + 1] + e * Math.sin(r), angle: r / DEG2 });
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
let i = 0;
|
|
983
|
+
while (i < curveCount - 1 && pos > cum[i]) i++;
|
|
984
|
+
const prev = i ? cum[i - 1] : 0, span = cum[i] - prev;
|
|
985
|
+
const f = span > 0 ? (pos - prev) / span : 0;
|
|
986
|
+
let t = f;
|
|
987
|
+
if (constantSpeed) {
|
|
988
|
+
const seg = chords(i, 10), target = f * seg[9];
|
|
989
|
+
let k = 0;
|
|
990
|
+
while (k < 9 && target > seg[k]) k++;
|
|
991
|
+
const sPrev = k ? seg[k - 1] : 0, sSpan = seg[k] - sPrev;
|
|
992
|
+
t = (k + (sSpan > 0 ? (target - sPrev) / sSpan : 0)) / 10;
|
|
993
|
+
}
|
|
994
|
+
out.push({ ...pointAt(i, t), angle: angleAt(i, t) });
|
|
995
|
+
}
|
|
996
|
+
return out;
|
|
997
|
+
}
|
|
998
|
+
var PATH_EPSILON = 1e-5;
|
|
999
|
+
function applyPathConstraint(skel, c) {
|
|
1000
|
+
const mixRotate = cnum(skel, "path", c.id, "mixRotate", c.mixRotate), mixX = cnum(skel, "path", c.id, "mixX", c.mixX), mixY = cnum(skel, "path", c.id, "mixY", c.mixY);
|
|
1001
|
+
if (!mixRotate && !mixX && !mixY) return;
|
|
1002
|
+
const att = skel.attachment.get(c.target);
|
|
1003
|
+
if (!att || att.type !== "path") return;
|
|
1004
|
+
const points = pathWorldVertices(skel, c.target, att);
|
|
1005
|
+
const position = cnum(skel, "path", c.id, "position", c.position), spacing = cnum(skel, "path", c.id, "spacing", c.spacing);
|
|
1006
|
+
const tangents = c.rotateMode === "tangent", scale = c.rotateMode === "chainScale";
|
|
1007
|
+
const bones = c.bones.map((id) => ({ b: boneOf(skel.doc, id), st: skel.bones.get(id) }));
|
|
1008
|
+
const spacesCount = tangents ? bones.length : bones.length + 1;
|
|
1009
|
+
const spaces = new Array(spacesCount).fill(0), worldLengths = [];
|
|
1010
|
+
let sum = 0;
|
|
1011
|
+
for (let i = 0; i < spacesCount - 1; i++) {
|
|
1012
|
+
const { b, st } = bones[i], setupLength = b.length;
|
|
1013
|
+
const len = setupLength < PATH_EPSILON ? 0 : setupLength * Math.hypot(st.world.a, st.world.b);
|
|
1014
|
+
worldLengths.push(len);
|
|
1015
|
+
if (c.spacingMode === "percent") spaces[i + 1] = spacing;
|
|
1016
|
+
else if (setupLength < PATH_EPSILON) spaces[i + 1] = spacing;
|
|
1017
|
+
else if (c.spacingMode === "proportional") {
|
|
1018
|
+
spaces[i + 1] = len;
|
|
1019
|
+
sum += len;
|
|
1020
|
+
} else spaces[i + 1] = (c.spacingMode === "length" ? setupLength + spacing : spacing) * len / setupLength;
|
|
1021
|
+
}
|
|
1022
|
+
if (c.spacingMode === "proportional" && sum > 0) {
|
|
1023
|
+
const f = spacesCount / sum * spacing;
|
|
1024
|
+
for (let i = 1; i < spacesCount; i++) spaces[i] *= f;
|
|
1025
|
+
}
|
|
1026
|
+
const total = measuredPathLength(points, att.closed, att.lengths, att.constantSpeed);
|
|
1027
|
+
const multiplier = c.spacingMode === "percent" ? total : c.spacingMode === "proportional" ? total / spacesCount : 1;
|
|
1028
|
+
let p = c.positionMode === "percent" ? position * total : position;
|
|
1029
|
+
const positions = [];
|
|
1030
|
+
for (let i = 0; i < spacesCount; i++) {
|
|
1031
|
+
p += spaces[i] * multiplier;
|
|
1032
|
+
positions.push(p);
|
|
1033
|
+
}
|
|
1034
|
+
const samples = samplePath(points, att.closed, att.lengths, att.constantSpeed, positions, false);
|
|
1035
|
+
const tip = c.rotation === 0 && c.rotateMode === "chain";
|
|
1036
|
+
let offsetRotation = c.rotation * DEG2;
|
|
1037
|
+
if (c.rotation !== 0) {
|
|
1038
|
+
const slot = skel.doc.slots.find((sl) => sl.id === c.target), tw = skel.bones.get(slot.bone).world;
|
|
1039
|
+
if (tw.a * tw.d - tw.b * tw.c < 0) offsetRotation = -offsetRotation;
|
|
1040
|
+
}
|
|
1041
|
+
let boneX = samples[0].x, boneY = samples[0].y;
|
|
1042
|
+
for (let i = 0; i < bones.length; i++) {
|
|
1043
|
+
const { b, st } = bones[i];
|
|
1044
|
+
let w = { ...st.world };
|
|
1045
|
+
w.tx += (boneX - w.tx) * mixX;
|
|
1046
|
+
w.ty += (boneY - w.ty) * mixY;
|
|
1047
|
+
const next = samples[i + 1] ?? { x: 0, y: 0, angle: 0 };
|
|
1048
|
+
const dx = next.x - boneX, dy = next.y - boneY;
|
|
1049
|
+
if (scale) {
|
|
1050
|
+
const len = worldLengths[i];
|
|
1051
|
+
if (len !== 0) {
|
|
1052
|
+
const f = (Math.hypot(dx, dy) / len - 1) * mixRotate + 1;
|
|
1053
|
+
w.a *= f;
|
|
1054
|
+
w.b *= f;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
boneX = next.x;
|
|
1058
|
+
boneY = next.y;
|
|
1059
|
+
if (mixRotate > 0) {
|
|
1060
|
+
const { a, b: bb, c: cc, d } = w;
|
|
1061
|
+
let r = (tangents ? samples[i].angle : spaces[i + 1] === 0 ? next.angle : Math.atan2(dy, dx) / DEG2) * DEG2;
|
|
1062
|
+
r -= Math.atan2(bb, a);
|
|
1063
|
+
if (tip) {
|
|
1064
|
+
const cos2 = Math.cos(r), sin2 = Math.sin(r), length = b.length;
|
|
1065
|
+
boneX += (length * (cos2 * a - sin2 * bb) - dx) * mixRotate;
|
|
1066
|
+
boneY += (length * (sin2 * a + cos2 * bb) - dy) * mixRotate;
|
|
1067
|
+
} else r += offsetRotation;
|
|
1068
|
+
if (r > Math.PI) r -= 2 * Math.PI;
|
|
1069
|
+
else if (r < -Math.PI) r += 2 * Math.PI;
|
|
1070
|
+
r *= mixRotate;
|
|
1071
|
+
const cos = Math.cos(r), sin = Math.sin(r);
|
|
1072
|
+
w = { a: cos * a - sin * bb, b: sin * a + cos * bb, c: cos * cc - sin * d, d: sin * cc + cos * d, tx: w.tx, ty: w.ty };
|
|
1073
|
+
}
|
|
1074
|
+
setEffectiveWorld(skel, b, st, w);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function measuredPathLength(points, closed, lengths, constantSpeed) {
|
|
1078
|
+
const n = points.length / 2, curveCount = closed ? n / 3 : n / 3 - 1;
|
|
1079
|
+
if (curveCount < 1) return 0;
|
|
1080
|
+
if (!constantSpeed) return pathLength(n, closed, lengths);
|
|
1081
|
+
let total = 0;
|
|
1082
|
+
for (let i = 0; i < curveCount; i++) {
|
|
1083
|
+
const a = (i * 3 + 1) % n * 2, c1 = (i * 3 + 2) % n * 2, c2 = (i * 3 + 3) % n * 2, b = (i * 3 + 4) % n * 2;
|
|
1084
|
+
let px = points[a], py = points[a + 1];
|
|
1085
|
+
for (let k = 1; k <= 4; k++) {
|
|
1086
|
+
const t = k / 4, u = 1 - t;
|
|
1087
|
+
const x = u * u * u * points[a] + 3 * u * u * t * points[c1] + 3 * u * t * t * points[c2] + t * t * t * points[b];
|
|
1088
|
+
const y = u * u * u * points[a + 1] + 3 * u * u * t * points[c1 + 1] + 3 * u * t * t * points[c2 + 1] + t * t * t * points[b + 1];
|
|
1089
|
+
total += Math.hypot(x - px, y - py);
|
|
1090
|
+
px = x;
|
|
1091
|
+
py = y;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return total;
|
|
1095
|
+
}
|
|
1096
|
+
function inheritedParentLinear(bone, parent) {
|
|
1097
|
+
switch (bone.inherit) {
|
|
1098
|
+
case "normal":
|
|
1099
|
+
return { ...parent, tx: 0, ty: 0 };
|
|
1100
|
+
case "onlyTranslation":
|
|
1101
|
+
return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
1102
|
+
case "noRotationOrReflection": {
|
|
1103
|
+
const lenX = Math.hypot(parent.a, parent.b) || 1;
|
|
1104
|
+
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1105
|
+
return { a: lenX, b: 0, c: 0, d: Math.abs(det) / lenX, tx: 0, ty: 0 };
|
|
1106
|
+
}
|
|
1107
|
+
case "noScale":
|
|
1108
|
+
case "noScaleOrReflection": {
|
|
1109
|
+
const lenX = Math.hypot(parent.a, parent.b) || 1;
|
|
1110
|
+
const ua = parent.a / lenX, ub = parent.b / lenX;
|
|
1111
|
+
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1112
|
+
const reflect = det < 0 && bone.inherit === "noScale" ? -1 : 1;
|
|
1113
|
+
return { a: ua, b: ub, c: -ub * reflect, d: ua * reflect, tx: 0, ty: 0 };
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function setEffectiveWorld(skel, bone, st, world) {
|
|
1118
|
+
const parent = bone.parent ? skel.bones.get(bone.parent).world : void 0;
|
|
1119
|
+
let lin = world, tx = world.tx, ty = world.ty;
|
|
1120
|
+
if (parent) {
|
|
1121
|
+
[tx, ty] = apply(invert(parent), world.tx, world.ty);
|
|
1122
|
+
lin = mul(invert(inheritedParentLinear(bone, parent)), { ...world, tx: 0, ty: 0 });
|
|
1123
|
+
}
|
|
1124
|
+
st.x = tx;
|
|
1125
|
+
st.y = ty;
|
|
1126
|
+
st.rotation = Math.atan2(lin.b, lin.a) / DEG2;
|
|
1127
|
+
st.scaleX = Math.hypot(lin.a, lin.b);
|
|
1128
|
+
st.scaleY = Math.hypot(lin.c, lin.d);
|
|
1129
|
+
st.shearX = 0;
|
|
1130
|
+
st.shearY = shortest(Math.atan2(lin.d, lin.c) / DEG2 - st.rotation - 90);
|
|
1131
|
+
updateSubtree(skel, bone.id);
|
|
1132
|
+
}
|
|
1133
|
+
function attachmentWorldVertices(skel, slotId, att) {
|
|
1134
|
+
const deform = skel.pose.get(pk("deform", att.id, "vertices"));
|
|
1135
|
+
const off = Array.isArray(deform) && typeof deform[0] === "number" ? deform : void 0;
|
|
1136
|
+
const out = [];
|
|
1137
|
+
if (att.weights) {
|
|
1138
|
+
let k = 0;
|
|
1139
|
+
for (const list of att.weights) {
|
|
1140
|
+
let x = 0, y = 0;
|
|
1141
|
+
for (const inf of list) {
|
|
1142
|
+
const w = skel.bones.get(inf.bone).world;
|
|
1143
|
+
const vx = inf.x + (off ? off[k] : 0), vy = inf.y + (off ? off[k + 1] : 0);
|
|
1144
|
+
k += 2;
|
|
1145
|
+
x += (w.a * vx + w.c * vy + w.tx) * inf.w;
|
|
1146
|
+
y += (w.b * vx + w.d * vy + w.ty) * inf.w;
|
|
1147
|
+
}
|
|
1148
|
+
out.push(x, y);
|
|
1149
|
+
}
|
|
1150
|
+
} else {
|
|
1151
|
+
const slot = skel.doc.slots.find((s) => s.id === slotId), w = skel.bones.get(slot.bone).world, v = att.vertices;
|
|
1152
|
+
for (let i = 0; i < v.length; i += 2) {
|
|
1153
|
+
const vx = v[i] + (off ? off[i] : 0), vy = v[i + 1] + (off ? off[i + 1] : 0);
|
|
1154
|
+
out.push(w.a * vx + w.c * vy + w.tx, w.b * vx + w.d * vy + w.ty);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return out;
|
|
1158
|
+
}
|
|
1159
|
+
function regionMatrix(skel, slotId, att) {
|
|
1160
|
+
const slot = skel.doc.slots.find((s) => s.id === slotId);
|
|
1161
|
+
return mul(skel.bones.get(slot.bone).world, affineOf(att.x, att.y, att.rotation, att.scaleX, att.scaleY));
|
|
1162
|
+
}
|
|
1163
|
+
function regionSize(doc, att) {
|
|
1164
|
+
const asset = doc.assets.find((a) => a.id === att.asset);
|
|
1165
|
+
return [att.width ?? asset.width, att.height ?? asset.height];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/rig-state.ts
|
|
1169
|
+
var AnimationState = class {
|
|
1170
|
+
constructor(sink, opts = {}) {
|
|
1171
|
+
this.sink = sink;
|
|
1172
|
+
this.defaultMix = opts.mix ?? 0;
|
|
1173
|
+
}
|
|
1174
|
+
onEvent;
|
|
1175
|
+
base;
|
|
1176
|
+
main;
|
|
1177
|
+
queue = [];
|
|
1178
|
+
overlays = /* @__PURE__ */ new Map();
|
|
1179
|
+
/** last composed pose without overlays — mix snapshots start here */
|
|
1180
|
+
pose = /* @__PURE__ */ new Map();
|
|
1181
|
+
from;
|
|
1182
|
+
mixLeft = 0;
|
|
1183
|
+
mixTotal = 0;
|
|
1184
|
+
/** a mix begun in this update shows weight 0 for the frame it starts on */
|
|
1185
|
+
mixFresh = false;
|
|
1186
|
+
defaultMix;
|
|
1187
|
+
/** id of the animation currently shown (main, else base) */
|
|
1188
|
+
get current() {
|
|
1189
|
+
return (this.main ?? this.base)?.anim.id;
|
|
1190
|
+
}
|
|
1191
|
+
get baseId() {
|
|
1192
|
+
return this.base?.anim.id;
|
|
1193
|
+
}
|
|
1194
|
+
channel(id, loop, priority = 0) {
|
|
1195
|
+
const anim = this.sink.doc.animations.find((a) => a.id === id);
|
|
1196
|
+
if (!anim) throw new Error(`unknown animation "${id}"`);
|
|
1197
|
+
return { anim, time: 0, loop: loop ?? anim.loop, done: false, priority, fresh: true };
|
|
1198
|
+
}
|
|
1199
|
+
setBase(id, opts = {}) {
|
|
1200
|
+
this.base = id === void 0 ? void 0 : this.channel(id);
|
|
1201
|
+
if (!this.main || this.main.done) {
|
|
1202
|
+
this.main = void 0;
|
|
1203
|
+
this.beginMix(opts.mix);
|
|
1204
|
+
this.update(0);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
play(id, opts = {}) {
|
|
1208
|
+
const ch = this.channel(id, opts.loop, opts.priority ?? 0);
|
|
1209
|
+
const promise = new Promise((resolve) => {
|
|
1210
|
+
ch.settle = (r) => {
|
|
1211
|
+
ch.settle = void 0;
|
|
1212
|
+
opts.onComplete?.(r);
|
|
1213
|
+
resolve(r);
|
|
1214
|
+
};
|
|
1215
|
+
});
|
|
1216
|
+
const cur = this.main, busy = cur !== void 0 && !cur.done, mode = opts.ifBusy ?? "queue";
|
|
1217
|
+
if (!busy || mode === "interrupt" || ch.priority >= cur.priority) {
|
|
1218
|
+
if (busy) cur.settle?.("interrupted");
|
|
1219
|
+
this.main = ch;
|
|
1220
|
+
this.beginMix(opts.mix);
|
|
1221
|
+
this.update(0);
|
|
1222
|
+
} else if (mode === "drop") ch.settle("dropped");
|
|
1223
|
+
else this.queue.push(ch);
|
|
1224
|
+
return promise;
|
|
1225
|
+
}
|
|
1226
|
+
/** Independent loop on top of the shown pose; `undefined` removes it. */
|
|
1227
|
+
setOverlay(name, id) {
|
|
1228
|
+
if (id === void 0) this.overlays.delete(name);
|
|
1229
|
+
else this.overlays.set(name, this.channel(id));
|
|
1230
|
+
}
|
|
1231
|
+
/** Clear everything and show the setup pose. Pending plays resolve as "stopped". */
|
|
1232
|
+
stop() {
|
|
1233
|
+
this.main?.settle?.("stopped");
|
|
1234
|
+
for (const ch of this.queue) ch.settle?.("stopped");
|
|
1235
|
+
this.main = this.base = void 0;
|
|
1236
|
+
this.queue = [];
|
|
1237
|
+
this.overlays.clear();
|
|
1238
|
+
this.from = void 0;
|
|
1239
|
+
this.mixLeft = this.mixTotal = 0;
|
|
1240
|
+
this.mixFresh = false;
|
|
1241
|
+
this.pose = /* @__PURE__ */ new Map();
|
|
1242
|
+
this.sink.setPose(/* @__PURE__ */ new Map());
|
|
1243
|
+
}
|
|
1244
|
+
update(dt) {
|
|
1245
|
+
if (this.base) this.advance(this.base, dt, this.main === void 0);
|
|
1246
|
+
if (this.main) {
|
|
1247
|
+
const wasDone = this.main.done;
|
|
1248
|
+
this.advance(this.main, dt, true);
|
|
1249
|
+
if (this.main.done && !wasDone) {
|
|
1250
|
+
this.main.settle?.("completed");
|
|
1251
|
+
const next = this.queue.shift();
|
|
1252
|
+
if (next || this.base) {
|
|
1253
|
+
this.main = next;
|
|
1254
|
+
this.beginMix();
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const shown = this.main ?? this.base;
|
|
1259
|
+
let pose = shown ? poseOf(shown.anim, shown.time) : /* @__PURE__ */ new Map();
|
|
1260
|
+
if (this.from) {
|
|
1261
|
+
if (!this.mixFresh) this.mixLeft = Math.max(0, this.mixLeft - dt);
|
|
1262
|
+
this.mixFresh = false;
|
|
1263
|
+
const w = this.mixTotal > 0 ? 1 - this.mixLeft / this.mixTotal : 1;
|
|
1264
|
+
pose = blendPoses(this.from, pose, w, (k) => restValue(this.sink.doc, k));
|
|
1265
|
+
if (this.mixLeft === 0) this.from = void 0;
|
|
1266
|
+
}
|
|
1267
|
+
this.pose = pose;
|
|
1268
|
+
if (this.overlays.size) {
|
|
1269
|
+
pose = new Map(pose);
|
|
1270
|
+
for (const [name, ov] of this.overlays) {
|
|
1271
|
+
this.advance(ov, dt, false);
|
|
1272
|
+
poseOf(ov.anim, ov.time, pose);
|
|
1273
|
+
if (ov.done) this.overlays.delete(name);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
this.sink.setPose(pose);
|
|
1277
|
+
}
|
|
1278
|
+
beginMix(mix = this.defaultMix) {
|
|
1279
|
+
if (mix > 0) {
|
|
1280
|
+
this.from = new Map(this.pose);
|
|
1281
|
+
this.mixLeft = this.mixTotal = mix;
|
|
1282
|
+
this.mixFresh = true;
|
|
1283
|
+
} else {
|
|
1284
|
+
this.from = void 0;
|
|
1285
|
+
this.mixLeft = this.mixTotal = 0;
|
|
1286
|
+
this.mixFresh = false;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
advance(ch, dt, emit) {
|
|
1290
|
+
if (ch.done) return;
|
|
1291
|
+
const prev = ch.fresh ? -1 : ch.time, d = ch.anim.duration;
|
|
1292
|
+
ch.fresh = false;
|
|
1293
|
+
ch.time += dt;
|
|
1294
|
+
if (emit) {
|
|
1295
|
+
for (const e of ch.anim.events) if (hit(e.t, prev, ch.time, d, ch.loop)) this.onEvent?.(e.name, e.payload);
|
|
1296
|
+
}
|
|
1297
|
+
if (ch.time >= d) {
|
|
1298
|
+
if (ch.loop) ch.time %= d;
|
|
1299
|
+
else {
|
|
1300
|
+
ch.time = d;
|
|
1301
|
+
ch.done = true;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
function hit(t, prev, cur, d, loop) {
|
|
1307
|
+
if (cur < d || !loop) return t > prev && t <= cur;
|
|
1308
|
+
return t > prev && t <= d || t <= cur % d;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/rig-runtime.ts
|
|
1312
|
+
var DEG3 = Math.PI / 180;
|
|
1313
|
+
var MESH_GRID = 9;
|
|
1314
|
+
async function createTextures(doc, loadSource) {
|
|
1315
|
+
const bySrc = /* @__PURE__ */ new Map(), out = /* @__PURE__ */ new Map();
|
|
1316
|
+
for (const a of doc.assets) {
|
|
1317
|
+
let base = bySrc.get(a.src);
|
|
1318
|
+
if (!base) bySrc.set(a.src, base = await loadSource(a.src));
|
|
1319
|
+
if (!a.frame) {
|
|
1320
|
+
out.set(a.id, base);
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
const [fx, fy, w, h] = a.frame;
|
|
1324
|
+
const trimmed = !!a.trim || w !== a.width || h !== a.height;
|
|
1325
|
+
const [ox, oy] = a.trim ?? [0, 0];
|
|
1326
|
+
out.set(a.id, new Texture({
|
|
1327
|
+
source: base.source,
|
|
1328
|
+
frame: new Rectangle(fx, fy, a.rotate ? h : w, a.rotate ? w : h),
|
|
1329
|
+
orig: new Rectangle(0, 0, a.width, a.height),
|
|
1330
|
+
// packer offsets: x from the left, y from the BOTTOM: top-left of the trimmed image in the original
|
|
1331
|
+
trim: trimmed ? new Rectangle(ox, a.height - oy - h, w, h) : void 0,
|
|
1332
|
+
rotate: a.rotate ? 2 : 0
|
|
1333
|
+
}));
|
|
1334
|
+
}
|
|
1335
|
+
return out;
|
|
1336
|
+
}
|
|
1337
|
+
function atlasUV(asset, u, v, sourceW, sourceH) {
|
|
1338
|
+
if (!asset.frame) return [u, v];
|
|
1339
|
+
const [fx, fy, w, h] = asset.frame, [ox, oy] = asset.trim ?? [0, 0], W = asset.width, H = asset.height;
|
|
1340
|
+
let ax, ay;
|
|
1341
|
+
if (asset.rotate) {
|
|
1342
|
+
ax = fx - (H - oy - h) + v * H;
|
|
1343
|
+
ay = fy - (W - ox - w) + (1 - u) * W;
|
|
1344
|
+
} else {
|
|
1345
|
+
ax = fx - ox + u * W;
|
|
1346
|
+
ay = fy - (H - oy - h) + v * H;
|
|
1347
|
+
}
|
|
1348
|
+
return [ax / sourceW, ay / sourceH];
|
|
1349
|
+
}
|
|
1350
|
+
var rgbOf = (c) => Math.round(c[0] * 255) << 16 | Math.round(c[1] * 255) << 8 | Math.round(c[2] * 255);
|
|
1351
|
+
var sameNumbers = (a, b) => a === b || a.length === b.length && a.every((v, i) => v === b[i]);
|
|
1352
|
+
var RigPlayer = class {
|
|
1353
|
+
/** `mix`: default crossfade seconds for state transitions (setBase, completion → base); play() passes its own */
|
|
1354
|
+
constructor(doc, textures, opts = {}) {
|
|
1355
|
+
this.doc = doc;
|
|
1356
|
+
parse(doc);
|
|
1357
|
+
this.textures = textures;
|
|
1358
|
+
this.view.sortableChildren = true;
|
|
1359
|
+
const latticed = /* @__PURE__ */ new Set();
|
|
1360
|
+
for (const a of doc.animations) for (const t of a.tracks) if (t.target === "deform" && t.prop !== "vertices") latticed.add(t.id);
|
|
1361
|
+
const slots = new Map(doc.slots.map((s) => [s.id, s]));
|
|
1362
|
+
for (const att of doc.attachments) {
|
|
1363
|
+
const slot = slots.get(att.slot);
|
|
1364
|
+
if (!slot || att.type === "path") continue;
|
|
1365
|
+
let item;
|
|
1366
|
+
if (att.type === "mesh") {
|
|
1367
|
+
const asset = doc.assets.find((a) => a.id === att.asset);
|
|
1368
|
+
const tex = this.textures.get(att.asset) ?? Texture.EMPTY;
|
|
1369
|
+
const page = this.pageTexture(tex);
|
|
1370
|
+
const n = att.uvs.length / 2, uvs = new Float32Array(n * 2), positions = new Float32Array(n * 2);
|
|
1371
|
+
for (let i = 0; i < n; i++) {
|
|
1372
|
+
const [u, v] = atlasUV(asset, att.uvs[i * 2], att.uvs[i * 2 + 1], page.width, page.height);
|
|
1373
|
+
uvs[i * 2] = u;
|
|
1374
|
+
uvs[i * 2 + 1] = v;
|
|
1375
|
+
}
|
|
1376
|
+
const geometry = new MeshGeometry({ positions, uvs, indices: new Uint32Array(att.triangles) });
|
|
1377
|
+
const mesh = new Mesh({ geometry, texture: page });
|
|
1378
|
+
item = { att, slot, node: mesh, skinned: { mesh, positions } };
|
|
1379
|
+
} else if (latticed.has(att.id)) {
|
|
1380
|
+
const plane = new MeshPlane({ texture: this.textures.get(att.asset) ?? Texture.EMPTY, verticesX: MESH_GRID, verticesY: MESH_GRID });
|
|
1381
|
+
plane.autoResize = false;
|
|
1382
|
+
item = { att, slot, node: plane, lattice: { plane, offsets: new Float64Array(18), applied: new Float64Array(18).fill(NaN) } };
|
|
1383
|
+
} else {
|
|
1384
|
+
const sp = new Sprite(this.textures.get(att.asset) ?? Texture.EMPTY);
|
|
1385
|
+
sp.anchor.set(0.5, 0.5);
|
|
1386
|
+
item = { att, slot, node: sp };
|
|
1387
|
+
}
|
|
1388
|
+
item.node.blendMode = slot.blend;
|
|
1389
|
+
this.items.push(item);
|
|
1390
|
+
this.view.addChild(item.node);
|
|
1391
|
+
}
|
|
1392
|
+
this.state = new AnimationState(this, { mix: opts.mix ?? 0 });
|
|
1393
|
+
this.setPose(/* @__PURE__ */ new Map());
|
|
1394
|
+
}
|
|
1395
|
+
view = new Container();
|
|
1396
|
+
state;
|
|
1397
|
+
items = [];
|
|
1398
|
+
skel;
|
|
1399
|
+
textures;
|
|
1400
|
+
pages = /* @__PURE__ */ new Map();
|
|
1401
|
+
/** slots the editor hides; honoured by setPose */
|
|
1402
|
+
hidden = /* @__PURE__ */ new Set();
|
|
1403
|
+
/** whole atlas page as a plain texture (identity uv mapping) for skinned meshes */
|
|
1404
|
+
pageTexture(tex) {
|
|
1405
|
+
let page = this.pages.get(tex.source);
|
|
1406
|
+
if (!page) this.pages.set(tex.source, page = tex.frame.x === 0 && tex.frame.y === 0 && tex.frame.width === tex.source.width && tex.frame.height === tex.source.height && !tex.rotate ? tex : new Texture({ source: tex.source }));
|
|
1407
|
+
return page;
|
|
1408
|
+
}
|
|
1409
|
+
/** Replace whatever is playing (no queueing). Resolves when the animation completes or is replaced. */
|
|
1410
|
+
play(animId, opts = {}) {
|
|
1411
|
+
this.state.onEvent = opts.onEvent;
|
|
1412
|
+
return this.state.play(animId, {
|
|
1413
|
+
loop: opts.loop,
|
|
1414
|
+
mix: opts.mix ?? 0,
|
|
1415
|
+
ifBusy: "interrupt",
|
|
1416
|
+
onComplete: (r) => {
|
|
1417
|
+
if (r === "completed") opts.onComplete?.();
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Swap the document for pose evaluation without rebuilding display objects — valid only when slots/attachments
|
|
1423
|
+
* keep their ids and types (the editor uses it during drags). Throws otherwise: rebuild the player.
|
|
1424
|
+
*/
|
|
1425
|
+
updateDoc(doc) {
|
|
1426
|
+
const atts = new Map(doc.attachments.map((a) => [a.id, a])), slots = new Map(doc.slots.map((s) => [s.id, s]));
|
|
1427
|
+
for (const it of this.items) {
|
|
1428
|
+
const a = atts.get(it.att.id), s = slots.get(it.slot.id);
|
|
1429
|
+
if (!a || !s || a.type !== it.att.type) throw new Error("updateDoc: structure changed \u2014 rebuild the player");
|
|
1430
|
+
if (a.type === "mesh" && it.att.type === "mesh" && (!sameNumbers(a.uvs, it.att.uvs) || !sameNumbers(a.triangles, it.att.triangles)))
|
|
1431
|
+
throw new Error("updateDoc: structure changed \u2014 rebuild the player");
|
|
1432
|
+
it.att = a;
|
|
1433
|
+
it.slot = s;
|
|
1434
|
+
it.node.blendMode = s.blend;
|
|
1435
|
+
}
|
|
1436
|
+
this.doc = doc;
|
|
1437
|
+
}
|
|
1438
|
+
/** Editor-only: hide a slot's display object regardless of the pose (restored by passing `true`). */
|
|
1439
|
+
setSlotVisible(slotId, visible) {
|
|
1440
|
+
if (visible) this.hidden.delete(slotId);
|
|
1441
|
+
else this.hidden.add(slotId);
|
|
1442
|
+
}
|
|
1443
|
+
/** Stop playback and immediately restore the complete setup pose. */
|
|
1444
|
+
stop() {
|
|
1445
|
+
this.state.stop();
|
|
1446
|
+
}
|
|
1447
|
+
/** dt in seconds */
|
|
1448
|
+
tick(dt) {
|
|
1449
|
+
this.state.update(dt);
|
|
1450
|
+
}
|
|
1451
|
+
/** Apply a pose (rest where silent) to the display objects. */
|
|
1452
|
+
setPose(pose) {
|
|
1453
|
+
const skel = this.skel = evaluate(this.doc, pose);
|
|
1454
|
+
const m = new Matrix();
|
|
1455
|
+
const orderV = pose.get(pk("drawOrder", "order", "order"));
|
|
1456
|
+
const order = /* @__PURE__ */ new Map();
|
|
1457
|
+
if (Array.isArray(orderV)) orderV.forEach((id, i) => order.set(id, i));
|
|
1458
|
+
for (const it of this.items) {
|
|
1459
|
+
const s = it.slot, shown = skel.attachment.get(s.id);
|
|
1460
|
+
if (shown !== it.att || this.hidden.has(s.id)) {
|
|
1461
|
+
it.node.visible = false;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
it.node.visible = true;
|
|
1465
|
+
const colorV = pose.get(pk("slot", s.id, "color"));
|
|
1466
|
+
const color = Array.isArray(colorV) && typeof colorV[0] === "number" ? colorV : colorToArray(s.color);
|
|
1467
|
+
it.node.alpha = numOf(pose, pk("slot", s.id, "alpha"), s.alpha) * color[3];
|
|
1468
|
+
it.node.tint = rgbOf(color);
|
|
1469
|
+
it.node.zIndex = order.size ? order.get(s.id) ?? 0 : numOf(pose, pk("slot", s.id, "z"), s.z);
|
|
1470
|
+
if (it.skinned) {
|
|
1471
|
+
const v = attachmentWorldVertices(skel, s.id, it.att);
|
|
1472
|
+
const buf = it.skinned.mesh.geometry.getBuffer("aPosition");
|
|
1473
|
+
buf.data.set(v);
|
|
1474
|
+
buf.update();
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
const att = it.att;
|
|
1478
|
+
const [w, h] = regionSize(this.doc, att);
|
|
1479
|
+
if (it.lattice) {
|
|
1480
|
+
const off = controlOffsets(deformParams(pose, att.id) ?? {}, w, h, { x: 0.5, y: 0.5 }, it.lattice.offsets);
|
|
1481
|
+
if (!same(off, it.lattice.applied)) {
|
|
1482
|
+
const buf = it.lattice.plane.geometry.getBuffer("aPosition");
|
|
1483
|
+
gridPositions(off, w, h, { x: 0.5, y: 0.5 }, MESH_GRID, buf.data);
|
|
1484
|
+
buf.update();
|
|
1485
|
+
it.lattice.applied.set(off);
|
|
1486
|
+
}
|
|
1487
|
+
} else {
|
|
1488
|
+
const sp = it.node, tex = this.textures.get(att.asset);
|
|
1489
|
+
if (tex && sp.texture !== tex) sp.texture = tex;
|
|
1490
|
+
if (tex && (att.width || att.height)) {
|
|
1491
|
+
sp.width = w;
|
|
1492
|
+
sp.height = h;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
const a = regionMatrix(skel, s.id, att);
|
|
1496
|
+
m.set(a.a, a.b, a.c, a.d, a.tx, a.ty);
|
|
1497
|
+
it.node.setFromMatrix(m);
|
|
1498
|
+
if (!it.lattice && (att.width || att.height)) {
|
|
1499
|
+
it.node.scale.x *= w / (this.textures.get(att.asset)?.width ?? w);
|
|
1500
|
+
it.node.scale.y *= h / (this.textures.get(att.asset)?.height ?? h);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
/** World-space bone positions — used by editor gizmos and renderPreview overlays */
|
|
1505
|
+
bonePositions() {
|
|
1506
|
+
const out = {};
|
|
1507
|
+
for (const [id, st] of this.skel.bones) out[id] = { x: st.world.tx, y: st.world.ty, rotation: Math.atan2(st.world.b, st.world.a) / DEG3 };
|
|
1508
|
+
return out;
|
|
1509
|
+
}
|
|
1510
|
+
/** current world transforms (for tests and tools) */
|
|
1511
|
+
get world() {
|
|
1512
|
+
return new Map([...this.skel.bones].map(([id, s]) => [id, s.world]));
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
var numOf = (pose, key, rest) => {
|
|
1516
|
+
const v = pose.get(key);
|
|
1517
|
+
return typeof v === "number" ? v : rest;
|
|
1518
|
+
};
|
|
1519
|
+
var same = (a, b) => {
|
|
1520
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1521
|
+
return true;
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
// src/rig-symbol.ts
|
|
1525
|
+
import { Container as Container2, Ticker } from "pixi.js";
|
|
1526
|
+
var RigSymbol = class extends Container2 {
|
|
1527
|
+
constructor(doc, textures, opts) {
|
|
1528
|
+
super();
|
|
1529
|
+
this.doc = doc;
|
|
1530
|
+
this.player = new RigPlayer(doc, textures, { mix: opts.mix ?? 0.15 });
|
|
1531
|
+
this.names = { idle: "idle", win: "win", dim: "dim", ...opts.animations };
|
|
1532
|
+
this.ticker = opts.ticker ?? Ticker.shared;
|
|
1533
|
+
this.addChild(this.player.view);
|
|
1534
|
+
this.resize(opts.size);
|
|
1535
|
+
this.ticker.add(this.onTick);
|
|
1536
|
+
}
|
|
1537
|
+
player;
|
|
1538
|
+
names;
|
|
1539
|
+
ticker;
|
|
1540
|
+
onTick = (t) => this.player.tick(t.deltaMS / 1e3);
|
|
1541
|
+
has(id) {
|
|
1542
|
+
return this.doc.animations.some((a) => a.id === id);
|
|
1543
|
+
}
|
|
1544
|
+
resize(size) {
|
|
1545
|
+
const { width, height } = this.doc.meta;
|
|
1546
|
+
const s = typeof size === "number" ? size / Math.max(width, height) : Math.min(size.width / width, size.height / height);
|
|
1547
|
+
this.player.view.scale.set(s);
|
|
1548
|
+
this.player.view.position.set(-width * s / 2, -height * s / 2);
|
|
1549
|
+
}
|
|
1550
|
+
playIdle() {
|
|
1551
|
+
if (this.has(this.names.idle)) this.player.state.setBase(this.names.idle);
|
|
1552
|
+
else this.showStatic();
|
|
1553
|
+
}
|
|
1554
|
+
/** resolves when the win animation completes (or immediately if there is none / it was replaced) */
|
|
1555
|
+
async playWin() {
|
|
1556
|
+
if (!this.has(this.names.win)) return;
|
|
1557
|
+
await this.player.state.play(this.names.win, { priority: 1 });
|
|
1558
|
+
}
|
|
1559
|
+
showStatic() {
|
|
1560
|
+
this.player.stop();
|
|
1561
|
+
}
|
|
1562
|
+
/** dim as a held base pose (non-winning symbol); off → back to idle */
|
|
1563
|
+
setDim(on) {
|
|
1564
|
+
if (on && this.has(this.names.dim)) this.player.state.setBase(this.names.dim);
|
|
1565
|
+
else this.playIdle();
|
|
1566
|
+
}
|
|
1567
|
+
destroy(options) {
|
|
1568
|
+
this.ticker.remove(this.onTick);
|
|
1569
|
+
super.destroy(options);
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
export {
|
|
1573
|
+
AnimationState,
|
|
1574
|
+
MESH_GRID,
|
|
1575
|
+
RigDocument as RigDocumentSchema,
|
|
1576
|
+
RigPlayer,
|
|
1577
|
+
RigSymbol,
|
|
1578
|
+
atlasUV,
|
|
1579
|
+
createTextures,
|
|
1580
|
+
migrateV1,
|
|
1581
|
+
parse,
|
|
1582
|
+
validate
|
|
1583
|
+
};
|
|
1584
|
+
//# sourceMappingURL=runtime.js.map
|