@almadar/ui 6.57.0 → 6.59.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.
Files changed (32) hide show
  1. package/dist/{AwaitingSkeletonContext-DPgW__WV.d.ts → AwaitingSkeletonContext-Bh399p1x.d.ts} +1 -3
  2. package/dist/{Canvas3DHost-DfAhg6EH.d.ts → Canvas3DHost-CNjANoaU.d.ts} +1 -1
  3. package/dist/{GameAudioProvider-DyBvYP2c.d.ts → GameAudioProvider-Dv_L-PHt.d.ts} +1 -1
  4. package/dist/{UISlotRenderer-NCGDIXN4.js → UISlotRenderer-YNY7V2UJ.js} +2 -2
  5. package/dist/avl/index.cjs +137 -71
  6. package/dist/avl/index.js +24 -8
  7. package/dist/{chunk-GZGDBS4S.js → chunk-7CVXM2KS.js} +1 -1
  8. package/dist/{chunk-PYJ2UQWM.js → chunk-E6MWYQVZ.js} +42 -35
  9. package/dist/{chunk-HA7CJWQX.js → chunk-J755QADM.js} +1184 -1184
  10. package/dist/{chunk-VHTJRS2N.js → chunk-L3MTZ3RH.js} +7 -0
  11. package/dist/{chunk-RHP5WL55.js → chunk-QJIAJ6EH.js} +77 -42
  12. package/dist/components/index.cjs +76 -32
  13. package/dist/components/index.d.ts +9 -7
  14. package/dist/components/index.js +5 -5
  15. package/dist/context/index.cjs +7 -0
  16. package/dist/context/index.js +1 -1
  17. package/dist/hooks/index.cjs +7 -0
  18. package/dist/hooks/index.js +1 -1
  19. package/dist/lib/drawable/three/index.d.ts +3 -3
  20. package/dist/lib/drawable/three/index.js +3 -3
  21. package/dist/lib/index.d.ts +2 -2
  22. package/dist/{paintDispatch-_6EhJCiL.d.ts → paintDispatch-Cp1_S93j.d.ts} +30 -2
  23. package/dist/providers/index.cjs +51 -50
  24. package/dist/providers/index.d.ts +4 -4
  25. package/dist/providers/index.js +8 -21
  26. package/dist/runtime/index.cjs +120 -70
  27. package/dist/runtime/index.d.ts +4 -4
  28. package/dist/runtime/index.js +4 -4
  29. package/dist/ssr/index.cjs +2 -0
  30. package/dist/ssr/index.js +2 -0
  31. package/dist/{verificationRegistry-CD7Xb40L.d.ts → verificationRegistry-B3dj1Zl8.d.ts} +1 -1
  32. package/package.json +9 -8
@@ -4,7 +4,7 @@ import { updateAssetStatus } from './chunk-Q47ZN2DW.js';
4
4
  import { cn, domPassthrough } from './chunk-64WACXM7.js';
5
5
  import { __publicField } from './chunk-QPXIAUJD.js';
6
6
  import { createLogger } from '@almadar/logger';
7
- import React, { useCallback, createContext, useState, useLayoutEffect, useContext } from 'react';
7
+ import React, { useCallback, createContext, useContext, useState, useLayoutEffect } from 'react';
8
8
  import { useThemeScope, useTheme } from '@almadar/ui/context';
9
9
  import { jsx } from 'react/jsx-runtime';
10
10
  import { renderUiEntriesOf } from '@almadar/core/patterns';
@@ -1000,1305 +1000,1305 @@ function parseTransitionLevel(schema, orbitalName, traitName, transitionIndex) {
1000
1000
  };
1001
1001
  }
1002
1002
 
1003
- // lib/atlasSlice.ts
1004
- var atlasCache = /* @__PURE__ */ new Map();
1005
- function isTilesheet(a) {
1006
- return typeof a.tileWidth === "number";
1003
+ // lib/drawable/contract.ts
1004
+ function isValidScenePos(pos) {
1005
+ return Number.isFinite(pos?.x) && Number.isFinite(pos?.y);
1007
1006
  }
1008
- function isSpriteSheetAtlas(a) {
1009
- const s = a;
1010
- return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
1007
+ function spriteAssetState(node) {
1008
+ if (!isValidScenePos(node.position)) return "invalid-position";
1009
+ return typeof node.asset?.url === "string" && node.asset.url.length > 0 ? "ready" : "asset-unset";
1011
1010
  }
1012
- function getAtlas(url, onReady) {
1013
- if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
1014
- atlasCache.set(url, void 0);
1015
- fetch(url).then((r) => r.json()).then((json) => {
1016
- atlasCache.set(url, json);
1017
- onReady();
1018
- }).catch(() => {
1019
- atlasCache.set(url, null);
1020
- onReady();
1021
- });
1022
- return void 0;
1011
+ var SPRITE_FALLBACK_FILL = "#9b8f7f";
1012
+ var SPRITE_FALLBACK_STROKE = "#5e564b";
1013
+ function spriteRect(projector, node, natural) {
1014
+ const tw = projector.tileWidth;
1015
+ const fallbackW = projector.worldPixelDirect ? natural?.w ?? 0 : tw;
1016
+ const fallbackH = projector.worldPixelDirect ? natural?.h ?? 0 : tw;
1017
+ const w = node.width !== void 0 ? node.width * tw : fallbackW;
1018
+ const h = node.height !== void 0 ? node.height * tw : fallbackH;
1019
+ const anchor = node.anchor ?? "top-left";
1020
+ const p = projector.anchorPoint(node.position, anchor);
1021
+ return {
1022
+ x: anchor === "top-left" ? p.x : p.x - w / 2,
1023
+ y: anchor === "ground" ? p.y - h : anchor === "center" ? p.y - h / 2 : p.y,
1024
+ w,
1025
+ h
1026
+ };
1023
1027
  }
1024
- function atlasFailed(url) {
1025
- return atlasCache.get(url) === null;
1028
+ var DrawableRegistryContext = createContext(null);
1029
+ var NUMERIC_TRACKS = [
1030
+ "offsetX",
1031
+ "offsetY",
1032
+ "rotate",
1033
+ "opacity",
1034
+ "radiusX",
1035
+ "radiusY",
1036
+ "width",
1037
+ "height",
1038
+ "strokeWidth",
1039
+ "strokeDashOffset",
1040
+ "blur"
1041
+ ];
1042
+ function isAnimatedShape(node) {
1043
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1026
1044
  }
1027
- function subRectFor(atlas, sprite) {
1028
- if (isTilesheet(atlas)) {
1029
- let col;
1030
- let row;
1031
- if (sprite.includes(",")) {
1032
- const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
1033
- col = c;
1034
- row = r;
1035
- } else {
1036
- const i = Number(sprite);
1037
- if (!Number.isFinite(i)) return null;
1038
- col = i % atlas.columns;
1039
- row = Math.floor(i / atlas.columns);
1045
+ var lerp = (a, b, k) => a + (b - a) * k;
1046
+ function applyShapeAnimation(node, timeMs) {
1047
+ const anim = node.animation;
1048
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
1049
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
1050
+ const cycle = timeMs / anim.durationMs;
1051
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
1052
+ const out = { ...node };
1053
+ const trackValue = (key) => {
1054
+ const defined = frames.filter((f) => f[key] !== void 0);
1055
+ if (defined.length === 0) return void 0;
1056
+ let prev;
1057
+ let next;
1058
+ for (const f of defined) {
1059
+ if (f.at <= t) prev = f;
1060
+ else if (!next) next = f;
1040
1061
  }
1041
- if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
1042
- const margin = atlas.margin ?? 0;
1043
- const spacing = atlas.spacing ?? 0;
1062
+ if (!prev) return defined[0][key];
1063
+ if (!next) return prev[key];
1064
+ const span = next.at - prev.at;
1065
+ const k = span > 0 ? (t - prev.at) / span : 1;
1066
+ const a = prev[key];
1067
+ const b = next[key];
1068
+ if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
1069
+ return a;
1070
+ };
1071
+ for (const key of NUMERIC_TRACKS) {
1072
+ const v = trackValue(key);
1073
+ if (v !== void 0) out[key] = v;
1074
+ }
1075
+ const fill = trackValue("fill");
1076
+ if (fill !== void 0) out.fill = fill;
1077
+ const stroke = trackValue("stroke");
1078
+ if (stroke !== void 0) out.stroke = stroke;
1079
+ const shadowFrames = frames.filter((f) => f.shadow !== void 0);
1080
+ if (shadowFrames.length > 0) {
1081
+ const sh = trackValue("shadow");
1082
+ if (sh !== void 0) {
1083
+ let prevSh;
1084
+ let nextSh;
1085
+ for (const f of shadowFrames) {
1086
+ if (f.at <= t) prevSh = f;
1087
+ else if (!nextSh) nextSh = f;
1088
+ }
1089
+ if (prevSh?.shadow && nextSh?.shadow) {
1090
+ const span = nextSh.at - prevSh.at;
1091
+ const k = span > 0 ? (t - prevSh.at) / span : 1;
1092
+ out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
1093
+ } else {
1094
+ out.shadow = sh;
1095
+ }
1096
+ }
1097
+ }
1098
+ return out;
1099
+ }
1100
+ function gradientStyle(g, ox, oy, scale) {
1101
+ if (g.kind === "linear") {
1102
+ if (!g.from || !g.to) return void 0;
1044
1103
  return {
1045
- sx: margin + col * (atlas.tileWidth + spacing),
1046
- sy: margin + row * (atlas.tileHeight + spacing),
1047
- sw: atlas.tileWidth,
1048
- sh: atlas.tileHeight
1104
+ kind: "linear",
1105
+ x1: ox + g.from.x * scale,
1106
+ y1: oy + g.from.y * scale,
1107
+ x2: ox + g.to.x * scale,
1108
+ y2: oy + g.to.y * scale,
1109
+ stops: g.stops
1049
1110
  };
1050
1111
  }
1051
- if (isSpriteSheetAtlas(atlas)) return null;
1052
- const st = atlas.subTextures[sprite];
1053
- if (!st) return null;
1054
- return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
1055
- }
1056
- function atlasUrlOf(asset) {
1057
- return typeof asset?.atlas === "string" && asset.atlas.length > 0 ? asset.atlas : void 0;
1058
- }
1059
- function isAtlasAsset(asset) {
1060
- return atlasUrlOf(asset) !== void 0 && typeof asset?.sprite === "string" && asset.sprite.length > 0;
1061
- }
1062
- function resolveAssetSource(img, asset, onReady) {
1063
- if (isAtlasAsset(asset)) {
1064
- const atlas = getAtlas(asset.atlas, onReady);
1065
- if (!atlas) return null;
1066
- const rect = subRectFor(atlas, asset.sprite);
1067
- if (!rect) return null;
1068
- return { img, rect, aspect: rect.sw / rect.sh };
1112
+ if (g.kind === "conic") {
1113
+ if (!g.center) return void 0;
1114
+ return {
1115
+ kind: "conic",
1116
+ cx: ox + g.center.x * scale,
1117
+ cy: oy + g.center.y * scale,
1118
+ angle: g.angle ?? 0,
1119
+ stops: g.stops
1120
+ };
1069
1121
  }
1070
- const natW = img.naturalWidth || 1;
1071
- const natH = img.naturalHeight || 1;
1072
- return { img, rect: null, aspect: natW / natH };
1073
- }
1074
- function blit(ctx, src, dx, dy, dw, dh) {
1075
- if (src.rect) ctx.drawImage(src.img, src.rect.sx, src.rect.sy, src.rect.sw, src.rect.sh, dx, dy, dw, dh);
1076
- else ctx.drawImage(src.img, dx, dy, dw, dh);
1077
- }
1078
-
1079
- // components/core/atoms/fx.ts
1080
- function resolveFxView(item, presets) {
1081
- const row = presets?.find((p) => p.type === item.type);
1082
- if (!row) return item;
1122
+ if (!g.center || g.radius === void 0) return void 0;
1083
1123
  return {
1084
- ...item,
1085
- space: item.space ?? row.space,
1086
- effect: item.effect ?? row.effect,
1087
- color: item.color ?? row.color,
1088
- size: item.size ?? row.size,
1089
- vx: item.vx ?? row.vx,
1090
- vy: item.vy ?? row.vy,
1091
- vz: item.vz ?? row.vz,
1092
- particleCount: item.particleCount ?? row.particleCount,
1093
- animation: item.animation ?? row.animation,
1094
- shape: row.shape,
1095
- count: row.count,
1096
- glow: row.glow,
1097
- gravity: row.gravity,
1098
- color2: row.color2
1124
+ kind: "radial",
1125
+ cx: ox + g.center.x * scale,
1126
+ cy: oy + g.center.y * scale,
1127
+ r: g.radius * scale,
1128
+ stops: g.stops
1099
1129
  };
1100
1130
  }
1101
- function fxLifecycle(item, epochNowMs, tickMs) {
1102
- const maxTtl = Math.max(item.maxTtl ?? item.ttl, item.ttl, 1);
1103
- const lifeMs = maxTtl * tickMs;
1104
- const ageMs = item.bornAt !== void 0 && epochNowMs > 0 ? Math.max(0, epochNowMs - item.bornAt) : (1 - item.ttl / maxTtl) * lifeMs;
1105
- const progress = Math.min(1, Math.max(0, ageMs / lifeMs));
1106
- return { lifeMs, ageMs, progress, fade: 1 - progress };
1107
- }
1108
- function fxHash01(seed, salt) {
1109
- let h = 2166136261 ^ salt;
1110
- for (let i = 0; i < seed.length; i++) {
1111
- h ^= seed.charCodeAt(i);
1112
- h = Math.imul(h, 16777619);
1131
+ function patternStyle(p, unit) {
1132
+ if (p.kind === "image") {
1133
+ if (!p.url) return void 0;
1134
+ return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
1113
1135
  }
1114
- h ^= h >>> 13;
1115
- h = Math.imul(h, 1274126177);
1116
- h ^= h >>> 16;
1117
- return (h >>> 0) / 4294967296;
1118
- }
1119
- var CONFETTI_COLORS = [
1120
- "var(--color-primary)",
1121
- "var(--color-success)",
1122
- "var(--color-warning)",
1123
- "var(--color-error)",
1124
- "gold",
1125
- "dodgerblue"
1126
- ];
1127
- function createConfettiParticles(count, seed) {
1128
- return Array.from({ length: count }, (_, i) => ({
1129
- id: i,
1130
- color: CONFETTI_COLORS[Math.floor(fxHash01(seed, i * 7 + 1) * CONFETTI_COLORS.length)],
1131
- left: 30 + fxHash01(seed, i * 7 + 2) * 40,
1132
- delay: fxHash01(seed, i * 7 + 3) * 300,
1133
- angle: fxHash01(seed, i * 7 + 4) * 360,
1134
- distance: 40 + fxHash01(seed, i * 7 + 5) * 80,
1135
- rotation: fxHash01(seed, i * 7 + 6) * 720 - 360,
1136
- size: 4 + fxHash01(seed, i * 7 + 7) * 6
1137
- }));
1136
+ return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
1138
1137
  }
1139
- var CONFETTI_BURST_KEYFRAMES = `
1140
- @keyframes confetti-burst {
1141
- 0% {
1142
- opacity: 1;
1143
- transform: translate(0, 0) rotate(0deg) scale(1);
1144
- }
1145
- 70% {
1146
- opacity: 1;
1147
- }
1148
- 100% {
1149
- opacity: 0;
1150
- transform: translate(var(--confetti-tx), var(--confetti-ty)) rotate(var(--confetti-rotate)) scale(0.5);
1151
- }
1152
- }
1153
- `;
1154
- var log2 = createLogger("almadar:ui:canvas-theme");
1155
- var toneVar = (tone) => `--color-${DIAGRAM_TONE_COLOR_KEYS[tone]}`;
1156
- var SERIES_TONES = ["series-1", "series-2", "series-3", "series-4", "series-5", "series-6", "series-7", "series-8"];
1157
- var TONE_SET = new Set(DIAGRAM_TONES);
1158
- function isDiagramTone(value) {
1159
- return TONE_SET.has(value);
1160
- }
1161
- function colorCss(c) {
1162
- return c.a >= 1 ? c.css : `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;
1163
- }
1164
- function oneOf(value, allowed, fallback, name) {
1165
- const hit = allowed.find((a) => a === value);
1166
- if (hit !== void 0) return hit;
1167
- if (value !== "") log2.warn("invalid-theme-keyword", { var: name, value, allowed });
1168
- return fallback;
1169
- }
1170
- var PLAIN_LENGTH = /^(-?[\d.]+)(px)?$/;
1171
- var ScopeReader = class {
1172
- constructor(scope) {
1173
- __publicField(this, "scope", scope);
1174
- __publicField(this, "style");
1175
- __publicField(this, "probe");
1176
- this.style = getComputedStyle(scope);
1177
- const doc = scope.ownerDocument;
1178
- this.probe = doc.createElement("span");
1179
- this.probe.style.position = "absolute";
1180
- this.probe.style.visibility = "hidden";
1181
- this.probe.style.pointerEvents = "none";
1182
- scope.appendChild(this.probe);
1183
- }
1184
- dispose() {
1185
- this.probe.remove();
1186
- }
1187
- raw(name) {
1188
- return this.style.getPropertyValue(name).trim();
1189
- }
1190
- /** A length var in px: a plain value parses directly, a derived one (`calc`, `var`) through the probe. */
1191
- length(name, fallback) {
1192
- const raw = this.raw(name);
1193
- const plain = PLAIN_LENGTH.exec(raw);
1194
- if (plain) return Number(plain[1]);
1195
- if (raw === "") return fallback;
1196
- this.probe.style.width = `var(${name})`;
1197
- const computed = PLAIN_LENGTH.exec(getComputedStyle(this.probe).width.trim());
1198
- this.probe.style.width = "";
1199
- if (computed) return Number(computed[1]);
1200
- log2.warn("unresolvable-theme-length", { var: name, raw });
1201
- return fallback;
1202
- }
1203
- number(name, fallback) {
1204
- const raw = this.raw(name);
1205
- if (raw === "") return fallback;
1206
- const n = Number(raw);
1207
- if (Number.isFinite(n)) return n;
1208
- log2.warn("unresolvable-theme-number", { var: name, raw });
1209
- return fallback;
1210
- }
1211
- /** A duration var in ms (`250ms`, `0.25s`, or a `var()` alias resolved through the probe). */
1212
- durationMs(name, fallback) {
1213
- const parse = (v) => {
1214
- const m = /^([\d.]+)(ms|s)$/.exec(v.trim());
1215
- return m ? Number(m[1]) * (m[2] === "s" ? 1e3 : 1) : null;
1216
- };
1217
- const raw = this.raw(name);
1218
- const direct = parse(raw);
1219
- if (direct !== null) return direct;
1220
- if (raw === "") return fallback;
1221
- this.probe.style.transitionDuration = `var(${name})`;
1222
- const computed = parse(getComputedStyle(this.probe).transitionDuration.split(",")[0] ?? "");
1223
- this.probe.style.transitionDuration = "";
1224
- return computed ?? fallback;
1225
- }
1226
- color(name) {
1227
- const raw = this.raw(name);
1228
- return parseComputedColor(raw) ?? resolveThemeColor(`var(${name})`, this.scope);
1229
- }
1230
- shadow(name) {
1231
- const raw = this.raw(name);
1232
- if (raw === "" || raw === "none") return null;
1233
- this.probe.style.boxShadow = `var(${name})`;
1234
- const computed = getComputedStyle(this.probe).boxShadow.trim() || raw;
1235
- this.probe.style.boxShadow = "";
1236
- const colorMatch = /(rgba?\([^)]*\)|#[0-9a-f]{3,8}|oklch\([^)]*\))/i.exec(computed);
1237
- const lengths = computed.replace(colorMatch?.[0] ?? "", "").trim().split(/\s+/).map((t) => PLAIN_LENGTH.exec(t)).filter((m) => m !== null).map((m) => Number(m[1]));
1238
- if (lengths.length < 2) return null;
1239
- const color = colorMatch ? parseComputedColor(colorMatch[0]) : null;
1240
- return { offsetX: lengths[0], offsetY: lengths[1], blur: lengths[2] ?? 0, color: color ? colorCss(color) : "rgba(0, 0, 0, 0.25)" };
1138
+ var paintShape = (painter, rawNode, dctx) => {
1139
+ const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
1140
+ if (!isValidScenePos(node.position)) return;
1141
+ painter.save();
1142
+ if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
1143
+ const origin = dctx.projector.project(node.position);
1144
+ const tileWidth = dctx.projector.tileWidth;
1145
+ const groupScale = dctx.groupScale ?? 1;
1146
+ const strokePx = (node.strokeWidth ?? 1) / groupScale;
1147
+ if (node.blendMode) painter.setBlend(node.blendMode);
1148
+ if (node.strokeDash && node.strokeDash.length > 0) {
1149
+ painter.setLineDash(
1150
+ node.strokeDash.map((v) => v / groupScale),
1151
+ (node.strokeDashOffset ?? 0) / groupScale
1152
+ );
1241
1153
  }
1242
- family(name, fallback) {
1243
- return this.raw(name) || fallback;
1154
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
1155
+ if (node.rotate) {
1156
+ const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
1157
+ const px = origin.x + pivot.x * tileWidth;
1158
+ const py = origin.y + pivot.y * tileWidth;
1159
+ painter.translate(px, py);
1160
+ painter.rotate(node.rotate);
1161
+ painter.translate(-px, -py);
1244
1162
  }
1245
- };
1246
- function dashList(raw, fallback) {
1247
- if (raw === "") return fallback;
1248
- const parts = raw.split(/[\s,]+/).map(Number);
1249
- return parts.every((n) => Number.isFinite(n) && n >= 0) ? parts : fallback;
1250
- }
1251
- function prefersReducedMotion(scope) {
1252
- const view = scope.ownerDocument.defaultView;
1253
- return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
1254
- }
1255
- function resolveCanvasTheme(scope) {
1256
- const r = new ScopeReader(scope);
1257
- try {
1258
- const tones = {};
1259
- for (const tone of DIAGRAM_TONES) {
1260
- const c = r.color(toneVar(tone));
1261
- tones[tone] = c ? colorCss(c) : "currentColor";
1163
+ if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
1164
+ const fill = node.fill === "none" ? void 0 : node.fill;
1165
+ const stroke = node.stroke === "none" ? void 0 : node.stroke;
1166
+ const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
1167
+ const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
1168
+ const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
1169
+ switch (node.shape) {
1170
+ case "cell": {
1171
+ const pts = dctx.projector.cellPath(node.position);
1172
+ if (pxFill) painter.fillPoly(pts, pxFill);
1173
+ if (patFill) painter.fillPoly(pts, patFill);
1174
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1175
+ break;
1262
1176
  }
1263
- const ground = r.color("--surface-diagram");
1264
- const text = {};
1265
- const TEXT_FALLBACK = { xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24 };
1266
- for (const size of DIAGRAM_TEXT_SIZES) text[size] = r.length(`--text-${size}`, TEXT_FALLBACK[size]);
1267
- const body = r.family("--font-family-body", r.family("--font-family", "system-ui, sans-serif"));
1268
- return {
1269
- ground: ground ? colorCss(ground) : "transparent",
1270
- tones,
1271
- series: SERIES_TONES.map((t) => tones[t]),
1272
- strokes: {
1273
- thin: r.length("--diagram-stroke-thin", 1),
1274
- normal: r.length("--diagram-stroke-normal", 1.5),
1275
- bold: r.length("--diagram-stroke-bold", 3)
1276
- },
1277
- lineCap: oneOf(r.raw("--diagram-line-cap"), ["butt", "round", "square"], "round", "--diagram-line-cap"),
1278
- lineJoin: oneOf(r.raw("--diagram-line-join"), ["miter", "round", "bevel"], "round", "--diagram-line-join"),
1279
- dash: dashList(r.raw("--diagram-dash"), [6, 4]),
1280
- dot: dashList(r.raw("--diagram-dot"), [1, 4]),
1281
- fillStyle: oneOf(r.raw("--diagram-fill-style"), ["solid", "tint", "outline", "hatch"], "tint", "--diagram-fill-style"),
1282
- fillOpacity: r.number("--diagram-fill-opacity", 0.18),
1283
- roughness: r.number("--diagram-roughness", 0),
1284
- glow: r.number("--diagram-glow", 0),
1285
- shadow: r.shadow("--diagram-shadow"),
1286
- marker: oneOf(r.raw("--diagram-marker"), ["triangle", "open", "line", "dot"], "triangle", "--diagram-marker"),
1287
- faces: {
1288
- body,
1289
- display: r.family("--font-family-display", body),
1290
- mono: r.family("--font-family-mono", "ui-monospace, monospace")
1291
- },
1292
- label: {
1293
- font: oneOf(r.raw("--diagram-label-font"), ["body", "display", "mono"], "body", "--diagram-label-font"),
1294
- size: oneOf(r.raw("--diagram-label-size"), DIAGRAM_TEXT_SIZES, "sm", "--diagram-label-size"),
1295
- weight: r.raw("--diagram-label-weight") || "500",
1296
- uppercase: oneOf(r.raw("--diagram-label-case"), ["none", "uppercase"], "none", "--diagram-label-case") === "uppercase"
1297
- },
1298
- text,
1299
- corner: r.length("--diagram-corner", 4),
1300
- motion: {
1301
- enabled: r.raw("--motion-enable") !== "off" && !prefersReducedMotion(scope),
1302
- fastMs: r.durationMs("--duration-fast", 150),
1303
- normalMs: r.durationMs("--duration-normal", 250),
1304
- slowMs: r.durationMs("--duration-slow", 400),
1305
- easing: r.raw("--easing-standard") || "ease"
1306
- },
1307
- scene: {
1308
- roughness: r.number("--material-roughness", 0.6),
1309
- metalness: r.number("--material-metalness", 0),
1310
- flat: r.number("--material-flat", 0) >= 1,
1311
- outline: r.number("--material-outline", 0),
1312
- ambient: r.number("--light-ambient", 0.55),
1313
- key: r.number("--light-key", 1),
1314
- keyColor: (() => {
1315
- const c = r.color("--light-key-color");
1316
- return c ? colorCss(c) : "#ffffff";
1317
- })(),
1318
- fog: r.number("--scene-fog", 0)
1177
+ case "rect": {
1178
+ const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "top-left");
1179
+ const tw = dctx.projector.tileWidth;
1180
+ const x = p.x + (node.offsetX ?? 0) * tw;
1181
+ const y = p.y + (node.offsetY ?? 0) * tw;
1182
+ const w = (node.width ?? 0) * tw;
1183
+ const h = (node.height ?? 0) * tw;
1184
+ if (pxFill) painter.fillRect(x, y, w, h, pxFill);
1185
+ if (patFill) painter.fillRect(x, y, w, h, patFill);
1186
+ if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
1187
+ break;
1188
+ }
1189
+ case "ellipse": {
1190
+ const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "ground");
1191
+ const tw = dctx.projector.tileWidth;
1192
+ const cx = p.x + (node.offsetX ?? 0) * tw;
1193
+ const cy = p.y + (node.offsetY ?? 0) * tw;
1194
+ const rx = (node.radiusX ?? 0) * tw;
1195
+ const ry = (node.radiusY ?? node.radiusX ?? 0) * tw;
1196
+ if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
1197
+ if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
1198
+ if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
1199
+ break;
1200
+ }
1201
+ case "poly": {
1202
+ const pts = (node.points ?? []).map((pt) => ({
1203
+ x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
1204
+ y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
1205
+ }));
1206
+ if (pxFill) painter.fillPoly(pts, pxFill);
1207
+ if (patFill) painter.fillPoly(pts, patFill);
1208
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1209
+ break;
1210
+ }
1211
+ case "path": {
1212
+ if (!node.d) break;
1213
+ painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
1214
+ painter.scale(tileWidth, tileWidth);
1215
+ if (node.strokeDash && node.strokeDash.length > 0) {
1216
+ painter.setLineDash(
1217
+ node.strokeDash.map((v) => v / (groupScale * tileWidth)),
1218
+ (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
1219
+ );
1319
1220
  }
1221
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
1222
+ const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
1223
+ const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
1224
+ const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
1225
+ if (localFill) painter.fillPath(node.d, localFill);
1226
+ if (localPat) painter.fillPath(node.d, localPat);
1227
+ if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
1228
+ break;
1229
+ }
1230
+ }
1231
+ painter.restore();
1232
+ };
1233
+ function DrawShape(props) {
1234
+ const register = useContext(DrawableRegistryContext);
1235
+ if (register) {
1236
+ const { opacity, ...rest } = props;
1237
+ const node = {
1238
+ ...rest,
1239
+ type: "draw-shape",
1240
+ ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
1320
1241
  };
1321
- } finally {
1322
- r.dispose();
1242
+ register(node);
1323
1243
  }
1244
+ return null;
1324
1245
  }
1325
- function withSeries(theme, series, scope) {
1326
- if (!theme || !series || series.length === 0 || !scope) return theme;
1327
- const colors = series.map((c, i) => markColor(theme, c, scope, SERIES_TONES[i % SERIES_TONES.length]));
1328
- const tones = { ...theme.tones };
1329
- SERIES_TONES.forEach((tone, i) => {
1330
- tones[tone] = colors[i % colors.length];
1331
- });
1332
- return { ...theme, tones, series: SERIES_TONES.map((t) => tones[t]) };
1333
- }
1334
- function markColor(theme, value, scope, fallback) {
1335
- if (value === void 0 || value === "") return theme.tones[fallback];
1336
- if (isDiagramTone(value)) return theme.tones[value];
1337
- if (value.startsWith("var(") || value.startsWith("color-mix(")) {
1338
- const resolved = resolveThemeColor(value, scope);
1339
- return resolved ? colorCss(resolved) : theme.tones[fallback];
1340
- }
1341
- return value;
1246
+ function isAnimatedGroup(node) {
1247
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1342
1248
  }
1343
- function markStrokeWidth(theme, literal, role, fallback = "normal") {
1344
- return literal ?? theme.strokes[role ?? fallback];
1249
+ function DrawGroup(props) {
1250
+ const register = useContext(DrawableRegistryContext);
1251
+ if (register) register({ ...props, type: "draw-group" });
1252
+ return null;
1345
1253
  }
1346
- function labelFont(theme, roles, literalPx, literalFamily) {
1347
- const px = literalPx ?? theme.text[roles.textSize ?? theme.label.size];
1348
- const family = literalFamily ?? theme.faces[roles.font ?? theme.label.font];
1349
- return { font: `${theme.label.weight} ${px}px ${family}`, px };
1254
+
1255
+ // lib/imageCache.ts
1256
+ var RETRY_AFTER_MS = 5e3;
1257
+ var cache = /* @__PURE__ */ new Map();
1258
+ function startLoad(url, onReady, existing) {
1259
+ const img = new Image();
1260
+ img.crossOrigin = "anonymous";
1261
+ const entry = existing ?? { img, status: "pending" };
1262
+ entry.img = img;
1263
+ if (onReady) entry.onReady = onReady;
1264
+ img.onload = () => {
1265
+ entry.status = "loaded";
1266
+ updateAssetStatus(url, "loaded");
1267
+ entry.onReady?.();
1268
+ };
1269
+ img.onerror = () => {
1270
+ entry.status = "failed";
1271
+ entry.failedAt = Date.now();
1272
+ updateAssetStatus(url, "failed");
1273
+ entry.onReady?.();
1274
+ };
1275
+ img.src = url;
1276
+ return entry;
1350
1277
  }
1351
- function labelText(theme, text, textCase = "theme") {
1352
- return theme.label.uppercase && textCase === "theme" ? text.toUpperCase() : text;
1278
+ function getOrLoadImage(url, onReady) {
1279
+ if (!url || typeof window === "undefined") return null;
1280
+ const existing = cache.get(url);
1281
+ if (existing) {
1282
+ if (existing.status === "loaded") return existing.img;
1283
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
1284
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
1285
+ existing.failedAt = Date.now();
1286
+ startLoad(url, onReady, existing);
1287
+ }
1288
+ return null;
1289
+ }
1290
+ const entry = startLoad(url, onReady);
1291
+ cache.set(url, entry);
1292
+ updateAssetStatus(url, "pending");
1293
+ return null;
1353
1294
  }
1354
- function fillLabel(ctx, theme, text, x, y) {
1355
- if (!text) return;
1356
- ctx.save();
1357
- ctx.strokeStyle = theme.ground;
1358
- ctx.lineWidth = Math.max(2, theme.text.xs * 0.3);
1359
- ctx.lineJoin = "round";
1360
- ctx.strokeText(text, x, y);
1361
- ctx.restore();
1362
- ctx.fillText(text, x, y);
1295
+ function getImageStatus(url) {
1296
+ return cache.get(url)?.status;
1363
1297
  }
1364
- function outlineStroke(stroke, fill, style, fillTone) {
1365
- return outlineCarriesFill(fill, style, fillTone) && fill !== void 0 ? fill : stroke;
1298
+
1299
+ // lib/skinning.ts
1300
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
1301
+ function composeAffine(a, b) {
1302
+ return [
1303
+ a[0] * b[0] + a[2] * b[1],
1304
+ a[1] * b[0] + a[3] * b[1],
1305
+ a[0] * b[2] + a[2] * b[3],
1306
+ a[1] * b[2] + a[3] * b[3],
1307
+ a[0] * b[4] + a[2] * b[5] + a[4],
1308
+ a[1] * b[4] + a[3] * b[5] + a[5]
1309
+ ];
1366
1310
  }
1367
- function outlineCarriesFill(fill, style, fillTone) {
1368
- return style === "outline" && fill !== void 0 && fillTone !== "fill";
1311
+ function invertAffine(m) {
1312
+ const det = m[0] * m[3] - m[1] * m[2];
1313
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return IDENTITY;
1314
+ const inv = 1 / det;
1315
+ return [
1316
+ m[3] * inv,
1317
+ -m[1] * inv,
1318
+ -m[2] * inv,
1319
+ m[0] * inv,
1320
+ (m[2] * m[5] - m[3] * m[4]) * inv,
1321
+ (m[1] * m[4] - m[0] * m[5]) * inv
1322
+ ];
1369
1323
  }
1370
- function applyLineCharacter(ctx, theme, strokeColor) {
1371
- ctx.lineCap = theme.lineCap;
1372
- ctx.lineJoin = theme.lineJoin;
1373
- if (theme.glow > 0) {
1374
- ctx.shadowColor = strokeColor;
1375
- ctx.shadowBlur = theme.glow;
1376
- ctx.shadowOffsetX = 0;
1377
- ctx.shadowOffsetY = 0;
1378
- }
1324
+ function applyAffine(m, x, y) {
1325
+ return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
1379
1326
  }
1380
- function markDash(theme, dash) {
1381
- if (dash === "dashed") return theme.dash;
1382
- if (dash === "dotted") return theme.dot;
1383
- return [];
1327
+ function rotationAbout(pivot, degrees) {
1328
+ const r = degrees * Math.PI / 180;
1329
+ const cos = Math.cos(r);
1330
+ const sin = Math.sin(r);
1331
+ const [px, py] = pivot;
1332
+ return [cos, sin, -sin, cos, px - cos * px + sin * py, py - sin * px - cos * py];
1384
1333
  }
1385
- function wobble(x, y, k) {
1386
- const s = Math.sin(x * 12.9898 + y * 78.233 + k * 37.719) * 43758.5453;
1387
- return (s - Math.floor(s)) * 2 - 1;
1334
+ function computeWorldMatrices(bones, pose) {
1335
+ const indexByName = /* @__PURE__ */ new Map();
1336
+ bones.forEach((b, i) => indexByName.set(b.name, i));
1337
+ const world = new Array(bones.length);
1338
+ const visiting = /* @__PURE__ */ new Set();
1339
+ const resolve = (i) => {
1340
+ const cached = world[i];
1341
+ if (cached) return cached;
1342
+ if (visiting.has(i)) return IDENTITY;
1343
+ visiting.add(i);
1344
+ const b = bones[i];
1345
+ const parentIndex = b.parent === null ? void 0 : indexByName.get(b.parent);
1346
+ const parentWorld = parentIndex === void 0 ? IDENTITY : resolve(parentIndex);
1347
+ const local = rotationAbout(b.pivot, pose[b.name] ?? 0);
1348
+ const m = composeAffine(parentWorld, local);
1349
+ visiting.delete(i);
1350
+ world[i] = m;
1351
+ return m;
1352
+ };
1353
+ return bones.map((_, i) => resolve(i));
1388
1354
  }
1389
- function tracePolyline(ctx, theme, points, closed) {
1390
- ctx.beginPath();
1391
- if (points.length === 0) return;
1392
- const amp = theme.roughness;
1393
- const pts = closed ? [...points, points[0]] : [...points];
1394
- ctx.moveTo(pts[0].x + amp * wobble(pts[0].x, pts[0].y, 0), pts[0].y + amp * wobble(pts[0].y, pts[0].x, 1));
1395
- for (let i = 1; i < pts.length; i++) {
1396
- const a = pts[i - 1];
1397
- const b = pts[i];
1398
- if (amp === 0) {
1399
- ctx.lineTo(b.x, b.y);
1400
- continue;
1401
- }
1402
- const steps = Math.max(1, Math.round(Math.hypot(b.x - a.x, b.y - a.y) / 24));
1403
- for (let s = 1; s <= steps; s++) {
1404
- const t = s / steps;
1405
- const x = a.x + (b.x - a.x) * t;
1406
- const y = a.y + (b.y - a.y) * t;
1407
- ctx.lineTo(x + amp * wobble(x, y, 2), y + amp * wobble(y, x, 3));
1355
+ function skinVertices(mesh, bindWorld, curWorld) {
1356
+ const skinMats = /* @__PURE__ */ new Map();
1357
+ const skinMat = (bone) => {
1358
+ let m = skinMats.get(bone);
1359
+ if (!m) {
1360
+ m = composeAffine(curWorld[bone] ?? IDENTITY, invertAffine(bindWorld[bone] ?? IDENTITY));
1361
+ skinMats.set(bone, m);
1408
1362
  }
1409
- }
1410
- if (closed) ctx.closePath();
1411
- }
1412
- function arcPoints(cx, cy, rx, ry, startRad = 0, endRad = Math.PI * 2) {
1413
- const steps = Math.max(12, Math.round(Math.abs(endRad - startRad) * Math.max(rx, ry) / 8));
1414
- const out = [];
1415
- for (let i = 0; i <= steps; i++) {
1416
- const t = startRad + (endRad - startRad) * i / steps;
1417
- out.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
1418
- }
1419
- return out;
1420
- }
1421
- function fillMark(ctx, theme, color, style = theme.fillStyle) {
1422
- if (style === "outline") return;
1423
- ctx.save();
1424
- ctx.shadowBlur = 0;
1425
- ctx.shadowColor = "transparent";
1426
- if (theme.shadow) {
1427
- ctx.shadowColor = theme.shadow.color;
1428
- ctx.shadowOffsetX = theme.shadow.offsetX;
1429
- ctx.shadowOffsetY = theme.shadow.offsetY;
1430
- ctx.shadowBlur = theme.shadow.blur;
1431
- }
1432
- if (style === "hatch") {
1433
- ctx.clip();
1434
- const span = ctx.canvas.width + ctx.canvas.height;
1435
- ctx.strokeStyle = color;
1436
- ctx.lineWidth = theme.strokes.thin;
1437
- ctx.setLineDash([]);
1438
- ctx.beginPath();
1439
- for (let d = -span; d < span; d += 6) {
1440
- ctx.moveTo(d, 0);
1441
- ctx.lineTo(d + span, span);
1363
+ return m;
1364
+ };
1365
+ return mesh.vertices.map((v) => {
1366
+ if (v.weights.length === 0) return [v.x, v.y];
1367
+ let x = 0;
1368
+ let y = 0;
1369
+ for (const { bone, w } of v.weights) {
1370
+ const [bx, by] = applyAffine(skinMat(bone), v.x, v.y);
1371
+ x += w * bx;
1372
+ y += w * by;
1442
1373
  }
1443
- ctx.stroke();
1444
- } else {
1445
- ctx.globalAlpha *= style === "tint" ? theme.fillOpacity : 1;
1446
- ctx.fillStyle = color;
1447
- ctx.fill();
1448
- }
1449
- ctx.restore();
1374
+ return [x, y];
1375
+ });
1450
1376
  }
1451
- function drawMarker(ctx, theme, x1, y1, x2, y2, color, width) {
1452
- const size = Math.max(8, width * 4);
1453
- const angle = Math.atan2(y2 - y1, x2 - x1);
1454
- const left = { x: x2 - size * Math.cos(angle - Math.PI / 6), y: y2 - size * Math.sin(angle - Math.PI / 6) };
1455
- const right = { x: x2 - size * Math.cos(angle + Math.PI / 6), y: y2 - size * Math.sin(angle + Math.PI / 6) };
1456
- ctx.save();
1457
- ctx.strokeStyle = color;
1458
- ctx.fillStyle = color;
1459
- ctx.lineWidth = width;
1460
- ctx.setLineDash([]);
1461
- switch (theme.marker) {
1462
- case "triangle": {
1463
- ctx.beginPath();
1464
- ctx.moveTo(x2, y2);
1465
- ctx.lineTo(left.x, left.y);
1466
- ctx.lineTo(right.x, right.y);
1467
- ctx.closePath();
1468
- ctx.fill();
1469
- break;
1470
- }
1471
- case "open":
1472
- case "line": {
1473
- ctx.beginPath();
1474
- ctx.moveTo(left.x, left.y);
1475
- ctx.lineTo(x2, y2);
1476
- ctx.lineTo(right.x, right.y);
1477
- if (theme.marker === "open") ctx.stroke();
1478
- else {
1479
- ctx.moveTo(x2 - size * 0.5 * Math.cos(angle + Math.PI / 2), y2 - size * 0.5 * Math.sin(angle + Math.PI / 2));
1480
- ctx.lineTo(x2 + size * 0.5 * Math.cos(angle + Math.PI / 2), y2 + size * 0.5 * Math.sin(angle + Math.PI / 2));
1481
- ctx.stroke();
1482
- }
1483
- break;
1484
- }
1485
- case "dot": {
1486
- ctx.beginPath();
1487
- ctx.arc(x2, y2, Math.max(2.5, width * 1.5), 0, Math.PI * 2);
1488
- ctx.fill();
1489
- break;
1490
- }
1491
- }
1492
- ctx.restore();
1377
+ function triangleAffine(src, dst) {
1378
+ const [s0, s1, s2] = src;
1379
+ const det = s0[0] * (s1[1] - s2[1]) + s1[0] * (s2[1] - s0[1]) + s2[0] * (s0[1] - s1[1]);
1380
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
1381
+ const [d0, d1, d2] = dst;
1382
+ const a = (d0[0] * (s1[1] - s2[1]) + d1[0] * (s2[1] - s0[1]) + d2[0] * (s0[1] - s1[1])) / det;
1383
+ const c = (d0[0] * (s2[0] - s1[0]) + d1[0] * (s0[0] - s2[0]) + d2[0] * (s1[0] - s0[0])) / det;
1384
+ const e = (d0[0] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[0] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[0] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1385
+ const b = (d0[1] * (s1[1] - s2[1]) + d1[1] * (s2[1] - s0[1]) + d2[1] * (s0[1] - s1[1])) / det;
1386
+ const d = (d0[1] * (s2[0] - s1[0]) + d1[1] * (s0[0] - s2[0]) + d2[1] * (s1[0] - s0[0])) / det;
1387
+ const f = (d0[1] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[1] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[1] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1388
+ return [a, b, c, d, e, f];
1493
1389
  }
1494
- function useCanvasTheme(scopeRef) {
1495
- const { appliedTheme } = useTheme();
1496
- const scopeKey = JSON.stringify(useThemeScope());
1497
- const [state, setState] = useState({ theme: null, version: 0 });
1498
- useLayoutEffect(() => {
1499
- const scope = scopeRef.current;
1500
- if (!scope) return;
1501
- const theme = resolveCanvasTheme(scope);
1502
- setState((prev) => ({ theme, version: prev.version + 1 }));
1503
- }, [appliedTheme, scopeKey, scopeRef]);
1504
- return state;
1390
+ var meshCache = /* @__PURE__ */ new Map();
1391
+ function isRigBundle(json) {
1392
+ return json.mesh !== void 0 || json.bones !== void 0;
1505
1393
  }
1506
-
1507
- // lib/drawable/contract.ts
1508
- function isValidScenePos(pos) {
1509
- return Number.isFinite(pos?.x) && Number.isFinite(pos?.y);
1394
+ function getRigBundle(url, onReady) {
1395
+ if (meshCache.has(url)) return meshCache.get(url) ?? void 0;
1396
+ meshCache.set(url, void 0);
1397
+ fetch(url).then((r) => {
1398
+ if (!r.ok) throw new Error(`rigbundle fetch failed: HTTP ${r.status}`);
1399
+ return r.json();
1400
+ }).then((json) => {
1401
+ const ok = isRigBundle(json) || Array.isArray(json.vertices);
1402
+ if (!ok) throw new Error("rigbundle payload is neither a SkinMesh nor a rigbundle");
1403
+ meshCache.set(url, json);
1404
+ onReady();
1405
+ }).catch(() => {
1406
+ meshCache.set(url, null);
1407
+ onReady();
1408
+ });
1409
+ return void 0;
1510
1410
  }
1511
- function spriteAssetState(node) {
1512
- if (!isValidScenePos(node.position)) return "invalid-position";
1513
- return typeof node.asset?.url === "string" && node.asset.url.length > 0 ? "ready" : "asset-unset";
1411
+ function skinMeshFailed(url) {
1412
+ return meshCache.get(url) === null;
1514
1413
  }
1515
- var SPRITE_FALLBACK_FILL = "#9b8f7f";
1516
- var SPRITE_FALLBACK_STROKE = "#5e564b";
1517
- function spriteRect(projector, node, natural) {
1518
- const tw = projector.tileWidth;
1519
- const fallbackW = projector.worldPixelDirect ? natural?.w ?? 0 : tw;
1520
- const fallbackH = projector.worldPixelDirect ? natural?.h ?? 0 : tw;
1521
- const w = node.width !== void 0 ? node.width * tw : fallbackW;
1522
- const h = node.height !== void 0 ? node.height * tw : fallbackH;
1523
- const anchor = node.anchor ?? "top-left";
1524
- const p = projector.anchorPoint(node.position, anchor);
1525
- return {
1526
- x: anchor === "top-left" ? p.x : p.x - w / 2,
1527
- y: anchor === "ground" ? p.y - h : anchor === "center" ? p.y - h / 2 : p.y,
1528
- w,
1529
- h
1530
- };
1414
+ function vertexBounds(points) {
1415
+ if (points.length === 0) return null;
1416
+ let minX = Infinity;
1417
+ let minY = Infinity;
1418
+ let maxX = -Infinity;
1419
+ let maxY = -Infinity;
1420
+ for (const [x, y] of points) {
1421
+ if (x < minX) minX = x;
1422
+ if (y < minY) minY = y;
1423
+ if (x > maxX) maxX = x;
1424
+ if (y > maxY) maxY = y;
1425
+ }
1426
+ return { minX, minY, maxX, maxY };
1531
1427
  }
1532
- var DrawableRegistryContext = createContext(null);
1533
- var NUMERIC_TRACKS = [
1534
- "offsetX",
1535
- "offsetY",
1536
- "rotate",
1537
- "opacity",
1538
- "radiusX",
1539
- "radiusY",
1540
- "width",
1541
- "height",
1542
- "strokeWidth",
1543
- "strokeDashOffset",
1544
- "blur"
1545
- ];
1546
- function isAnimatedShape(node) {
1547
- return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1428
+
1429
+ // components/game/atoms/DrawSkinnedMesh.tsx
1430
+ var skinnedLog = createLogger("almadar:ui:draw-skinned-mesh");
1431
+ var loggedMissing = /* @__PURE__ */ new Set();
1432
+ function warnMissingOnce(reason, node) {
1433
+ const key = `${reason}:${node.asset.url}:${String(node.meshUrl)}`;
1434
+ if (loggedMissing.has(key)) return;
1435
+ loggedMissing.add(key);
1436
+ skinnedLog.warn("draw-skinned-mesh unresolvable \u2014 painting fallback", { reason, url: node.asset.url, meshUrl: node.meshUrl });
1548
1437
  }
1549
- var lerp = (a, b, k) => a + (b - a) * k;
1550
- function applyShapeAnimation(node, timeMs) {
1551
- const anim = node.animation;
1552
- if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
1553
- const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
1554
- const cycle = timeMs / anim.durationMs;
1555
- const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
1556
- const out = { ...node };
1557
- const trackValue = (key) => {
1558
- const defined = frames.filter((f) => f[key] !== void 0);
1559
- if (defined.length === 0) return void 0;
1560
- let prev;
1561
- let next;
1562
- for (const f of defined) {
1563
- if (f.at <= t) prev = f;
1564
- else if (!next) next = f;
1565
- }
1566
- if (!prev) return defined[0][key];
1567
- if (!next) return prev[key];
1568
- const span = next.at - prev.at;
1569
- const k = span > 0 ? (t - prev.at) / span : 1;
1570
- const a = prev[key];
1571
- const b = next[key];
1572
- if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
1573
- return a;
1574
- };
1575
- for (const key of NUMERIC_TRACKS) {
1576
- const v = trackValue(key);
1577
- if (v !== void 0) out[key] = v;
1578
- }
1579
- const fill = trackValue("fill");
1580
- if (fill !== void 0) out.fill = fill;
1581
- const stroke = trackValue("stroke");
1582
- if (stroke !== void 0) out.stroke = stroke;
1583
- const shadowFrames = frames.filter((f) => f.shadow !== void 0);
1584
- if (shadowFrames.length > 0) {
1585
- const sh = trackValue("shadow");
1586
- if (sh !== void 0) {
1587
- let prevSh;
1588
- let nextSh;
1589
- for (const f of shadowFrames) {
1590
- if (f.at <= t) prevSh = f;
1591
- else if (!nextSh) nextSh = f;
1592
- }
1593
- if (prevSh?.shadow && nextSh?.shadow) {
1594
- const span = nextSh.at - prevSh.at;
1595
- const k = span > 0 ? (t - prevSh.at) / span : 1;
1596
- out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
1597
- } else {
1598
- out.shadow = sh;
1599
- }
1438
+ var WEIGHT_PALETTE = Array.from({ length: 10 }, (_, i) => `hsl(${i * 36 % 360}, 70%, 55%)`);
1439
+ function dominantBone(v) {
1440
+ let best = -1;
1441
+ let bestW = 0;
1442
+ for (const { bone, w } of v.weights) {
1443
+ if (w > bestW) {
1444
+ bestW = w;
1445
+ best = bone;
1600
1446
  }
1601
1447
  }
1602
- return out;
1448
+ return best;
1603
1449
  }
1604
- function gradientStyle(g, ox, oy, scale) {
1605
- if (g.kind === "linear") {
1606
- if (!g.from || !g.to) return void 0;
1607
- return {
1608
- kind: "linear",
1609
- x1: ox + g.from.x * scale,
1610
- y1: oy + g.from.y * scale,
1611
- x2: ox + g.to.x * scale,
1612
- y2: oy + g.to.y * scale,
1613
- stops: g.stops
1614
- };
1615
- }
1616
- if (g.kind === "conic") {
1617
- if (!g.center) return void 0;
1618
- return {
1619
- kind: "conic",
1620
- cx: ox + g.center.x * scale,
1621
- cy: oy + g.center.y * scale,
1622
- angle: g.angle ?? 0,
1623
- stops: g.stops
1624
- };
1450
+ function resolveSkinPose(node, clip) {
1451
+ const effectiveClip = clip ?? node.clip;
1452
+ if (effectiveClip && node.frame !== void 0 && effectiveClip.frames.length > 0) {
1453
+ return effectiveClip.frames[node.frame % effectiveClip.frames.length];
1625
1454
  }
1626
- if (!g.center || g.radius === void 0) return void 0;
1627
- return {
1628
- kind: "radial",
1629
- cx: ox + g.center.x * scale,
1630
- cy: oy + g.center.y * scale,
1631
- r: g.radius * scale,
1632
- stops: g.stops
1633
- };
1455
+ return node.pose ?? {};
1634
1456
  }
1635
- function patternStyle(p, unit) {
1636
- if (p.kind === "image") {
1637
- if (!p.url) return void 0;
1638
- return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
1639
- }
1640
- return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
1457
+ function resolveRig(node, dctx) {
1458
+ const fetched = node.meshUrl ? getRigBundle(node.meshUrl, dctx.invalidate) : void 0;
1459
+ if (node.meshUrl && fetched === void 0) return void 0;
1460
+ const rb = fetched && isRigBundle(fetched) ? fetched : void 0;
1461
+ const mesh = node.mesh ?? rb?.mesh ?? (!rb ? fetched : void 0);
1462
+ if (!mesh) return void 0;
1463
+ const bones = node.bones.length > 0 ? node.bones : rb?.bones ?? [];
1464
+ if (bones.length === 0) return void 0;
1465
+ const clip = node.clip ?? (node.clipName ? rb?.clips?.[node.clipName] : void 0);
1466
+ return { mesh, bones, clip };
1641
1467
  }
1642
- var paintShape = (painter, rawNode, dctx) => {
1643
- const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
1644
- if (!isValidScenePos(node.position)) return;
1645
- painter.save();
1646
- if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
1647
- const origin = dctx.projector.project(node.position);
1648
- const tileWidth = dctx.projector.tileWidth;
1649
- const groupScale = dctx.groupScale ?? 1;
1650
- const strokePx = (node.strokeWidth ?? 1) / groupScale;
1651
- if (node.blendMode) painter.setBlend(node.blendMode);
1652
- if (node.strokeDash && node.strokeDash.length > 0) {
1653
- painter.setLineDash(
1654
- node.strokeDash.map((v) => v / groupScale),
1655
- (node.strokeDashOffset ?? 0) / groupScale
1656
- );
1657
- }
1658
- if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
1659
- if (node.rotate) {
1660
- const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
1661
- const px = origin.x + pivot.x * tileWidth;
1662
- const py = origin.y + pivot.y * tileWidth;
1663
- painter.translate(px, py);
1664
- painter.rotate(node.rotate);
1665
- painter.translate(-px, -py);
1666
- }
1667
- if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
1668
- const fill = node.fill === "none" ? void 0 : node.fill;
1669
- const stroke = node.stroke === "none" ? void 0 : node.stroke;
1670
- const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
1671
- const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
1672
- const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
1673
- switch (node.shape) {
1674
- case "cell": {
1675
- const pts = dctx.projector.cellPath(node.position);
1676
- if (pxFill) painter.fillPoly(pts, pxFill);
1677
- if (patFill) painter.fillPoly(pts, patFill);
1678
- if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1679
- break;
1680
- }
1681
- case "rect": {
1682
- const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "top-left");
1683
- const tw = dctx.projector.tileWidth;
1684
- const x = p.x + (node.offsetX ?? 0) * tw;
1685
- const y = p.y + (node.offsetY ?? 0) * tw;
1686
- const w = (node.width ?? 0) * tw;
1687
- const h = (node.height ?? 0) * tw;
1688
- if (pxFill) painter.fillRect(x, y, w, h, pxFill);
1689
- if (patFill) painter.fillRect(x, y, w, h, patFill);
1690
- if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
1691
- break;
1692
- }
1693
- case "ellipse": {
1694
- const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "ground");
1695
- const tw = dctx.projector.tileWidth;
1696
- const cx = p.x + (node.offsetX ?? 0) * tw;
1697
- const cy = p.y + (node.offsetY ?? 0) * tw;
1698
- const rx = (node.radiusX ?? 0) * tw;
1699
- const ry = (node.radiusY ?? node.radiusX ?? 0) * tw;
1700
- if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
1701
- if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
1702
- if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
1703
- break;
1704
- }
1705
- case "poly": {
1706
- const pts = (node.points ?? []).map((pt) => ({
1707
- x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
1708
- y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
1709
- }));
1710
- if (pxFill) painter.fillPoly(pts, pxFill);
1711
- if (patFill) painter.fillPoly(pts, patFill);
1712
- if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1468
+ var pixelUVByMesh = /* @__PURE__ */ new WeakMap();
1469
+ function meshHasPixelUVs(mesh) {
1470
+ const cached = pixelUVByMesh.get(mesh);
1471
+ if (cached !== void 0) return cached;
1472
+ let pixel = false;
1473
+ for (const v of mesh.vertices) {
1474
+ if (v.u < 0 || v.u > 1 || v.v < 0 || v.v > 1) {
1475
+ pixel = true;
1713
1476
  break;
1714
1477
  }
1715
- case "path": {
1716
- if (!node.d) break;
1717
- painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
1718
- painter.scale(tileWidth, tileWidth);
1719
- if (node.strokeDash && node.strokeDash.length > 0) {
1720
- painter.setLineDash(
1721
- node.strokeDash.map((v) => v / (groupScale * tileWidth)),
1722
- (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
1723
- );
1724
- }
1725
- if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
1726
- const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
1727
- const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
1728
- const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
1729
- if (localFill) painter.fillPath(node.d, localFill);
1730
- if (localPat) painter.fillPath(node.d, localPat);
1731
- if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
1732
- break;
1478
+ }
1479
+ pixelUVByMesh.set(mesh, pixel);
1480
+ return pixel;
1481
+ }
1482
+ function paintMeshPass(painter, node, bones, mesh, tex, dctx, pose, opacity) {
1483
+ const bindWorld = computeWorldMatrices(bones, {});
1484
+ const curWorld = computeWorldMatrices(bones, pose);
1485
+ const skinned = skinVertices(mesh, bindWorld, curWorld);
1486
+ const origin = dctx.projector.project(node.position);
1487
+ const tw = dctx.projector.tileWidth;
1488
+ painter.save();
1489
+ if (opacity !== 1) painter.setAlpha(opacity);
1490
+ for (const [i0, i1, i2] of mesh.triangles) {
1491
+ const v0 = mesh.vertices[i0];
1492
+ const v1 = mesh.vertices[i1];
1493
+ const v2 = mesh.vertices[i2];
1494
+ const p0 = skinned[i0];
1495
+ const p1 = skinned[i1];
1496
+ const p2 = skinned[i2];
1497
+ if (!v0 || !v1 || !v2 || !p0 || !p1 || !p2) continue;
1498
+ const dst = [
1499
+ [origin.x + p0[0] * tw, origin.y + p0[1] * tw],
1500
+ [origin.x + p1[0] * tw, origin.y + p1[1] * tw],
1501
+ [origin.x + p2[0] * tw, origin.y + p2[1] * tw]
1502
+ ];
1503
+ if (node.weightsOverlay || !tex) {
1504
+ if (!node.weightsOverlay) continue;
1505
+ const bone = dominantBone(v0);
1506
+ painter.fillPoly(
1507
+ dst.map(([x, y]) => ({ x, y })),
1508
+ WEIGHT_PALETTE[(bone % WEIGHT_PALETTE.length + WEIGHT_PALETTE.length) % WEIGHT_PALETTE.length]
1509
+ );
1510
+ continue;
1733
1511
  }
1512
+ const pixelUV = meshHasPixelUVs(mesh);
1513
+ const uv = pixelUV ? [[v0.u, v0.v], [v1.u, v1.v], [v2.u, v2.v]] : [
1514
+ [v0.u * tex.width, v0.v * tex.height],
1515
+ [v1.u * tex.width, v1.v * tex.height],
1516
+ [v2.u * tex.width, v2.v * tex.height]
1517
+ ];
1518
+ const affine = triangleAffine(uv, dst);
1519
+ if (!affine) continue;
1520
+ painter.save();
1521
+ painter.clipPath(`M ${dst[0][0]} ${dst[0][1]} L ${dst[1][0]} ${dst[1][1]} L ${dst[2][0]} ${dst[2][1]} Z`);
1522
+ painter.blitTransformed(tex, { x: 0, y: 0, w: tex.width, h: tex.height }, affine);
1523
+ painter.restore();
1734
1524
  }
1735
1525
  painter.restore();
1526
+ }
1527
+ var paintSkinnedMesh = (painter, node, dctx) => {
1528
+ if (!isValidScenePos(node.position)) return;
1529
+ const rig = resolveRig(node, dctx);
1530
+ if (!rig) {
1531
+ if (node.meshUrl && skinMeshFailed(node.meshUrl)) warnMissingOnce("mesh-failed", node);
1532
+ else if (!node.mesh && !node.meshUrl) warnMissingOnce("mesh-missing", node);
1533
+ return;
1534
+ }
1535
+ const tex = node.weightsOverlay ? null : painter.resolveTexture(node.asset.url);
1536
+ if (!node.weightsOverlay && !tex) {
1537
+ if (getImageStatus(node.asset.url) === "failed") warnMissingOnce("texture-failed", node);
1538
+ return;
1539
+ }
1540
+ if (node.ghost && rig.clip && rig.clip.frames.length > 0) {
1541
+ paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, rig.clip.frames[node.ghost.frame % rig.clip.frames.length], node.ghost.opacity);
1542
+ }
1543
+ paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, resolveSkinPose(node, rig.clip), node.opacity ?? 1);
1736
1544
  };
1737
- function DrawShape(props) {
1545
+ function DrawSkinnedMesh(props) {
1738
1546
  const register = useContext(DrawableRegistryContext);
1739
- if (register) {
1740
- const { opacity, ...rest } = props;
1741
- const node = {
1742
- ...rest,
1743
- type: "draw-shape",
1744
- ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
1745
- };
1746
- register(node);
1747
- }
1547
+ if (register) register({ ...props, type: "draw-skinned-mesh" });
1748
1548
  return null;
1749
1549
  }
1750
- function isAnimatedGroup(node) {
1550
+ var MESH_SEGMENTS_DEFAULT = 24;
1551
+ function polyhedronBounds(vertices) {
1552
+ if (!vertices || vertices.length < 3) return null;
1553
+ const min = [Infinity, Infinity, Infinity];
1554
+ const max = [-Infinity, -Infinity, -Infinity];
1555
+ for (const v of vertices) {
1556
+ if (v.length < 3 || !v.every((c) => Number.isFinite(c))) return null;
1557
+ for (let axis = 0; axis < 3; axis++) {
1558
+ min[axis] = Math.min(min[axis], v[axis]);
1559
+ max[axis] = Math.max(max[axis], v[axis]);
1560
+ }
1561
+ }
1562
+ return { min, max };
1563
+ }
1564
+ function clampSegments(segments) {
1565
+ const s = segments ?? MESH_SEGMENTS_DEFAULT;
1566
+ return Math.min(64, Math.max(3, Math.round(s)));
1567
+ }
1568
+ function isAnimatedMesh(node) {
1751
1569
  return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1752
1570
  }
1753
- function DrawGroup(props) {
1571
+ var lerp2 = (a, b, k) => a + (b - a) * k;
1572
+ var NUMERIC_TRACKS2 = [
1573
+ "offsetX",
1574
+ "offsetY",
1575
+ "offsetZ",
1576
+ "rotateX",
1577
+ "rotateY",
1578
+ "rotateZ",
1579
+ "scale",
1580
+ "opacity",
1581
+ "emissiveIntensity"
1582
+ ];
1583
+ function applyMeshAnimation(node, timeMs) {
1584
+ const anim = node.animation;
1585
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
1586
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
1587
+ const cycle = timeMs / anim.durationMs;
1588
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
1589
+ const trackValue = (key) => {
1590
+ const defined = frames.filter((f) => f[key] !== void 0);
1591
+ if (defined.length === 0) return void 0;
1592
+ let prev;
1593
+ let next;
1594
+ for (const f of defined) {
1595
+ if (f.at <= t) prev = f;
1596
+ else if (!next) next = f;
1597
+ }
1598
+ if (!prev) return defined[0][key];
1599
+ if (!next) return prev[key];
1600
+ const span = next.at - prev.at;
1601
+ const k = span > 0 ? (t - prev.at) / span : 1;
1602
+ const a = prev[key];
1603
+ const b = next[key];
1604
+ if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
1605
+ return a;
1606
+ };
1607
+ const num = {};
1608
+ for (const key of NUMERIC_TRACKS2) {
1609
+ const v = trackValue(key);
1610
+ if (v !== void 0) num[key] = v;
1611
+ }
1612
+ return {
1613
+ offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
1614
+ rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
1615
+ scale: num.scale ?? 1,
1616
+ opacity: num.opacity,
1617
+ emissiveIntensity: num.emissiveIntensity,
1618
+ color: trackValue("color"),
1619
+ emissive: trackValue("emissive")
1620
+ };
1621
+ }
1622
+ function DrawMesh(props) {
1754
1623
  const register = useContext(DrawableRegistryContext);
1755
- if (register) register({ ...props, type: "draw-group" });
1624
+ if (register) register({ ...props, type: "draw-mesh" });
1756
1625
  return null;
1757
1626
  }
1758
1627
 
1759
- // lib/imageCache.ts
1760
- var RETRY_AFTER_MS = 5e3;
1761
- var cache = /* @__PURE__ */ new Map();
1762
- function startLoad(url, onReady, existing) {
1763
- const img = new Image();
1764
- img.crossOrigin = "anonymous";
1765
- const entry = existing ?? { img, status: "pending" };
1766
- entry.img = img;
1767
- if (onReady) entry.onReady = onReady;
1768
- img.onload = () => {
1769
- entry.status = "loaded";
1770
- updateAssetStatus(url, "loaded");
1771
- entry.onReady?.();
1772
- };
1773
- img.onerror = () => {
1774
- entry.status = "failed";
1775
- entry.failedAt = Date.now();
1776
- updateAssetStatus(url, "failed");
1777
- entry.onReady?.();
1628
+ // components/core/atoms/fx.ts
1629
+ function resolveFxView(item, presets) {
1630
+ const row = presets?.find((p) => p.type === item.type);
1631
+ if (!row) return item;
1632
+ return {
1633
+ ...item,
1634
+ space: item.space ?? row.space,
1635
+ effect: item.effect ?? row.effect,
1636
+ color: item.color ?? row.color,
1637
+ size: item.size ?? row.size,
1638
+ vx: item.vx ?? row.vx,
1639
+ vy: item.vy ?? row.vy,
1640
+ vz: item.vz ?? row.vz,
1641
+ particleCount: item.particleCount ?? row.particleCount,
1642
+ animation: item.animation ?? row.animation,
1643
+ shape: row.shape,
1644
+ count: row.count,
1645
+ glow: row.glow,
1646
+ gravity: row.gravity,
1647
+ color2: row.color2
1778
1648
  };
1779
- img.src = url;
1780
- return entry;
1781
- }
1782
- function getOrLoadImage(url, onReady) {
1783
- if (!url || typeof window === "undefined") return null;
1784
- const existing = cache.get(url);
1785
- if (existing) {
1786
- if (existing.status === "loaded") return existing.img;
1787
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
1788
- if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
1789
- existing.failedAt = Date.now();
1790
- startLoad(url, onReady, existing);
1791
- }
1792
- return null;
1793
- }
1794
- const entry = startLoad(url, onReady);
1795
- cache.set(url, entry);
1796
- updateAssetStatus(url, "pending");
1797
- return null;
1798
1649
  }
1799
- function getImageStatus(url) {
1800
- return cache.get(url)?.status;
1650
+ function fxLifecycle(item, epochNowMs, tickMs) {
1651
+ const maxTtl = Math.max(item.maxTtl ?? item.ttl, item.ttl, 1);
1652
+ const lifeMs = maxTtl * tickMs;
1653
+ const ageMs = item.bornAt !== void 0 && epochNowMs > 0 ? Math.max(0, epochNowMs - item.bornAt) : (1 - item.ttl / maxTtl) * lifeMs;
1654
+ const progress = Math.min(1, Math.max(0, ageMs / lifeMs));
1655
+ return { lifeMs, ageMs, progress, fade: 1 - progress };
1801
1656
  }
1802
-
1803
- // lib/skinning.ts
1804
- var IDENTITY = [1, 0, 0, 1, 0, 0];
1805
- function composeAffine(a, b) {
1806
- return [
1807
- a[0] * b[0] + a[2] * b[1],
1808
- a[1] * b[0] + a[3] * b[1],
1809
- a[0] * b[2] + a[2] * b[3],
1810
- a[1] * b[2] + a[3] * b[3],
1811
- a[0] * b[4] + a[2] * b[5] + a[4],
1812
- a[1] * b[4] + a[3] * b[5] + a[5]
1813
- ];
1657
+ function fxHash01(seed, salt) {
1658
+ let h = 2166136261 ^ salt;
1659
+ for (let i = 0; i < seed.length; i++) {
1660
+ h ^= seed.charCodeAt(i);
1661
+ h = Math.imul(h, 16777619);
1662
+ }
1663
+ h ^= h >>> 13;
1664
+ h = Math.imul(h, 1274126177);
1665
+ h ^= h >>> 16;
1666
+ return (h >>> 0) / 4294967296;
1814
1667
  }
1815
- function invertAffine(m) {
1816
- const det = m[0] * m[3] - m[1] * m[2];
1817
- if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return IDENTITY;
1818
- const inv = 1 / det;
1819
- return [
1820
- m[3] * inv,
1821
- -m[1] * inv,
1822
- -m[2] * inv,
1823
- m[0] * inv,
1824
- (m[2] * m[5] - m[3] * m[4]) * inv,
1825
- (m[1] * m[4] - m[0] * m[5]) * inv
1826
- ];
1668
+ var CONFETTI_COLORS = [
1669
+ "var(--color-primary)",
1670
+ "var(--color-success)",
1671
+ "var(--color-warning)",
1672
+ "var(--color-error)",
1673
+ "gold",
1674
+ "dodgerblue"
1675
+ ];
1676
+ function createConfettiParticles(count, seed) {
1677
+ return Array.from({ length: count }, (_, i) => ({
1678
+ id: i,
1679
+ color: CONFETTI_COLORS[Math.floor(fxHash01(seed, i * 7 + 1) * CONFETTI_COLORS.length)],
1680
+ left: 30 + fxHash01(seed, i * 7 + 2) * 40,
1681
+ delay: fxHash01(seed, i * 7 + 3) * 300,
1682
+ angle: fxHash01(seed, i * 7 + 4) * 360,
1683
+ distance: 40 + fxHash01(seed, i * 7 + 5) * 80,
1684
+ rotation: fxHash01(seed, i * 7 + 6) * 720 - 360,
1685
+ size: 4 + fxHash01(seed, i * 7 + 7) * 6
1686
+ }));
1827
1687
  }
1828
- function applyAffine(m, x, y) {
1829
- return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
1688
+ var CONFETTI_BURST_KEYFRAMES = `
1689
+ @keyframes confetti-burst {
1690
+ 0% {
1691
+ opacity: 1;
1692
+ transform: translate(0, 0) rotate(0deg) scale(1);
1693
+ }
1694
+ 70% {
1695
+ opacity: 1;
1696
+ }
1697
+ 100% {
1698
+ opacity: 0;
1699
+ transform: translate(var(--confetti-tx), var(--confetti-ty)) rotate(var(--confetti-rotate)) scale(0.5);
1700
+ }
1830
1701
  }
1831
- function rotationAbout(pivot, degrees) {
1832
- const r = degrees * Math.PI / 180;
1833
- const cos = Math.cos(r);
1834
- const sin = Math.sin(r);
1835
- const [px, py] = pivot;
1836
- return [cos, sin, -sin, cos, px - cos * px + sin * py, py - sin * px - cos * py];
1702
+ `;
1703
+
1704
+ // components/game/molecules/DrawFxLayer.tsx
1705
+ var DEFAULT_TICK_MS = 500;
1706
+ var DEFAULT_TEXT_COLOR = "#ffe066";
1707
+ var DEFAULT_SPARK_COLOR = "#ffffff";
1708
+ var easeOut = (t) => 1 - (1 - t) * (1 - t);
1709
+ function fxPosition(view, dim, ageSec) {
1710
+ const g = view.gravity ?? 0;
1711
+ const fall = 0.5 * g * ageSec * ageSec;
1712
+ if (dim === "2d") {
1713
+ const base2 = view.position ?? { x: view.x, y: view.z ?? view.y ?? 0 };
1714
+ if (!Number.isFinite(base2.x) || !Number.isFinite(base2.y)) return void 0;
1715
+ return {
1716
+ x: base2.x + (view.vx ?? 0) * ageSec,
1717
+ y: base2.y + (view.vz ?? 0) * ageSec + fall
1718
+ };
1719
+ }
1720
+ const base = view.position ?? { x: view.x, y: view.z ?? 0, z: view.y };
1721
+ if (!Number.isFinite(base.x) || !Number.isFinite(base.y)) return void 0;
1722
+ return {
1723
+ x: base.x + (view.vx ?? 0) * ageSec,
1724
+ y: base.y + (view.vz ?? 0) * ageSec,
1725
+ ...base.z !== void 0 || view.vy !== void 0 || g !== 0 ? { z: (base.z ?? 0) + (view.vy ?? 0) * ageSec - fall } : {}
1726
+ };
1837
1727
  }
1838
- function computeWorldMatrices(bones, pose) {
1839
- const indexByName = /* @__PURE__ */ new Map();
1840
- bones.forEach((b, i) => indexByName.set(b.name, i));
1841
- const world = new Array(bones.length);
1842
- const visiting = /* @__PURE__ */ new Set();
1843
- const resolve = (i) => {
1844
- const cached = world[i];
1845
- if (cached) return cached;
1846
- if (visiting.has(i)) return IDENTITY;
1847
- visiting.add(i);
1848
- const b = bones[i];
1849
- const parentIndex = b.parent === null ? void 0 : indexByName.get(b.parent);
1850
- const parentWorld = parentIndex === void 0 ? IDENTITY : resolve(parentIndex);
1851
- const local = rotationAbout(b.pivot, pose[b.name] ?? 0);
1852
- const m = composeAffine(parentWorld, local);
1853
- visiting.delete(i);
1854
- world[i] = m;
1855
- return m;
1728
+ function sparkShape(view, pos, i, fade, progress) {
1729
+ const size = view.size ?? 0.5;
1730
+ const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
1731
+ const dist = size * (0.4 + 0.6 * fxHash01(view.id, i * 3 + 1)) * easeOut(progress);
1732
+ const r = size * (0.06 + 0.06 * fxHash01(view.id, i * 3 + 2));
1733
+ const color = view.color ?? DEFAULT_SPARK_COLOR;
1734
+ return {
1735
+ type: "draw-shape",
1736
+ shape: "ellipse",
1737
+ position: { ...pos, x: pos.x + Math.cos(angle) * dist, y: pos.y + Math.sin(angle) * dist },
1738
+ anchor: "center",
1739
+ radiusX: r,
1740
+ fill: color,
1741
+ blendMode: "lighter",
1742
+ opacity: fade,
1743
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
1856
1744
  };
1857
- return bones.map((_, i) => resolve(i));
1858
1745
  }
1859
- function skinVertices(mesh, bindWorld, curWorld) {
1860
- const skinMats = /* @__PURE__ */ new Map();
1861
- const skinMat = (bone) => {
1862
- let m = skinMats.get(bone);
1863
- if (!m) {
1864
- m = composeAffine(curWorld[bone] ?? IDENTITY, invertAffine(bindWorld[bone] ?? IDENTITY));
1865
- skinMats.set(bone, m);
1746
+ function proceduralShapes(view, pos, fade, progress) {
1747
+ const size = view.size ?? 0.5;
1748
+ const color = view.color ?? DEFAULT_SPARK_COLOR;
1749
+ switch (view.shape ?? "spark") {
1750
+ case "ring": {
1751
+ return [
1752
+ {
1753
+ type: "draw-shape",
1754
+ shape: "ellipse",
1755
+ position: pos,
1756
+ anchor: "center",
1757
+ radiusX: size * easeOut(progress),
1758
+ stroke: view.color2 ?? color,
1759
+ strokeWidth: 2,
1760
+ blendMode: "lighter",
1761
+ opacity: fade,
1762
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
1763
+ }
1764
+ ];
1866
1765
  }
1867
- return m;
1868
- };
1869
- return mesh.vertices.map((v) => {
1870
- if (v.weights.length === 0) return [v.x, v.y];
1871
- let x = 0;
1872
- let y = 0;
1873
- for (const { bone, w } of v.weights) {
1874
- const [bx, by] = applyAffine(skinMat(bone), v.x, v.y);
1875
- x += w * bx;
1876
- y += w * by;
1766
+ case "puff": {
1767
+ const count = view.count ?? 3;
1768
+ return Array.from({ length: count }, (_, i) => {
1769
+ const angle = fxHash01(view.id, i * 5) * Math.PI * 2;
1770
+ const drift = size * 0.3 * fxHash01(view.id, i * 5 + 1) * easeOut(progress);
1771
+ return {
1772
+ type: "draw-shape",
1773
+ shape: "ellipse",
1774
+ position: { ...pos, x: pos.x + Math.cos(angle) * drift, y: pos.y + Math.sin(angle) * drift },
1775
+ anchor: "center",
1776
+ radiusX: size * (0.15 + 0.35 * progress) * (0.7 + 0.6 * fxHash01(view.id, i * 5 + 2)),
1777
+ fill: i === 0 && view.color2 ? view.color2 : color,
1778
+ opacity: fade * 0.6,
1779
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
1780
+ };
1781
+ });
1877
1782
  }
1878
- return [x, y];
1879
- });
1783
+ case "streak": {
1784
+ const count = view.count ?? 5;
1785
+ return Array.from({ length: count }, (_, i) => {
1786
+ const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
1787
+ const inner = size * (0.2 + 0.8 * easeOut(progress)) * (0.6 + 0.4 * fxHash01(view.id, i * 3 + 1));
1788
+ const len = size * 0.35;
1789
+ return {
1790
+ type: "draw-shape",
1791
+ shape: "ellipse",
1792
+ position: pos,
1793
+ anchor: "center",
1794
+ offsetX: inner + len / 2,
1795
+ offsetY: 0,
1796
+ radiusX: len / 2,
1797
+ radiusY: size * 0.04,
1798
+ rotate: angle,
1799
+ fill: color,
1800
+ blendMode: "lighter",
1801
+ opacity: fade,
1802
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
1803
+ };
1804
+ });
1805
+ }
1806
+ default: {
1807
+ const count = view.count ?? 6;
1808
+ return Array.from({ length: count }, (_, i) => sparkShape(view, pos, i, fade, progress));
1809
+ }
1810
+ }
1811
+ }
1812
+ function expandFxItem(view, node, epochNowMs, dim, projector, fontFamily) {
1813
+ if (view.space === "screen") return [];
1814
+ const tickMs = node.tickMs ?? DEFAULT_TICK_MS;
1815
+ const { ageMs, progress, fade } = fxLifecycle(view, epochNowMs, tickMs);
1816
+ if (progress >= 1) return [];
1817
+ const pos = fxPosition(view, dim, ageMs / 1e3);
1818
+ if (!pos) return [];
1819
+ const out = [];
1820
+ const artItems = node.art?.[view.type]?.["idle"];
1821
+ const sprite = node.sprites?.[view.type];
1822
+ if (Array.isArray(artItems)) {
1823
+ out.push({
1824
+ type: "draw-group",
1825
+ position: pos,
1826
+ opacity: fade,
1827
+ ...view.size !== void 0 ? { scale: view.size } : {},
1828
+ items: artItems
1829
+ });
1830
+ } else if (sprite?.url) {
1831
+ const spriteSize = view.size ?? 0.6;
1832
+ out.push({
1833
+ type: "draw-sprite",
1834
+ position: pos,
1835
+ asset: sprite,
1836
+ anchor: "center",
1837
+ width: spriteSize,
1838
+ height: spriteSize,
1839
+ opacity: fade,
1840
+ animation: view.animation ?? sprite.animations?.[0],
1841
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
1842
+ clockMs: ageMs
1843
+ });
1844
+ } else {
1845
+ out.push(...proceduralShapes(view, pos, fade, progress));
1846
+ }
1847
+ if (view.message) {
1848
+ const pxSize = projector && view.size !== void 0 ? Math.max(10, Math.round(view.size * projector.tileWidth)) : void 0;
1849
+ const family = fontFamily ?? "system-ui, sans-serif";
1850
+ const text = {
1851
+ type: "draw-text",
1852
+ text: view.message,
1853
+ position: pos,
1854
+ offsetY: -(0.15 + 0.55 * progress),
1855
+ color: view.color ?? node.textColor ?? DEFAULT_TEXT_COLOR,
1856
+ opacity: fade,
1857
+ font: `bold ${pxSize ?? 14}px ${family}`
1858
+ };
1859
+ out.push(text);
1860
+ }
1861
+ return out;
1862
+ }
1863
+ function expandFxLayer(node, epochNowMs, dim, projector, fontFamily) {
1864
+ if (!Array.isArray(node.items)) return [];
1865
+ const out = [];
1866
+ for (const item of node.items) {
1867
+ if (!item || typeof item.id !== "string") continue;
1868
+ out.push(...expandFxItem(resolveFxView(item, node.presets), node, epochNowMs, dim, projector, fontFamily));
1869
+ }
1870
+ return out;
1880
1871
  }
1881
- function triangleAffine(src, dst) {
1882
- const [s0, s1, s2] = src;
1883
- const det = s0[0] * (s1[1] - s2[1]) + s1[0] * (s2[1] - s0[1]) + s2[0] * (s0[1] - s1[1]);
1884
- if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
1885
- const [d0, d1, d2] = dst;
1886
- const a = (d0[0] * (s1[1] - s2[1]) + d1[0] * (s2[1] - s0[1]) + d2[0] * (s0[1] - s1[1])) / det;
1887
- const c = (d0[0] * (s2[0] - s1[0]) + d1[0] * (s0[0] - s2[0]) + d2[0] * (s1[0] - s0[0])) / det;
1888
- const e = (d0[0] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[0] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[0] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1889
- const b = (d0[1] * (s1[1] - s2[1]) + d1[1] * (s2[1] - s0[1]) + d2[1] * (s0[1] - s1[1])) / det;
1890
- const d = (d0[1] * (s2[0] - s1[0]) + d1[1] * (s0[0] - s2[0]) + d2[1] * (s1[0] - s0[0])) / det;
1891
- const f = (d0[1] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[1] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[1] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1892
- return [a, b, c, d, e, f];
1872
+ function DrawFxLayer(_props) {
1873
+ return null;
1893
1874
  }
1894
- var meshCache = /* @__PURE__ */ new Map();
1895
- function isRigBundle(json) {
1896
- return json.mesh !== void 0 || json.bones !== void 0;
1875
+
1876
+ // lib/atlasSlice.ts
1877
+ var atlasCache = /* @__PURE__ */ new Map();
1878
+ function isTilesheet(a) {
1879
+ return typeof a.tileWidth === "number";
1897
1880
  }
1898
- function getRigBundle(url, onReady) {
1899
- if (meshCache.has(url)) return meshCache.get(url) ?? void 0;
1900
- meshCache.set(url, void 0);
1901
- fetch(url).then((r) => {
1902
- if (!r.ok) throw new Error(`rigbundle fetch failed: HTTP ${r.status}`);
1903
- return r.json();
1904
- }).then((json) => {
1905
- const ok = isRigBundle(json) || Array.isArray(json.vertices);
1906
- if (!ok) throw new Error("rigbundle payload is neither a SkinMesh nor a rigbundle");
1907
- meshCache.set(url, json);
1881
+ function isSpriteSheetAtlas(a) {
1882
+ const s = a;
1883
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
1884
+ }
1885
+ function getAtlas(url, onReady) {
1886
+ if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
1887
+ atlasCache.set(url, void 0);
1888
+ fetch(url).then((r) => r.json()).then((json) => {
1889
+ atlasCache.set(url, json);
1908
1890
  onReady();
1909
1891
  }).catch(() => {
1910
- meshCache.set(url, null);
1892
+ atlasCache.set(url, null);
1911
1893
  onReady();
1912
1894
  });
1913
1895
  return void 0;
1914
1896
  }
1915
- function skinMeshFailed(url) {
1916
- return meshCache.get(url) === null;
1897
+ function atlasFailed(url) {
1898
+ return atlasCache.get(url) === null;
1917
1899
  }
1918
- function vertexBounds(points) {
1919
- if (points.length === 0) return null;
1920
- let minX = Infinity;
1921
- let minY = Infinity;
1922
- let maxX = -Infinity;
1923
- let maxY = -Infinity;
1924
- for (const [x, y] of points) {
1925
- if (x < minX) minX = x;
1926
- if (y < minY) minY = y;
1927
- if (x > maxX) maxX = x;
1928
- if (y > maxY) maxY = y;
1900
+ function subRectFor(atlas, sprite) {
1901
+ if (isTilesheet(atlas)) {
1902
+ let col;
1903
+ let row;
1904
+ if (sprite.includes(",")) {
1905
+ const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
1906
+ col = c;
1907
+ row = r;
1908
+ } else {
1909
+ const i = Number(sprite);
1910
+ if (!Number.isFinite(i)) return null;
1911
+ col = i % atlas.columns;
1912
+ row = Math.floor(i / atlas.columns);
1913
+ }
1914
+ if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
1915
+ const margin = atlas.margin ?? 0;
1916
+ const spacing = atlas.spacing ?? 0;
1917
+ return {
1918
+ sx: margin + col * (atlas.tileWidth + spacing),
1919
+ sy: margin + row * (atlas.tileHeight + spacing),
1920
+ sw: atlas.tileWidth,
1921
+ sh: atlas.tileHeight
1922
+ };
1929
1923
  }
1930
- return { minX, minY, maxX, maxY };
1924
+ if (isSpriteSheetAtlas(atlas)) return null;
1925
+ const st = atlas.subTextures[sprite];
1926
+ if (!st) return null;
1927
+ return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
1931
1928
  }
1932
-
1933
- // components/game/atoms/DrawSkinnedMesh.tsx
1934
- var skinnedLog = createLogger("almadar:ui:draw-skinned-mesh");
1935
- var loggedMissing = /* @__PURE__ */ new Set();
1936
- function warnMissingOnce(reason, node) {
1937
- const key = `${reason}:${node.asset.url}:${String(node.meshUrl)}`;
1938
- if (loggedMissing.has(key)) return;
1939
- loggedMissing.add(key);
1940
- skinnedLog.warn("draw-skinned-mesh unresolvable \u2014 painting fallback", { reason, url: node.asset.url, meshUrl: node.meshUrl });
1929
+ function atlasUrlOf(asset) {
1930
+ return typeof asset?.atlas === "string" && asset.atlas.length > 0 ? asset.atlas : void 0;
1941
1931
  }
1942
- var WEIGHT_PALETTE = Array.from({ length: 10 }, (_, i) => `hsl(${i * 36 % 360}, 70%, 55%)`);
1943
- function dominantBone(v) {
1944
- let best = -1;
1945
- let bestW = 0;
1946
- for (const { bone, w } of v.weights) {
1947
- if (w > bestW) {
1948
- bestW = w;
1949
- best = bone;
1950
- }
1951
- }
1952
- return best;
1932
+ function isAtlasAsset(asset) {
1933
+ return atlasUrlOf(asset) !== void 0 && typeof asset?.sprite === "string" && asset.sprite.length > 0;
1953
1934
  }
1954
- function resolveSkinPose(node, clip) {
1955
- const effectiveClip = clip ?? node.clip;
1956
- if (effectiveClip && node.frame !== void 0 && effectiveClip.frames.length > 0) {
1957
- return effectiveClip.frames[node.frame % effectiveClip.frames.length];
1935
+ function resolveAssetSource(img, asset, onReady) {
1936
+ if (isAtlasAsset(asset)) {
1937
+ const atlas = getAtlas(asset.atlas, onReady);
1938
+ if (!atlas) return null;
1939
+ const rect = subRectFor(atlas, asset.sprite);
1940
+ if (!rect) return null;
1941
+ return { img, rect, aspect: rect.sw / rect.sh };
1958
1942
  }
1959
- return node.pose ?? {};
1943
+ const natW = img.naturalWidth || 1;
1944
+ const natH = img.naturalHeight || 1;
1945
+ return { img, rect: null, aspect: natW / natH };
1960
1946
  }
1961
- function resolveRig(node, dctx) {
1962
- const fetched = node.meshUrl ? getRigBundle(node.meshUrl, dctx.invalidate) : void 0;
1963
- if (node.meshUrl && fetched === void 0) return void 0;
1964
- const rb = fetched && isRigBundle(fetched) ? fetched : void 0;
1965
- const mesh = node.mesh ?? rb?.mesh ?? (!rb ? fetched : void 0);
1966
- if (!mesh) return void 0;
1967
- const bones = node.bones.length > 0 ? node.bones : rb?.bones ?? [];
1968
- if (bones.length === 0) return void 0;
1969
- const clip = node.clip ?? (node.clipName ? rb?.clips?.[node.clipName] : void 0);
1970
- return { mesh, bones, clip };
1947
+ function blit(ctx, src, dx, dy, dw, dh) {
1948
+ if (src.rect) ctx.drawImage(src.img, src.rect.sx, src.rect.sy, src.rect.sw, src.rect.sh, dx, dy, dw, dh);
1949
+ else ctx.drawImage(src.img, dx, dy, dw, dh);
1971
1950
  }
1972
- var pixelUVByMesh = /* @__PURE__ */ new WeakMap();
1973
- function meshHasPixelUVs(mesh) {
1974
- const cached = pixelUVByMesh.get(mesh);
1975
- if (cached !== void 0) return cached;
1976
- let pixel = false;
1977
- for (const v of mesh.vertices) {
1978
- if (v.u < 0 || v.u > 1 || v.v < 0 || v.v > 1) {
1979
- pixel = true;
1980
- break;
1981
- }
1982
- }
1983
- pixelUVByMesh.set(mesh, pixel);
1984
- return pixel;
1951
+ var log2 = createLogger("almadar:ui:canvas-theme");
1952
+ var toneVar = (tone) => `--color-${DIAGRAM_TONE_COLOR_KEYS[tone]}`;
1953
+ var SERIES_TONES = ["series-1", "series-2", "series-3", "series-4", "series-5", "series-6", "series-7", "series-8"];
1954
+ var TONE_SET = new Set(DIAGRAM_TONES);
1955
+ function isDiagramTone(value) {
1956
+ return TONE_SET.has(value);
1985
1957
  }
1986
- function paintMeshPass(painter, node, bones, mesh, tex, dctx, pose, opacity) {
1987
- const bindWorld = computeWorldMatrices(bones, {});
1988
- const curWorld = computeWorldMatrices(bones, pose);
1989
- const skinned = skinVertices(mesh, bindWorld, curWorld);
1990
- const origin = dctx.projector.project(node.position);
1991
- const tw = dctx.projector.tileWidth;
1992
- painter.save();
1993
- if (opacity !== 1) painter.setAlpha(opacity);
1994
- for (const [i0, i1, i2] of mesh.triangles) {
1995
- const v0 = mesh.vertices[i0];
1996
- const v1 = mesh.vertices[i1];
1997
- const v2 = mesh.vertices[i2];
1998
- const p0 = skinned[i0];
1999
- const p1 = skinned[i1];
2000
- const p2 = skinned[i2];
2001
- if (!v0 || !v1 || !v2 || !p0 || !p1 || !p2) continue;
2002
- const dst = [
2003
- [origin.x + p0[0] * tw, origin.y + p0[1] * tw],
2004
- [origin.x + p1[0] * tw, origin.y + p1[1] * tw],
2005
- [origin.x + p2[0] * tw, origin.y + p2[1] * tw]
2006
- ];
2007
- if (node.weightsOverlay || !tex) {
2008
- if (!node.weightsOverlay) continue;
2009
- const bone = dominantBone(v0);
2010
- painter.fillPoly(
2011
- dst.map(([x, y]) => ({ x, y })),
2012
- WEIGHT_PALETTE[(bone % WEIGHT_PALETTE.length + WEIGHT_PALETTE.length) % WEIGHT_PALETTE.length]
2013
- );
2014
- continue;
2015
- }
2016
- const pixelUV = meshHasPixelUVs(mesh);
2017
- const uv = pixelUV ? [[v0.u, v0.v], [v1.u, v1.v], [v2.u, v2.v]] : [
2018
- [v0.u * tex.width, v0.v * tex.height],
2019
- [v1.u * tex.width, v1.v * tex.height],
2020
- [v2.u * tex.width, v2.v * tex.height]
2021
- ];
2022
- const affine = triangleAffine(uv, dst);
2023
- if (!affine) continue;
2024
- painter.save();
2025
- painter.clipPath(`M ${dst[0][0]} ${dst[0][1]} L ${dst[1][0]} ${dst[1][1]} L ${dst[2][0]} ${dst[2][1]} Z`);
2026
- painter.blitTransformed(tex, { x: 0, y: 0, w: tex.width, h: tex.height }, affine);
2027
- painter.restore();
1958
+ function colorCss(c) {
1959
+ return c.a >= 1 ? c.css : `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;
1960
+ }
1961
+ function oneOf(value, allowed, fallback, name) {
1962
+ const hit = allowed.find((a) => a === value);
1963
+ if (hit !== void 0) return hit;
1964
+ if (value !== "") log2.warn("invalid-theme-keyword", { var: name, value, allowed });
1965
+ return fallback;
1966
+ }
1967
+ var PLAIN_LENGTH = /^(-?[\d.]+)(px)?$/;
1968
+ var ScopeReader = class {
1969
+ constructor(scope) {
1970
+ __publicField(this, "scope", scope);
1971
+ __publicField(this, "style");
1972
+ __publicField(this, "probe");
1973
+ this.style = getComputedStyle(scope);
1974
+ const doc = scope.ownerDocument;
1975
+ this.probe = doc.createElement("span");
1976
+ this.probe.style.position = "absolute";
1977
+ this.probe.style.visibility = "hidden";
1978
+ this.probe.style.pointerEvents = "none";
1979
+ scope.appendChild(this.probe);
1980
+ }
1981
+ dispose() {
1982
+ this.probe.remove();
1983
+ }
1984
+ raw(name) {
1985
+ return this.style.getPropertyValue(name).trim();
1986
+ }
1987
+ /** A length var in px: a plain value parses directly, a derived one (`calc`, `var`) through the probe. */
1988
+ length(name, fallback) {
1989
+ const raw = this.raw(name);
1990
+ const plain = PLAIN_LENGTH.exec(raw);
1991
+ if (plain) return Number(plain[1]);
1992
+ if (raw === "") return fallback;
1993
+ this.probe.style.width = `var(${name})`;
1994
+ const computed = PLAIN_LENGTH.exec(getComputedStyle(this.probe).width.trim());
1995
+ this.probe.style.width = "";
1996
+ if (computed) return Number(computed[1]);
1997
+ log2.warn("unresolvable-theme-length", { var: name, raw });
1998
+ return fallback;
1999
+ }
2000
+ number(name, fallback) {
2001
+ const raw = this.raw(name);
2002
+ if (raw === "") return fallback;
2003
+ const n = Number(raw);
2004
+ if (Number.isFinite(n)) return n;
2005
+ log2.warn("unresolvable-theme-number", { var: name, raw });
2006
+ return fallback;
2007
+ }
2008
+ /** A duration var in ms (`250ms`, `0.25s`, or a `var()` alias resolved through the probe). */
2009
+ durationMs(name, fallback) {
2010
+ const parse = (v) => {
2011
+ const m = /^([\d.]+)(ms|s)$/.exec(v.trim());
2012
+ return m ? Number(m[1]) * (m[2] === "s" ? 1e3 : 1) : null;
2013
+ };
2014
+ const raw = this.raw(name);
2015
+ const direct = parse(raw);
2016
+ if (direct !== null) return direct;
2017
+ if (raw === "") return fallback;
2018
+ this.probe.style.transitionDuration = `var(${name})`;
2019
+ const computed = parse(getComputedStyle(this.probe).transitionDuration.split(",")[0] ?? "");
2020
+ this.probe.style.transitionDuration = "";
2021
+ return computed ?? fallback;
2028
2022
  }
2029
- painter.restore();
2030
- }
2031
- var paintSkinnedMesh = (painter, node, dctx) => {
2032
- if (!isValidScenePos(node.position)) return;
2033
- const rig = resolveRig(node, dctx);
2034
- if (!rig) {
2035
- if (node.meshUrl && skinMeshFailed(node.meshUrl)) warnMissingOnce("mesh-failed", node);
2036
- else if (!node.mesh && !node.meshUrl) warnMissingOnce("mesh-missing", node);
2037
- return;
2023
+ color(name) {
2024
+ const raw = this.raw(name);
2025
+ return parseComputedColor(raw) ?? resolveThemeColor(`var(${name})`, this.scope);
2038
2026
  }
2039
- const tex = node.weightsOverlay ? null : painter.resolveTexture(node.asset.url);
2040
- if (!node.weightsOverlay && !tex) {
2041
- if (getImageStatus(node.asset.url) === "failed") warnMissingOnce("texture-failed", node);
2042
- return;
2027
+ shadow(name) {
2028
+ const raw = this.raw(name);
2029
+ if (raw === "" || raw === "none") return null;
2030
+ this.probe.style.boxShadow = `var(${name})`;
2031
+ const computed = getComputedStyle(this.probe).boxShadow.trim() || raw;
2032
+ this.probe.style.boxShadow = "";
2033
+ const colorMatch = /(rgba?\([^)]*\)|#[0-9a-f]{3,8}|oklch\([^)]*\))/i.exec(computed);
2034
+ const lengths = computed.replace(colorMatch?.[0] ?? "", "").trim().split(/\s+/).map((t) => PLAIN_LENGTH.exec(t)).filter((m) => m !== null).map((m) => Number(m[1]));
2035
+ if (lengths.length < 2) return null;
2036
+ const color = colorMatch ? parseComputedColor(colorMatch[0]) : null;
2037
+ return { offsetX: lengths[0], offsetY: lengths[1], blur: lengths[2] ?? 0, color: color ? colorCss(color) : "rgba(0, 0, 0, 0.25)" };
2043
2038
  }
2044
- if (node.ghost && rig.clip && rig.clip.frames.length > 0) {
2045
- paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, rig.clip.frames[node.ghost.frame % rig.clip.frames.length], node.ghost.opacity);
2039
+ family(name, fallback) {
2040
+ return this.raw(name) || fallback;
2046
2041
  }
2047
- paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, resolveSkinPose(node, rig.clip), node.opacity ?? 1);
2048
2042
  };
2049
- function DrawSkinnedMesh(props) {
2050
- const register = useContext(DrawableRegistryContext);
2051
- if (register) register({ ...props, type: "draw-skinned-mesh" });
2052
- return null;
2043
+ function dashList(raw, fallback) {
2044
+ if (raw === "") return fallback;
2045
+ const parts = raw.split(/[\s,]+/).map(Number);
2046
+ return parts.every((n) => Number.isFinite(n) && n >= 0) ? parts : fallback;
2053
2047
  }
2054
- var MESH_SEGMENTS_DEFAULT = 24;
2055
- function polyhedronBounds(vertices) {
2056
- if (!vertices || vertices.length < 3) return null;
2057
- const min = [Infinity, Infinity, Infinity];
2058
- const max = [-Infinity, -Infinity, -Infinity];
2059
- for (const v of vertices) {
2060
- if (v.length < 3 || !v.every((c) => Number.isFinite(c))) return null;
2061
- for (let axis = 0; axis < 3; axis++) {
2062
- min[axis] = Math.min(min[axis], v[axis]);
2063
- max[axis] = Math.max(max[axis], v[axis]);
2048
+ function prefersReducedMotion(scope) {
2049
+ const view = scope.ownerDocument.defaultView;
2050
+ return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
2051
+ }
2052
+ function resolveCanvasTheme(scope) {
2053
+ const r = new ScopeReader(scope);
2054
+ try {
2055
+ const tones = {};
2056
+ for (const tone of DIAGRAM_TONES) {
2057
+ const c = r.color(toneVar(tone));
2058
+ tones[tone] = c ? colorCss(c) : "currentColor";
2064
2059
  }
2060
+ const ground = r.color("--surface-diagram");
2061
+ const text = {};
2062
+ const TEXT_FALLBACK = { xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24 };
2063
+ for (const size of DIAGRAM_TEXT_SIZES) text[size] = r.length(`--text-${size}`, TEXT_FALLBACK[size]);
2064
+ const body = r.family("--font-family-body", r.family("--font-family", "system-ui, sans-serif"));
2065
+ return {
2066
+ ground: ground ? colorCss(ground) : "transparent",
2067
+ tones,
2068
+ series: SERIES_TONES.map((t) => tones[t]),
2069
+ strokes: {
2070
+ thin: r.length("--diagram-stroke-thin", 1),
2071
+ normal: r.length("--diagram-stroke-normal", 1.5),
2072
+ bold: r.length("--diagram-stroke-bold", 3)
2073
+ },
2074
+ lineCap: oneOf(r.raw("--diagram-line-cap"), ["butt", "round", "square"], "round", "--diagram-line-cap"),
2075
+ lineJoin: oneOf(r.raw("--diagram-line-join"), ["miter", "round", "bevel"], "round", "--diagram-line-join"),
2076
+ dash: dashList(r.raw("--diagram-dash"), [6, 4]),
2077
+ dot: dashList(r.raw("--diagram-dot"), [1, 4]),
2078
+ fillStyle: oneOf(r.raw("--diagram-fill-style"), ["solid", "tint", "outline", "hatch"], "tint", "--diagram-fill-style"),
2079
+ fillOpacity: r.number("--diagram-fill-opacity", 0.18),
2080
+ roughness: r.number("--diagram-roughness", 0),
2081
+ glow: r.number("--diagram-glow", 0),
2082
+ shadow: r.shadow("--diagram-shadow"),
2083
+ marker: oneOf(r.raw("--diagram-marker"), ["triangle", "open", "line", "dot"], "triangle", "--diagram-marker"),
2084
+ faces: {
2085
+ body,
2086
+ display: r.family("--font-family-display", body),
2087
+ mono: r.family("--font-family-mono", "ui-monospace, monospace")
2088
+ },
2089
+ label: {
2090
+ font: oneOf(r.raw("--diagram-label-font"), ["body", "display", "mono"], "body", "--diagram-label-font"),
2091
+ size: oneOf(r.raw("--diagram-label-size"), DIAGRAM_TEXT_SIZES, "sm", "--diagram-label-size"),
2092
+ weight: r.raw("--diagram-label-weight") || "500",
2093
+ uppercase: oneOf(r.raw("--diagram-label-case"), ["none", "uppercase"], "none", "--diagram-label-case") === "uppercase"
2094
+ },
2095
+ text,
2096
+ corner: r.length("--diagram-corner", 4),
2097
+ motion: {
2098
+ enabled: r.raw("--motion-enable") !== "off" && !prefersReducedMotion(scope),
2099
+ fastMs: r.durationMs("--duration-fast", 150),
2100
+ normalMs: r.durationMs("--duration-normal", 250),
2101
+ slowMs: r.durationMs("--duration-slow", 400),
2102
+ easing: r.raw("--easing-standard") || "ease"
2103
+ },
2104
+ scene: {
2105
+ roughness: r.number("--material-roughness", 0.6),
2106
+ metalness: r.number("--material-metalness", 0),
2107
+ flat: r.number("--material-flat", 0) >= 1,
2108
+ outline: r.number("--material-outline", 0),
2109
+ ambient: r.number("--light-ambient", 0.55),
2110
+ key: r.number("--light-key", 1),
2111
+ keyColor: (() => {
2112
+ const c = r.color("--light-key-color");
2113
+ return c ? colorCss(c) : "#ffffff";
2114
+ })(),
2115
+ fog: r.number("--scene-fog", 0)
2116
+ }
2117
+ };
2118
+ } finally {
2119
+ r.dispose();
2065
2120
  }
2066
- return { min, max };
2067
- }
2068
- function clampSegments(segments) {
2069
- const s = segments ?? MESH_SEGMENTS_DEFAULT;
2070
- return Math.min(64, Math.max(3, Math.round(s)));
2071
2121
  }
2072
- function isAnimatedMesh(node) {
2073
- return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
2122
+ function withSeries(theme, series, scope) {
2123
+ if (!theme || !series || series.length === 0 || !scope) return theme;
2124
+ const colors = series.map((c, i) => markColor(theme, c, scope, SERIES_TONES[i % SERIES_TONES.length]));
2125
+ const tones = { ...theme.tones };
2126
+ SERIES_TONES.forEach((tone, i) => {
2127
+ tones[tone] = colors[i % colors.length];
2128
+ });
2129
+ return { ...theme, tones, series: SERIES_TONES.map((t) => tones[t]) };
2074
2130
  }
2075
- var lerp2 = (a, b, k) => a + (b - a) * k;
2076
- var NUMERIC_TRACKS2 = [
2077
- "offsetX",
2078
- "offsetY",
2079
- "offsetZ",
2080
- "rotateX",
2081
- "rotateY",
2082
- "rotateZ",
2083
- "scale",
2084
- "opacity",
2085
- "emissiveIntensity"
2086
- ];
2087
- function applyMeshAnimation(node, timeMs) {
2088
- const anim = node.animation;
2089
- if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
2090
- const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
2091
- const cycle = timeMs / anim.durationMs;
2092
- const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
2093
- const trackValue = (key) => {
2094
- const defined = frames.filter((f) => f[key] !== void 0);
2095
- if (defined.length === 0) return void 0;
2096
- let prev;
2097
- let next;
2098
- for (const f of defined) {
2099
- if (f.at <= t) prev = f;
2100
- else if (!next) next = f;
2101
- }
2102
- if (!prev) return defined[0][key];
2103
- if (!next) return prev[key];
2104
- const span = next.at - prev.at;
2105
- const k = span > 0 ? (t - prev.at) / span : 1;
2106
- const a = prev[key];
2107
- const b = next[key];
2108
- if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
2109
- return a;
2110
- };
2111
- const num = {};
2112
- for (const key of NUMERIC_TRACKS2) {
2113
- const v = trackValue(key);
2114
- if (v !== void 0) num[key] = v;
2131
+ function markColor(theme, value, scope, fallback) {
2132
+ if (value === void 0 || value === "") return theme.tones[fallback];
2133
+ if (isDiagramTone(value)) return theme.tones[value];
2134
+ if (value.startsWith("var(") || value.startsWith("color-mix(")) {
2135
+ const resolved = resolveThemeColor(value, scope);
2136
+ return resolved ? colorCss(resolved) : theme.tones[fallback];
2115
2137
  }
2116
- return {
2117
- offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
2118
- rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
2119
- scale: num.scale ?? 1,
2120
- opacity: num.opacity,
2121
- emissiveIntensity: num.emissiveIntensity,
2122
- color: trackValue("color"),
2123
- emissive: trackValue("emissive")
2124
- };
2138
+ return value;
2125
2139
  }
2126
- function DrawMesh(props) {
2127
- const register = useContext(DrawableRegistryContext);
2128
- if (register) register({ ...props, type: "draw-mesh" });
2129
- return null;
2140
+ function markStrokeWidth(theme, literal, role, fallback = "normal") {
2141
+ return literal ?? theme.strokes[role ?? fallback];
2130
2142
  }
2131
-
2132
- // components/game/molecules/DrawFxLayer.tsx
2133
- var DEFAULT_TICK_MS = 500;
2134
- var DEFAULT_TEXT_COLOR = "#ffe066";
2135
- var DEFAULT_SPARK_COLOR = "#ffffff";
2136
- var easeOut = (t) => 1 - (1 - t) * (1 - t);
2137
- function fxPosition(view, dim, ageSec) {
2138
- const g = view.gravity ?? 0;
2139
- const fall = 0.5 * g * ageSec * ageSec;
2140
- if (dim === "2d") {
2141
- const base2 = view.position ?? { x: view.x, y: view.z ?? view.y ?? 0 };
2142
- if (!Number.isFinite(base2.x) || !Number.isFinite(base2.y)) return void 0;
2143
- return {
2144
- x: base2.x + (view.vx ?? 0) * ageSec,
2145
- y: base2.y + (view.vz ?? 0) * ageSec + fall
2146
- };
2143
+ function labelFont(theme, roles, literalPx, literalFamily) {
2144
+ const px = literalPx ?? theme.text[roles.textSize ?? theme.label.size];
2145
+ const family = literalFamily ?? theme.faces[roles.font ?? theme.label.font];
2146
+ return { font: `${theme.label.weight} ${px}px ${family}`, px };
2147
+ }
2148
+ function labelText(theme, text, textCase = "theme") {
2149
+ return theme.label.uppercase && textCase === "theme" ? text.toUpperCase() : text;
2150
+ }
2151
+ function fillLabel(ctx, theme, text, x, y) {
2152
+ if (!text) return;
2153
+ ctx.save();
2154
+ ctx.strokeStyle = theme.ground;
2155
+ ctx.lineWidth = Math.max(2, theme.text.xs * 0.3);
2156
+ ctx.lineJoin = "round";
2157
+ ctx.strokeText(text, x, y);
2158
+ ctx.restore();
2159
+ ctx.fillText(text, x, y);
2160
+ }
2161
+ function outlineStroke(stroke, fill, style, fillTone) {
2162
+ return outlineCarriesFill(fill, style, fillTone) && fill !== void 0 ? fill : stroke;
2163
+ }
2164
+ function outlineCarriesFill(fill, style, fillTone) {
2165
+ return style === "outline" && fill !== void 0 && fillTone !== "fill";
2166
+ }
2167
+ function applyLineCharacter(ctx, theme, strokeColor) {
2168
+ ctx.lineCap = theme.lineCap;
2169
+ ctx.lineJoin = theme.lineJoin;
2170
+ if (theme.glow > 0) {
2171
+ ctx.shadowColor = strokeColor;
2172
+ ctx.shadowBlur = theme.glow;
2173
+ ctx.shadowOffsetX = 0;
2174
+ ctx.shadowOffsetY = 0;
2147
2175
  }
2148
- const base = view.position ?? { x: view.x, y: view.z ?? 0, z: view.y };
2149
- if (!Number.isFinite(base.x) || !Number.isFinite(base.y)) return void 0;
2150
- return {
2151
- x: base.x + (view.vx ?? 0) * ageSec,
2152
- y: base.y + (view.vz ?? 0) * ageSec,
2153
- ...base.z !== void 0 || view.vy !== void 0 || g !== 0 ? { z: (base.z ?? 0) + (view.vy ?? 0) * ageSec - fall } : {}
2154
- };
2155
2176
  }
2156
- function sparkShape(view, pos, i, fade, progress) {
2157
- const size = view.size ?? 0.5;
2158
- const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
2159
- const dist = size * (0.4 + 0.6 * fxHash01(view.id, i * 3 + 1)) * easeOut(progress);
2160
- const r = size * (0.06 + 0.06 * fxHash01(view.id, i * 3 + 2));
2161
- const color = view.color ?? DEFAULT_SPARK_COLOR;
2162
- return {
2163
- type: "draw-shape",
2164
- shape: "ellipse",
2165
- position: { ...pos, x: pos.x + Math.cos(angle) * dist, y: pos.y + Math.sin(angle) * dist },
2166
- anchor: "center",
2167
- radiusX: r,
2168
- fill: color,
2169
- blendMode: "lighter",
2170
- opacity: fade,
2171
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2172
- };
2177
+ function markDash(theme, dash) {
2178
+ if (dash === "dashed") return theme.dash;
2179
+ if (dash === "dotted") return theme.dot;
2180
+ return [];
2173
2181
  }
2174
- function proceduralShapes(view, pos, fade, progress) {
2175
- const size = view.size ?? 0.5;
2176
- const color = view.color ?? DEFAULT_SPARK_COLOR;
2177
- switch (view.shape ?? "spark") {
2178
- case "ring": {
2179
- return [
2180
- {
2181
- type: "draw-shape",
2182
- shape: "ellipse",
2183
- position: pos,
2184
- anchor: "center",
2185
- radiusX: size * easeOut(progress),
2186
- stroke: view.color2 ?? color,
2187
- strokeWidth: 2,
2188
- blendMode: "lighter",
2189
- opacity: fade,
2190
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2191
- }
2192
- ];
2193
- }
2194
- case "puff": {
2195
- const count = view.count ?? 3;
2196
- return Array.from({ length: count }, (_, i) => {
2197
- const angle = fxHash01(view.id, i * 5) * Math.PI * 2;
2198
- const drift = size * 0.3 * fxHash01(view.id, i * 5 + 1) * easeOut(progress);
2199
- return {
2200
- type: "draw-shape",
2201
- shape: "ellipse",
2202
- position: { ...pos, x: pos.x + Math.cos(angle) * drift, y: pos.y + Math.sin(angle) * drift },
2203
- anchor: "center",
2204
- radiusX: size * (0.15 + 0.35 * progress) * (0.7 + 0.6 * fxHash01(view.id, i * 5 + 2)),
2205
- fill: i === 0 && view.color2 ? view.color2 : color,
2206
- opacity: fade * 0.6,
2207
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2208
- };
2209
- });
2210
- }
2211
- case "streak": {
2212
- const count = view.count ?? 5;
2213
- return Array.from({ length: count }, (_, i) => {
2214
- const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
2215
- const inner = size * (0.2 + 0.8 * easeOut(progress)) * (0.6 + 0.4 * fxHash01(view.id, i * 3 + 1));
2216
- const len = size * 0.35;
2217
- return {
2218
- type: "draw-shape",
2219
- shape: "ellipse",
2220
- position: pos,
2221
- anchor: "center",
2222
- offsetX: inner + len / 2,
2223
- offsetY: 0,
2224
- radiusX: len / 2,
2225
- radiusY: size * 0.04,
2226
- rotate: angle,
2227
- fill: color,
2228
- blendMode: "lighter",
2229
- opacity: fade,
2230
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2231
- };
2232
- });
2182
+ function wobble(x, y, k) {
2183
+ const s = Math.sin(x * 12.9898 + y * 78.233 + k * 37.719) * 43758.5453;
2184
+ return (s - Math.floor(s)) * 2 - 1;
2185
+ }
2186
+ function tracePolyline(ctx, theme, points, closed) {
2187
+ ctx.beginPath();
2188
+ if (points.length === 0) return;
2189
+ const amp = theme.roughness;
2190
+ const pts = closed ? [...points, points[0]] : [...points];
2191
+ ctx.moveTo(pts[0].x + amp * wobble(pts[0].x, pts[0].y, 0), pts[0].y + amp * wobble(pts[0].y, pts[0].x, 1));
2192
+ for (let i = 1; i < pts.length; i++) {
2193
+ const a = pts[i - 1];
2194
+ const b = pts[i];
2195
+ if (amp === 0) {
2196
+ ctx.lineTo(b.x, b.y);
2197
+ continue;
2233
2198
  }
2234
- default: {
2235
- const count = view.count ?? 6;
2236
- return Array.from({ length: count }, (_, i) => sparkShape(view, pos, i, fade, progress));
2199
+ const steps = Math.max(1, Math.round(Math.hypot(b.x - a.x, b.y - a.y) / 24));
2200
+ for (let s = 1; s <= steps; s++) {
2201
+ const t = s / steps;
2202
+ const x = a.x + (b.x - a.x) * t;
2203
+ const y = a.y + (b.y - a.y) * t;
2204
+ ctx.lineTo(x + amp * wobble(x, y, 2), y + amp * wobble(y, x, 3));
2237
2205
  }
2238
2206
  }
2207
+ if (closed) ctx.closePath();
2239
2208
  }
2240
- function expandFxItem(view, node, epochNowMs, dim, projector, fontFamily) {
2241
- if (view.space === "screen") return [];
2242
- const tickMs = node.tickMs ?? DEFAULT_TICK_MS;
2243
- const { ageMs, progress, fade } = fxLifecycle(view, epochNowMs, tickMs);
2244
- if (progress >= 1) return [];
2245
- const pos = fxPosition(view, dim, ageMs / 1e3);
2246
- if (!pos) return [];
2209
+ function arcPoints(cx, cy, rx, ry, startRad = 0, endRad = Math.PI * 2) {
2210
+ const steps = Math.max(12, Math.round(Math.abs(endRad - startRad) * Math.max(rx, ry) / 8));
2247
2211
  const out = [];
2248
- const artItems = node.art?.[view.type]?.["idle"];
2249
- const sprite = node.sprites?.[view.type];
2250
- if (Array.isArray(artItems)) {
2251
- out.push({
2252
- type: "draw-group",
2253
- position: pos,
2254
- opacity: fade,
2255
- ...view.size !== void 0 ? { scale: view.size } : {},
2256
- items: artItems
2257
- });
2258
- } else if (sprite?.url) {
2259
- const spriteSize = view.size ?? 0.6;
2260
- out.push({
2261
- type: "draw-sprite",
2262
- position: pos,
2263
- asset: sprite,
2264
- anchor: "center",
2265
- width: spriteSize,
2266
- height: spriteSize,
2267
- opacity: fade,
2268
- animation: view.animation ?? sprite.animations?.[0],
2269
- // One-shot fx rows (burst) play from the item's birth, not wall-time.
2270
- clockMs: ageMs
2271
- });
2272
- } else {
2273
- out.push(...proceduralShapes(view, pos, fade, progress));
2274
- }
2275
- if (view.message) {
2276
- const pxSize = projector && view.size !== void 0 ? Math.max(10, Math.round(view.size * projector.tileWidth)) : void 0;
2277
- const family = fontFamily ?? "system-ui, sans-serif";
2278
- const text = {
2279
- type: "draw-text",
2280
- text: view.message,
2281
- position: pos,
2282
- offsetY: -(0.15 + 0.55 * progress),
2283
- color: view.color ?? node.textColor ?? DEFAULT_TEXT_COLOR,
2284
- opacity: fade,
2285
- font: `bold ${pxSize ?? 14}px ${family}`
2286
- };
2287
- out.push(text);
2212
+ for (let i = 0; i <= steps; i++) {
2213
+ const t = startRad + (endRad - startRad) * i / steps;
2214
+ out.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
2288
2215
  }
2289
2216
  return out;
2290
2217
  }
2291
- function expandFxLayer(node, epochNowMs, dim, projector, fontFamily) {
2292
- if (!Array.isArray(node.items)) return [];
2293
- const out = [];
2294
- for (const item of node.items) {
2295
- if (!item || typeof item.id !== "string") continue;
2296
- out.push(...expandFxItem(resolveFxView(item, node.presets), node, epochNowMs, dim, projector, fontFamily));
2218
+ function fillMark(ctx, theme, color, style = theme.fillStyle) {
2219
+ if (style === "outline") return;
2220
+ ctx.save();
2221
+ ctx.shadowBlur = 0;
2222
+ ctx.shadowColor = "transparent";
2223
+ if (theme.shadow) {
2224
+ ctx.shadowColor = theme.shadow.color;
2225
+ ctx.shadowOffsetX = theme.shadow.offsetX;
2226
+ ctx.shadowOffsetY = theme.shadow.offsetY;
2227
+ ctx.shadowBlur = theme.shadow.blur;
2297
2228
  }
2298
- return out;
2229
+ if (style === "hatch") {
2230
+ ctx.clip();
2231
+ const span = ctx.canvas.width + ctx.canvas.height;
2232
+ ctx.strokeStyle = color;
2233
+ ctx.lineWidth = theme.strokes.thin;
2234
+ ctx.setLineDash([]);
2235
+ ctx.beginPath();
2236
+ for (let d = -span; d < span; d += 6) {
2237
+ ctx.moveTo(d, 0);
2238
+ ctx.lineTo(d + span, span);
2239
+ }
2240
+ ctx.stroke();
2241
+ } else {
2242
+ ctx.globalAlpha *= style === "tint" ? theme.fillOpacity : 1;
2243
+ ctx.fillStyle = color;
2244
+ ctx.fill();
2245
+ }
2246
+ ctx.restore();
2299
2247
  }
2300
- function DrawFxLayer(_props) {
2301
- return null;
2248
+ function drawMarker(ctx, theme, x1, y1, x2, y2, color, width) {
2249
+ const size = Math.max(8, width * 4);
2250
+ const angle = Math.atan2(y2 - y1, x2 - x1);
2251
+ const left = { x: x2 - size * Math.cos(angle - Math.PI / 6), y: y2 - size * Math.sin(angle - Math.PI / 6) };
2252
+ const right = { x: x2 - size * Math.cos(angle + Math.PI / 6), y: y2 - size * Math.sin(angle + Math.PI / 6) };
2253
+ ctx.save();
2254
+ ctx.strokeStyle = color;
2255
+ ctx.fillStyle = color;
2256
+ ctx.lineWidth = width;
2257
+ ctx.setLineDash([]);
2258
+ switch (theme.marker) {
2259
+ case "triangle": {
2260
+ ctx.beginPath();
2261
+ ctx.moveTo(x2, y2);
2262
+ ctx.lineTo(left.x, left.y);
2263
+ ctx.lineTo(right.x, right.y);
2264
+ ctx.closePath();
2265
+ ctx.fill();
2266
+ break;
2267
+ }
2268
+ case "open":
2269
+ case "line": {
2270
+ ctx.beginPath();
2271
+ ctx.moveTo(left.x, left.y);
2272
+ ctx.lineTo(x2, y2);
2273
+ ctx.lineTo(right.x, right.y);
2274
+ if (theme.marker === "open") ctx.stroke();
2275
+ else {
2276
+ ctx.moveTo(x2 - size * 0.5 * Math.cos(angle + Math.PI / 2), y2 - size * 0.5 * Math.sin(angle + Math.PI / 2));
2277
+ ctx.lineTo(x2 + size * 0.5 * Math.cos(angle + Math.PI / 2), y2 + size * 0.5 * Math.sin(angle + Math.PI / 2));
2278
+ ctx.stroke();
2279
+ }
2280
+ break;
2281
+ }
2282
+ case "dot": {
2283
+ ctx.beginPath();
2284
+ ctx.arc(x2, y2, Math.max(2.5, width * 1.5), 0, Math.PI * 2);
2285
+ ctx.fill();
2286
+ break;
2287
+ }
2288
+ }
2289
+ ctx.restore();
2290
+ }
2291
+ function useCanvasTheme(scopeRef) {
2292
+ const { appliedTheme } = useTheme();
2293
+ const scopeKey = JSON.stringify(useThemeScope());
2294
+ const [state, setState] = useState({ theme: null, version: 0 });
2295
+ useLayoutEffect(() => {
2296
+ const scope = scopeRef.current;
2297
+ if (!scope) return;
2298
+ const theme = resolveCanvasTheme(scope);
2299
+ setState((prev) => ({ theme, version: prev.version + 1 }));
2300
+ }, [appliedTheme, scopeKey, scopeRef]);
2301
+ return state;
2302
2302
  }
2303
2303
 
2304
2304
  // lib/drawable/themeDrawables.ts