@energy8platform/golem 0.2.0 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  // src/rig-runtime.ts
2
- import { Container, Sprite, Mesh, MeshGeometry, MeshPlane, Texture, Matrix, Rectangle } from "pixi.js";
2
+ import { Container, Sprite, Mesh, MeshGeometry, MeshPlane, Texture, Matrix, Rectangle, Graphics } from "pixi.js";
3
3
 
4
4
  // src/rig-format.ts
5
5
  import { z } from "zod";
@@ -48,7 +48,7 @@ function gridPositions(offsets, w, h, pivot, n, out = new Float32Array(2 * n * n
48
48
  var ID_RE = /^[a-z][a-z0-9_]*$/;
49
49
  var Id = z.string().regex(ID_RE, "snake_case id");
50
50
  var Ease = z.union([
51
- z.enum(["linear", "step", "ease_in", "ease_out", "ease_in_out", "back_out", "elastic_out"]),
51
+ z.enum(["linear", "step", "ease_in", "ease_out", "ease_in_out", "minimum_jerk", "back_out", "elastic_out"]),
52
52
  // cubic-bezier control points, same semantics as CSS
53
53
  z.object({ bezier: z.tuple([z.number(), z.number(), z.number(), z.number()]) })
54
54
  ]);
@@ -80,6 +80,7 @@ var Asset = z.object({
80
80
  /** where this layer came from — provenance for agents */
81
81
  origin: z.object({
82
82
  sourceImage: z.string().optional(),
83
+ bboxSpace: z.enum(["source", "document"]).optional(),
83
84
  /** bbox in source image: [x, y, w, h] */
84
85
  bbox: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(),
85
86
  inpainted: z.boolean().default(false),
@@ -102,6 +103,8 @@ var Bone = z.object({
102
103
  var Slot = z.object({
103
104
  id: Id,
104
105
  bone: Id,
106
+ /** Closed path attachment used as a moving clipping mask. */
107
+ clip: Id.optional(),
105
108
  /** active attachment id; undefined = hidden */
106
109
  attachment: Id.optional(),
107
110
  alpha: z.number().min(0).max(1).default(1),
@@ -164,6 +167,8 @@ var IkConstraint = z.object({
164
167
  order: z.number().int().default(0),
165
168
  bones: z.array(Id).min(1),
166
169
  target: Id,
170
+ iterations: z.number().int().min(1).max(1e3).optional(),
171
+ tolerance: z.number().positive().optional(),
167
172
  mix: z.number().default(1),
168
173
  softness: z.number().default(0),
169
174
  bendPositive: z.boolean().default(true),
@@ -211,7 +216,7 @@ var BoneProp = z.enum(["x", "y", "rotation", "scaleX", "scaleY", "shearX", "shea
211
216
  var SlotProp = z.enum(["alpha", "attachment", "z", "color"]);
212
217
  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
218
  var IkProp = z.enum(["mix", "softness", "bendPositive", "compress", "stretch"]);
214
- var TransformProp = z.enum(["mixRotate", "mixX", "mixY", "mixScaleX", "mixScaleY", "mixShearY"]);
219
+ var TransformProp = z.enum(["mixRotate", "mixX", "mixY", "mixScaleX", "mixScaleY", "mixShearY", "x", "y", "rotation", "scaleX", "scaleY", "shearY"]);
215
220
  var PathProp = z.enum(["position", "spacing", "mixRotate", "mixX", "mixY"]);
216
221
  var KeyValue = z.union([z.number(), z.string(), z.null(), z.array(z.number()), z.array(z.string())]);
217
222
  var Key = z.object({
@@ -220,14 +225,15 @@ var Key = z.object({
220
225
  /** easing INTO the next key */
221
226
  ease: Ease.default("ease_in_out")
222
227
  });
228
+ var Interpolation = { interpolation: z.enum(["segment", "pchip"]).optional() };
223
229
  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) })
230
+ z.object({ target: z.literal("bone"), id: Id, prop: BoneProp, ...Interpolation, keys: z.array(Key).min(1) }),
231
+ z.object({ target: z.literal("slot"), id: Id, prop: SlotProp, ...Interpolation, keys: z.array(Key).min(1) }),
232
+ z.object({ target: z.literal("deform"), id: Id, prop: DeformProp, ...Interpolation, keys: z.array(Key).min(1) }),
233
+ z.object({ target: z.literal("ik"), id: Id, prop: IkProp, ...Interpolation, keys: z.array(Key).min(1) }),
234
+ z.object({ target: z.literal("transform"), id: Id, prop: TransformProp, ...Interpolation, keys: z.array(Key).min(1) }),
235
+ z.object({ target: z.literal("path"), id: Id, prop: PathProp, ...Interpolation, keys: z.array(Key).min(1) }),
236
+ z.object({ target: z.literal("drawOrder"), id: Id.default("order"), prop: z.literal("order").default("order"), ...Interpolation, keys: z.array(Key).min(1) })
231
237
  ]);
232
238
  var Event = z.object({
233
239
  t: z.number().nonnegative(),
@@ -369,6 +375,10 @@ function validate(doc) {
369
375
  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
376
  }
371
377
  for (const s of doc.slots) {
378
+ if (s.clip) {
379
+ const mask = attachments.get(s.clip);
380
+ if (mask?.type !== "path" || !mask.closed) errs.push(`slot "${s.id}": clip must name a closed path attachment`);
381
+ }
372
382
  if (!bones.has(s.bone)) errs.push(`slot "${s.id}": unknown bone "${s.bone}"`);
373
383
  if (s.attachment) {
374
384
  const a = attachments.get(s.attachment);
@@ -400,7 +410,8 @@ function validate(doc) {
400
410
  }
401
411
  }
402
412
  for (const c of doc.ik) {
403
- if (c.bones.length > 2) errs.push(`ik "${c.id}": must have 1 or 2 bones`);
413
+ for (let i = 1; i < c.bones.length; i++) if (doc.bones.find((b) => b.id === c.bones[i])?.parent !== c.bones[i - 1])
414
+ errs.push(`ik "${c.id}": bones must form a contiguous parent-to-child chain`);
404
415
  for (const b of c.bones) if (!bones.has(b)) errs.push(`ik "${c.id}": unknown bone "${b}"`);
405
416
  if (!bones.has(c.target)) errs.push(`ik "${c.id}": unknown target bone "${c.target}"`);
406
417
  }
@@ -472,6 +483,7 @@ var named = {
472
483
  ease_in: (t) => t * t,
473
484
  ease_out: (t) => 1 - (1 - t) * (1 - t),
474
485
  ease_in_out: (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2,
486
+ minimum_jerk: (t) => t * t * t * (10 + t * (-15 + 6 * t)),
475
487
  back_out: (t) => 1 + 2.70158 * (t - 1) ** 3 + 1.70158 * (t - 1) ** 2,
476
488
  elastic_out: (t) => t === 0 ? 0 : t === 1 ? 1 : 2 ** (-10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI / 3)) + 1
477
489
  };
@@ -503,7 +515,25 @@ function ease(spec) {
503
515
  return f;
504
516
  }
505
517
  var pk = (target, id, prop) => `${target}:${id}:${prop}`;
506
- function sample(track, t) {
518
+ function smoothValue(track, i, t, component, loop) {
519
+ const k = track.keys, n = k.length;
520
+ const v = (j) => component === void 0 ? k[j].v : k[j].v[component];
521
+ const h = (j) => k[j + 1].t - k[j].t;
522
+ const d = (j) => (v(j + 1) - v(j)) / h(j);
523
+ const harmonic = (a, b, h0, h1) => a * b <= 0 ? 0 : 3 * (h0 + h1) / ((2 * h1 + h0) / a + (h1 + 2 * h0) / b);
524
+ const tangent = (j) => {
525
+ if (j === 0 || j === n - 1) {
526
+ if (loop && n > 2 && v(0) === v(n - 1) && k[0].ease !== "step" && k[n - 2].ease !== "step")
527
+ return harmonic(d(n - 2), d(0), h(n - 2), h(0));
528
+ return 0;
529
+ }
530
+ if (k[j - 1].ease === "step" || k[j].ease === "step") return 0;
531
+ return harmonic(d(j - 1), d(j), h(j - 1), h(j));
532
+ };
533
+ const u = (t - k[i].t) / h(i), u2 = u * u, u3 = u2 * u;
534
+ return (2 * u3 - 3 * u2 + 1) * v(i) + (u3 - 2 * u2 + u) * h(i) * tangent(i) + (-2 * u3 + 3 * u2) * v(i + 1) + (u3 - u2) * h(i) * tangent(i + 1);
535
+ }
536
+ function sample(track, t, loop = false) {
507
537
  const keys = track.keys;
508
538
  if (t <= keys[0].t) return keys[0].v;
509
539
  const last = keys[keys.length - 1];
@@ -511,31 +541,38 @@ function sample(track, t) {
511
541
  let i = 0;
512
542
  while (keys[i + 1].t <= t) i++;
513
543
  const a = keys[i], b = keys[i + 1];
544
+ if (a.ease === "step") return a.v;
514
545
  if (typeof a.v === "number" && typeof b.v === "number") {
546
+ if (track.interpolation === "pchip") return smoothValue(track, i, t, void 0, loop);
515
547
  const u = ease(a.ease)((t - a.t) / (b.t - a.t));
516
548
  return a.v + (b.v - a.v) * u;
517
549
  }
518
550
  if (Array.isArray(a.v) && Array.isArray(b.v) && typeof a.v[0] === "number") {
551
+ if (track.interpolation === "pchip") return a.v.map((_, j) => smoothValue(track, i, t, j, loop));
519
552
  const u = ease(a.ease)((t - a.t) / (b.t - a.t)), av = a.v, bv = b.v;
520
553
  return av.map((v, k) => v + ((bv[k] ?? v) - v) * u);
521
554
  }
522
555
  return a.v;
523
556
  }
524
557
  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));
558
+ for (const tr of anim.tracks) out.set(pk(tr.target, tr.id, tr.prop), sample(tr, t, anim.loop && tr.keys[0].t === 0 && tr.keys.at(-1).t === anim.duration));
526
559
  return out;
527
560
  }
528
561
  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") {
562
+ if (w >= 1) return new Map(to);
563
+ const out = /* @__PURE__ */ new Map();
564
+ for (const k of /* @__PURE__ */ new Set([...from.keys(), ...to.keys()])) {
565
+ const f = from.has(k) ? from.get(k) : restOf(k);
566
+ const cur = to.has(k) ? to.get(k) : restOf(k);
567
+ if (typeof f === "number" && !Array.isArray(cur)) {
533
568
  const target = typeof cur === "number" ? cur : restOf(k);
534
569
  out.set(k, f + (target - f) * w);
535
570
  } else if (Array.isArray(f) && typeof f[0] === "number") {
536
571
  const fv = f, tv = Array.isArray(cur) && typeof cur[0] === "number" ? cur : fv.map(() => 0);
537
572
  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);
573
+ } else if (Array.isArray(cur) && typeof cur[0] === "number" && typeof f !== "string") {
574
+ out.set(k, cur.map((v) => v * w));
575
+ } else out.set(k, f ?? null);
539
576
  }
540
577
  return out;
541
578
  }
@@ -704,10 +741,43 @@ function aimAt(skel, boneId, tx, ty, mix, compress = false, stretch = false, uni
704
741
  updateSubtree(skel, boneId);
705
742
  }
706
743
  var shortest = (deg) => ((deg + 180) % 360 + 360) % 360 - 180;
744
+ function applyLongIk(skel, c, tx, ty, mix) {
745
+ const initial = c.bones.map((id) => ({ ...skel.bones.get(id) }));
746
+ const last = c.bones.at(-1), len = boneOf(skel.doc, last).length;
747
+ const end = () => apply(skel.bones.get(last).world, len, 0);
748
+ const tolerance = c.tolerance ?? 0.05;
749
+ const root = skel.bones.get(c.bones[0]).world, tip = end();
750
+ if (Math.abs((tip[0] - root.tx) * (ty - root.ty) - (tip[1] - root.ty) * (tx - root.tx)) < 1e-7 && Math.hypot(tx - root.tx, ty - root.ty) < Math.hypot(tip[0] - root.tx, tip[1] - root.ty) - tolerance) {
751
+ skel.bones.get(last).rotation += cnum(skel, "ik", c.id, "bendPositive", c.bendPositive) > 0.5 ? 5 : -5;
752
+ updateSubtree(skel, last);
753
+ }
754
+ for (let iter = 0; iter < (c.iterations ?? 100); iter++) {
755
+ const e = end();
756
+ if (Math.hypot(e[0] - tx, e[1] - ty) <= tolerance) break;
757
+ for (let i = c.bones.length - 1; i >= 0; i--) {
758
+ const id = c.bones[i], b = boneOf(skel.doc, id), st = skel.bones.get(id), w = st.world, e2 = end();
759
+ const angle = wrapRad(Math.atan2(ty - w.ty, tx - w.tx) - Math.atan2(e2[1] - w.ty, e2[0] - w.tx));
760
+ const cos = Math.cos(angle), sin = Math.sin(angle);
761
+ setEffectiveWorld(skel, b, st, { ...w, a: cos * w.a - sin * w.b, b: sin * w.a + cos * w.b, c: cos * w.c - sin * w.d, d: sin * w.c + cos * w.d });
762
+ }
763
+ }
764
+ if (mix < 1) {
765
+ c.bones.forEach((id, i) => {
766
+ const st = skel.bones.get(id), from = initial[i];
767
+ for (const p of ["rotation", "shearX", "shearY"]) st[p] = from[p] + shortest(st[p] - from[p]) * mix;
768
+ for (const p of ["x", "y", "scaleX", "scaleY"]) st[p] = from[p] + (st[p] - from[p]) * mix;
769
+ });
770
+ updateSubtree(skel, c.bones[0]);
771
+ }
772
+ }
707
773
  function applyIk(skel, c) {
708
774
  const mix = cnum(skel, "ik", c.id, "mix", c.mix);
709
775
  if (mix <= 0) return;
710
776
  const target = skel.bones.get(c.target).world;
777
+ if (c.bones.length > 2) {
778
+ applyLongIk(skel, c, target.tx, target.ty, mix);
779
+ return;
780
+ }
711
781
  const bendDir = cnum(skel, "ik", c.id, "bendPositive", c.bendPositive) > 0.5 ? 1 : -1;
712
782
  const compress = cnum(skel, "ik", c.id, "compress", c.compress) > 0.5, stretch = cnum(skel, "ik", c.id, "stretch", c.stretch) > 0.5;
713
783
  let softness = cnum(skel, "ik", c.id, "softness", c.softness);
@@ -842,6 +912,7 @@ function applyTransformConstraint(skel, c) {
842
912
  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
913
  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
914
  if (!mixRotate && !mixX && !mixY && !mixScaleX && !mixScaleY && !mixShearY) return;
915
+ c = { ...c, ...Object.fromEntries(["x", "y", "rotation", "scaleX", "scaleY", "shearY"].map((p) => [p, cnum(skel, "transform", c.id, p, c[p])])) };
845
916
  const target = skel.bones.get(c.target), tw = target.world;
846
917
  const offsetRotation = c.rotation * DEG2, offsetShearY = c.shearY * DEG2;
847
918
  for (const id of c.bones) {
@@ -1169,6 +1240,7 @@ function regionSize(doc, att) {
1169
1240
  var AnimationState = class {
1170
1241
  constructor(sink, opts = {}) {
1171
1242
  this.sink = sink;
1243
+ this.opts = opts;
1172
1244
  this.defaultMix = opts.mix ?? 0;
1173
1245
  }
1174
1246
  onEvent;
@@ -1193,11 +1265,18 @@ var AnimationState = class {
1193
1265
  }
1194
1266
  channel(id, loop, priority = 0) {
1195
1267
  const anim = this.sink.doc.animations.find((a) => a.id === id);
1196
- if (!anim) throw new Error(`unknown animation "${id}"`);
1268
+ if (!anim) {
1269
+ const message = `unknown animation "${id}"; available: ${this.sink.doc.animations.map((a) => a.id).join(", ")}`;
1270
+ if (this.opts.missingAnimation === "throw") throw new Error(message);
1271
+ (this.opts.onWarning ?? console.warn)(message);
1272
+ return void 0;
1273
+ }
1197
1274
  return { anim, time: 0, loop: loop ?? anim.loop, done: false, priority, fresh: true };
1198
1275
  }
1199
1276
  setBase(id, opts = {}) {
1200
- this.base = id === void 0 ? void 0 : this.channel(id);
1277
+ const next = id === void 0 ? void 0 : this.channel(id);
1278
+ if (id !== void 0 && !next) return;
1279
+ this.base = next;
1201
1280
  if (!this.main || this.main.done) {
1202
1281
  this.main = void 0;
1203
1282
  this.beginMix(opts.mix);
@@ -1206,6 +1285,16 @@ var AnimationState = class {
1206
1285
  }
1207
1286
  play(id, opts = {}) {
1208
1287
  const ch = this.channel(id, opts.loop, opts.priority ?? 0);
1288
+ if (!ch) {
1289
+ this.main?.settle?.("interrupted");
1290
+ for (const queued of this.queue) queued.settle?.("dropped");
1291
+ this.main = void 0;
1292
+ this.queue = [];
1293
+ this.beginMix(opts.mix);
1294
+ this.update(0);
1295
+ opts.onComplete?.("dropped");
1296
+ return Promise.resolve("dropped");
1297
+ }
1209
1298
  const promise = new Promise((resolve) => {
1210
1299
  ch.settle = (r) => {
1211
1300
  ch.settle = void 0;
@@ -1226,7 +1315,10 @@ var AnimationState = class {
1226
1315
  /** Independent loop on top of the shown pose; `undefined` removes it. */
1227
1316
  setOverlay(name, id) {
1228
1317
  if (id === void 0) this.overlays.delete(name);
1229
- else this.overlays.set(name, this.channel(id));
1318
+ else {
1319
+ const ch = this.channel(id);
1320
+ if (ch) this.overlays.set(name, ch);
1321
+ }
1230
1322
  }
1231
1323
  /** Clear everything and show the setup pose. Pending plays resolve as "stopped". */
1232
1324
  stop() {
@@ -1311,6 +1403,7 @@ function hit(t, prev, cur, d, loop) {
1311
1403
  // src/rig-runtime.ts
1312
1404
  var DEG3 = Math.PI / 180;
1313
1405
  var MESH_GRID = 9;
1406
+ var GHOST_ALPHA = 0.5;
1314
1407
  async function createTextures(doc, loadSource) {
1315
1408
  const bySrc = /* @__PURE__ */ new Map(), out = /* @__PURE__ */ new Map();
1316
1409
  for (const a of doc.assets) {
@@ -1350,7 +1443,7 @@ function atlasUV(asset, u, v, sourceW, sourceH) {
1350
1443
  var rgbOf = (c) => Math.round(c[0] * 255) << 16 | Math.round(c[1] * 255) << 8 | Math.round(c[2] * 255);
1351
1444
  var sameNumbers = (a, b) => a === b || a.length === b.length && a.every((v, i) => v === b[i]);
1352
1445
  var RigPlayer = class {
1353
- /** `mix`: default crossfade seconds for state transitions (setBase, completion → base); play() passes its own */
1446
+ /** Default crossfade is 0.18 seconds; explicit mix: 0 requests a hard cut. */
1354
1447
  constructor(doc, textures, opts = {}) {
1355
1448
  this.doc = doc;
1356
1449
  parse(doc);
@@ -1389,7 +1482,7 @@ var RigPlayer = class {
1389
1482
  this.items.push(item);
1390
1483
  this.view.addChild(item.node);
1391
1484
  }
1392
- this.state = new AnimationState(this, { mix: opts.mix ?? 0 });
1485
+ this.state = new AnimationState(this, { ...opts, mix: opts.mix ?? 0.18 });
1393
1486
  this.setPose(/* @__PURE__ */ new Map());
1394
1487
  }
1395
1488
  view = new Container();
@@ -1400,6 +1493,9 @@ var RigPlayer = class {
1400
1493
  pages = /* @__PURE__ */ new Map();
1401
1494
  /** slots the editor hides; honoured by setPose */
1402
1495
  hidden = /* @__PURE__ */ new Set();
1496
+ /** slots the editor has selected; setPose draws them at GHOST_ALPHA at least */
1497
+ ghosts = /* @__PURE__ */ new Set();
1498
+ clipGraphics = /* @__PURE__ */ new Map();
1403
1499
  /** whole atlas page as a plain texture (identity uv mapping) for skinned meshes */
1404
1500
  pageTexture(tex) {
1405
1501
  let page = this.pages.get(tex.source);
@@ -1411,7 +1507,7 @@ var RigPlayer = class {
1411
1507
  this.state.onEvent = opts.onEvent;
1412
1508
  return this.state.play(animId, {
1413
1509
  loop: opts.loop,
1414
- mix: opts.mix ?? 0,
1510
+ mix: opts.mix,
1415
1511
  ifBusy: "interrupt",
1416
1512
  onComplete: (r) => {
1417
1513
  if (r === "completed") opts.onComplete?.();
@@ -1440,6 +1536,10 @@ var RigPlayer = class {
1440
1536
  if (visible) this.hidden.delete(slotId);
1441
1537
  else this.hidden.add(slotId);
1442
1538
  }
1539
+ /** Editor-only: draw these slots at GHOST_ALPHA at least, whatever the pose says (an empty list clears it). */
1540
+ setGhostSlots(slotIds) {
1541
+ this.ghosts = new Set(slotIds);
1542
+ }
1443
1543
  /** Stop playback and immediately restore the complete setup pose. */
1444
1544
  stop() {
1445
1545
  this.state.stop();
@@ -1451,12 +1551,32 @@ var RigPlayer = class {
1451
1551
  /** Apply a pose (rest where silent) to the display objects. */
1452
1552
  setPose(pose) {
1453
1553
  const skel = this.skel = evaluate(this.doc, pose);
1554
+ const clips = new Set(this.doc.slots.flatMap((s) => s.clip ? [s.clip] : []));
1555
+ for (const [id, g] of this.clipGraphics) if (!clips.has(id)) {
1556
+ g.destroy();
1557
+ this.clipGraphics.delete(id);
1558
+ }
1559
+ for (const id of clips) {
1560
+ const att = this.doc.attachments.find((a) => a.id === id);
1561
+ if (att?.type !== "path" || !att.closed) continue;
1562
+ let g = this.clipGraphics.get(id);
1563
+ if (!g) {
1564
+ g = new Graphics();
1565
+ this.clipGraphics.set(id, g);
1566
+ this.view.addChild(g);
1567
+ }
1568
+ const vertices = attachmentWorldVertices(skel, att.slot, att);
1569
+ const n = Math.max(32, vertices.length * 4);
1570
+ const points = samplePath(vertices, true, att.lengths, true, Array.from({ length: n }, (_, i) => i / n), true);
1571
+ g.clear().poly(points.flatMap((p) => [p.x, p.y]), true).fill(16777215);
1572
+ }
1454
1573
  const m = new Matrix();
1455
1574
  const orderV = pose.get(pk("drawOrder", "order", "order"));
1456
1575
  const order = /* @__PURE__ */ new Map();
1457
1576
  if (Array.isArray(orderV)) orderV.forEach((id, i) => order.set(id, i));
1458
1577
  for (const it of this.items) {
1459
1578
  const s = it.slot, shown = skel.attachment.get(s.id);
1579
+ it.node.mask = s.clip ? this.clipGraphics.get(s.clip) ?? null : null;
1460
1580
  if (shown !== it.att || this.hidden.has(s.id)) {
1461
1581
  it.node.visible = false;
1462
1582
  continue;
@@ -1464,7 +1584,8 @@ var RigPlayer = class {
1464
1584
  it.node.visible = true;
1465
1585
  const colorV = pose.get(pk("slot", s.id, "color"));
1466
1586
  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];
1587
+ const alpha = numOf(pose, pk("slot", s.id, "alpha"), s.alpha) * color[3];
1588
+ it.node.alpha = this.ghosts.has(s.id) ? Math.max(alpha, GHOST_ALPHA) : alpha;
1468
1589
  it.node.tint = rgbOf(color);
1469
1590
  it.node.zIndex = order.size ? order.get(s.id) ?? 0 : numOf(pose, pk("slot", s.id, "z"), s.z);
1470
1591
  if (it.skinned) {