hayao 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anim/blend.d.ts +68 -0
- package/dist/anim/clip.d.ts +87 -0
- package/dist/anim/ik.d.ts +40 -0
- package/dist/anim/skeleton.d.ts +64 -0
- package/dist/hayao.global.js +12 -12
- package/dist/index.d.ts +11 -1
- package/dist/index.js +1683 -516
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +12 -12
- package/dist/render/canvas2d-core.d.ts +4 -0
- package/dist/render/commands.d.ts +19 -0
- package/dist/render/lightRun.d.ts +35 -0
- package/dist/render/svgString.d.ts +8 -3
- package/dist/scene/clipPlayer.d.ts +58 -0
- package/dist/scene/ikTarget.d.ts +25 -0
- package/dist/scene/light.d.ts +80 -0
- package/dist/scene/shadow2d.d.ts +25 -0
- package/dist/scene/skeletonDebug.d.ts +28 -0
- package/dist/verify/gates.d.ts +25 -0
- package/docs/API.md +66 -8
- package/docs/CONVENTIONS.md +56 -0
- package/package.json +1 -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
|
|
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
|
|
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
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
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
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
const
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
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.
|
|
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,
|