hayao 0.4.0 → 0.4.2

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 (131) hide show
  1. package/design/00-process/README.md +49 -0
  2. package/design/00-process/composition.md +148 -0
  3. package/design/00-process/core-loop.md +146 -0
  4. package/design/00-process/intent-to-brief.md +128 -0
  5. package/design/00-process/pillars.md +139 -0
  6. package/design/00-process/refine-and-handoff.md +156 -0
  7. package/design/00-process/the-twist.md +108 -0
  8. package/design/10-anchors/README.md +99 -0
  9. package/design/10-anchors/age-of-empires.md +103 -0
  10. package/design/10-anchors/baba-is-you.md +127 -0
  11. package/design/10-anchors/balatro.md +132 -0
  12. package/design/10-anchors/celeste.md +136 -0
  13. package/design/10-anchors/civilization.md +101 -0
  14. package/design/10-anchors/dead-cells.md +125 -0
  15. package/design/10-anchors/factorio.md +100 -0
  16. package/design/10-anchors/hades.md +127 -0
  17. package/design/10-anchors/into-the-breach.md +125 -0
  18. package/design/10-anchors/it-takes-two.md +104 -0
  19. package/design/10-anchors/loop-hero.md +131 -0
  20. package/design/10-anchors/nuclear-throne.md +130 -0
  21. package/design/10-anchors/outer-wilds.md +107 -0
  22. package/design/10-anchors/overcooked.md +102 -0
  23. package/design/10-anchors/peggle.md +133 -0
  24. package/design/10-anchors/reigns.md +99 -0
  25. package/design/10-anchors/return-of-the-obra-dinn.md +108 -0
  26. package/design/10-anchors/rimworld.md +101 -0
  27. package/design/10-anchors/shadow-of-mordor.md +106 -0
  28. package/design/10-anchors/slay-the-spire.md +127 -0
  29. package/design/10-anchors/starcraft.md +98 -0
  30. package/design/10-anchors/stardew-valley.md +103 -0
  31. package/design/10-anchors/tetris.md +122 -0
  32. package/design/10-anchors/vampire-survivors.md +122 -0
  33. package/design/20-genres/README.md +62 -0
  34. package/design/20-genres/action-adventure.md +126 -0
  35. package/design/20-genres/auto-battler.md +121 -0
  36. package/design/20-genres/bullet-hell.md +123 -0
  37. package/design/20-genres/city-builder.md +124 -0
  38. package/design/20-genres/coop-chaos.md +124 -0
  39. package/design/20-genres/deckbuilder.md +122 -0
  40. package/design/20-genres/exploration.md +131 -0
  41. package/design/20-genres/farming-sim.md +122 -0
  42. package/design/20-genres/grid-puzzle.md +126 -0
  43. package/design/20-genres/horde-survival.md +128 -0
  44. package/design/20-genres/incremental.md +120 -0
  45. package/design/20-genres/match3.md +120 -0
  46. package/design/20-genres/metroidvania.md +132 -0
  47. package/design/20-genres/narrative-decisions.md +122 -0
  48. package/design/20-genres/physics-arcade.md +124 -0
  49. package/design/20-genres/precision-platformer.md +127 -0
  50. package/design/20-genres/racing.md +126 -0
  51. package/design/20-genres/rhythm.md +125 -0
  52. package/design/20-genres/roguelike.md +122 -0
  53. package/design/20-genres/rts.md +169 -0
  54. package/design/20-genres/stealth.md +125 -0
  55. package/design/20-genres/survival-horror.md +124 -0
  56. package/design/20-genres/tactics.md +123 -0
  57. package/design/20-genres/tower-defense.md +120 -0
  58. package/design/30-systems/README.md +69 -0
  59. package/design/30-systems/accessibility.md +110 -0
  60. package/design/30-systems/boss-design.md +126 -0
  61. package/design/30-systems/build-diversity.md +120 -0
  62. package/design/30-systems/collectibles.md +108 -0
  63. package/design/30-systems/combat-model.md +113 -0
  64. package/design/30-systems/coop-and-competition.md +118 -0
  65. package/design/30-systems/counter-systems.md +115 -0
  66. package/design/30-systems/crafting.md +115 -0
  67. package/design/30-systems/difficulty-and-dda.md +114 -0
  68. package/design/30-systems/economy.md +117 -0
  69. package/design/30-systems/emergent-systems.md +114 -0
  70. package/design/30-systems/encounter-design.md +107 -0
  71. package/design/30-systems/enemy-ai.md +121 -0
  72. package/design/30-systems/enemy-archetypes.md +117 -0
  73. package/design/30-systems/faction-asymmetry.md +144 -0
  74. package/design/30-systems/grace.md +124 -0
  75. package/design/30-systems/mastery-curve.md +116 -0
  76. package/design/30-systems/meta-progression.md +114 -0
  77. package/design/30-systems/onboarding.md +115 -0
  78. package/design/30-systems/procgen-design.md +118 -0
  79. package/design/30-systems/progression.md +120 -0
  80. package/design/30-systems/resource-loops.md +112 -0
  81. package/design/30-systems/reward-schedules.md +124 -0
  82. package/design/30-systems/save-and-checkpoint.md +113 -0
  83. package/design/30-systems/session-structure.md +113 -0
  84. package/design/30-systems/skill-trees.md +111 -0
  85. package/design/30-systems/status-effects.md +111 -0
  86. package/design/30-systems/tech-tree.md +112 -0
  87. package/design/30-systems/telegraphs.md +106 -0
  88. package/design/30-systems/unit-rosters.md +123 -0
  89. package/design/40-worldbuilding/README.md +49 -0
  90. package/design/40-worldbuilding/aesthetic-direction.md +155 -0
  91. package/design/40-worldbuilding/faction-identity.md +136 -0
  92. package/design/40-worldbuilding/naming-and-tone.md +130 -0
  93. package/design/40-worldbuilding/narrative-delivery.md +129 -0
  94. package/design/40-worldbuilding/theme-vectors.md +134 -0
  95. package/design/40-worldbuilding/worldbuilding-scaffold.md +132 -0
  96. package/design/50-patterns/README.md +54 -0
  97. package/design/50-patterns/anti-frustration.md +121 -0
  98. package/design/50-patterns/emergence.md +121 -0
  99. package/design/50-patterns/feedback-loops.md +121 -0
  100. package/design/50-patterns/juice-choreography.md +124 -0
  101. package/design/50-patterns/mastery-and-flow.md +121 -0
  102. package/design/50-patterns/pacing-and-tension.md +120 -0
  103. package/design/50-patterns/readability.md +121 -0
  104. package/design/50-patterns/risk-reward.md +120 -0
  105. package/design/CONTRIBUTING.md +183 -0
  106. package/design/INDEX.md +133 -0
  107. package/design/README.md +86 -0
  108. package/design/_TEMPLATE.md +69 -0
  109. package/design/index.json +2720 -0
  110. package/dist/anim/blend.d.ts +68 -0
  111. package/dist/anim/clip.d.ts +87 -0
  112. package/dist/anim/ik.d.ts +40 -0
  113. package/dist/anim/skeleton.d.ts +64 -0
  114. package/dist/hayao.global.js +12 -12
  115. package/dist/index.d.ts +11 -1
  116. package/dist/index.js +1683 -516
  117. package/dist/index.js.map +4 -4
  118. package/dist/index.min.js +12 -12
  119. package/dist/render/canvas2d-core.d.ts +4 -0
  120. package/dist/render/commands.d.ts +19 -0
  121. package/dist/render/lightRun.d.ts +35 -0
  122. package/dist/render/svgString.d.ts +8 -3
  123. package/dist/scene/clipPlayer.d.ts +58 -0
  124. package/dist/scene/ikTarget.d.ts +25 -0
  125. package/dist/scene/light.d.ts +80 -0
  126. package/dist/scene/shadow2d.d.ts +25 -0
  127. package/dist/scene/skeletonDebug.d.ts +28 -0
  128. package/dist/verify/gates.d.ts +25 -0
  129. package/docs/API.md +66 -8
  130. package/docs/CONVENTIONS.md +56 -0
  131. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -1831,6 +1831,1366 @@ var VerletChain = class extends Node {
1831
1831
  }
1832
1832
  };
1833
1833
 
1834
+ // src/anim/clip.ts
1835
+ var CLIP_CHANNELS = ["x", "y", "rotation", "scaleX", "scaleY", "opacity"];
1836
+ function clipTime(def, elapsed) {
1837
+ const d = def.duration;
1838
+ if (d <= 0) return 0;
1839
+ if (def.loop === "once") return elapsed < 0 ? 0 : elapsed > d ? d : elapsed;
1840
+ if (def.loop === "loop") {
1841
+ const m2 = elapsed % d;
1842
+ return m2 < 0 ? m2 + d : m2;
1843
+ }
1844
+ const p = 2 * d;
1845
+ let m = elapsed % p;
1846
+ if (m < 0) m += p;
1847
+ return m <= d ? m : p - m;
1848
+ }
1849
+ function clipFinished(def, elapsed) {
1850
+ return def.loop === "once" && def.duration > 0 && elapsed >= def.duration;
1851
+ }
1852
+ function sampleTrack(keys, t) {
1853
+ const n5 = keys.length;
1854
+ if (n5 === 0) return 0;
1855
+ if (n5 === 1 || t <= keys[0].t) return keys[0].v;
1856
+ if (t >= keys[n5 - 1].t) return keys[n5 - 1].v;
1857
+ let lo = 0;
1858
+ let hi = n5 - 1;
1859
+ while (lo < hi) {
1860
+ const mid = lo + hi >> 1;
1861
+ if (keys[mid].t < t) lo = mid + 1;
1862
+ else hi = mid;
1863
+ }
1864
+ const b = keys[hi];
1865
+ const a = keys[hi - 1];
1866
+ const span = b.t - a.t;
1867
+ if (span <= 0) return b.v;
1868
+ const raw = (t - a.t) / span;
1869
+ const ease = EASINGS[b.ease ?? "linear"] ?? EASINGS.linear;
1870
+ return a.v + (b.v - a.v) * ease(raw);
1871
+ }
1872
+ function sampleClip(def, elapsed) {
1873
+ const t = clipTime(def, elapsed);
1874
+ return def.tracks.map((tr) => ({ target: tr.target, channel: tr.channel, value: sampleTrack(tr.keys, t) }));
1875
+ }
1876
+ function clipEvents(def, prev, next) {
1877
+ const evs = def.events;
1878
+ if (!evs || evs.length === 0 || next <= prev) return [];
1879
+ const d = def.duration;
1880
+ const out = [];
1881
+ const inWindow = (t, lo, hi) => t > lo && t <= hi;
1882
+ const emit = (lo, hi) => {
1883
+ for (const e of evs) if (inWindow(e.t, lo, hi)) out.push(e.name);
1884
+ };
1885
+ if (def.loop === "once" || d <= 0) {
1886
+ const lo = prev < 0 ? 0 : prev > d ? d : prev;
1887
+ const hi = next < 0 ? 0 : next > d ? d : next;
1888
+ emit(lo, hi);
1889
+ return out;
1890
+ }
1891
+ const prevLocal = clipTime(def, prev);
1892
+ const nextLocal = clipTime(def, next);
1893
+ const periodLen = def.loop === "pingpong" ? 2 * d : d;
1894
+ const wraps = Math.floor(next / periodLen) - Math.floor(prev / periodLen);
1895
+ if (def.loop === "pingpong") {
1896
+ if (wraps === 0 && nextLocal >= prevLocal) emit(prevLocal, nextLocal);
1897
+ else if (wraps === 0) emit(nextLocal, prevLocal);
1898
+ else {
1899
+ emit(Math.min(prevLocal, nextLocal), d);
1900
+ emit(0, Math.max(prevLocal, nextLocal));
1901
+ }
1902
+ return out;
1903
+ }
1904
+ if (wraps === 0 && nextLocal >= prevLocal) {
1905
+ emit(prevLocal, nextLocal);
1906
+ } else {
1907
+ emit(prevLocal, d);
1908
+ emit(0, nextLocal);
1909
+ }
1910
+ return out;
1911
+ }
1912
+ function clipIssues(def, knownTargets) {
1913
+ const issues = [];
1914
+ if (!(def.duration > 0)) issues.push(`duration must be > 0 (got ${def.duration})`);
1915
+ if (def.loop !== "loop" && def.loop !== "once" && def.loop !== "pingpong") {
1916
+ issues.push(`loop must be 'loop' | 'once' | 'pingpong' (got '${String(def.loop)}')`);
1917
+ }
1918
+ if (!def.tracks || def.tracks.length === 0) issues.push("clip has no tracks");
1919
+ const known = knownTargets ? new Set(knownTargets) : null;
1920
+ (def.tracks ?? []).forEach((tr, i) => {
1921
+ const where = `track ${i} (${tr.target}/${tr.channel})`;
1922
+ if (!CLIP_CHANNELS.includes(tr.channel)) issues.push(`${where}: unknown channel '${tr.channel}'`);
1923
+ if (known && !known.has(tr.target)) issues.push(`${where}: target '${tr.target}' is not in the rig`);
1924
+ if (!tr.keys || tr.keys.length === 0) {
1925
+ issues.push(`${where}: no keyframes`);
1926
+ } else {
1927
+ for (let k = 0; k < tr.keys.length; k++) {
1928
+ const key = tr.keys[k];
1929
+ if (!Number.isFinite(key.t) || !Number.isFinite(key.v)) issues.push(`${where}: key ${k} has non-finite t/v`);
1930
+ if (key.t < 0 || key.t > def.duration) issues.push(`${where}: key ${k} time ${key.t} is outside [0, ${def.duration}]`);
1931
+ if (k > 0 && key.t < tr.keys[k - 1].t) issues.push(`${where}: keys must be ascending in t (key ${k} = ${key.t} < ${tr.keys[k - 1].t})`);
1932
+ if (key.ease !== void 0 && !(key.ease in EASINGS)) issues.push(`${where}: key ${k} unknown ease '${String(key.ease)}'`);
1933
+ }
1934
+ }
1935
+ });
1936
+ (def.events ?? []).forEach((e, i) => {
1937
+ if (!Number.isFinite(e.t) || e.t < 0 || e.t > def.duration) issues.push(`event ${i} ('${e.name}') time ${e.t} is outside [0, ${def.duration}]`);
1938
+ if (!e.name) issues.push(`event ${i} has no name`);
1939
+ });
1940
+ return [...new Set(issues)];
1941
+ }
1942
+
1943
+ // src/anim/blend.ts
1944
+ var poseKey = (target, channel) => `${target}/${channel}`;
1945
+ function samplePose(def, phase) {
1946
+ const local = clipTime(def, phase * def.duration);
1947
+ const pose = {};
1948
+ for (const tr of def.tracks) pose[poseKey(tr.target, tr.channel)] = sampleTrack(tr.keys, local);
1949
+ return pose;
1950
+ }
1951
+ function mixPose(a, b, t, out = {}) {
1952
+ for (const k in out) delete out[k];
1953
+ for (const k in a) {
1954
+ out[k] = k in b ? a[k] + (b[k] - a[k]) * t : a[k];
1955
+ }
1956
+ for (const k in b) {
1957
+ if (!(k in a)) out[k] = b[k];
1958
+ }
1959
+ return out;
1960
+ }
1961
+ var Blend1D = class {
1962
+ samples;
1963
+ constructor(samples) {
1964
+ this.samples = samples.slice().sort((p, q) => p.x - q.x);
1965
+ }
1966
+ /** The clip weights at parameter `param` (sums to 1; empty space → {}). */
1967
+ weights(param) {
1968
+ const s = this.samples;
1969
+ if (s.length === 0) return [];
1970
+ if (s.length === 1 || param <= s[0].x) return [{ clip: s[0].clip, weight: 1 }];
1971
+ if (param >= s[s.length - 1].x) return [{ clip: s[s.length - 1].clip, weight: 1 }];
1972
+ let i = 0;
1973
+ while (i < s.length - 1 && s[i + 1].x <= param) i++;
1974
+ const a = s[i];
1975
+ const b = s[i + 1];
1976
+ const span = b.x - a.x;
1977
+ const t = span <= 0 ? 0 : (param - a.x) / span;
1978
+ return [
1979
+ { clip: a.clip, weight: 1 - t },
1980
+ { clip: b.clip, weight: t }
1981
+ ];
1982
+ }
1983
+ /** Blend the neighbor clips at `param`, all sampled at normalized `phase`. */
1984
+ sample(param, phase) {
1985
+ const w = this.weights(param);
1986
+ if (w.length === 0) return {};
1987
+ let pose = samplePose(w[0].clip, phase);
1988
+ if (w.length === 2) pose = mixPose(pose, samplePose(w[1].clip, phase), w[1].weight);
1989
+ return pose;
1990
+ }
1991
+ };
1992
+ var Blend2D = class {
1993
+ samples;
1994
+ constructor(samples) {
1995
+ this.samples = samples.map((s) => ({ clip: s.clip, x: s.x, y: s.y ?? 0 }));
1996
+ }
1997
+ /** Clip weights at (px,py): nearest-3 barycentric, clamped + renormalized. */
1998
+ weights(px, py) {
1999
+ const s = this.samples;
2000
+ if (s.length === 0) return [];
2001
+ if (s.length <= 3) return this.barycentric(s, px, py);
2002
+ const idx = s.map((_, i) => i);
2003
+ idx.sort((a, b) => {
2004
+ const da = (s[a].x - px) ** 2 + (s[a].y - py) ** 2;
2005
+ const db = (s[b].x - px) ** 2 + (s[b].y - py) ** 2;
2006
+ return da - db || a - b;
2007
+ });
2008
+ return this.barycentric([s[idx[0]], s[idx[1]], s[idx[2]]], px, py);
2009
+ }
2010
+ barycentric(tri, px, py) {
2011
+ if (tri.length === 1) return [{ clip: tri[0].clip, weight: 1 }];
2012
+ if (tri.length === 2) {
2013
+ const [a2, b2] = tri;
2014
+ const dx = b2.x - a2.x;
2015
+ const dy = b2.y - a2.y;
2016
+ const len2 = dx * dx + dy * dy;
2017
+ const t = len2 <= 0 ? 0 : clamp(((px - a2.x) * dx + (py - a2.y) * dy) / len2, 0, 1);
2018
+ return [
2019
+ { clip: a2.clip, weight: 1 - t },
2020
+ { clip: b2.clip, weight: t }
2021
+ ];
2022
+ }
2023
+ const [a, b, c] = tri;
2024
+ const det = (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y);
2025
+ let wa;
2026
+ let wb;
2027
+ if (det === 0) {
2028
+ wa = wb = 1 / 3;
2029
+ } else {
2030
+ wa = ((b.y - c.y) * (px - c.x) + (c.x - b.x) * (py - c.y)) / det;
2031
+ wb = ((c.y - a.y) * (px - c.x) + (a.x - c.x) * (py - c.y)) / det;
2032
+ }
2033
+ let wc = 1 - wa - wb;
2034
+ wa = wa < 0 ? 0 : wa;
2035
+ wb = wb < 0 ? 0 : wb;
2036
+ wc = wc < 0 ? 0 : wc;
2037
+ const sum = wa + wb + wc;
2038
+ if (sum <= 0) return [{ clip: a.clip, weight: 1 }];
2039
+ return [
2040
+ { clip: a.clip, weight: wa / sum },
2041
+ { clip: b.clip, weight: wb / sum },
2042
+ { clip: c.clip, weight: wc / sum }
2043
+ ];
2044
+ }
2045
+ /** Blend the nearest-3 clips at (px,py), all sampled at normalized `phase`. */
2046
+ sample(px, py, phase) {
2047
+ const w = this.weights(px, py);
2048
+ if (w.length === 0) return {};
2049
+ const out = {};
2050
+ for (const { clip: clip2, weight } of w) {
2051
+ if (weight === 0) continue;
2052
+ const p = samplePose(clip2, phase);
2053
+ for (const k in p) out[k] = (out[k] ?? 0) + p[k] * weight;
2054
+ }
2055
+ return out;
2056
+ }
2057
+ };
2058
+ function blendIssues(samples, dims = 1) {
2059
+ const issues = [];
2060
+ if (!samples || samples.length === 0) {
2061
+ issues.push("blend space has no samples");
2062
+ return issues;
2063
+ }
2064
+ samples.forEach((s, i) => {
2065
+ if (!Number.isFinite(s.x)) issues.push(`sample ${i}: x is not finite`);
2066
+ if (dims === 2 && !Number.isFinite(s.y ?? 0)) issues.push(`sample ${i}: y is not finite`);
2067
+ if (!s.clip) issues.push(`sample ${i}: missing clip`);
2068
+ });
2069
+ const seen = /* @__PURE__ */ new Map();
2070
+ samples.forEach((s, i) => {
2071
+ const key = dims === 2 ? `${s.x},${s.y ?? 0}` : `${s.x}`;
2072
+ if (seen.has(key)) issues.push(`sample ${i} duplicates the position of sample ${seen.get(key)} (${key})`);
2073
+ else seen.set(key, i);
2074
+ });
2075
+ return [...new Set(issues)];
2076
+ }
2077
+
2078
+ // src/anim/skeleton.ts
2079
+ var Bone2D = class extends Node {
2080
+ type = "Bone2D";
2081
+ /** Reach in local px along +x. Used by IK, skinning, and the debug overlay. */
2082
+ length;
2083
+ constructor(config = {}) {
2084
+ super(config);
2085
+ this.length = config.length ?? 0;
2086
+ }
2087
+ /** Tip position in LOCAL space (root is the node origin; tip is `length` along +x). */
2088
+ get tip() {
2089
+ return { x: this.length, y: 0 };
2090
+ }
2091
+ serializeProps() {
2092
+ return { length: this.length };
2093
+ }
2094
+ applyProps(props) {
2095
+ if (typeof props.length === "number") this.length = props.length;
2096
+ }
2097
+ };
2098
+ var Skeleton = class {
2099
+ constructor(root) {
2100
+ this.root = root;
2101
+ this.index(root, "");
2102
+ }
2103
+ root;
2104
+ byPath = /* @__PURE__ */ new Map();
2105
+ index(node, path) {
2106
+ if (path !== "") this.byPath.set(path, node);
2107
+ for (const c of node.children) {
2108
+ const childPath = path === "" ? c.name : `${path}/${c.name}`;
2109
+ this.index(c, childPath);
2110
+ }
2111
+ }
2112
+ /** Resolve a track/IK target path to a node (null if the rig lacks it). */
2113
+ resolve(path) {
2114
+ return this.byPath.get(path) ?? null;
2115
+ }
2116
+ /** All target paths in the rig — feed to `clipIssues(def, skeleton.targets())`. */
2117
+ targets() {
2118
+ return [...this.byPath.keys()];
2119
+ }
2120
+ /** Only the Bone2D joints, by path (for IK / skinning). */
2121
+ bones() {
2122
+ const out = [];
2123
+ for (const node of this.byPath.values()) if (node instanceof Bone2D) out.push(node);
2124
+ return out;
2125
+ }
2126
+ };
2127
+ function buildSkeleton(root) {
2128
+ return new Skeleton(root);
2129
+ }
2130
+ function resolveTracks(def, skeleton) {
2131
+ const out = [];
2132
+ for (const tr of def.tracks) {
2133
+ const node = skeleton.resolve(tr.target);
2134
+ if (node) out.push({ node, channel: tr.channel, keys: tr.keys, target: tr.target });
2135
+ }
2136
+ return out;
2137
+ }
2138
+ function applyChannel(node, channel, value) {
2139
+ switch (channel) {
2140
+ case "x":
2141
+ node.pos.x = value;
2142
+ break;
2143
+ case "y":
2144
+ node.pos.y = value;
2145
+ break;
2146
+ case "rotation":
2147
+ node.rotation = value;
2148
+ break;
2149
+ case "scaleX":
2150
+ node.scale.x = value;
2151
+ break;
2152
+ case "scaleY":
2153
+ node.scale.y = value;
2154
+ break;
2155
+ case "opacity":
2156
+ applyOpacity(node, value);
2157
+ break;
2158
+ }
2159
+ }
2160
+ function applyOpacity(node, value) {
2161
+ const withPaint = node;
2162
+ if (withPaint.paint) withPaint.paint.opacity = value;
2163
+ }
2164
+
2165
+ // src/scene/clipPlayer.ts
2166
+ var ClipPlayer = class extends Node {
2167
+ type = "ClipPlayer";
2168
+ rig;
2169
+ skeleton = null;
2170
+ clips = /* @__PURE__ */ new Map();
2171
+ currentName = null;
2172
+ elapsed = 0;
2173
+ // Crossfade state: the clip we are fading FROM, its frozen playhead, and progress.
2174
+ prevName = null;
2175
+ prevElapsed = 0;
2176
+ fadeDur = 0;
2177
+ fadeT = 0;
2178
+ // Reusable pose buffers for the crossfade path (allocation-free).
2179
+ poseA = {};
2180
+ poseB = {};
2181
+ poseMix = {};
2182
+ constructor(config = {}) {
2183
+ super(config);
2184
+ this.cosmetic = true;
2185
+ this.rig = config.rig ?? null;
2186
+ }
2187
+ onReady() {
2188
+ if (!this.rig) this.rig = this.parent;
2189
+ this.rebind();
2190
+ }
2191
+ /** Register a clip under `name`. Rebinds its tracks against the current rig. */
2192
+ add(name, def) {
2193
+ this.clips.set(name, { def, tracks: this.rig ? resolveTracks(def, this.ensureSkeleton()) : [] });
2194
+ return this;
2195
+ }
2196
+ /** Re-resolve the rig + every clip's tracks (call after the rig subtree changes). */
2197
+ rebind(rig) {
2198
+ if (rig) this.rig = rig;
2199
+ if (!this.rig) return;
2200
+ this.skeleton = buildSkeleton(this.rig);
2201
+ for (const [, entry] of this.clips) entry.tracks = resolveTracks(entry.def, this.skeleton);
2202
+ }
2203
+ ensureSkeleton() {
2204
+ if (!this.skeleton && this.rig) this.skeleton = buildSkeleton(this.rig);
2205
+ return this.skeleton;
2206
+ }
2207
+ /**
2208
+ * Play `name`, optionally crossfading over `fade` seconds from whatever is
2209
+ * playing. Restarting the same clip with no fade rewinds it. A fade freezes the
2210
+ * outgoing clip's playhead and blends it out with EASINGS.quadInOut.
2211
+ */
2212
+ play(name, opts = {}) {
2213
+ if (!this.clips.has(name)) return;
2214
+ const fade = opts.fade ?? 0;
2215
+ if (fade > 0 && this.currentName && this.currentName !== name) {
2216
+ this.prevName = this.currentName;
2217
+ this.prevElapsed = this.elapsed;
2218
+ this.fadeDur = fade;
2219
+ this.fadeT = 0;
2220
+ } else {
2221
+ this.prevName = null;
2222
+ this.fadeDur = 0;
2223
+ this.fadeT = 0;
2224
+ }
2225
+ this.currentName = name;
2226
+ this.elapsed = 0;
2227
+ }
2228
+ /** Stop playback (freezes the rig at its current pose). */
2229
+ stop() {
2230
+ this.currentName = null;
2231
+ this.prevName = null;
2232
+ this.fadeDur = 0;
2233
+ }
2234
+ /** Current playhead in seconds (raw elapsed, pre-loop-wrap). */
2235
+ get time() {
2236
+ return this.elapsed;
2237
+ }
2238
+ /** The clip currently playing, or null. */
2239
+ get current() {
2240
+ return this.currentName;
2241
+ }
2242
+ /** Fired with the event name each time the playhead crosses a clip event. */
2243
+ get event() {
2244
+ return this.signal("event");
2245
+ }
2246
+ /** Fired when a `once` clip reaches its end. */
2247
+ get finished() {
2248
+ return this.signal("finished");
2249
+ }
2250
+ onProcess(dt) {
2251
+ const name = this.currentName;
2252
+ if (!name) return;
2253
+ const entry = this.clips.get(name);
2254
+ if (!entry) return;
2255
+ const prevElapsed = this.elapsed;
2256
+ this.elapsed += dt;
2257
+ if (this.fadeDur > 0 && this.prevName) {
2258
+ this.fadeT += dt;
2259
+ const raw = this.fadeT >= this.fadeDur ? 1 : this.fadeT / this.fadeDur;
2260
+ const w = EASINGS.quadInOut(raw);
2261
+ const prevEntry = this.clips.get(this.prevName);
2262
+ if (prevEntry) this.samplePose(prevEntry, this.prevElapsed, this.poseA);
2263
+ this.samplePose(entry, this.elapsed, this.poseB);
2264
+ mixPose(this.poseA, this.poseB, w, this.poseMix);
2265
+ this.applyPose(entry, this.poseMix);
2266
+ if (raw >= 1) {
2267
+ this.prevName = null;
2268
+ this.fadeDur = 0;
2269
+ }
2270
+ } else {
2271
+ this.applyDirect(entry, this.elapsed);
2272
+ }
2273
+ if (entry.def.events && entry.def.events.length > 0) {
2274
+ const names = clipEvents(entry.def, prevElapsed, this.elapsed);
2275
+ for (const ev of names) this.emit("event", ev);
2276
+ }
2277
+ if (clipFinished(entry.def, this.elapsed) && !clipFinished(entry.def, prevElapsed)) {
2278
+ this.emit("finished", name);
2279
+ }
2280
+ }
2281
+ /** Direct steady-state apply: sample each bound track and write its channel. */
2282
+ applyDirect(entry, elapsed) {
2283
+ const t = clipTime(entry.def, elapsed);
2284
+ const tracks = entry.tracks;
2285
+ for (let i = 0; i < tracks.length; i++) {
2286
+ const tr = tracks[i];
2287
+ applyChannel(tr.node, tr.channel, sampleTrack(tr.keys, t));
2288
+ }
2289
+ }
2290
+ /** Sample an entry's bound tracks into a reusable Pose (clears then fills). */
2291
+ samplePose(entry, elapsed, out) {
2292
+ for (const k in out) delete out[k];
2293
+ const t = clipTime(entry.def, elapsed);
2294
+ const tracks = entry.tracks;
2295
+ for (let i = 0; i < tracks.length; i++) {
2296
+ const tr = tracks[i];
2297
+ out[poseKey(tr.target, tr.channel)] = sampleTrack(tr.keys, t);
2298
+ }
2299
+ }
2300
+ /** Write a mixed pose back onto the incoming entry's bound nodes. */
2301
+ applyPose(entry, pose) {
2302
+ const tracks = entry.tracks;
2303
+ for (let i = 0; i < tracks.length; i++) {
2304
+ const tr = tracks[i];
2305
+ const v = pose[poseKey(tr.target, tr.channel)];
2306
+ if (v !== void 0) applyChannel(tr.node, tr.channel, v);
2307
+ }
2308
+ }
2309
+ };
2310
+
2311
+ // src/anim/ik.ts
2312
+ function solveTwoBoneIK(root, l0, l1, target, bendDir = 1) {
2313
+ const dx = target.x - root.x;
2314
+ const dy = target.y - root.y;
2315
+ let dist = dhypot(dx, dy);
2316
+ const maxReach = l0 + l1;
2317
+ const minReach = Math.abs(l0 - l1);
2318
+ let reachable = true;
2319
+ if (dist > maxReach) {
2320
+ dist = maxReach;
2321
+ reachable = false;
2322
+ } else if (dist < minReach) {
2323
+ dist = minReach || 1e-9;
2324
+ reachable = false;
2325
+ }
2326
+ if (dist < 1e-9) dist = 1e-9;
2327
+ const toTarget = datan2(dy, dx);
2328
+ const cosRoot = clampUnit((l0 * l0 + dist * dist - l1 * l1) / (2 * l0 * dist));
2329
+ const rootInterior = dacos(cosRoot);
2330
+ const cosMid = clampUnit((l0 * l0 + l1 * l1 - dist * dist) / (2 * l0 * l1));
2331
+ const midInterior = dacos(cosMid);
2332
+ const angle0 = toTarget - bendDir * rootInterior;
2333
+ const angle1 = angle0 + bendDir * (Math.PI - midInterior);
2334
+ return { angle0, angle1, reachable };
2335
+ }
2336
+ function clampUnit(v) {
2337
+ return v < -1 ? -1 : v > 1 ? 1 : v;
2338
+ }
2339
+ function dacos(x) {
2340
+ return datan2(Math.sqrt(1 - x * x), x);
2341
+ }
2342
+ function solveFabrik(root, lengths, target, opts = {}) {
2343
+ const n5 = lengths.length;
2344
+ const maxIter = opts.maxIter ?? 16;
2345
+ const tolerance = opts.tolerance ?? 1e-4;
2346
+ const total = lengths.reduce((a, b) => a + b, 0);
2347
+ const joints = [];
2348
+ for (let i = 0; i <= n5; i++) {
2349
+ const seed = opts.initial?.[i];
2350
+ joints.push(seed ? { x: seed.x, y: seed.y } : { x: root.x + (i === 0 ? 0 : 1e-6 * i), y: root.y });
2351
+ }
2352
+ joints[0] = { x: root.x, y: root.y };
2353
+ const rootToTarget = dhypot(target.x - root.x, target.y - root.y);
2354
+ if (rootToTarget > total) {
2355
+ const ux = (target.x - root.x) / (rootToTarget || 1);
2356
+ const uy = (target.y - root.y) / (rootToTarget || 1);
2357
+ let px = root.x;
2358
+ let py = root.y;
2359
+ joints[0] = { x: px, y: py };
2360
+ for (let i = 0; i < n5; i++) {
2361
+ px += ux * lengths[i];
2362
+ py += uy * lengths[i];
2363
+ joints[i + 1] = { x: px, y: py };
2364
+ }
2365
+ return { joints, angles: anglesOf(joints), reachable: false, iterations: 0 };
2366
+ }
2367
+ let iter = 0;
2368
+ for (; iter < maxIter; iter++) {
2369
+ const end = joints[n5];
2370
+ if (dhypot(end.x - target.x, end.y - target.y) <= tolerance) break;
2371
+ joints[n5] = { x: target.x, y: target.y };
2372
+ for (let i = n5 - 1; i >= 0; i--) {
2373
+ const a = joints[i];
2374
+ const b = joints[i + 1];
2375
+ const d = dhypot(a.x - b.x, a.y - b.y) || 1e-9;
2376
+ const r = lengths[i] / d;
2377
+ joints[i] = { x: b.x + (a.x - b.x) * r, y: b.y + (a.y - b.y) * r };
2378
+ }
2379
+ joints[0] = { x: root.x, y: root.y };
2380
+ for (let i = 0; i < n5; i++) {
2381
+ const a = joints[i];
2382
+ const b = joints[i + 1];
2383
+ const d = dhypot(a.x - b.x, a.y - b.y) || 1e-9;
2384
+ const r = lengths[i] / d;
2385
+ joints[i + 1] = { x: a.x + (b.x - a.x) * r, y: a.y + (b.y - a.y) * r };
2386
+ }
2387
+ }
2388
+ return { joints, angles: anglesOf(joints), reachable: true, iterations: iter };
2389
+ }
2390
+ function anglesOf(joints) {
2391
+ const out = [];
2392
+ for (let i = 0; i < joints.length - 1; i++) {
2393
+ out.push(datan2(joints[i + 1].y - joints[i].y, joints[i + 1].x - joints[i].x));
2394
+ }
2395
+ return out;
2396
+ }
2397
+
2398
+ // src/scene/ikTarget.ts
2399
+ var IkTarget = class extends Node {
2400
+ type = "IkTarget";
2401
+ bones;
2402
+ bendDir;
2403
+ maxIter;
2404
+ /** True if the chain reached the target on the last solve (else it was clamped). */
2405
+ reached = true;
2406
+ constructor(config = {}) {
2407
+ super(config);
2408
+ this.cosmetic = true;
2409
+ this.bones = config.bones ?? [];
2410
+ this.bendDir = config.bendDir ?? 1;
2411
+ this.maxIter = config.maxIter ?? 16;
2412
+ }
2413
+ /** Repoint the solver at a new chain (root-first). */
2414
+ setBones(bones) {
2415
+ this.bones = bones;
2416
+ }
2417
+ onProcess(_dt) {
2418
+ const bones = this.bones;
2419
+ if (bones.length < 1) return;
2420
+ const rootBone = bones[0];
2421
+ const rootParent = rootBone.parent;
2422
+ if (!rootParent) return;
2423
+ const wt = this.worldTransform();
2424
+ const goalWorld = { x: wt.e, y: wt.f };
2425
+ const parentWorld = rootParent.worldTransform();
2426
+ const invParent = invertTransform(parentWorld);
2427
+ const goal = applyInv(invParent, goalWorld);
2428
+ const rootLocal = { x: rootBone.pos.x, y: rootBone.pos.y };
2429
+ if (bones.length === 2) {
2430
+ const res2 = solveTwoBoneIK(rootLocal, bones[0].length, bones[1].length, goal, this.bendDir);
2431
+ this.reached = res2.reachable;
2432
+ bones[0].rotation = res2.angle0;
2433
+ bones[1].rotation = res2.angle1 - res2.angle0;
2434
+ return;
2435
+ }
2436
+ const lengths = bones.map((b) => b.length);
2437
+ const initial = this.currentJoints(rootLocal);
2438
+ const res = solveFabrik(rootLocal, lengths, goal, { maxIter: this.maxIter, initial });
2439
+ this.reached = res.reachable;
2440
+ let parentAngle = 0;
2441
+ for (let i = 0; i < bones.length; i++) {
2442
+ bones[i].rotation = res.angles[i] - parentAngle;
2443
+ parentAngle = res.angles[i];
2444
+ }
2445
+ }
2446
+ /** Current joint positions (parent space) for FABRIK seeding — stable elbow side. */
2447
+ currentJoints(rootLocal) {
2448
+ const joints = [{ x: rootLocal.x, y: rootLocal.y }];
2449
+ let angle = 0;
2450
+ let px = rootLocal.x;
2451
+ let py = rootLocal.y;
2452
+ for (const b of this.bones) {
2453
+ angle += b.rotation;
2454
+ px += dcos(angle) * b.length;
2455
+ py += dsin(angle) * b.length;
2456
+ joints.push({ x: px, y: py });
2457
+ }
2458
+ return joints;
2459
+ }
2460
+ };
2461
+ function applyInv(inv, p) {
2462
+ return { x: inv.a * p.x + inv.c * p.y + inv.e, y: inv.b * p.x + inv.d * p.y + inv.f };
2463
+ }
2464
+
2465
+ // src/scene/skeletonDebug.ts
2466
+ var SkeletonDebug = class extends Node {
2467
+ type = "SkeletonDebug";
2468
+ rig;
2469
+ boneColor;
2470
+ pivotColor;
2471
+ pivotRadius;
2472
+ strokeWidth;
2473
+ constructor(config = {}) {
2474
+ super(config);
2475
+ this.cosmetic = true;
2476
+ this.rig = config.rig ?? null;
2477
+ this.boneColor = config.boneColor ?? "#e0483b";
2478
+ this.pivotColor = config.pivotColor ?? "#f4d35e";
2479
+ this.pivotRadius = config.pivotRadius ?? 3;
2480
+ this.strokeWidth = config.strokeWidth ?? 2;
2481
+ }
2482
+ onReady() {
2483
+ if (!this.rig) this.rig = this.parent;
2484
+ }
2485
+ // The overlay draws in WORLD space directly (each command carries the bone's own
2486
+ // world transform), so `world` — the debug node's own frame — is only the base
2487
+ // the rig's world transforms are already absolute against. We walk the rig and
2488
+ // emit per-bone commands with each bone's worldTransform().
2489
+ draw(out, _world) {
2490
+ const rig = this.rig;
2491
+ if (!rig) return;
2492
+ this.emitFor(rig, out);
2493
+ }
2494
+ emitFor(node, out) {
2495
+ if (node instanceof Bone2D) {
2496
+ const wt = node.worldTransform();
2497
+ out.push({
2498
+ kind: "poly",
2499
+ points: [0, 0, node.length, 0],
2500
+ closed: false,
2501
+ stroke: this.boneColor,
2502
+ strokeWidth: this.strokeWidth,
2503
+ round: true,
2504
+ transform: wt,
2505
+ z: this.z,
2506
+ transient: true
2507
+ });
2508
+ out.push({
2509
+ kind: "circle",
2510
+ cx: 0,
2511
+ cy: 0,
2512
+ radius: this.pivotRadius,
2513
+ fill: this.pivotColor,
2514
+ transform: wt,
2515
+ z: this.z,
2516
+ transient: true
2517
+ });
2518
+ if (node.pivot) {
2519
+ const at2 = composeTransform(wt, makeTransform({ x: node.pivot.x, y: node.pivot.y }, 0, { x: 1, y: 1 }));
2520
+ out.push({
2521
+ kind: "circle",
2522
+ cx: 0,
2523
+ cy: 0,
2524
+ radius: this.pivotRadius * 0.6,
2525
+ stroke: this.pivotColor,
2526
+ strokeWidth: 1,
2527
+ transform: at2,
2528
+ z: this.z,
2529
+ transient: true
2530
+ });
2531
+ }
2532
+ }
2533
+ for (const c of node.children) this.emitFor(c, out);
2534
+ }
2535
+ };
2536
+
2537
+ // src/art/palette.ts
2538
+ var KENTO = {
2539
+ // neutrals: light ground → dark ink, one continuous ramp
2540
+ gofun: "#f7f1e2",
2541
+ // 胡粉 shell-white, lightest paper
2542
+ washi: "#efe7d3",
2543
+ // 和紙 default light ground
2544
+ kinu: "#e4d8bd",
2545
+ // 絹 warm raised panel / card
2546
+ line: "#d8cbac",
2547
+ // ─ hairline / grid on light
2548
+ kinako: "#b9a882",
2549
+ // 黄粉 tan mid-neutral / muted ink on dark
2550
+ stone: "#6c6252",
2551
+ // 石 secondary text on light (AA at body size)
2552
+ sumiSoft: "#494133",
2553
+ // 墨薄 body ink
2554
+ sumi: "#23201a",
2555
+ // 墨 primary ink / default dark-ish ground
2556
+ kuro: "#181820",
2557
+ // 玄 cool near-black ground for dark games
2558
+ yohaku: "#12121a",
2559
+ // 余白 deepest dark ground
2560
+ darkLine: "#2c2c36",
2561
+ // hairline / grid on dark
2562
+ // eight hues: `Deep` for light-mode fills, bright for dark-mode / emphasis
2563
+ shuDeep: "#b23a24",
2564
+ shu: "#d9583c",
2565
+ // 朱 vermilion
2566
+ kakiDeep: "#bf6a1c",
2567
+ kaki: "#e79a49",
2568
+ // 柿 persimmon / orange
2569
+ koDeep: "#94741d",
2570
+ ko: "#e3c054",
2571
+ // 黄土 ochre-gold
2572
+ matsuDeep: "#4a7a3a",
2573
+ matsu: "#8bad52",
2574
+ // 松葉 pine green
2575
+ asagiDeep: "#2c7a90",
2576
+ asagi: "#57bad2",
2577
+ // 浅葱 teal-cyan
2578
+ aiDeep: "#2b4257",
2579
+ ai: "#5a86ad",
2580
+ // 藍 indigo
2581
+ fujiDeep: "#63548c",
2582
+ fuji: "#a091cf",
2583
+ // 藤 wisteria violet
2584
+ sakuDeep: "#b0506e",
2585
+ saku: "#e097ac"
2586
+ // 桜 dusty rose
2587
+ };
2588
+ var MEADOW = {
2589
+ name: "meadow",
2590
+ bg: KENTO.washi,
2591
+ ink: KENTO.sumi,
2592
+ inkSoft: KENTO.sumiSoft,
2593
+ line: KENTO.line,
2594
+ accent: KENTO.shuDeep,
2595
+ accent2: KENTO.aiDeep,
2596
+ good: KENTO.matsuDeep,
2597
+ warn: KENTO.kakiDeep,
2598
+ // deep tones so categorical fills hold contrast on the light ground
2599
+ ramp: [KENTO.shuDeep, KENTO.kakiDeep, KENTO.koDeep, KENTO.matsuDeep, KENTO.asagiDeep, KENTO.aiDeep, KENTO.fujiDeep, KENTO.sakuDeep],
2600
+ swatches: KENTO
2601
+ };
2602
+ var DUSK = {
2603
+ name: "dusk",
2604
+ bg: KENTO.kuro,
2605
+ ink: KENTO.gofun,
2606
+ inkSoft: KENTO.kinako,
2607
+ line: KENTO.darkLine,
2608
+ accent: KENTO.shu,
2609
+ accent2: KENTO.asagi,
2610
+ good: KENTO.matsu,
2611
+ warn: KENTO.ko,
2612
+ // bright tones so categorical fills pop on the dark ground
2613
+ ramp: [KENTO.shu, KENTO.kaki, KENTO.ko, KENTO.matsu, KENTO.asagi, KENTO.ai, KENTO.fuji, KENTO.saku],
2614
+ swatches: KENTO
2615
+ };
2616
+ var PAPER = {
2617
+ name: "paper",
2618
+ bg: KENTO.gofun,
2619
+ ink: KENTO.sumi,
2620
+ inkSoft: KENTO.stone,
2621
+ line: KENTO.kinu,
2622
+ accent: KENTO.sakuDeep,
2623
+ accent2: KENTO.asagiDeep,
2624
+ good: KENTO.matsuDeep,
2625
+ warn: KENTO.kakiDeep,
2626
+ ramp: [KENTO.sakuDeep, KENTO.kakiDeep, KENTO.koDeep, KENTO.matsuDeep, KENTO.asagiDeep, KENTO.aiDeep, KENTO.fujiDeep, KENTO.shuDeep],
2627
+ swatches: KENTO
2628
+ };
2629
+ var PALETTES = { meadow: MEADOW, dusk: DUSK, paper: PAPER };
2630
+ function mix(a, b, t) {
2631
+ const pa = hexToRgb(a);
2632
+ const pb = hexToRgb(b);
2633
+ const r = Math.round(pa[0] + (pb[0] - pa[0]) * t);
2634
+ const g = Math.round(pa[1] + (pb[1] - pa[1]) * t);
2635
+ const bl = Math.round(pa[2] + (pb[2] - pa[2]) * t);
2636
+ return rgbToHex(r, g, bl);
2637
+ }
2638
+ function withAlpha(hex, alpha) {
2639
+ const [r, g, b] = hexToRgb(hex);
2640
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
2641
+ }
2642
+ function hexToRgb(hex) {
2643
+ let h = hex.replace("#", "");
2644
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
2645
+ const n5 = parseInt(h, 16);
2646
+ return [n5 >> 16 & 255, n5 >> 8 & 255, n5 & 255];
2647
+ }
2648
+ function rgbToHex(r, g, b) {
2649
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")).join("");
2650
+ }
2651
+ var mod360 = (h) => (h % 360 + 360) % 360;
2652
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
2653
+ function hsl(h, s, l) {
2654
+ h = mod360(h) / 360;
2655
+ s = clamp01(s);
2656
+ l = clamp01(l);
2657
+ if (s === 0) {
2658
+ const v = l * 255;
2659
+ return rgbToHex(v, v, v);
2660
+ }
2661
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
2662
+ const p = 2 * l - q;
2663
+ const hue = (t) => {
2664
+ t = t < 0 ? t + 1 : t > 1 ? t - 1 : t;
2665
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
2666
+ if (t < 1 / 2) return q;
2667
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
2668
+ return p;
2669
+ };
2670
+ return rgbToHex(hue(h + 1 / 3) * 255, hue(h) * 255, hue(h - 1 / 3) * 255);
2671
+ }
2672
+ function hsv(h, s, v) {
2673
+ h = mod360(h) / 60;
2674
+ s = clamp01(s);
2675
+ v = clamp01(v);
2676
+ const c = v * s;
2677
+ const x = c * (1 - Math.abs(h % 2 - 1));
2678
+ const m = v - c;
2679
+ let r = 0;
2680
+ let g = 0;
2681
+ let b = 0;
2682
+ if (h < 1) [r, g, b] = [c, x, 0];
2683
+ else if (h < 2) [r, g, b] = [x, c, 0];
2684
+ else if (h < 3) [r, g, b] = [0, c, x];
2685
+ else if (h < 4) [r, g, b] = [0, x, c];
2686
+ else if (h < 5) [r, g, b] = [x, 0, c];
2687
+ else [r, g, b] = [c, 0, x];
2688
+ return rgbToHex((r + m) * 255, (g + m) * 255, (b + m) * 255);
2689
+ }
2690
+ function hexToHsl(hex) {
2691
+ const [r255, g255, b255] = hexToRgb(hex);
2692
+ const r = r255 / 255;
2693
+ const g = g255 / 255;
2694
+ const b = b255 / 255;
2695
+ const max = Math.max(r, g, b);
2696
+ const min = Math.min(r, g, b);
2697
+ const l = (max + min) / 2;
2698
+ const d = max - min;
2699
+ if (d === 0) return { h: 0, s: 0, l };
2700
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
2701
+ let h;
2702
+ if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
2703
+ else if (max === g) h = (b - r) / d + 2;
2704
+ else h = (r - g) / d + 4;
2705
+ return { h: h * 60, s, l };
2706
+ }
2707
+ function mutateColor(rng, hex, amounts = {}) {
2708
+ const c = hexToHsl(hex);
2709
+ const dh = amounts.hue ?? 0;
2710
+ const ds = amounts.sat ?? 0;
2711
+ const dl = amounts.light ?? 0;
2712
+ return hsl(
2713
+ c.h + rng.range(-dh, dh),
2714
+ c.s + rng.range(-ds, ds),
2715
+ c.l + rng.range(-dl, dl)
2716
+ );
2717
+ }
2718
+ var srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : dpow((c + 0.055) / 1.055, 2.4);
2719
+ var linearToSrgb = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * dpow(c, 1 / 2.4) - 0.055;
2720
+ function mixLinear(a, b, t) {
2721
+ const pa = hexToRgb(a);
2722
+ const pb = hexToRgb(b);
2723
+ const chan = (i) => {
2724
+ const la = srgbToLinear(pa[i] / 255);
2725
+ const lb = srgbToLinear(pb[i] / 255);
2726
+ return linearToSrgb(la + (lb - la) * t) * 255;
2727
+ };
2728
+ return rgbToHex(chan(0), chan(1), chan(2));
2729
+ }
2730
+ function sampleGradient(stops, t) {
2731
+ if (stops.length === 0) return "#000000";
2732
+ if (stops.length === 1) return stops[0];
2733
+ const clamped = clamp01(t);
2734
+ const scaled = clamped * (stops.length - 1);
2735
+ const i = Math.min(stops.length - 2, Math.floor(scaled));
2736
+ return mixLinear(stops[i], stops[i + 1], scaled - i);
2737
+ }
2738
+ function gradient(stops, n5) {
2739
+ if (n5 <= 0) return [];
2740
+ if (n5 === 1) return [sampleGradient(stops, 0)];
2741
+ return Array.from({ length: n5 }, (_, i) => sampleGradient(stops, i / (n5 - 1)));
2742
+ }
2743
+
2744
+ // src/render/commands.ts
2745
+ var LAYER_WORLD = 0;
2746
+ var LAYER_LIGHT = 0.5;
2747
+ var LAYER_HUD = 1;
2748
+ function sortCommands(cmds) {
2749
+ return cmds.map((c, i) => [c, i]).sort((a, b) => (a[0].layer ?? 0) - (b[0].layer ?? 0) || a[0].z - b[0].z || a[1] - b[1]).map(([c]) => c);
2750
+ }
2751
+
2752
+ // src/art/autotile.ts
2753
+ function gridFromRows(rows, solid = "#") {
2754
+ return rows.map((r) => [...r].map((c) => solid.includes(c)));
2755
+ }
2756
+ function at(grid, x, y) {
2757
+ return y >= 0 && y < grid.length && x >= 0 && x < grid[y].length && grid[y][x];
2758
+ }
2759
+ var Edge = { N: 1, E: 2, S: 4, W: 8 };
2760
+ function mask4(grid, x, y) {
2761
+ return (at(grid, x, y - 1) ? Edge.N : 0) | (at(grid, x + 1, y) ? Edge.E : 0) | (at(grid, x, y + 1) ? Edge.S : 0) | (at(grid, x - 1, y) ? Edge.W : 0);
2762
+ }
2763
+ function mask8(grid, x, y) {
2764
+ let m = 0;
2765
+ const dirs = [
2766
+ [0, -1],
2767
+ [1, -1],
2768
+ [1, 0],
2769
+ [1, 1],
2770
+ [0, 1],
2771
+ [-1, 1],
2772
+ [-1, 0],
2773
+ [-1, -1]
2774
+ ];
2775
+ for (let i = 0; i < 8; i++) if (at(grid, x + dirs[i][0], y + dirs[i][1])) m |= 1 << i;
2776
+ return m;
2777
+ }
2778
+ var WangFrame = { Isolated: 0, Cap: 1, Straight: 2, Bend: 3, Tee: 4, Cross: 5 };
2779
+ function rotateCW(m) {
2780
+ let r = 0;
2781
+ if (m & Edge.N) r |= Edge.E;
2782
+ if (m & Edge.E) r |= Edge.S;
2783
+ if (m & Edge.S) r |= Edge.W;
2784
+ if (m & Edge.W) r |= Edge.N;
2785
+ return r;
2786
+ }
2787
+ var WANG4 = (() => {
2788
+ const bases = [
2789
+ [WangFrame.Isolated, 0],
2790
+ [WangFrame.Cap, Edge.N],
2791
+ [WangFrame.Straight, Edge.N | Edge.S],
2792
+ [WangFrame.Bend, Edge.N | Edge.E],
2793
+ [WangFrame.Tee, Edge.N | Edge.E | Edge.S],
2794
+ [WangFrame.Cross, 15]
2795
+ ];
2796
+ const table = new Array(16);
2797
+ for (const [frame, mask0] of bases) {
2798
+ let m = mask0;
2799
+ for (let rotation = 0; rotation < 4; rotation++) {
2800
+ if (table[m] === void 0) table[m] = { mask: m, frame, rotation };
2801
+ m = rotateCW(m);
2802
+ }
2803
+ }
2804
+ return table;
2805
+ })();
2806
+ function wangTile(mask) {
2807
+ return WANG4[mask & 15];
2808
+ }
2809
+ function autotile4(grid) {
2810
+ return grid.map((row, y) => row.map((solid, x) => solid ? wangTile(mask4(grid, x, y)) : null));
2811
+ }
2812
+ function marchingSquaresCases(grid) {
2813
+ const h = grid.length;
2814
+ const w = h ? grid[0].length : 0;
2815
+ const out = [];
2816
+ for (let y = 0; y < h - 1; y++) {
2817
+ const row = [];
2818
+ for (let x = 0; x < w - 1; x++) {
2819
+ row.push(
2820
+ (at(grid, x, y) ? 1 : 0) | (at(grid, x + 1, y) ? 2 : 0) | (at(grid, x + 1, y + 1) ? 4 : 0) | (at(grid, x, y + 1) ? 8 : 0)
2821
+ );
2822
+ }
2823
+ out.push(row);
2824
+ }
2825
+ return out;
2826
+ }
2827
+ var MS_SEGMENTS = [
2828
+ [],
2829
+ // 0
2830
+ [["L", "T"]],
2831
+ // 1 TL
2832
+ [["T", "R"]],
2833
+ // 2 TR
2834
+ [["L", "R"]],
2835
+ // 3 TL,TR
2836
+ [["R", "B"]],
2837
+ // 4 BR
2838
+ [["L", "T"], ["R", "B"]],
2839
+ // 5 saddle
2840
+ [["T", "B"]],
2841
+ // 6 TR,BR
2842
+ [["L", "B"]],
2843
+ // 7 TL,TR,BR
2844
+ [["B", "L"]],
2845
+ // 8 BL
2846
+ [["T", "B"]],
2847
+ // 9 TL,BL
2848
+ [["T", "R"], ["B", "L"]],
2849
+ // 10 saddle
2850
+ [["R", "B"]],
2851
+ // 11 TL,TR,BL
2852
+ [["L", "R"]],
2853
+ // 12 BR,BL
2854
+ [["T", "R"]],
2855
+ // 13 TL,BR,BL
2856
+ [["L", "T"]],
2857
+ // 14 TR,BR,BL
2858
+ []
2859
+ // 15
2860
+ ];
2861
+ function marchingSquaresContours(grid, options = {}) {
2862
+ const cell = options.cell ?? 1;
2863
+ const ox = options.x ?? 0;
2864
+ const oy = options.y ?? 0;
2865
+ const cases = marchingSquaresCases(grid);
2866
+ const segs = [];
2867
+ const mid = (edge, cx, cy) => {
2868
+ switch (edge) {
2869
+ case "T":
2870
+ return { x: cx + cell / 2, y: cy };
2871
+ case "R":
2872
+ return { x: cx + cell, y: cy + cell / 2 };
2873
+ case "B":
2874
+ return { x: cx + cell / 2, y: cy + cell };
2875
+ case "L":
2876
+ return { x: cx, y: cy + cell / 2 };
2877
+ }
2878
+ };
2879
+ for (let y = 0; y < cases.length; y++) {
2880
+ for (let x = 0; x < cases[y].length; x++) {
2881
+ const cx = ox + x * cell;
2882
+ const cy = oy + y * cell;
2883
+ for (const [e1, e2] of MS_SEGMENTS[cases[y][x]]) segs.push({ a: mid(e1, cx, cy), b: mid(e2, cx, cy) });
2884
+ }
2885
+ }
2886
+ return segs;
2887
+ }
2888
+ function autotileToCommands(grid, options = {}) {
2889
+ const tile = options.tile ?? 8;
2890
+ const ox = options.x ?? 0;
2891
+ const oy = options.y ?? 0;
2892
+ const z = options.z ?? 0;
2893
+ const transform = options.transform ?? IDENTITY;
2894
+ const fill = options.fill ?? "#888";
2895
+ const edge = options.edge;
2896
+ const edgeWidth = options.edgeWidth ?? Math.max(1, tile / 8);
2897
+ const out = [];
2898
+ for (let y = 0; y < grid.length; y++) {
2899
+ for (let x = 0; x < grid[y].length; x++) {
2900
+ if (!grid[y][x]) continue;
2901
+ const px = ox + x * tile;
2902
+ const py = oy + y * tile;
2903
+ out.push({ kind: "rect", x: px, y: py, w: tile, h: tile, transform, z, fill });
2904
+ if (!edge) continue;
2905
+ const m = mask4(grid, x, y);
2906
+ const line = (points) => out.push({ kind: "poly", points, closed: false, transform, z: z + 1, stroke: edge, strokeWidth: edgeWidth });
2907
+ if (!(m & Edge.N)) line([px, py, px + tile, py]);
2908
+ if (!(m & Edge.E)) line([px + tile, py, px + tile, py + tile]);
2909
+ if (!(m & Edge.S)) line([px, py + tile, px + tile, py + tile]);
2910
+ if (!(m & Edge.W)) line([px, py, px, py + tile]);
2911
+ }
2912
+ }
2913
+ return out;
2914
+ }
2915
+ function contourToCommands(grid, options = {}) {
2916
+ const tile = options.tile ?? 8;
2917
+ const transform = options.transform ?? IDENTITY;
2918
+ const z = options.z ?? 0;
2919
+ const stroke = options.edge ?? options.fill ?? "#333";
2920
+ const strokeWidth = options.edgeWidth ?? Math.max(1, tile / 8);
2921
+ const segs = marchingSquaresContours(grid, { cell: tile, x: options.x ?? 0, y: options.y ?? 0 });
2922
+ return segs.map((s) => ({
2923
+ kind: "poly",
2924
+ points: [s.a.x, s.a.y, s.b.x, s.b.y],
2925
+ closed: false,
2926
+ transform,
2927
+ z,
2928
+ stroke,
2929
+ strokeWidth,
2930
+ round: true
2931
+ }));
2932
+ }
2933
+
2934
+ // src/physics/tilemap.ts
2935
+ var TILE = {
2936
+ EMPTY: 0,
2937
+ SOLID: 1,
2938
+ /** Passable from below/sides; solid only when landing on its top edge. */
2939
+ ONEWAY: 2,
2940
+ HAZARD: 3
2941
+ };
2942
+ var DEFAULT_TILE_CHARS = {
2943
+ "#": TILE.SOLID,
2944
+ "-": TILE.ONEWAY,
2945
+ "^": TILE.HAZARD
2946
+ };
2947
+ function tilemapFromAscii(rowsAscii, tileSize = 32, chars = DEFAULT_TILE_CHARS) {
2948
+ const rows = rowsAscii.length;
2949
+ const cols = Math.max(...rowsAscii.map((r) => r.length));
2950
+ const tiles = new Array(cols * rows).fill(TILE.EMPTY);
2951
+ rowsAscii.forEach((row, ty) => {
2952
+ for (let tx = 0; tx < row.length; tx++) tiles[ty * cols + tx] = chars[row[tx]] ?? TILE.EMPTY;
2953
+ });
2954
+ return { cols, rows, tileSize, tiles };
2955
+ }
2956
+ function asciiEntities(rowsAscii, tileSize = 32, chars = DEFAULT_TILE_CHARS) {
2957
+ const out = [];
2958
+ rowsAscii.forEach((row, ty) => {
2959
+ for (let tx = 0; tx < row.length; tx++) {
2960
+ const c = row[tx];
2961
+ if (c === " " || c === "." || chars[c] !== void 0) continue;
2962
+ out.push({ char: c, tx, ty, x: (tx + 0.5) * tileSize, y: (ty + 0.5) * tileSize });
2963
+ }
2964
+ });
2965
+ return out;
2966
+ }
2967
+ function tileAt(map, tx, ty) {
2968
+ if (tx < 0 || ty < 0 || tx >= map.cols || ty >= map.rows) return TILE.SOLID;
2969
+ return map.tiles[ty * map.cols + tx];
2970
+ }
2971
+ function tileAtPoint(map, x, y) {
2972
+ return tileAt(map, Math.floor(x / map.tileSize), Math.floor(y / map.tileSize));
2973
+ }
2974
+ var mapWidth = (map) => map.cols * map.tileSize;
2975
+ var mapHeight = (map) => map.rows * map.tileSize;
2976
+
2977
+ // src/scene/shadow2d.ts
2978
+ function occludersFromTilemap(map) {
2979
+ const grid = [];
2980
+ for (let ty = 0; ty < map.rows; ty++) {
2981
+ const row = [];
2982
+ for (let tx = 0; tx < map.cols; tx++) row.push(tileAt(map, tx, ty) === TILE.SOLID);
2983
+ grid.push(row);
2984
+ }
2985
+ return marchingSquaresContours(grid, { cell: map.tileSize });
2986
+ }
2987
+ function cullSegments(light, radius, segs) {
2988
+ const out = [];
2989
+ const r2 = radius * radius;
2990
+ for (const s of segs) {
2991
+ const dax = s.a.x - light.x;
2992
+ const day = s.a.y - light.y;
2993
+ const dbx = s.b.x - light.x;
2994
+ const dby = s.b.y - light.y;
2995
+ const da2 = dax * dax + day * day;
2996
+ const db2 = dbx * dbx + dby * dby;
2997
+ if (da2 <= r2 || db2 <= r2 || closestDist2(light, s) <= r2) out.push(s);
2998
+ }
2999
+ return out;
3000
+ }
3001
+ function closestDist2(p, s) {
3002
+ const ex = s.b.x - s.a.x;
3003
+ const ey = s.b.y - s.a.y;
3004
+ const len2 = ex * ex + ey * ey;
3005
+ if (len2 === 0) {
3006
+ const dx2 = p.x - s.a.x;
3007
+ const dy2 = p.y - s.a.y;
3008
+ return dx2 * dx2 + dy2 * dy2;
3009
+ }
3010
+ let t = ((p.x - s.a.x) * ex + (p.y - s.a.y) * ey) / len2;
3011
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
3012
+ const cx = s.a.x + ex * t;
3013
+ const cy = s.a.y + ey * t;
3014
+ const dx = p.x - cx;
3015
+ const dy = p.y - cy;
3016
+ return dx * dx + dy * dy;
3017
+ }
3018
+ function shadowQuads(light, radius, segs) {
3019
+ const E = 2 * radius;
3020
+ const quads = [];
3021
+ for (const s of segs) {
3022
+ const ax = s.a.x;
3023
+ const ay = s.a.y;
3024
+ const bx = s.b.x;
3025
+ const by = s.b.y;
3026
+ const adx = ax - light.x;
3027
+ const ady = ay - light.y;
3028
+ const bdx = bx - light.x;
3029
+ const bdy = by - light.y;
3030
+ const al = dhypot(adx, ady);
3031
+ const bl = dhypot(bdx, bdy);
3032
+ if (al === 0 || bl === 0) continue;
3033
+ const aex = ax + adx / al * E;
3034
+ const aey = ay + ady / al * E;
3035
+ const bex = bx + bdx / bl * E;
3036
+ const bey = by + bdy / bl * E;
3037
+ quads.push([ax, ay, bx, by, bex, bey, aex, aey]);
3038
+ }
3039
+ return quads;
3040
+ }
3041
+
3042
+ // src/scene/light.ts
3043
+ var PointLight = class extends Node {
3044
+ type = "PointLight";
3045
+ radius;
3046
+ color;
3047
+ intensity;
3048
+ falloff;
3049
+ flicker;
3050
+ /** Current intensity after flicker — read by the LightLayer during draw. */
3051
+ litIntensity;
3052
+ rng;
3053
+ phase = 0;
3054
+ constructor(config = {}) {
3055
+ super(config);
3056
+ this.cosmetic = true;
3057
+ this.radius = config.radius ?? 160;
3058
+ this.color = config.color ?? "#ffe6b0";
3059
+ this.intensity = config.intensity ?? 1;
3060
+ this.falloff = config.falloff ?? 1;
3061
+ this.flicker = config.flicker ?? { amount: 0, speed: 0 };
3062
+ this.litIntensity = this.intensity;
3063
+ this.rng = new Rng(config.seed ?? 43);
3064
+ }
3065
+ onProcess(dt) {
3066
+ if (this.flicker.amount <= 0) {
3067
+ this.litIntensity = this.intensity;
3068
+ return;
3069
+ }
3070
+ this.phase += dt * this.flicker.speed;
3071
+ const wobble = dsin(this.phase) * 0.5 + (this.rng.float() - 0.5);
3072
+ const factor = 1 + wobble * this.flicker.amount;
3073
+ this.litIntensity = Math.max(0, this.intensity * factor);
3074
+ }
3075
+ serializeProps() {
3076
+ return {};
3077
+ }
3078
+ };
3079
+ var LightLayer = class extends Node {
3080
+ type = "LightLayer";
3081
+ ambient;
3082
+ softShadows;
3083
+ width;
3084
+ height;
3085
+ occluders = [];
3086
+ constructor(config = {}) {
3087
+ super(config);
3088
+ this.cosmetic = true;
3089
+ this.ambient = {
3090
+ color: config.ambient?.color ?? DUSK.bg,
3091
+ level: config.ambient?.level ?? 0
3092
+ };
3093
+ this.softShadows = config.softShadows ?? false;
3094
+ this.width = config.width ?? 1280;
3095
+ this.height = config.height ?? 720;
3096
+ }
3097
+ /** Set the occluder edges (design-space segments) this layer casts shadows from. */
3098
+ setOccluders(segs) {
3099
+ this.occluders = segs;
3100
+ }
3101
+ draw(out, world) {
3102
+ const ambientFill = this.ambient.level >= 1 ? "#ffffff" : mixLinear(this.ambient.color, "#ffffff", clamp012(this.ambient.level));
3103
+ const ambient = {
3104
+ kind: "rect",
3105
+ x: 0,
3106
+ y: 0,
3107
+ w: this.width,
3108
+ h: this.height,
3109
+ fill: ambientFill,
3110
+ blend: "multiply",
3111
+ transform: IDENTITY,
3112
+ z: 0,
3113
+ layer: LAYER_LIGHT
3114
+ };
3115
+ out.push(ambient);
3116
+ for (const child of this.children) {
3117
+ if (!(child instanceof PointLight) || !child.visible) continue;
3118
+ const lt = child.localTransform();
3119
+ const lx = lt.e;
3120
+ const ly = lt.f;
3121
+ const radius = child.radius;
3122
+ const intensity = Math.max(0, Math.min(1, child.litIntensity));
3123
+ const tint = child.color;
3124
+ const core = mixLinear("#000000", tint, intensity);
3125
+ const mid = mixLinear("#000000", tint, intensity * 0.5);
3126
+ const edge = "#000000";
3127
+ const midStop = clamp012(0.6 / Math.max(0.01, child.falloff));
3128
+ const pool = {
3129
+ kind: "circle",
3130
+ cx: lx,
3131
+ cy: ly,
3132
+ radius,
3133
+ gradient: {
3134
+ type: "radial",
3135
+ cx: 0.5,
3136
+ cy: 0.5,
3137
+ r: 0.5,
3138
+ stops: [
3139
+ { offset: 0, color: core },
3140
+ { offset: midStop, color: mid },
3141
+ { offset: 1, color: edge }
3142
+ ]
3143
+ },
3144
+ blend: "screen",
3145
+ transform: world,
3146
+ z: 0,
3147
+ layer: LAYER_LIGHT
3148
+ };
3149
+ out.push(pool);
3150
+ if (this.occluders.length) {
3151
+ const culled = cullSegments({ x: lx, y: ly }, radius, this.occluders);
3152
+ const quads = shadowQuads({ x: lx, y: ly }, radius, culled);
3153
+ for (const q of quads) {
3154
+ out.push(shadowPoly(q, ambientFill, world, 1));
3155
+ if (this.softShadows) out.push(shadowPoly(expandQuad(q), ambientFill, world, 0.5));
3156
+ }
3157
+ }
3158
+ }
3159
+ }
3160
+ };
3161
+ var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
3162
+ function shadowPoly(points, fill, world, opacity) {
3163
+ const c = {
3164
+ kind: "poly",
3165
+ points,
3166
+ closed: true,
3167
+ fill,
3168
+ blend: "multiply",
3169
+ transform: world,
3170
+ z: 0,
3171
+ layer: LAYER_LIGHT
3172
+ };
3173
+ if (opacity !== 1) c.opacity = opacity;
3174
+ return c;
3175
+ }
3176
+ function expandQuad(q) {
3177
+ const midNearX = (q[0] + q[2]) / 2;
3178
+ const midNearY = (q[1] + q[3]) / 2;
3179
+ const spread = (fx, fy) => {
3180
+ const dx = fx - midNearX;
3181
+ const dy = fy - midNearY;
3182
+ const l = dhypot(dx, dy);
3183
+ if (l === 0) return [fx, fy];
3184
+ const px = -dy / l;
3185
+ const py = dx / l;
3186
+ const s = l * 0.08;
3187
+ return [fx + px * s, fy + py * s];
3188
+ };
3189
+ const [aex, aey] = spread(q[6], q[7]);
3190
+ const [bex, bey] = spread(q[4], q[5]);
3191
+ return [q[0], q[1], q[2], q[3], bex, bey, aex, aey];
3192
+ }
3193
+
1834
3194
  // src/scene/registry.ts
1835
3195
  var registry = /* @__PURE__ */ new Map();
1836
3196
  function registerNode(type, factory) {
@@ -1842,6 +3202,10 @@ registerNode("Text", () => new Text({ text: "" }));
1842
3202
  registerNode("Camera2D", () => new Camera2D());
1843
3203
  registerNode("Timer", () => new Timer({ duration: 1 }));
1844
3204
  registerNode("AnimationPlayer", () => new AnimationPlayer());
3205
+ registerNode("Bone2D", () => new Bone2D());
3206
+ registerNode("ClipPlayer", () => new ClipPlayer());
3207
+ registerNode("PointLight", () => new PointLight());
3208
+ registerNode("LightLayer", () => new LightLayer());
1845
3209
  function deserializeNode(data) {
1846
3210
  const factory = registry.get(data.type);
1847
3211
  if (!factory) throw new Error(`hayao: unknown node type "${data.type}" (register it first)`);
@@ -2306,49 +3670,6 @@ var DEFAULT_GAMEPAD_MAP = {
2306
3670
  15: "right"
2307
3671
  };
2308
3672
 
2309
- // src/physics/tilemap.ts
2310
- var TILE = {
2311
- EMPTY: 0,
2312
- SOLID: 1,
2313
- /** Passable from below/sides; solid only when landing on its top edge. */
2314
- ONEWAY: 2,
2315
- HAZARD: 3
2316
- };
2317
- var DEFAULT_TILE_CHARS = {
2318
- "#": TILE.SOLID,
2319
- "-": TILE.ONEWAY,
2320
- "^": TILE.HAZARD
2321
- };
2322
- function tilemapFromAscii(rowsAscii, tileSize = 32, chars = DEFAULT_TILE_CHARS) {
2323
- const rows = rowsAscii.length;
2324
- const cols = Math.max(...rowsAscii.map((r) => r.length));
2325
- const tiles = new Array(cols * rows).fill(TILE.EMPTY);
2326
- rowsAscii.forEach((row, ty) => {
2327
- for (let tx = 0; tx < row.length; tx++) tiles[ty * cols + tx] = chars[row[tx]] ?? TILE.EMPTY;
2328
- });
2329
- return { cols, rows, tileSize, tiles };
2330
- }
2331
- function asciiEntities(rowsAscii, tileSize = 32, chars = DEFAULT_TILE_CHARS) {
2332
- const out = [];
2333
- rowsAscii.forEach((row, ty) => {
2334
- for (let tx = 0; tx < row.length; tx++) {
2335
- const c = row[tx];
2336
- if (c === " " || c === "." || chars[c] !== void 0) continue;
2337
- out.push({ char: c, tx, ty, x: (tx + 0.5) * tileSize, y: (ty + 0.5) * tileSize });
2338
- }
2339
- });
2340
- return out;
2341
- }
2342
- function tileAt(map, tx, ty) {
2343
- if (tx < 0 || ty < 0 || tx >= map.cols || ty >= map.rows) return TILE.SOLID;
2344
- return map.tiles[ty * map.cols + tx];
2345
- }
2346
- function tileAtPoint(map, x, y) {
2347
- return tileAt(map, Math.floor(x / map.tileSize), Math.floor(y / map.tileSize));
2348
- }
2349
- var mapWidth = (map) => map.cols * map.tileSize;
2350
- var mapHeight = (map) => map.rows * map.tileSize;
2351
-
2352
3673
  // src/physics/aabb.ts
2353
3674
  var EPS = 1e-6;
2354
3675
  var spanTiles = (lo, hi, ts) => [Math.floor(lo / ts), Math.floor((hi - EPS) / ts)];
@@ -3631,11 +4952,6 @@ function rayCastRigid(rw, x0, y0, x1, y1, mask = 65535) {
3631
4952
  return best;
3632
4953
  }
3633
4954
 
3634
- // src/render/commands.ts
3635
- function sortCommands(cmds) {
3636
- return cmds.map((c, i) => [c, i]).sort((a, b) => (a[0].layer ?? 0) - (b[0].layer ?? 0) || a[0].z - b[0].z || a[1] - b[1]).map(([c]) => c);
3637
- }
3638
-
3639
4955
  // src/render/paint.ts
3640
4956
  var n = (v) => Number.isInteger(v) ? String(v) : (Math.round(v * 1e3) / 1e3).toString();
3641
4957
  function toStops(stops) {
@@ -3745,6 +5061,41 @@ function canvasGradient(ctx, g, bbox) {
3745
5061
  return grad;
3746
5062
  }
3747
5063
 
5064
+ // src/render/lightRun.ts
5065
+ function splitByLightLayer(sorted) {
5066
+ const below = [];
5067
+ const light = [];
5068
+ const above = [];
5069
+ for (const c of sorted) {
5070
+ const layer = c.layer ?? 0;
5071
+ if (layer === LAYER_LIGHT) light.push(c);
5072
+ else if (layer < LAYER_LIGHT) below.push(c);
5073
+ else above.push(c);
5074
+ }
5075
+ return { below, light, above };
5076
+ }
5077
+ function parseLightRun(run) {
5078
+ if (run.length === 0) return null;
5079
+ const first = run[0];
5080
+ if (first.kind !== "rect" || first.blend !== "multiply") return null;
5081
+ const ambient = first;
5082
+ const lights = [];
5083
+ let i = 1;
5084
+ while (i < run.length) {
5085
+ const c = run[i];
5086
+ if (c.kind !== "circle" || c.blend !== "screen") return null;
5087
+ const light = { circle: c, shadows: [] };
5088
+ i++;
5089
+ while (i < run.length && run[i].kind === "poly" && run[i].blend === "multiply") {
5090
+ light.shadows.push(run[i]);
5091
+ i++;
5092
+ }
5093
+ lights.push(light);
5094
+ }
5095
+ return { ambient, lights };
5096
+ }
5097
+ var isLightCommand = (c) => (c.layer ?? 0) === LAYER_LIGHT;
5098
+
3748
5099
  // src/render/renderer.ts
3749
5100
  function clientToDesign(rect, width, height, clientX, clientY) {
3750
5101
  const scale = Math.min(rect.width / width, rect.height / height) || 1;
@@ -3782,6 +5133,7 @@ function paintAttrs(p, fillOverride, filterId) {
3782
5133
  }
3783
5134
  if (p.opacity !== void 0 && p.opacity !== 1) parts.push(`opacity="${n2(p.opacity)}"`);
3784
5135
  if (filterId) parts.push(`filter="url(#${filterId})"`);
5136
+ if (p.blend) parts.push(`style="mix-blend-mode:${p.blend}"`);
3785
5137
  return parts.join(" ");
3786
5138
  }
3787
5139
  function escapeText(s) {
@@ -3852,20 +5204,62 @@ function commandToSVG(c, defs, idBase) {
3852
5204
  }
3853
5205
  }
3854
5206
  }
3855
- function commandsToSVGInner(commands, idPrefix = "h") {
5207
+ function polyPoints(p) {
5208
+ const pts = [];
5209
+ for (let i = 0; i < p.points.length; i += 2) pts.push(`${n2(p.points[i])},${n2(p.points[i + 1])}`);
5210
+ return pts.join(" ");
5211
+ }
5212
+ function lightRunToSVG(parsed, defs, idPrefix, viewW, viewH) {
5213
+ const a = parsed.ambient;
5214
+ const ambientRect = `<rect x="${n2(a.x)}" y="${n2(a.y)}" width="${n2(a.w)}" height="${n2(a.h)}" transform="${matrix(a.transform)}" fill="${a.fill ?? "#000"}"/>`;
5215
+ const groups = [];
5216
+ parsed.lights.forEach((light, li) => {
5217
+ const c = light.circle;
5218
+ const gid = `${idPrefix}lg${li}`;
5219
+ if (c.gradient) defs.push(gradientDef(c.gradient, gid));
5220
+ const fill = c.gradient ? `url(#${gid})` : c.fill ?? "#fff";
5221
+ const circle = `<circle cx="${n2(c.cx)}" cy="${n2(c.cy)}" r="${n2(c.radius)}" transform="${matrix(c.transform)}" fill="${fill}"/>`;
5222
+ const mid = `${idPrefix}lm${li}`;
5223
+ const shadowPolys = light.shadows.map((s) => {
5224
+ const grey = s.opacity !== void 0 && s.opacity !== 1 ? Math.round((1 - s.opacity) * 255) : 0;
5225
+ const shade = `#${grey.toString(16).padStart(2, "0").repeat(3)}`;
5226
+ return `<polygon points="${polyPoints(s)}" transform="${matrix(s.transform)}" fill="${shade}"/>`;
5227
+ }).join("");
5228
+ if (light.shadows.length) {
5229
+ defs.push(
5230
+ `<mask id="${mid}"><rect x="0" y="0" width="${n2(viewW)}" height="${n2(viewH)}" fill="#ffffff"/>${shadowPolys}</mask>`
5231
+ );
5232
+ groups.push(`<g style="mix-blend-mode:screen" mask="url(#${mid})">${circle}</g>`);
5233
+ } else {
5234
+ groups.push(`<g style="mix-blend-mode:screen">${circle}</g>`);
5235
+ }
5236
+ });
5237
+ return `<g style="mix-blend-mode:multiply; isolation:isolate">${ambientRect}${groups.join("")}</g>`;
5238
+ }
5239
+ function commandsToSVGInner(commands, idPrefix = "h", viewW = 1280, viewH = 720) {
3856
5240
  const defs = [];
3857
- const body = sortCommands(commands).map((c, i) => {
5241
+ const sorted = sortCommands(commands);
5242
+ const { below, light, above } = splitByLightLayer(sorted);
5243
+ const parsed = light.length ? parseLightRun(light) : null;
5244
+ const flatList = parsed ? [...below, ...above] : sorted;
5245
+ const render = (list, offset) => list.map((c, i) => {
3858
5246
  const bad = invalidCommandReason(c);
3859
5247
  if (bad) {
3860
5248
  warnCommandOnce(c.kind, bad, c);
3861
5249
  return "";
3862
5250
  }
3863
- return commandToSVG(c, defs, `${idPrefix}${i}`);
5251
+ return commandToSVG(c, defs, `${idPrefix}${offset + i}`);
3864
5252
  }).join("");
5253
+ let body;
5254
+ if (parsed) {
5255
+ body = render(below, 0) + lightRunToSVG(parsed, defs, idPrefix, viewW, viewH) + render(above, below.length);
5256
+ } else {
5257
+ body = render(flatList, 0);
5258
+ }
3865
5259
  return (defs.length ? `<defs>${defs.join("")}</defs>` : "") + body;
3866
5260
  }
3867
5261
  function renderToSVGString(commands, width, height, background = "#ffffff") {
3868
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"><rect x="0" y="0" width="${width}" height="${height}" fill="${background}"/>` + commandsToSVGInner(commands) + `</svg>`;
5262
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"><rect x="0" y="0" width="${width}" height="${height}" fill="${background}"/>` + commandsToSVGInner(commands, "h", width, height) + `</svg>`;
3869
5263
  }
3870
5264
 
3871
5265
  // src/render/svg.ts
@@ -4028,36 +5422,123 @@ function paintCommand(ctx, c) {
4028
5422
  break;
4029
5423
  }
4030
5424
  }
5425
+ function paintOne(ctx, c) {
5426
+ const bad = invalidCommandReason(c);
5427
+ if (bad) {
5428
+ warnCommandOnce(c.kind, bad, c);
5429
+ return;
5430
+ }
5431
+ ctx.save();
5432
+ try {
5433
+ const t = c.transform;
5434
+ ctx.transform(t.a, t.b, t.c, t.d, t.e, t.f);
5435
+ ctx.globalAlpha = c.opacity ?? 1;
5436
+ const sh = c.shadow;
5437
+ if (sh) {
5438
+ ctx.shadowColor = sh.color;
5439
+ ctx.shadowBlur = sh.blur;
5440
+ ctx.shadowOffsetX = sh.dx ?? 0;
5441
+ ctx.shadowOffsetY = sh.dy ?? 0;
5442
+ }
5443
+ const dash = c.lineDash;
5444
+ if (dash && dash.length) ctx.setLineDash(dash);
5445
+ if (c.blend) ctx.globalCompositeOperation = c.blend;
5446
+ paintCommand(ctx, c);
5447
+ } catch (err) {
5448
+ warnCommandOnce(c.kind, "paint threw", err);
5449
+ }
5450
+ ctx.restore();
5451
+ }
5452
+ var lightBufferCache = /* @__PURE__ */ new WeakMap();
5453
+ function makeCanvas(ctx, w, h) {
5454
+ if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(w, h);
5455
+ const doc = ctx.canvas.ownerDocument;
5456
+ if (doc && typeof doc.createElement === "function") {
5457
+ const el = doc.createElement("canvas");
5458
+ el.width = w;
5459
+ el.height = h;
5460
+ return el;
5461
+ }
5462
+ return null;
5463
+ }
5464
+ function lightBuffers(ctx, w, h) {
5465
+ const cached = lightBufferCache.get(ctx);
5466
+ if (cached && cached.w === w && cached.h === h) return cached;
5467
+ const buffer = makeCanvas(ctx, w, h);
5468
+ const scratch = makeCanvas(ctx, w, h);
5469
+ if (!buffer || !scratch) return null;
5470
+ buffer.width = w;
5471
+ buffer.height = h;
5472
+ scratch.width = w;
5473
+ scratch.height = h;
5474
+ const bufs = { buffer, scratch, w, h };
5475
+ lightBufferCache.set(ctx, bufs);
5476
+ return bufs;
5477
+ }
5478
+ function compositeLightRun(ctx, parsed, width, height, scale) {
5479
+ const w = Math.max(1, Math.round(width * scale));
5480
+ const h = Math.max(1, Math.round(height * scale));
5481
+ const bufs = lightBuffers(ctx, w, h);
5482
+ if (!bufs) return false;
5483
+ const bctx = bufs.buffer.getContext("2d");
5484
+ const sctx = bufs.scratch.getContext("2d");
5485
+ if (!bctx || !sctx) return false;
5486
+ bctx.setTransform(1, 0, 0, 1, 0, 0);
5487
+ bctx.globalCompositeOperation = "source-over";
5488
+ bctx.clearRect(0, 0, w, h);
5489
+ bctx.setTransform(scale, 0, 0, scale, 0, 0);
5490
+ paintOne(bctx, { ...parsed.ambient, blend: void 0 });
5491
+ for (const light of parsed.lights) {
5492
+ sctx.setTransform(1, 0, 0, 1, 0, 0);
5493
+ sctx.globalCompositeOperation = "source-over";
5494
+ sctx.clearRect(0, 0, w, h);
5495
+ sctx.setTransform(scale, 0, 0, scale, 0, 0);
5496
+ paintOne(sctx, { ...light.circle, blend: void 0 });
5497
+ for (const q of light.shadows) {
5498
+ sctx.save();
5499
+ const t = q.transform;
5500
+ sctx.setTransform(scale, 0, 0, scale, 0, 0);
5501
+ sctx.transform(t.a, t.b, t.c, t.d, t.e, t.f);
5502
+ sctx.globalCompositeOperation = "destination-out";
5503
+ sctx.globalAlpha = q.opacity ?? 1;
5504
+ paintCommand(sctx, { ...q, blend: void 0, fill: "#000000" });
5505
+ sctx.restore();
5506
+ }
5507
+ bctx.setTransform(1, 0, 0, 1, 0, 0);
5508
+ bctx.globalCompositeOperation = "lighter";
5509
+ bctx.globalAlpha = 1;
5510
+ bctx.drawImage(bufs.scratch, 0, 0);
5511
+ }
5512
+ ctx.save();
5513
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
5514
+ ctx.globalCompositeOperation = "multiply";
5515
+ ctx.globalAlpha = 1;
5516
+ ctx.drawImage(bufs.buffer, 0, 0);
5517
+ ctx.restore();
5518
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
5519
+ ctx.globalCompositeOperation = "source-over";
5520
+ return true;
5521
+ }
4031
5522
  function drawToCanvas2D(ctx, commands, width, height, background, scale) {
4032
5523
  ctx.setTransform(scale, 0, 0, scale, 0, 0);
5524
+ ctx.globalCompositeOperation = "source-over";
4033
5525
  ctx.fillStyle = background;
4034
5526
  ctx.fillRect(0, 0, width, height);
4035
- for (const c of sortCommands(commands)) {
4036
- const bad = invalidCommandReason(c);
4037
- if (bad) {
4038
- warnCommandOnce(c.kind, bad, c);
4039
- continue;
5527
+ const sorted = sortCommands(commands);
5528
+ const { below, light, above } = splitByLightLayer(sorted);
5529
+ for (const c of below) paintOne(ctx, c);
5530
+ if (light.length) {
5531
+ const parsed = parseLightRun(light);
5532
+ const composited = parsed ? compositeLightRun(ctx, parsed, width, height, scale) : false;
5533
+ if (!composited) {
5534
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
5535
+ for (const c of light) paintOne(ctx, c);
5536
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
5537
+ ctx.globalCompositeOperation = "source-over";
4040
5538
  }
4041
- ctx.save();
4042
- try {
4043
- const t = c.transform;
4044
- ctx.transform(t.a, t.b, t.c, t.d, t.e, t.f);
4045
- ctx.globalAlpha = c.opacity ?? 1;
4046
- const sh = c.shadow;
4047
- if (sh) {
4048
- ctx.shadowColor = sh.color;
4049
- ctx.shadowBlur = sh.blur;
4050
- ctx.shadowOffsetX = sh.dx ?? 0;
4051
- ctx.shadowOffsetY = sh.dy ?? 0;
4052
- }
4053
- const dash = c.lineDash;
4054
- if (dash && dash.length) ctx.setLineDash(dash);
4055
- paintCommand(ctx, c);
4056
- } catch (err) {
4057
- warnCommandOnce(c.kind, "paint threw", err);
4058
- }
4059
- ctx.restore();
4060
5539
  }
5540
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
5541
+ for (const c of above) paintOne(ctx, c);
4061
5542
  }
4062
5543
 
4063
5544
  // src/render/canvas.ts
@@ -4640,265 +6121,58 @@ var HeadlessRenderer = class {
4640
6121
  toSVGString() {
4641
6122
  return renderToSVGString(this.last, this.width, this.height, this.background);
4642
6123
  }
4643
- };
4644
-
4645
- // src/render/nineSlice.ts
4646
- function nineSlice(rect, style, z = 0, transform = IDENTITY) {
4647
- const b = Math.max(0, Math.min(style.border, rect.w / 2, rect.h / 2));
4648
- const fill = style.fill ?? "#fbf6ea";
4649
- const edge = style.edge ?? fill;
4650
- const corner = style.corner ?? edge;
4651
- const out = [];
4652
- const x0 = rect.x;
4653
- const x1 = rect.x + b;
4654
- const x2 = rect.x + rect.w - b;
4655
- const y0 = rect.y;
4656
- const y1 = rect.y + b;
4657
- const y2 = rect.y + rect.h - b;
4658
- const iw = rect.w - 2 * b;
4659
- const ih = rect.h - 2 * b;
4660
- const push = (x, y, w, h, f, r2) => {
4661
- if (w <= 0 || h <= 0) return;
4662
- out.push({ kind: "rect", x, y, w, h, r: r2, fill: f, transform, z });
4663
- };
4664
- push(x1, y1, iw, ih, fill);
4665
- push(x1, y0, iw, b, style.highlight ?? edge);
4666
- push(x1, y2, iw, b, style.shadow ?? edge);
4667
- push(x0, y1, b, ih, style.highlight ?? edge);
4668
- push(x2, y1, b, ih, style.shadow ?? edge);
4669
- const r = style.radius;
4670
- push(x0, y0, b, b, corner, r);
4671
- push(x2, y0, b, b, corner, r);
4672
- push(x0, y2, b, b, corner, r);
4673
- push(x2, y2, b, b, corner, r);
4674
- if (style.stroke) {
4675
- out.push({
4676
- kind: "rect",
4677
- x: rect.x,
4678
- y: rect.y,
4679
- w: rect.w,
4680
- h: rect.h,
4681
- r: style.radius,
4682
- fill: "none",
4683
- stroke: style.stroke,
4684
- strokeWidth: style.strokeWidth ?? 2,
4685
- transform,
4686
- z: z + 1e-3
4687
- });
4688
- }
4689
- return out;
4690
- }
4691
- var PANEL_PRESETS = {
4692
- parchment: () => ({ border: 10, fill: "#fbf6ea", edge: "#efe4c8", corner: "#e2d3a8", highlight: "#fdfaf0", shadow: "#d9c79c", stroke: "#b8a06a", strokeWidth: 2, radius: 4 }),
4693
- slate: () => ({ border: 8, fill: "#2c3040", edge: "#363c4f", corner: "#454c63", highlight: "#4a5268", shadow: "#20242f", stroke: "#151821", strokeWidth: 2, radius: 3 })
4694
- };
4695
-
4696
- // src/art/palette.ts
4697
- var KENTO = {
4698
- // neutrals: light ground → dark ink, one continuous ramp
4699
- gofun: "#f7f1e2",
4700
- // 胡粉 shell-white, lightest paper
4701
- washi: "#efe7d3",
4702
- // 和紙 default light ground
4703
- kinu: "#e4d8bd",
4704
- // 絹 warm raised panel / card
4705
- line: "#d8cbac",
4706
- // ─ hairline / grid on light
4707
- kinako: "#b9a882",
4708
- // 黄粉 tan mid-neutral / muted ink on dark
4709
- stone: "#6c6252",
4710
- // 石 secondary text on light (AA at body size)
4711
- sumiSoft: "#494133",
4712
- // 墨薄 body ink
4713
- sumi: "#23201a",
4714
- // 墨 primary ink / default dark-ish ground
4715
- kuro: "#181820",
4716
- // 玄 cool near-black ground for dark games
4717
- yohaku: "#12121a",
4718
- // 余白 deepest dark ground
4719
- darkLine: "#2c2c36",
4720
- // hairline / grid on dark
4721
- // eight hues: `Deep` for light-mode fills, bright for dark-mode / emphasis
4722
- shuDeep: "#b23a24",
4723
- shu: "#d9583c",
4724
- // 朱 vermilion
4725
- kakiDeep: "#bf6a1c",
4726
- kaki: "#e79a49",
4727
- // 柿 persimmon / orange
4728
- koDeep: "#94741d",
4729
- ko: "#e3c054",
4730
- // 黄土 ochre-gold
4731
- matsuDeep: "#4a7a3a",
4732
- matsu: "#8bad52",
4733
- // 松葉 pine green
4734
- asagiDeep: "#2c7a90",
4735
- asagi: "#57bad2",
4736
- // 浅葱 teal-cyan
4737
- aiDeep: "#2b4257",
4738
- ai: "#5a86ad",
4739
- // 藍 indigo
4740
- fujiDeep: "#63548c",
4741
- fuji: "#a091cf",
4742
- // 藤 wisteria violet
4743
- sakuDeep: "#b0506e",
4744
- saku: "#e097ac"
4745
- // 桜 dusty rose
4746
- };
4747
- var MEADOW = {
4748
- name: "meadow",
4749
- bg: KENTO.washi,
4750
- ink: KENTO.sumi,
4751
- inkSoft: KENTO.sumiSoft,
4752
- line: KENTO.line,
4753
- accent: KENTO.shuDeep,
4754
- accent2: KENTO.aiDeep,
4755
- good: KENTO.matsuDeep,
4756
- warn: KENTO.kakiDeep,
4757
- // deep tones so categorical fills hold contrast on the light ground
4758
- ramp: [KENTO.shuDeep, KENTO.kakiDeep, KENTO.koDeep, KENTO.matsuDeep, KENTO.asagiDeep, KENTO.aiDeep, KENTO.fujiDeep, KENTO.sakuDeep],
4759
- swatches: KENTO
4760
- };
4761
- var DUSK = {
4762
- name: "dusk",
4763
- bg: KENTO.kuro,
4764
- ink: KENTO.gofun,
4765
- inkSoft: KENTO.kinako,
4766
- line: KENTO.darkLine,
4767
- accent: KENTO.shu,
4768
- accent2: KENTO.asagi,
4769
- good: KENTO.matsu,
4770
- warn: KENTO.ko,
4771
- // bright tones so categorical fills pop on the dark ground
4772
- ramp: [KENTO.shu, KENTO.kaki, KENTO.ko, KENTO.matsu, KENTO.asagi, KENTO.ai, KENTO.fuji, KENTO.saku],
4773
- swatches: KENTO
4774
- };
4775
- var PAPER = {
4776
- name: "paper",
4777
- bg: KENTO.gofun,
4778
- ink: KENTO.sumi,
4779
- inkSoft: KENTO.stone,
4780
- line: KENTO.kinu,
4781
- accent: KENTO.sakuDeep,
4782
- accent2: KENTO.asagiDeep,
4783
- good: KENTO.matsuDeep,
4784
- warn: KENTO.kakiDeep,
4785
- ramp: [KENTO.sakuDeep, KENTO.kakiDeep, KENTO.koDeep, KENTO.matsuDeep, KENTO.asagiDeep, KENTO.aiDeep, KENTO.fujiDeep, KENTO.shuDeep],
4786
- swatches: KENTO
4787
- };
4788
- var PALETTES = { meadow: MEADOW, dusk: DUSK, paper: PAPER };
4789
- function mix(a, b, t) {
4790
- const pa = hexToRgb(a);
4791
- const pb = hexToRgb(b);
4792
- const r = Math.round(pa[0] + (pb[0] - pa[0]) * t);
4793
- const g = Math.round(pa[1] + (pb[1] - pa[1]) * t);
4794
- const bl = Math.round(pa[2] + (pb[2] - pa[2]) * t);
4795
- return rgbToHex(r, g, bl);
4796
- }
4797
- function withAlpha(hex, alpha) {
4798
- const [r, g, b] = hexToRgb(hex);
4799
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
4800
- }
4801
- function hexToRgb(hex) {
4802
- let h = hex.replace("#", "");
4803
- if (h.length === 3) h = h.split("").map((c) => c + c).join("");
4804
- const n5 = parseInt(h, 16);
4805
- return [n5 >> 16 & 255, n5 >> 8 & 255, n5 & 255];
4806
- }
4807
- function rgbToHex(r, g, b) {
4808
- return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")).join("");
4809
- }
4810
- var mod360 = (h) => (h % 360 + 360) % 360;
4811
- var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
4812
- function hsl(h, s, l) {
4813
- h = mod360(h) / 360;
4814
- s = clamp01(s);
4815
- l = clamp01(l);
4816
- if (s === 0) {
4817
- const v = l * 255;
4818
- return rgbToHex(v, v, v);
4819
- }
4820
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
4821
- const p = 2 * l - q;
4822
- const hue = (t) => {
4823
- t = t < 0 ? t + 1 : t > 1 ? t - 1 : t;
4824
- if (t < 1 / 6) return p + (q - p) * 6 * t;
4825
- if (t < 1 / 2) return q;
4826
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
4827
- return p;
4828
- };
4829
- return rgbToHex(hue(h + 1 / 3) * 255, hue(h) * 255, hue(h - 1 / 3) * 255);
4830
- }
4831
- function hsv(h, s, v) {
4832
- h = mod360(h) / 60;
4833
- s = clamp01(s);
4834
- v = clamp01(v);
4835
- const c = v * s;
4836
- const x = c * (1 - Math.abs(h % 2 - 1));
4837
- const m = v - c;
4838
- let r = 0;
4839
- let g = 0;
4840
- let b = 0;
4841
- if (h < 1) [r, g, b] = [c, x, 0];
4842
- else if (h < 2) [r, g, b] = [x, c, 0];
4843
- else if (h < 3) [r, g, b] = [0, c, x];
4844
- else if (h < 4) [r, g, b] = [0, x, c];
4845
- else if (h < 5) [r, g, b] = [x, 0, c];
4846
- else [r, g, b] = [c, 0, x];
4847
- return rgbToHex((r + m) * 255, (g + m) * 255, (b + m) * 255);
4848
- }
4849
- function hexToHsl(hex) {
4850
- const [r255, g255, b255] = hexToRgb(hex);
4851
- const r = r255 / 255;
4852
- const g = g255 / 255;
4853
- const b = b255 / 255;
4854
- const max = Math.max(r, g, b);
4855
- const min = Math.min(r, g, b);
4856
- const l = (max + min) / 2;
4857
- const d = max - min;
4858
- if (d === 0) return { h: 0, s: 0, l };
4859
- const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
4860
- let h;
4861
- if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
4862
- else if (max === g) h = (b - r) / d + 2;
4863
- else h = (r - g) / d + 4;
4864
- return { h: h * 60, s, l };
4865
- }
4866
- function mutateColor(rng, hex, amounts = {}) {
4867
- const c = hexToHsl(hex);
4868
- const dh = amounts.hue ?? 0;
4869
- const ds = amounts.sat ?? 0;
4870
- const dl = amounts.light ?? 0;
4871
- return hsl(
4872
- c.h + rng.range(-dh, dh),
4873
- c.s + rng.range(-ds, ds),
4874
- c.l + rng.range(-dl, dl)
4875
- );
4876
- }
4877
- var srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : dpow((c + 0.055) / 1.055, 2.4);
4878
- var linearToSrgb = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * dpow(c, 1 / 2.4) - 0.055;
4879
- function mixLinear(a, b, t) {
4880
- const pa = hexToRgb(a);
4881
- const pb = hexToRgb(b);
4882
- const chan = (i) => {
4883
- const la = srgbToLinear(pa[i] / 255);
4884
- const lb = srgbToLinear(pb[i] / 255);
4885
- return linearToSrgb(la + (lb - la) * t) * 255;
6124
+ };
6125
+
6126
+ // src/render/nineSlice.ts
6127
+ function nineSlice(rect, style, z = 0, transform = IDENTITY) {
6128
+ const b = Math.max(0, Math.min(style.border, rect.w / 2, rect.h / 2));
6129
+ const fill = style.fill ?? "#fbf6ea";
6130
+ const edge = style.edge ?? fill;
6131
+ const corner = style.corner ?? edge;
6132
+ const out = [];
6133
+ const x0 = rect.x;
6134
+ const x1 = rect.x + b;
6135
+ const x2 = rect.x + rect.w - b;
6136
+ const y0 = rect.y;
6137
+ const y1 = rect.y + b;
6138
+ const y2 = rect.y + rect.h - b;
6139
+ const iw = rect.w - 2 * b;
6140
+ const ih = rect.h - 2 * b;
6141
+ const push = (x, y, w, h, f, r2) => {
6142
+ if (w <= 0 || h <= 0) return;
6143
+ out.push({ kind: "rect", x, y, w, h, r: r2, fill: f, transform, z });
4886
6144
  };
4887
- return rgbToHex(chan(0), chan(1), chan(2));
4888
- }
4889
- function sampleGradient(stops, t) {
4890
- if (stops.length === 0) return "#000000";
4891
- if (stops.length === 1) return stops[0];
4892
- const clamped = clamp01(t);
4893
- const scaled = clamped * (stops.length - 1);
4894
- const i = Math.min(stops.length - 2, Math.floor(scaled));
4895
- return mixLinear(stops[i], stops[i + 1], scaled - i);
4896
- }
4897
- function gradient(stops, n5) {
4898
- if (n5 <= 0) return [];
4899
- if (n5 === 1) return [sampleGradient(stops, 0)];
4900
- return Array.from({ length: n5 }, (_, i) => sampleGradient(stops, i / (n5 - 1)));
6145
+ push(x1, y1, iw, ih, fill);
6146
+ push(x1, y0, iw, b, style.highlight ?? edge);
6147
+ push(x1, y2, iw, b, style.shadow ?? edge);
6148
+ push(x0, y1, b, ih, style.highlight ?? edge);
6149
+ push(x2, y1, b, ih, style.shadow ?? edge);
6150
+ const r = style.radius;
6151
+ push(x0, y0, b, b, corner, r);
6152
+ push(x2, y0, b, b, corner, r);
6153
+ push(x0, y2, b, b, corner, r);
6154
+ push(x2, y2, b, b, corner, r);
6155
+ if (style.stroke) {
6156
+ out.push({
6157
+ kind: "rect",
6158
+ x: rect.x,
6159
+ y: rect.y,
6160
+ w: rect.w,
6161
+ h: rect.h,
6162
+ r: style.radius,
6163
+ fill: "none",
6164
+ stroke: style.stroke,
6165
+ strokeWidth: style.strokeWidth ?? 2,
6166
+ transform,
6167
+ z: z + 1e-3
6168
+ });
6169
+ }
6170
+ return out;
4901
6171
  }
6172
+ var PANEL_PRESETS = {
6173
+ parchment: () => ({ border: 10, fill: "#fbf6ea", edge: "#efe4c8", corner: "#e2d3a8", highlight: "#fdfaf0", shadow: "#d9c79c", stroke: "#b8a06a", strokeWidth: 2, radius: 4 }),
6174
+ slate: () => ({ border: 8, fill: "#2c3040", edge: "#363c4f", corner: "#454c63", highlight: "#4a5268", shadow: "#20242f", stroke: "#151821", strokeWidth: 2, radius: 3 })
6175
+ };
4902
6176
 
4903
6177
  // src/art/shapes.ts
4904
6178
  function regularPolygon(sides, radius, rotation = 0) {
@@ -5381,188 +6655,6 @@ var BitmapText = class extends Node {
5381
6655
  }
5382
6656
  };
5383
6657
 
5384
- // src/art/autotile.ts
5385
- function gridFromRows(rows, solid = "#") {
5386
- return rows.map((r) => [...r].map((c) => solid.includes(c)));
5387
- }
5388
- function at(grid, x, y) {
5389
- return y >= 0 && y < grid.length && x >= 0 && x < grid[y].length && grid[y][x];
5390
- }
5391
- var Edge = { N: 1, E: 2, S: 4, W: 8 };
5392
- function mask4(grid, x, y) {
5393
- return (at(grid, x, y - 1) ? Edge.N : 0) | (at(grid, x + 1, y) ? Edge.E : 0) | (at(grid, x, y + 1) ? Edge.S : 0) | (at(grid, x - 1, y) ? Edge.W : 0);
5394
- }
5395
- function mask8(grid, x, y) {
5396
- let m = 0;
5397
- const dirs = [
5398
- [0, -1],
5399
- [1, -1],
5400
- [1, 0],
5401
- [1, 1],
5402
- [0, 1],
5403
- [-1, 1],
5404
- [-1, 0],
5405
- [-1, -1]
5406
- ];
5407
- for (let i = 0; i < 8; i++) if (at(grid, x + dirs[i][0], y + dirs[i][1])) m |= 1 << i;
5408
- return m;
5409
- }
5410
- var WangFrame = { Isolated: 0, Cap: 1, Straight: 2, Bend: 3, Tee: 4, Cross: 5 };
5411
- function rotateCW(m) {
5412
- let r = 0;
5413
- if (m & Edge.N) r |= Edge.E;
5414
- if (m & Edge.E) r |= Edge.S;
5415
- if (m & Edge.S) r |= Edge.W;
5416
- if (m & Edge.W) r |= Edge.N;
5417
- return r;
5418
- }
5419
- var WANG4 = (() => {
5420
- const bases = [
5421
- [WangFrame.Isolated, 0],
5422
- [WangFrame.Cap, Edge.N],
5423
- [WangFrame.Straight, Edge.N | Edge.S],
5424
- [WangFrame.Bend, Edge.N | Edge.E],
5425
- [WangFrame.Tee, Edge.N | Edge.E | Edge.S],
5426
- [WangFrame.Cross, 15]
5427
- ];
5428
- const table = new Array(16);
5429
- for (const [frame, mask0] of bases) {
5430
- let m = mask0;
5431
- for (let rotation = 0; rotation < 4; rotation++) {
5432
- if (table[m] === void 0) table[m] = { mask: m, frame, rotation };
5433
- m = rotateCW(m);
5434
- }
5435
- }
5436
- return table;
5437
- })();
5438
- function wangTile(mask) {
5439
- return WANG4[mask & 15];
5440
- }
5441
- function autotile4(grid) {
5442
- return grid.map((row, y) => row.map((solid, x) => solid ? wangTile(mask4(grid, x, y)) : null));
5443
- }
5444
- function marchingSquaresCases(grid) {
5445
- const h = grid.length;
5446
- const w = h ? grid[0].length : 0;
5447
- const out = [];
5448
- for (let y = 0; y < h - 1; y++) {
5449
- const row = [];
5450
- for (let x = 0; x < w - 1; x++) {
5451
- row.push(
5452
- (at(grid, x, y) ? 1 : 0) | (at(grid, x + 1, y) ? 2 : 0) | (at(grid, x + 1, y + 1) ? 4 : 0) | (at(grid, x, y + 1) ? 8 : 0)
5453
- );
5454
- }
5455
- out.push(row);
5456
- }
5457
- return out;
5458
- }
5459
- var MS_SEGMENTS = [
5460
- [],
5461
- // 0
5462
- [["L", "T"]],
5463
- // 1 TL
5464
- [["T", "R"]],
5465
- // 2 TR
5466
- [["L", "R"]],
5467
- // 3 TL,TR
5468
- [["R", "B"]],
5469
- // 4 BR
5470
- [["L", "T"], ["R", "B"]],
5471
- // 5 saddle
5472
- [["T", "B"]],
5473
- // 6 TR,BR
5474
- [["L", "B"]],
5475
- // 7 TL,TR,BR
5476
- [["B", "L"]],
5477
- // 8 BL
5478
- [["T", "B"]],
5479
- // 9 TL,BL
5480
- [["T", "R"], ["B", "L"]],
5481
- // 10 saddle
5482
- [["R", "B"]],
5483
- // 11 TL,TR,BL
5484
- [["L", "R"]],
5485
- // 12 BR,BL
5486
- [["T", "R"]],
5487
- // 13 TL,BR,BL
5488
- [["L", "T"]],
5489
- // 14 TR,BR,BL
5490
- []
5491
- // 15
5492
- ];
5493
- function marchingSquaresContours(grid, options = {}) {
5494
- const cell = options.cell ?? 1;
5495
- const ox = options.x ?? 0;
5496
- const oy = options.y ?? 0;
5497
- const cases = marchingSquaresCases(grid);
5498
- const segs = [];
5499
- const mid = (edge, cx, cy) => {
5500
- switch (edge) {
5501
- case "T":
5502
- return { x: cx + cell / 2, y: cy };
5503
- case "R":
5504
- return { x: cx + cell, y: cy + cell / 2 };
5505
- case "B":
5506
- return { x: cx + cell / 2, y: cy + cell };
5507
- case "L":
5508
- return { x: cx, y: cy + cell / 2 };
5509
- }
5510
- };
5511
- for (let y = 0; y < cases.length; y++) {
5512
- for (let x = 0; x < cases[y].length; x++) {
5513
- const cx = ox + x * cell;
5514
- const cy = oy + y * cell;
5515
- for (const [e1, e2] of MS_SEGMENTS[cases[y][x]]) segs.push({ a: mid(e1, cx, cy), b: mid(e2, cx, cy) });
5516
- }
5517
- }
5518
- return segs;
5519
- }
5520
- function autotileToCommands(grid, options = {}) {
5521
- const tile = options.tile ?? 8;
5522
- const ox = options.x ?? 0;
5523
- const oy = options.y ?? 0;
5524
- const z = options.z ?? 0;
5525
- const transform = options.transform ?? IDENTITY;
5526
- const fill = options.fill ?? "#888";
5527
- const edge = options.edge;
5528
- const edgeWidth = options.edgeWidth ?? Math.max(1, tile / 8);
5529
- const out = [];
5530
- for (let y = 0; y < grid.length; y++) {
5531
- for (let x = 0; x < grid[y].length; x++) {
5532
- if (!grid[y][x]) continue;
5533
- const px = ox + x * tile;
5534
- const py = oy + y * tile;
5535
- out.push({ kind: "rect", x: px, y: py, w: tile, h: tile, transform, z, fill });
5536
- if (!edge) continue;
5537
- const m = mask4(grid, x, y);
5538
- const line = (points) => out.push({ kind: "poly", points, closed: false, transform, z: z + 1, stroke: edge, strokeWidth: edgeWidth });
5539
- if (!(m & Edge.N)) line([px, py, px + tile, py]);
5540
- if (!(m & Edge.E)) line([px + tile, py, px + tile, py + tile]);
5541
- if (!(m & Edge.S)) line([px, py + tile, px + tile, py + tile]);
5542
- if (!(m & Edge.W)) line([px, py, px, py + tile]);
5543
- }
5544
- }
5545
- return out;
5546
- }
5547
- function contourToCommands(grid, options = {}) {
5548
- const tile = options.tile ?? 8;
5549
- const transform = options.transform ?? IDENTITY;
5550
- const z = options.z ?? 0;
5551
- const stroke = options.edge ?? options.fill ?? "#333";
5552
- const strokeWidth = options.edgeWidth ?? Math.max(1, tile / 8);
5553
- const segs = marchingSquaresContours(grid, { cell: tile, x: options.x ?? 0, y: options.y ?? 0 });
5554
- return segs.map((s) => ({
5555
- kind: "poly",
5556
- points: [s.a.x, s.a.y, s.b.x, s.b.y],
5557
- closed: false,
5558
- transform,
5559
- z,
5560
- stroke,
5561
- strokeWidth,
5562
- round: true
5563
- }));
5564
- }
5565
-
5566
6658
  // src/procgen/grid.ts
5567
6659
  function makeGrid(cols, rows, fill = 0) {
5568
6660
  return { cols, rows, cells: new Array(cols * rows).fill(fill) };
@@ -8714,6 +9806,41 @@ function telegraphIssues(timeline, minFrames, label = "threat") {
8714
9806
  }
8715
9807
  return dedupe(issues);
8716
9808
  }
9809
+ var litLuminance = (fill, level) => relLuminance(fill) * level;
9810
+ function lightingIssues(commands, avatarFill, avatarWorld, opts = {}) {
9811
+ const minLit = opts.minLitLevel ?? 0.35;
9812
+ const { light } = splitByLightLayer(sortCommands(commands));
9813
+ const parsed = light.length ? parseLightRun(light) : null;
9814
+ if (!parsed) return [];
9815
+ let level = relLuminance(parsed.ambient.fill ?? "#000000");
9816
+ for (const l of parsed.lights) {
9817
+ const c = l.circle;
9818
+ const p = applyTransform(invertTransform(c.transform), avatarWorld);
9819
+ const d = dhypot(p.x - c.cx, p.y - c.cy);
9820
+ if (d >= c.radius) continue;
9821
+ const peak2 = poolPeak(c);
9822
+ const reach = 1 - d / c.radius;
9823
+ level = Math.min(1, level + peak2 * reach);
9824
+ }
9825
+ const issues = [];
9826
+ if (level < minLit) {
9827
+ const lum = litLuminance(avatarFill, level);
9828
+ issues.push(
9829
+ `avatar (${avatarFill}) sits in light level ${level.toFixed(2)} (needs \u2265 ${minLit}) \u2014 it sinks into the ambient darkness (effective luminance ${lum.toFixed(3)}); add a pool over it or raise ambient level`
9830
+ );
9831
+ }
9832
+ return issues;
9833
+ }
9834
+ function poolPeak(c) {
9835
+ const g = c.gradient;
9836
+ if (g && g.stops.length) {
9837
+ let max = 0;
9838
+ for (const s of g.stops) max = Math.max(max, relLuminance(s.color));
9839
+ return max;
9840
+ }
9841
+ const fill = c.fill;
9842
+ return fill ? relLuminance(fill) : 1;
9843
+ }
8717
9844
  function cameraIssues(samples, opts = {}) {
8718
9845
  const dt = opts.dt ?? 1 / 60;
8719
9846
  const maxSpeed = opts.maxSpeed ?? 1800;
@@ -8755,6 +9882,11 @@ function runFeelGates(spec, ctx = {}) {
8755
9882
  if (ctx.commands) sections.push({ gate: "salience", issues: salienceIssues(ctx.commands, spec.avatarFill, bg) });
8756
9883
  else skipped.push("salience (no rendered frame)");
8757
9884
  }
9885
+ if (spec.lit) {
9886
+ if (spec.avatarFill && ctx.commands && ctx.avatarWorld) {
9887
+ sections.push({ gate: "lighting", issues: lightingIssues(ctx.commands, spec.avatarFill, ctx.avatarWorld) });
9888
+ } else skipped.push("lighting (needs avatarFill + rendered frame + avatarWorld)");
9889
+ }
8758
9890
  if (spec.scrolls) {
8759
9891
  if (ctx.camSamples && ctx.camSamples.length >= 3) {
8760
9892
  const issues = cameraIssues(ctx.camSamples, { dt: ctx.dt });
@@ -12757,7 +13889,7 @@ function runStudio(baseDef, mount, opts = {}) {
12757
13889
  }
12758
13890
 
12759
13891
  // src/index.ts
12760
- var VERSION = "0.4.0";
13892
+ var VERSION = "0.4.1";
12761
13893
  export {
12762
13894
  ALBUM,
12763
13895
  AMBIENT_PRESETS,
@@ -12766,11 +13898,16 @@ export {
12766
13898
  AudioBus,
12767
13899
  BLOOM_PIPELINE,
12768
13900
  BitmapText,
13901
+ Blend1D,
13902
+ Blend2D,
13903
+ Bone2D,
12769
13904
  BroadcastChannelTransport,
13905
+ CLIP_CHANNELS,
12770
13906
  Camera2D,
12771
13907
  CameraController,
12772
13908
  Canvas2DRenderer,
12773
13909
  CinematicPlayer,
13910
+ ClipPlayer,
12774
13911
  Clock,
12775
13912
  Coroutines,
12776
13913
  DEFAULT_GAMEPAD_MAP,
@@ -12793,12 +13930,17 @@ export {
12793
13930
  HeadlessRenderer,
12794
13931
  IDENTITY,
12795
13932
  INSTRUMENTS,
13933
+ IkTarget,
12796
13934
  InputBuffer,
12797
13935
  InputRecorder,
12798
13936
  InputState,
12799
13937
  IsoPrism,
12800
13938
  KENTO,
12801
13939
  KeyboardSource,
13940
+ LAYER_HUD,
13941
+ LAYER_LIGHT,
13942
+ LAYER_WORLD,
13943
+ LightLayer,
12802
13944
  LocalStorageAdapter,
12803
13945
  LockstepSession,
12804
13946
  LoopbackHub,
@@ -12824,6 +13966,7 @@ export {
12824
13966
  PhaseClock,
12825
13967
  PixelBuffer,
12826
13968
  PlayerInput,
13969
+ PointLight,
12827
13970
  PointerSource,
12828
13971
  RingBuffer,
12829
13972
  Rng,
@@ -12838,6 +13981,8 @@ export {
12838
13981
  Shaker,
12839
13982
  Shell,
12840
13983
  Signal,
13984
+ Skeleton,
13985
+ SkeletonDebug,
12841
13986
  SnapshotRing,
12842
13987
  SpatialHash,
12843
13988
  Sprite,
@@ -12863,6 +14008,7 @@ export {
12863
14008
  albumTrack,
12864
14009
  all,
12865
14010
  analyzePlaytest,
14011
+ applyChannel,
12866
14012
  applyImpulse,
12867
14013
  applyReverb,
12868
14014
  applyTransform,
@@ -12880,10 +14026,12 @@ export {
12880
14026
  availableUpgrades,
12881
14027
  bandBalance,
12882
14028
  bfs,
14029
+ blendIssues,
12883
14030
  blobPath,
12884
14031
  bodyAABB,
12885
14032
  bodyContains,
12886
14033
  bootDom,
14034
+ buildSkeleton,
12887
14035
  cadenceResolves,
12888
14036
  cameraIssues,
12889
14037
  canvasGradient,
@@ -12896,6 +14044,10 @@ export {
12896
14044
  chordNotes,
12897
14045
  clamp,
12898
14046
  clientToDesign,
14047
+ clipEvents,
14048
+ clipFinished,
14049
+ clipIssues,
14050
+ clipTime,
12899
14051
  collide,
12900
14052
  commandsToSVGInner,
12901
14053
  composeCampaign,
@@ -12910,6 +14062,7 @@ export {
12910
14062
  createStereo,
12911
14063
  createWorld,
12912
14064
  crestFactorDb,
14065
+ cullSegments,
12913
14066
  dashJumpDistance,
12914
14067
  datan,
12915
14068
  datan2,
@@ -12992,6 +14145,7 @@ export {
12992
14145
  invertTransform,
12993
14146
  isCaptureMode,
12994
14147
  isGround,
14148
+ isLightCommand,
12995
14149
  isMonotonic,
12996
14150
  iso,
12997
14151
  joinRoom,
@@ -13010,6 +14164,7 @@ export {
13010
14164
  levelIssues,
13011
14165
  levelReachable,
13012
14166
  levelToTilemap,
14167
+ lightingIssues,
13013
14168
  lineOfSight,
13014
14169
  linearGradient,
13015
14170
  lintSong,
@@ -13035,6 +14190,7 @@ export {
13035
14190
  mix,
13036
14191
  mixLinear,
13037
14192
  mixMono,
14193
+ mixPose,
13038
14194
  moveRect,
13039
14195
  mutateColor,
13040
14196
  netMessage,
@@ -13042,11 +14198,13 @@ export {
13042
14198
  nineSlice,
13043
14199
  normalize,
13044
14200
  noteToMidi,
14201
+ occludersFromTilemap,
13045
14202
  onsetDensity,
13046
14203
  openVoicing,
13047
14204
  packVarints,
13048
14205
  panFromOffset,
13049
14206
  panGains,
14207
+ parseLightRun,
13050
14208
  parseRich,
13051
14209
  parseSnapshot,
13052
14210
  passableFromTilemap,
@@ -13062,6 +14220,7 @@ export {
13062
14220
  pointQuery,
13063
14221
  pollDirector,
13064
14222
  polygonBox,
14223
+ poseKey,
13065
14224
  progression,
13066
14225
  progressionPuzzle,
13067
14226
  proveCompletable,
@@ -13097,6 +14256,7 @@ export {
13097
14256
  replaySession,
13098
14257
  resetNodeIds,
13099
14258
  resetZzfxWarnings,
14259
+ resolveTracks,
13100
14260
  resolveTuning,
13101
14261
  rigidStep,
13102
14262
  rleDecode,
@@ -13110,7 +14270,10 @@ export {
13110
14270
  runHeadless,
13111
14271
  runStudio,
13112
14272
  salienceIssues,
14273
+ sampleClip,
13113
14274
  sampleGradient,
14275
+ samplePose,
14276
+ sampleTrack,
13114
14277
  scaleMidis,
13115
14278
  scalePitchClasses,
13116
14279
  scatter,
@@ -13127,6 +14290,7 @@ export {
13127
14290
  settings,
13128
14291
  shadeHex,
13129
14292
  shadowDef,
14293
+ shadowQuads,
13130
14294
  shapeBBox,
13131
14295
  shapeBox,
13132
14296
  showScreen,
@@ -13140,13 +14304,16 @@ export {
13140
14304
  softClipInPlace,
13141
14305
  solidNeighbours,
13142
14306
  solve,
14307
+ solveFabrik,
13143
14308
  solveJoint,
14309
+ solveTwoBoneIK,
13144
14310
  songBeats,
13145
14311
  songDuration,
13146
14312
  sortCommands,
13147
14313
  spatialMix,
13148
14314
  specFromZzfx,
13149
14315
  spectralCentroid,
14316
+ splitByLightLayer,
13150
14317
  spring,
13151
14318
  springStep,
13152
14319
  star,