hayao 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/bin/create-hayao.mjs +1 -1
- package/dist/app/browser.d.ts +33 -3
- package/dist/app/game.d.ts +15 -11
- package/dist/audio/audio.d.ts +18 -1
- package/dist/audio/synth.d.ts +7 -0
- package/dist/audio/zzfx.d.ts +14 -0
- package/dist/core/clock.d.ts +1 -1
- package/dist/core/projection.d.ts +36 -0
- package/dist/core/rng.d.ts +9 -0
- package/dist/hayao.global.js +190 -5
- package/dist/index.d.ts +10 -1
- package/dist/index.js +2341 -328
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +190 -5
- package/dist/input/actions.d.ts +60 -6
- package/dist/input/gamepad.d.ts +101 -0
- package/dist/input/source.d.ts +85 -4
- package/dist/logic/coroutine.d.ts +68 -0
- package/dist/render/canvas.d.ts +3 -7
- package/dist/render/canvas2d-core.d.ts +9 -0
- package/dist/render/commands.d.ts +36 -2
- package/dist/render/paint.d.ts +8 -0
- package/dist/render/renderer.d.ts +25 -0
- package/dist/render/svg.d.ts +2 -1
- package/dist/render/webgl.d.ts +176 -0
- package/dist/scene/iso.d.ts +73 -0
- package/dist/scene/node.d.ts +81 -2
- package/dist/scene/nodes.d.ts +34 -0
- package/dist/scene/particles.d.ts +19 -0
- package/dist/scene/verletChain.d.ts +76 -0
- package/dist/ui/touch.d.ts +51 -0
- package/dist/verify/dom.d.ts +26 -0
- package/dist/world.d.ts +72 -7
- package/docs/API.md +79 -26
- package/docs/CONVENTIONS.md +290 -2
- package/docs/EMBED.md +5 -0
- package/docs/QUICKSTART.md +13 -1
- package/docs/VERIFICATION.md +34 -4
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -254,6 +254,32 @@ function invertTransform(m) {
|
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// src/core/projection.ts
|
|
258
|
+
function iso(cfg) {
|
|
259
|
+
const { tileW, tileH } = cfg;
|
|
260
|
+
const origin = cfg.origin ? { x: cfg.origin.x, y: cfg.origin.y } : { x: 0, y: 0 };
|
|
261
|
+
const elevStep = cfg.elevStep ?? tileH;
|
|
262
|
+
const hw = tileW / 2;
|
|
263
|
+
const hh = tileH / 2;
|
|
264
|
+
return {
|
|
265
|
+
tileW,
|
|
266
|
+
tileH,
|
|
267
|
+
origin,
|
|
268
|
+
elevStep,
|
|
269
|
+
toScreen(gx, gy, elev = 0) {
|
|
270
|
+
return {
|
|
271
|
+
x: origin.x + (gx - gy) * hw,
|
|
272
|
+
y: origin.y + (gx + gy) * hh - elev * elevStep
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
toGrid(sx, sy) {
|
|
276
|
+
const dx = (sx - origin.x) / hw;
|
|
277
|
+
const dy = (sy - origin.y) / hh;
|
|
278
|
+
return { x: (dx + dy) / 2, y: (dy - dx) / 2 };
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
257
283
|
// src/core/rng.ts
|
|
258
284
|
function splitmix32(seed) {
|
|
259
285
|
let a = seed >>> 0;
|
|
@@ -347,6 +373,21 @@ var Rng = class _Rng {
|
|
|
347
373
|
[this.s0, this.s1, this.s2, this.s3] = state.s;
|
|
348
374
|
}
|
|
349
375
|
};
|
|
376
|
+
var __noiseBits = new DataView(new ArrayBuffer(8));
|
|
377
|
+
function hashNoise(...values) {
|
|
378
|
+
let h = 2654435769 >>> 0;
|
|
379
|
+
for (const v of values) {
|
|
380
|
+
__noiseBits.setFloat64(0, v);
|
|
381
|
+
h = Math.imul(h ^ __noiseBits.getUint32(0), 2246822507) >>> 0;
|
|
382
|
+
h = (h ^ h >>> 13) >>> 0;
|
|
383
|
+
h = Math.imul(h ^ __noiseBits.getUint32(4), 3266489909) >>> 0;
|
|
384
|
+
h = (h ^ h >>> 16) >>> 0;
|
|
385
|
+
}
|
|
386
|
+
h = Math.imul(h ^ h >>> 16, 2246822507) >>> 0;
|
|
387
|
+
h = Math.imul(h ^ h >>> 13, 3266489909) >>> 0;
|
|
388
|
+
h = (h ^ h >>> 16) >>> 0;
|
|
389
|
+
return h / 4294967296;
|
|
390
|
+
}
|
|
350
391
|
function hashString(str) {
|
|
351
392
|
let h = 2166136261 >>> 0;
|
|
352
393
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -540,6 +581,24 @@ var Node = class {
|
|
|
540
581
|
* particles, tweened positions) never pollutes the canonical, verifiable state.
|
|
541
582
|
*/
|
|
542
583
|
cosmetic = false;
|
|
584
|
+
/**
|
|
585
|
+
* Pause behaviour for this subtree (see PauseMode). Serialized only when
|
|
586
|
+
* non-default, so existing trees keep their pinned hashes.
|
|
587
|
+
*/
|
|
588
|
+
pauseMode = "inherit";
|
|
589
|
+
/**
|
|
590
|
+
* Screen-space overlay: this subtree ignores the camera — its transforms
|
|
591
|
+
* compose from IDENTITY (design-space coordinates) — and every command it
|
|
592
|
+
* emits is tagged `layer: 1` so HUD/overlay content always paints above the
|
|
593
|
+
* world, whatever the z values. Serialized only when true.
|
|
594
|
+
*/
|
|
595
|
+
screenSpace = false;
|
|
596
|
+
/**
|
|
597
|
+
* Optional local anchor: when set, rotation/scale pivot around this point in
|
|
598
|
+
* local space instead of the node origin (the local transform gains a
|
|
599
|
+
* trailing translation of −pivot). Serialized only when set.
|
|
600
|
+
*/
|
|
601
|
+
pivot;
|
|
543
602
|
parent = null;
|
|
544
603
|
children = [];
|
|
545
604
|
world = null;
|
|
@@ -562,6 +621,21 @@ var Node = class {
|
|
|
562
621
|
this.z = config.z ?? 0;
|
|
563
622
|
this.visible = config.visible ?? true;
|
|
564
623
|
}
|
|
624
|
+
// ── Convenience accessors ─────────────────────────────────────
|
|
625
|
+
/** Shorthand for `pos.x` — reads and writes the same vector. */
|
|
626
|
+
get x() {
|
|
627
|
+
return this.pos.x;
|
|
628
|
+
}
|
|
629
|
+
set x(v) {
|
|
630
|
+
this.pos.x = v;
|
|
631
|
+
}
|
|
632
|
+
/** Shorthand for `pos.y` — reads and writes the same vector. */
|
|
633
|
+
get y() {
|
|
634
|
+
return this.pos.y;
|
|
635
|
+
}
|
|
636
|
+
set y(v) {
|
|
637
|
+
this.pos.y = v;
|
|
638
|
+
}
|
|
565
639
|
// ── Tree structure ─────────────────────────────────────────────
|
|
566
640
|
addChild(child) {
|
|
567
641
|
child.parent = this;
|
|
@@ -580,6 +654,18 @@ var Node = class {
|
|
|
580
654
|
free() {
|
|
581
655
|
this.world?.requestFree(this);
|
|
582
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Immediately exit + detach ALL children. Unlike `free()` this is NOT
|
|
659
|
+
* deferred — the children are gone when the call returns (safe here because
|
|
660
|
+
* it iterates a snapshot of the array). Use it to rebuild a container's
|
|
661
|
+
* contents wholesale; prefer `free()` for removals during an update.
|
|
662
|
+
*/
|
|
663
|
+
clearChildren() {
|
|
664
|
+
for (const c of this.children.slice()) {
|
|
665
|
+
c.exitTree();
|
|
666
|
+
this.removeChild(c);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
583
669
|
/** Depth-first find by name. */
|
|
584
670
|
find(name) {
|
|
585
671
|
if (this.name === name) return this;
|
|
@@ -589,6 +675,15 @@ var Node = class {
|
|
|
589
675
|
}
|
|
590
676
|
return null;
|
|
591
677
|
}
|
|
678
|
+
/** Depth-first find of the first node (self included) that is an instance of `ctor` — typed. */
|
|
679
|
+
findOfType(ctor) {
|
|
680
|
+
if (this instanceof ctor) return this;
|
|
681
|
+
for (const c of this.children) {
|
|
682
|
+
const r = c.findOfType(ctor);
|
|
683
|
+
if (r) return r;
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
592
687
|
/** All descendants (and self) of a given type tag. */
|
|
593
688
|
query(type, out = []) {
|
|
594
689
|
if (this.type === type) out.push(this);
|
|
@@ -623,13 +718,25 @@ var Node = class {
|
|
|
623
718
|
}
|
|
624
719
|
for (const c of this.children) c.enterTree(world);
|
|
625
720
|
}
|
|
626
|
-
|
|
721
|
+
/**
|
|
722
|
+
* Advance this subtree. `dt` is the (time-scaled) step delta; `unscaledDt`
|
|
723
|
+
* and `paused` come from the world, and `parentMode` is the effective pause
|
|
724
|
+
* mode flowing down the tree. The walk always descends — a paused node just
|
|
725
|
+
* skips its callbacks — so an `'always'` descendant (pause menu, transition)
|
|
726
|
+
* keeps running inside a paused parent.
|
|
727
|
+
*/
|
|
728
|
+
updateTree(dt, unscaledDt = dt, parentMode = "pausable", paused = false) {
|
|
627
729
|
if (this._freed) return;
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
730
|
+
const mode = this.pauseMode === "inherit" ? parentMode : this.pauseMode;
|
|
731
|
+
const running = mode === "always" || mode === "pausable" && !paused;
|
|
732
|
+
if (running) {
|
|
733
|
+
const ctx = this.world;
|
|
734
|
+
const useDt = mode === "always" ? unscaledDt : dt;
|
|
735
|
+
for (const b of this.behaviors) b.update?.(this, useDt, ctx);
|
|
736
|
+
this.onUpdate?.(this, useDt, ctx);
|
|
737
|
+
this.onProcess(useDt);
|
|
738
|
+
}
|
|
739
|
+
for (const c of this.children.slice()) c.updateTree(dt, unscaledDt, mode, paused);
|
|
633
740
|
}
|
|
634
741
|
exitTree() {
|
|
635
742
|
for (const b of this.behaviors) b.exit?.(this);
|
|
@@ -642,7 +749,8 @@ var Node = class {
|
|
|
642
749
|
}
|
|
643
750
|
// ── Transforms ────────────────────────────────────────────────
|
|
644
751
|
localTransform() {
|
|
645
|
-
|
|
752
|
+
const t = makeTransform(this.pos, this.rotation, this.scale);
|
|
753
|
+
return this.pivot ? composeTransform(t, makeTransform({ x: -this.pivot.x, y: -this.pivot.y }, 0, { x: 1, y: 1 })) : t;
|
|
646
754
|
}
|
|
647
755
|
worldTransform() {
|
|
648
756
|
const local = this.localTransform();
|
|
@@ -652,9 +760,16 @@ var Node = class {
|
|
|
652
760
|
/** Walk the subtree, appending draw commands with computed world transforms. */
|
|
653
761
|
collectDraw(out, parentWorld = IDENTITY) {
|
|
654
762
|
if (!this.visible) return;
|
|
655
|
-
const world = composeTransform(parentWorld, this.localTransform());
|
|
763
|
+
const world = composeTransform(this.screenSpace ? IDENTITY : parentWorld, this.localTransform());
|
|
764
|
+
const start = out.length;
|
|
656
765
|
this.draw(out, world);
|
|
657
766
|
for (const c of this.children) c.collectDraw(out, world);
|
|
767
|
+
if (this.screenSpace) {
|
|
768
|
+
for (let i = start; i < out.length; i++) {
|
|
769
|
+
const cmd = out[i];
|
|
770
|
+
if ((cmd.layer ?? 0) < 1) cmd.layer = 1;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
658
773
|
}
|
|
659
774
|
/** Subclasses emit their own commands here. Base draws nothing. */
|
|
660
775
|
draw(_out, _world) {
|
|
@@ -676,7 +791,13 @@ var Node = class {
|
|
|
676
791
|
scale: { ...this.scale },
|
|
677
792
|
z: this.z,
|
|
678
793
|
visible: this.visible,
|
|
679
|
-
|
|
794
|
+
// New base fields only when non-default, so pinned hashes survive.
|
|
795
|
+
...this.pivot ? { pivot: { ...this.pivot } } : {},
|
|
796
|
+
props: {
|
|
797
|
+
...this.serializeProps(),
|
|
798
|
+
...this.pauseMode !== "inherit" ? { pauseMode: this.pauseMode } : {},
|
|
799
|
+
...this.screenSpace ? { screenSpace: true } : {}
|
|
800
|
+
},
|
|
680
801
|
children: this.children.filter((c) => !c.cosmetic).map((c) => c.serialize())
|
|
681
802
|
};
|
|
682
803
|
}
|
|
@@ -690,6 +811,17 @@ var Node = class {
|
|
|
690
811
|
};
|
|
691
812
|
|
|
692
813
|
// src/scene/nodes.ts
|
|
814
|
+
function diamondPoints(w, h) {
|
|
815
|
+
return [0, -h / 2, w / 2, 0, 0, h / 2, -w / 2, 0];
|
|
816
|
+
}
|
|
817
|
+
function regularPolyPoints(sides, r, rotation = 0) {
|
|
818
|
+
const pts = [];
|
|
819
|
+
for (let i = 0; i < sides; i++) {
|
|
820
|
+
const a = rotation - TAU / 4 + TAU * i / sides;
|
|
821
|
+
pts.push(dcos(a) * r, dsin(a) * r);
|
|
822
|
+
}
|
|
823
|
+
return pts;
|
|
824
|
+
}
|
|
693
825
|
var Sprite = class extends Node {
|
|
694
826
|
type = "Sprite";
|
|
695
827
|
shape;
|
|
@@ -704,7 +836,8 @@ var Sprite = class extends Node {
|
|
|
704
836
|
opacity: config.opacity,
|
|
705
837
|
round: config.round,
|
|
706
838
|
gradient: config.gradient,
|
|
707
|
-
shadow: config.shadow
|
|
839
|
+
shadow: config.shadow,
|
|
840
|
+
lineDash: config.lineDash
|
|
708
841
|
};
|
|
709
842
|
}
|
|
710
843
|
draw(out, world) {
|
|
@@ -719,6 +852,12 @@ var Sprite = class extends Node {
|
|
|
719
852
|
case "circle":
|
|
720
853
|
out.push({ kind: "circle", cx: 0, cy: 0, radius: this.shape.radius, ...base });
|
|
721
854
|
break;
|
|
855
|
+
case "ellipse":
|
|
856
|
+
out.push({ kind: "ellipse", cx: 0, cy: 0, rx: this.shape.rx, ry: this.shape.ry, ...base });
|
|
857
|
+
break;
|
|
858
|
+
case "arc":
|
|
859
|
+
out.push({ kind: "arc", cx: 0, cy: 0, radius: this.shape.radius, start: this.shape.start, end: this.shape.end, sector: this.shape.sector, ...base });
|
|
860
|
+
break;
|
|
722
861
|
case "poly":
|
|
723
862
|
out.push({ kind: "poly", points: this.shape.points, closed: this.shape.closed ?? true, ...base });
|
|
724
863
|
break;
|
|
@@ -728,6 +867,12 @@ var Sprite = class extends Node {
|
|
|
728
867
|
case "glyph":
|
|
729
868
|
out.push({ kind: "text", text: this.shape.char, x: 0, y: 0, size: this.shape.size, align: "center", ...base });
|
|
730
869
|
break;
|
|
870
|
+
case "diamond":
|
|
871
|
+
out.push({ kind: "poly", points: diamondPoints(this.shape.w, this.shape.h), closed: true, ...base });
|
|
872
|
+
break;
|
|
873
|
+
case "regularPoly":
|
|
874
|
+
out.push({ kind: "poly", points: regularPolyPoints(this.shape.sides, this.shape.r, this.shape.rotation ?? 0), closed: true, ...base });
|
|
875
|
+
break;
|
|
731
876
|
}
|
|
732
877
|
}
|
|
733
878
|
serializeProps() {
|
|
@@ -848,6 +993,85 @@ var Timer = class extends Node {
|
|
|
848
993
|
}
|
|
849
994
|
};
|
|
850
995
|
|
|
996
|
+
// src/scene/iso.ts
|
|
997
|
+
function shadeHex(hex, f) {
|
|
998
|
+
let h = hex.replace("#", "");
|
|
999
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
1000
|
+
if (h.length !== 6) return hex;
|
|
1001
|
+
const ch = (i) => {
|
|
1002
|
+
const v = Math.round(Math.min(255, Math.max(0, parseInt(h.slice(i, i + 2), 16) * f)));
|
|
1003
|
+
return v.toString(16).padStart(2, "0");
|
|
1004
|
+
};
|
|
1005
|
+
return `#${ch(0)}${ch(2)}${ch(4)}`;
|
|
1006
|
+
}
|
|
1007
|
+
var IsoPrism = class extends Node {
|
|
1008
|
+
type = "IsoPrism";
|
|
1009
|
+
tileW;
|
|
1010
|
+
tileH;
|
|
1011
|
+
height;
|
|
1012
|
+
fill;
|
|
1013
|
+
topPaint;
|
|
1014
|
+
leftPaint;
|
|
1015
|
+
rightPaint;
|
|
1016
|
+
stroke;
|
|
1017
|
+
strokeWidth;
|
|
1018
|
+
opacity;
|
|
1019
|
+
constructor(config) {
|
|
1020
|
+
super(config);
|
|
1021
|
+
this.tileW = config.tileW;
|
|
1022
|
+
this.tileH = config.tileH;
|
|
1023
|
+
this.height = config.height;
|
|
1024
|
+
this.fill = config.fill;
|
|
1025
|
+
this.topPaint = config.top;
|
|
1026
|
+
this.leftPaint = config.left;
|
|
1027
|
+
this.rightPaint = config.right;
|
|
1028
|
+
this.stroke = config.stroke;
|
|
1029
|
+
this.strokeWidth = config.strokeWidth;
|
|
1030
|
+
this.opacity = config.opacity;
|
|
1031
|
+
}
|
|
1032
|
+
draw(out, world) {
|
|
1033
|
+
const w = this.tileW;
|
|
1034
|
+
const h = this.tileH;
|
|
1035
|
+
const H = this.height;
|
|
1036
|
+
const seam = { stroke: this.stroke, strokeWidth: this.strokeWidth, opacity: this.opacity };
|
|
1037
|
+
const top = this.topPaint ?? { fill: this.fill };
|
|
1038
|
+
const left = this.leftPaint ?? { fill: shadeHex(this.fill, 0.65) };
|
|
1039
|
+
const right = this.rightPaint ?? { fill: shadeHex(this.fill, 0.85) };
|
|
1040
|
+
const base = { transform: world, z: this.z };
|
|
1041
|
+
if (H > 0) {
|
|
1042
|
+
out.push({ kind: "poly", closed: true, points: [-w / 2, 0, 0, h / 2, 0, h / 2 - H, -w / 2, -H], ...seam, ...left, ...base });
|
|
1043
|
+
out.push({ kind: "poly", closed: true, points: [0, h / 2, w / 2, 0, w / 2, -H, 0, h / 2 - H], ...seam, ...right, ...base });
|
|
1044
|
+
}
|
|
1045
|
+
const tp = diamondPoints(w, h).map((v, i) => i % 2 === 1 ? v - H : v);
|
|
1046
|
+
out.push({ kind: "poly", closed: true, points: tp, ...seam, ...top, ...base });
|
|
1047
|
+
}
|
|
1048
|
+
serializeProps() {
|
|
1049
|
+
return { tileW: this.tileW, tileH: this.tileH, height: this.height, fill: this.fill };
|
|
1050
|
+
}
|
|
1051
|
+
applyProps(props) {
|
|
1052
|
+
if (typeof props.tileW === "number") this.tileW = props.tileW;
|
|
1053
|
+
if (typeof props.tileH === "number") this.tileH = props.tileH;
|
|
1054
|
+
if (typeof props.height === "number") this.height = props.height;
|
|
1055
|
+
if (typeof props.fill === "string") this.fill = props.fill;
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
var DepthSort = class extends Node {
|
|
1059
|
+
type = "DepthSort";
|
|
1060
|
+
key;
|
|
1061
|
+
depthScale;
|
|
1062
|
+
/** DepthSort is a pure ordering container — its z assignments are view-only. */
|
|
1063
|
+
cosmetic = true;
|
|
1064
|
+
constructor(config) {
|
|
1065
|
+
super(config);
|
|
1066
|
+
this.key = config.key;
|
|
1067
|
+
this.depthScale = config.depthScale ?? 1;
|
|
1068
|
+
}
|
|
1069
|
+
collectDraw(out, parentWorld) {
|
|
1070
|
+
for (const c of this.children) c.z = this.key(c) * this.depthScale;
|
|
1071
|
+
super.collectDraw(out, parentWorld);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
|
|
851
1075
|
// src/scene/cameraController.ts
|
|
852
1076
|
var CameraController = class extends Node {
|
|
853
1077
|
type = "CameraController";
|
|
@@ -1111,6 +1335,36 @@ var Particles = class extends Node {
|
|
|
1111
1335
|
gravity = 0;
|
|
1112
1336
|
drag = 0;
|
|
1113
1337
|
shrink = true;
|
|
1338
|
+
// ── Continuous emission ─────────────────────────────────────────
|
|
1339
|
+
// A steady stream (torch smoke, thruster exhaust) on top of the one-shot
|
|
1340
|
+
// burst(): a fractional accumulator over sim dt spawns whole particles
|
|
1341
|
+
// through the same private-Rng burst path, so it stays deterministic and
|
|
1342
|
+
// cosmetic by the same construction.
|
|
1343
|
+
emitRate = 0;
|
|
1344
|
+
emitAcc = 0;
|
|
1345
|
+
emitStyle = null;
|
|
1346
|
+
emitOrigin = { x: 0, y: 0 };
|
|
1347
|
+
emit(rateOrName, styleOrPayload) {
|
|
1348
|
+
if (typeof rateOrName === "string") {
|
|
1349
|
+
super.emit(rateOrName, styleOrPayload);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
this.emitRate = rateOrName;
|
|
1353
|
+
this.emitAcc = 0;
|
|
1354
|
+
const style = styleOrPayload;
|
|
1355
|
+
if (style) {
|
|
1356
|
+
const { at: at2, ...rest3 } = style;
|
|
1357
|
+
this.emitStyle = rest3;
|
|
1358
|
+
this.emitOrigin = at2 ? { x: at2.x, y: at2.y } : { x: 0, y: 0 };
|
|
1359
|
+
} else if (!this.emitStyle) {
|
|
1360
|
+
this.emitStyle = PARTICLE_PRESETS.burst();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
/** Stop continuous emission. Live particles finish their lives naturally. */
|
|
1364
|
+
stopEmit() {
|
|
1365
|
+
this.emitRate = 0;
|
|
1366
|
+
this.emitAcc = 0;
|
|
1367
|
+
}
|
|
1114
1368
|
onProcess(dt) {
|
|
1115
1369
|
const drag = Math.max(0, 1 - this.drag * dt);
|
|
1116
1370
|
let write = 0;
|
|
@@ -1125,6 +1379,14 @@ var Particles = class extends Node {
|
|
|
1125
1379
|
this.pool[write++] = p;
|
|
1126
1380
|
}
|
|
1127
1381
|
this.pool.length = write;
|
|
1382
|
+
if (this.emitRate > 0 && this.emitStyle) {
|
|
1383
|
+
this.emitAcc += this.emitRate * dt;
|
|
1384
|
+
const spawn = Math.floor(this.emitAcc);
|
|
1385
|
+
if (spawn > 0) {
|
|
1386
|
+
this.emitAcc -= spawn;
|
|
1387
|
+
this.burst(spawn, this.emitOrigin, this.emitStyle);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1128
1390
|
}
|
|
1129
1391
|
draw(out, world) {
|
|
1130
1392
|
for (const p of this.pool) {
|
|
@@ -1371,6 +1633,204 @@ var FLOAT_PRESETS = {
|
|
|
1371
1633
|
label: (color = "#3d3323") => ({ color, size: 18, weight: 600, rise: 40, gravity: 0, life: 1.2, jitter: 0, fade: 0.5 })
|
|
1372
1634
|
};
|
|
1373
1635
|
|
|
1636
|
+
// src/scene/verletChain.ts
|
|
1637
|
+
var VerletChain = class extends Node {
|
|
1638
|
+
type = "VerletChain";
|
|
1639
|
+
gravity;
|
|
1640
|
+
damping;
|
|
1641
|
+
iterations;
|
|
1642
|
+
pinTail;
|
|
1643
|
+
stroke;
|
|
1644
|
+
strokeWidth;
|
|
1645
|
+
taper;
|
|
1646
|
+
/**
|
|
1647
|
+
* Where the tail is pinned when `pinTail` is true, in NODE-LOCAL space
|
|
1648
|
+
* (same space as `points`). null leaves the tail free even with pinTail set.
|
|
1649
|
+
*/
|
|
1650
|
+
tailTarget = null;
|
|
1651
|
+
segments;
|
|
1652
|
+
/** Rest length of one segment. */
|
|
1653
|
+
segmentLength;
|
|
1654
|
+
// World-space simulation state (flat arrays: allocation-free hot path).
|
|
1655
|
+
px;
|
|
1656
|
+
py;
|
|
1657
|
+
prevx;
|
|
1658
|
+
prevy;
|
|
1659
|
+
/** Node-local mirror of the sim points, updated after each step. */
|
|
1660
|
+
local;
|
|
1661
|
+
initialized = false;
|
|
1662
|
+
// Pending impulse (world px/s), applied to free points on the next step.
|
|
1663
|
+
impX = 0;
|
|
1664
|
+
impY = 0;
|
|
1665
|
+
constructor(config) {
|
|
1666
|
+
super(config);
|
|
1667
|
+
this.cosmetic = true;
|
|
1668
|
+
this.segments = Math.max(1, Math.floor(config.segments));
|
|
1669
|
+
this.segmentLength = config.length / this.segments;
|
|
1670
|
+
this.gravity = config.gravity ? { ...config.gravity } : { x: 0, y: 600 };
|
|
1671
|
+
this.damping = config.damping ?? 0.985;
|
|
1672
|
+
this.iterations = Math.max(1, config.iterations ?? 3);
|
|
1673
|
+
this.pinTail = config.pinTail ?? false;
|
|
1674
|
+
this.stroke = config.stroke ?? "#333";
|
|
1675
|
+
this.strokeWidth = config.strokeWidth ?? 3;
|
|
1676
|
+
this.taper = config.taper ?? false;
|
|
1677
|
+
const n5 = this.segments + 1;
|
|
1678
|
+
this.px = new Float64Array(n5);
|
|
1679
|
+
this.py = new Float64Array(n5);
|
|
1680
|
+
this.prevx = new Float64Array(n5);
|
|
1681
|
+
this.prevy = new Float64Array(n5);
|
|
1682
|
+
this.local = [];
|
|
1683
|
+
for (let i = 0; i < n5; i++) this.local.push({ x: 0, y: i * this.segmentLength });
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Chain points in NODE-LOCAL space, head first (`points[0]` ≈ {0,0}).
|
|
1687
|
+
* The Vec2 objects are reused across steps — read, don't retain copies-by-ref.
|
|
1688
|
+
*/
|
|
1689
|
+
get points() {
|
|
1690
|
+
return this.local;
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Direction of segment i (0..segments-1) in node-local space, radians
|
|
1694
|
+
* (0 = +x, y-down screen convention). Map child sprites to segments with
|
|
1695
|
+
* `sprite.pos = chain.points[i]; sprite.rotation = chain.segmentAngle(i)`.
|
|
1696
|
+
*/
|
|
1697
|
+
segmentAngle(i) {
|
|
1698
|
+
const a = this.local[i];
|
|
1699
|
+
const b = this.local[i + 1];
|
|
1700
|
+
return datan2(b.y - a.y, b.x - a.x);
|
|
1701
|
+
}
|
|
1702
|
+
/** Flick the chain: add a velocity (world px/s) to every free point next step. */
|
|
1703
|
+
impulse(v) {
|
|
1704
|
+
this.impX += v.x;
|
|
1705
|
+
this.impY += v.y;
|
|
1706
|
+
}
|
|
1707
|
+
onProcess(dt) {
|
|
1708
|
+
const wt = this.worldTransform();
|
|
1709
|
+
const headX = wt.e;
|
|
1710
|
+
const headY = wt.f;
|
|
1711
|
+
const n5 = this.segments + 1;
|
|
1712
|
+
const px = this.px;
|
|
1713
|
+
const py = this.py;
|
|
1714
|
+
const prevx = this.prevx;
|
|
1715
|
+
const prevy = this.prevy;
|
|
1716
|
+
if (!this.initialized) {
|
|
1717
|
+
for (let i = 0; i < n5; i++) {
|
|
1718
|
+
px[i] = headX;
|
|
1719
|
+
py[i] = headY + i * this.segmentLength;
|
|
1720
|
+
prevx[i] = px[i];
|
|
1721
|
+
prevy[i] = py[i];
|
|
1722
|
+
}
|
|
1723
|
+
this.initialized = true;
|
|
1724
|
+
}
|
|
1725
|
+
const tailPinned = this.pinTail && this.tailTarget !== null;
|
|
1726
|
+
let tailX = 0;
|
|
1727
|
+
let tailY = 0;
|
|
1728
|
+
if (tailPinned) {
|
|
1729
|
+
const t = this.tailTarget;
|
|
1730
|
+
tailX = wt.a * t.x + wt.c * t.y + wt.e;
|
|
1731
|
+
tailY = wt.b * t.x + wt.d * t.y + wt.f;
|
|
1732
|
+
}
|
|
1733
|
+
const gdt2x = this.gravity.x * dt * dt;
|
|
1734
|
+
const gdt2y = this.gravity.y * dt * dt;
|
|
1735
|
+
const impDx = this.impX * dt;
|
|
1736
|
+
const impDy = this.impY * dt;
|
|
1737
|
+
const damp = this.damping;
|
|
1738
|
+
const last = n5 - 1;
|
|
1739
|
+
for (let i = 1; i < n5; i++) {
|
|
1740
|
+
if (tailPinned && i === last) continue;
|
|
1741
|
+
const x = px[i];
|
|
1742
|
+
const y = py[i];
|
|
1743
|
+
px[i] = x + (x - prevx[i]) * damp + gdt2x + impDx;
|
|
1744
|
+
py[i] = y + (y - prevy[i]) * damp + gdt2y + impDy;
|
|
1745
|
+
prevx[i] = x;
|
|
1746
|
+
prevy[i] = y;
|
|
1747
|
+
}
|
|
1748
|
+
this.impX = 0;
|
|
1749
|
+
this.impY = 0;
|
|
1750
|
+
const rest3 = this.segmentLength;
|
|
1751
|
+
for (let k = 0; k < this.iterations; k++) {
|
|
1752
|
+
px[0] = headX;
|
|
1753
|
+
py[0] = headY;
|
|
1754
|
+
if (tailPinned) {
|
|
1755
|
+
px[last] = tailX;
|
|
1756
|
+
py[last] = tailY;
|
|
1757
|
+
}
|
|
1758
|
+
for (let i = 0; i < last; i++) {
|
|
1759
|
+
const dx = px[i + 1] - px[i];
|
|
1760
|
+
const dy = py[i + 1] - py[i];
|
|
1761
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1762
|
+
if (d === 0) continue;
|
|
1763
|
+
const diff = (d - rest3) / d;
|
|
1764
|
+
const aFixed = i === 0;
|
|
1765
|
+
const bFixed = tailPinned && i + 1 === last;
|
|
1766
|
+
if (aFixed && bFixed) continue;
|
|
1767
|
+
const wA = aFixed ? 0 : bFixed ? 1 : 0.5;
|
|
1768
|
+
const wB = bFixed ? 0 : aFixed ? 1 : 0.5;
|
|
1769
|
+
px[i] += dx * diff * wA;
|
|
1770
|
+
py[i] += dy * diff * wA;
|
|
1771
|
+
px[i + 1] -= dx * diff * wB;
|
|
1772
|
+
py[i + 1] -= dy * diff * wB;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
px[0] = headX;
|
|
1776
|
+
py[0] = headY;
|
|
1777
|
+
if (tailPinned) {
|
|
1778
|
+
px[last] = tailX;
|
|
1779
|
+
py[last] = tailY;
|
|
1780
|
+
}
|
|
1781
|
+
const inv = invertTransform(wt);
|
|
1782
|
+
for (let i = 0; i < n5; i++) {
|
|
1783
|
+
const p = this.local[i];
|
|
1784
|
+
const wx = px[i];
|
|
1785
|
+
const wy = py[i];
|
|
1786
|
+
p.x = inv.a * wx + inv.c * wy + inv.e;
|
|
1787
|
+
p.y = inv.b * wx + inv.d * wy + inv.f;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Stroked open poly of the chain. `points` are node-local and `world` is the
|
|
1792
|
+
* self-inclusive world transform handed to draw(), so commands emit with it
|
|
1793
|
+
* directly. With `taper`, each segment is its own 2-point poly at a linearly
|
|
1794
|
+
* decreasing width (segments are few, so this stays cheap); otherwise one poly.
|
|
1795
|
+
*/
|
|
1796
|
+
draw(out, world) {
|
|
1797
|
+
const n5 = this.local.length;
|
|
1798
|
+
if (n5 < 2) return;
|
|
1799
|
+
if (this.taper) {
|
|
1800
|
+
for (let i = 0; i < n5 - 1; i++) {
|
|
1801
|
+
const a = this.local[i];
|
|
1802
|
+
const b = this.local[i + 1];
|
|
1803
|
+
const t = 1 - i / (n5 - 1);
|
|
1804
|
+
out.push({
|
|
1805
|
+
kind: "poly",
|
|
1806
|
+
points: [a.x, a.y, b.x, b.y],
|
|
1807
|
+
closed: false,
|
|
1808
|
+
stroke: this.stroke,
|
|
1809
|
+
strokeWidth: Math.max(0.5, this.strokeWidth * t),
|
|
1810
|
+
round: true,
|
|
1811
|
+
transform: world,
|
|
1812
|
+
z: this.z,
|
|
1813
|
+
transient: true
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const pts = [];
|
|
1819
|
+
for (const p of this.local) pts.push(p.x, p.y);
|
|
1820
|
+
out.push({
|
|
1821
|
+
kind: "poly",
|
|
1822
|
+
points: pts,
|
|
1823
|
+
closed: false,
|
|
1824
|
+
stroke: this.stroke,
|
|
1825
|
+
strokeWidth: this.strokeWidth,
|
|
1826
|
+
round: true,
|
|
1827
|
+
transform: world,
|
|
1828
|
+
z: this.z,
|
|
1829
|
+
transient: true
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1374
1834
|
// src/scene/registry.ts
|
|
1375
1835
|
var registry = /* @__PURE__ */ new Map();
|
|
1376
1836
|
function registerNode(type, factory) {
|
|
@@ -1392,6 +1852,9 @@ function deserializeNode(data) {
|
|
|
1392
1852
|
node.scale = { ...data.scale };
|
|
1393
1853
|
node.z = data.z;
|
|
1394
1854
|
node.visible = data.visible;
|
|
1855
|
+
if (data.pivot) node.pivot = { ...data.pivot };
|
|
1856
|
+
if (data.props.pauseMode === "always" || data.props.pauseMode === "stopped") node.pauseMode = data.props.pauseMode;
|
|
1857
|
+
if (data.props.screenSpace === true) node.screenSpace = true;
|
|
1395
1858
|
node.applyProps(data.props);
|
|
1396
1859
|
for (const child of data.children) node.addChild(deserializeNode(child));
|
|
1397
1860
|
return node;
|
|
@@ -1401,20 +1864,72 @@ function deserializeNode(data) {
|
|
|
1401
1864
|
var InputState = class {
|
|
1402
1865
|
down = /* @__PURE__ */ new Set();
|
|
1403
1866
|
prev = /* @__PURE__ */ new Set();
|
|
1404
|
-
/**
|
|
1867
|
+
/**
|
|
1868
|
+
* Analog axes (e.g. from pointer or gamepad), sampled per step. By default
|
|
1869
|
+
* these are LIVE host samples — NOT hashed or logged (see PointerSource).
|
|
1870
|
+
* Axes passed to `beginFrame`'s second argument are the exception: they are
|
|
1871
|
+
* *logged* (enter `getState()` → hash, and the input log), so quantized analog
|
|
1872
|
+
* aim replays bit-exactly. Both live and logged values read back via `axis()`.
|
|
1873
|
+
*/
|
|
1405
1874
|
axes = /* @__PURE__ */ new Map();
|
|
1406
|
-
/**
|
|
1407
|
-
|
|
1875
|
+
/** The subset of axes that are part of the deterministic log + hash this frame. */
|
|
1876
|
+
logged = /* @__PURE__ */ new Map();
|
|
1877
|
+
/**
|
|
1878
|
+
* Action names some source has declared it can produce (null until the first
|
|
1879
|
+
* declareActions call). Headless worlds with no sources never declare, so
|
|
1880
|
+
* they never warn — the typo guard only arms once a host wires real input.
|
|
1881
|
+
*/
|
|
1882
|
+
declared = null;
|
|
1883
|
+
/** Unknown names already warned about (warn ONCE per name). */
|
|
1884
|
+
warned = /* @__PURE__ */ new Set();
|
|
1885
|
+
/**
|
|
1886
|
+
* Register action names a source (or input map) can produce. The browser
|
|
1887
|
+
* driver declares the keyboard map's actions and the pointer's `mouse.*`
|
|
1888
|
+
* buttons; custom sources should declare theirs too. Once at least one
|
|
1889
|
+
* declaration has happened, querying an undeclared action name via
|
|
1890
|
+
* `isDown`/`justPressed`/`justReleased` logs a console.warn ONCE per name —
|
|
1891
|
+
* catching `isDown('jmup')` typos that would otherwise fail silently.
|
|
1892
|
+
*/
|
|
1893
|
+
declareActions(names) {
|
|
1894
|
+
this.declared ??= /* @__PURE__ */ new Set();
|
|
1895
|
+
for (const n5 of names) this.declared.add(n5);
|
|
1896
|
+
}
|
|
1897
|
+
/** Warn-once guard for querying an action no source has declared. O(1). */
|
|
1898
|
+
checkDeclared(action) {
|
|
1899
|
+
if (this.declared === null || this.declared.has(action) || this.warned.has(action)) return;
|
|
1900
|
+
this.warned.add(action);
|
|
1901
|
+
console.warn(
|
|
1902
|
+
`hayao: input action "${action}" is not declared by any input source \u2014 declared actions: ${[...this.declared].sort().join(", ") || "(none)"}`
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Engine: set which actions are down this step (from live input or replay).
|
|
1907
|
+
* `axes` (optional) are QUANTIZED analog values that become part of the log
|
|
1908
|
+
* and hash for this frame — use `snapAxis`/`quantizeAngle` at the host edge so
|
|
1909
|
+
* replay and lockstep netplay reproduce analog aim exactly.
|
|
1910
|
+
*/
|
|
1911
|
+
beginFrame(actionsDown, axes) {
|
|
1408
1912
|
this.prev = new Set(this.down);
|
|
1409
1913
|
this.down = new Set(actionsDown);
|
|
1914
|
+
if (this.declared !== null) for (const a of this.down) this.declared.add(a);
|
|
1915
|
+
this.logged.clear();
|
|
1916
|
+
if (axes) {
|
|
1917
|
+
for (const k in axes) {
|
|
1918
|
+
this.logged.set(k, axes[k]);
|
|
1919
|
+
this.axes.set(k, axes[k]);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1410
1922
|
}
|
|
1411
1923
|
isDown(action) {
|
|
1924
|
+
this.checkDeclared(action);
|
|
1412
1925
|
return this.down.has(action);
|
|
1413
1926
|
}
|
|
1414
1927
|
justPressed(action) {
|
|
1928
|
+
this.checkDeclared(action);
|
|
1415
1929
|
return this.down.has(action) && !this.prev.has(action);
|
|
1416
1930
|
}
|
|
1417
1931
|
justReleased(action) {
|
|
1932
|
+
this.checkDeclared(action);
|
|
1418
1933
|
return !this.down.has(action) && this.prev.has(action);
|
|
1419
1934
|
}
|
|
1420
1935
|
axis(name) {
|
|
@@ -1425,13 +1940,33 @@ var InputState = class {
|
|
|
1425
1940
|
return [...this.down].sort();
|
|
1426
1941
|
}
|
|
1427
1942
|
getState() {
|
|
1428
|
-
|
|
1943
|
+
const s = {
|
|
1944
|
+
down: [...this.down].sort(),
|
|
1945
|
+
prev: [...this.prev].sort()
|
|
1946
|
+
};
|
|
1947
|
+
if (this.logged.size > 0) s.axes = [...this.logged].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
1948
|
+
return s;
|
|
1429
1949
|
}
|
|
1430
1950
|
setState(s) {
|
|
1431
1951
|
this.down = new Set(s.down);
|
|
1432
1952
|
this.prev = new Set(s.prev);
|
|
1953
|
+
this.logged.clear();
|
|
1954
|
+
if (s.axes) for (const [k, v] of s.axes) {
|
|
1955
|
+
this.logged.set(k, v);
|
|
1956
|
+
this.axes.set(k, v);
|
|
1957
|
+
}
|
|
1433
1958
|
}
|
|
1434
1959
|
};
|
|
1960
|
+
function snapAxis(value, buckets, min = -1, max = 1) {
|
|
1961
|
+
if (buckets < 2 || max === min) return value;
|
|
1962
|
+
const t = Math.min(1, Math.max(0, (value - min) / (max - min)));
|
|
1963
|
+
const b = Math.round(t * (buckets - 1));
|
|
1964
|
+
return min + b / (buckets - 1) * (max - min);
|
|
1965
|
+
}
|
|
1966
|
+
function quantizeAngle(radians, buckets) {
|
|
1967
|
+
const step = Math.PI * 2 / buckets;
|
|
1968
|
+
return Math.round(radians / step) * step;
|
|
1969
|
+
}
|
|
1435
1970
|
function keysToActions(map, keysDown) {
|
|
1436
1971
|
const actions = [];
|
|
1437
1972
|
for (const action in map) {
|
|
@@ -1441,19 +1976,32 @@ function keysToActions(map, keysDown) {
|
|
|
1441
1976
|
}
|
|
1442
1977
|
var InputRecorder = class {
|
|
1443
1978
|
frames = [];
|
|
1444
|
-
|
|
1979
|
+
axes = [];
|
|
1980
|
+
anyAxes = false;
|
|
1981
|
+
record(actionsDown, axes) {
|
|
1445
1982
|
this.frames.push(actionsDown.slice().sort());
|
|
1983
|
+
if (axes && Object.keys(axes).length > 0) {
|
|
1984
|
+
this.axes.push({ ...axes });
|
|
1985
|
+
this.anyAxes = true;
|
|
1986
|
+
} else {
|
|
1987
|
+
this.axes.push(void 0);
|
|
1988
|
+
}
|
|
1446
1989
|
}
|
|
1447
1990
|
get length() {
|
|
1448
1991
|
return this.frames.length;
|
|
1449
1992
|
}
|
|
1450
1993
|
toLog() {
|
|
1451
|
-
|
|
1994
|
+
const log = { frames: this.frames.map((f) => f.slice()) };
|
|
1995
|
+
if (this.anyAxes) log.axes = this.axes.map((a) => a ? { ...a } : void 0);
|
|
1996
|
+
return log;
|
|
1452
1997
|
}
|
|
1453
1998
|
};
|
|
1454
1999
|
function frameActions(log, i) {
|
|
1455
2000
|
return log.frames[i] ?? [];
|
|
1456
2001
|
}
|
|
2002
|
+
function frameAxes(log, i) {
|
|
2003
|
+
return log.axes?.[i];
|
|
2004
|
+
}
|
|
1457
2005
|
var DEFAULT_INPUT_MAP = {
|
|
1458
2006
|
up: ["ArrowUp", "KeyW"],
|
|
1459
2007
|
down: ["ArrowDown", "KeyS"],
|
|
@@ -1472,6 +2020,8 @@ var KeyboardSource = class {
|
|
|
1472
2020
|
keysDown = /* @__PURE__ */ new Set();
|
|
1473
2021
|
/** Virtual action taps (from DOM buttons etc.) pending consumption by a step. */
|
|
1474
2022
|
pressed = /* @__PURE__ */ new Set();
|
|
2023
|
+
/** Sustained virtual actions (held on-screen controls) — persist until released. */
|
|
2024
|
+
held = /* @__PURE__ */ new Set();
|
|
1475
2025
|
map;
|
|
1476
2026
|
target;
|
|
1477
2027
|
onDown;
|
|
@@ -1492,12 +2042,18 @@ var KeyboardSource = class {
|
|
|
1492
2042
|
}
|
|
1493
2043
|
/** The actions currently held down, as a stable sorted array. */
|
|
1494
2044
|
currentActions() {
|
|
1495
|
-
const
|
|
1496
|
-
|
|
2045
|
+
const codes = this.held.size === 0 ? this.keysDown : /* @__PURE__ */ new Set([...this.keysDown, ...this.held]);
|
|
2046
|
+
const acts = keysToActions(this.map, codes);
|
|
2047
|
+
if (this.pressed.size === 0 && this.held.size === 0) return acts;
|
|
1497
2048
|
const merged = new Set(acts);
|
|
1498
2049
|
for (const a of this.pressed) merged.add(a);
|
|
2050
|
+
for (const a of this.held) merged.add(a);
|
|
1499
2051
|
return [...merged].sort();
|
|
1500
2052
|
}
|
|
2053
|
+
/** The action names this source's map can produce (for InputState.declareActions). */
|
|
2054
|
+
actionNames() {
|
|
2055
|
+
return Object.keys(this.map);
|
|
2056
|
+
}
|
|
1501
2057
|
/**
|
|
1502
2058
|
* Virtually tap an action (DOM button, touch control). The tap is held until
|
|
1503
2059
|
* at least one fixed step has sampled it — the driver calls clearPressed()
|
|
@@ -1511,6 +2067,27 @@ var KeyboardSource = class {
|
|
|
1511
2067
|
clearPressed() {
|
|
1512
2068
|
this.pressed.clear();
|
|
1513
2069
|
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Sustain an action for as long as an on-screen control is held. Unlike
|
|
2072
|
+
* `press()` (an edge-shaped tap cleared after one step), a held action models
|
|
2073
|
+
* STATE: it stays in `currentActions()` every step until `setHeld(action,
|
|
2074
|
+
* false)` / `releaseHeld(action)`. This is what a virtual joystick or
|
|
2075
|
+
* hold-to-fire button wants — set it on drag-start, clear it on release, no
|
|
2076
|
+
* re-press-per-frame. Held actions still flow through the same deterministic
|
|
2077
|
+
* action set as keys.
|
|
2078
|
+
*/
|
|
2079
|
+
setHeld(action, on = true) {
|
|
2080
|
+
if (on) this.held.add(action);
|
|
2081
|
+
else this.held.delete(action);
|
|
2082
|
+
}
|
|
2083
|
+
/** Release a sustained action (sugar for `setHeld(action, false)`). */
|
|
2084
|
+
releaseHeld(action) {
|
|
2085
|
+
this.held.delete(action);
|
|
2086
|
+
}
|
|
2087
|
+
/** Drop all sustained actions (e.g. on blur / control teardown). */
|
|
2088
|
+
clearHeld() {
|
|
2089
|
+
this.held.clear();
|
|
2090
|
+
}
|
|
1514
2091
|
setMap(map) {
|
|
1515
2092
|
this.map = map;
|
|
1516
2093
|
}
|
|
@@ -1520,35 +2097,74 @@ var KeyboardSource = class {
|
|
|
1520
2097
|
globalThis.removeEventListener?.("blur", this.onBlur);
|
|
1521
2098
|
}
|
|
1522
2099
|
};
|
|
2100
|
+
var MOUSE_ACTIONS = ["mouse.left", "mouse.right", "mouse.middle"];
|
|
1523
2101
|
var PointerSource = class {
|
|
1524
2102
|
clientX = 0;
|
|
1525
2103
|
clientY = 0;
|
|
1526
2104
|
isDown = false;
|
|
2105
|
+
rightDown = false;
|
|
2106
|
+
middleDown = false;
|
|
1527
2107
|
seen = false;
|
|
2108
|
+
/** Live touches by pointerId (a pressed finger/button); excludes hover-only mouse. */
|
|
2109
|
+
touches = /* @__PURE__ */ new Map();
|
|
1528
2110
|
target;
|
|
1529
2111
|
el;
|
|
2112
|
+
keyboard;
|
|
1530
2113
|
onMove;
|
|
1531
2114
|
onDown;
|
|
1532
2115
|
onUp;
|
|
1533
|
-
|
|
2116
|
+
onCtx;
|
|
2117
|
+
constructor(target, opts = {}) {
|
|
1534
2118
|
this.target = target;
|
|
1535
2119
|
this.el = target.element;
|
|
2120
|
+
this.keyboard = opts.keyboard;
|
|
2121
|
+
const readButtons = (e) => {
|
|
2122
|
+
if (typeof e.buttons !== "number") return false;
|
|
2123
|
+
this.isDown = (e.buttons & 1) !== 0;
|
|
2124
|
+
this.rightDown = (e.buttons & 2) !== 0;
|
|
2125
|
+
this.middleDown = (e.buttons & 4) !== 0;
|
|
2126
|
+
return true;
|
|
2127
|
+
};
|
|
1536
2128
|
this.onMove = (e) => {
|
|
1537
2129
|
this.clientX = e.clientX;
|
|
1538
2130
|
this.clientY = e.clientY;
|
|
1539
2131
|
this.seen = true;
|
|
2132
|
+
if (typeof e.buttons === "number" && e.buttons !== 0) readButtons(e);
|
|
2133
|
+
const t = this.touches.get(e.pointerId);
|
|
2134
|
+
if (t) {
|
|
2135
|
+
t.clientX = e.clientX;
|
|
2136
|
+
t.clientY = e.clientY;
|
|
2137
|
+
}
|
|
1540
2138
|
};
|
|
1541
2139
|
this.onDown = (e) => {
|
|
1542
2140
|
this.clientX = e.clientX;
|
|
1543
2141
|
this.clientY = e.clientY;
|
|
1544
|
-
this.isDown = true;
|
|
1545
2142
|
this.seen = true;
|
|
2143
|
+
if (!readButtons(e)) {
|
|
2144
|
+
if (e.button === 2) this.rightDown = true;
|
|
2145
|
+
else if (e.button === 1) this.middleDown = true;
|
|
2146
|
+
else this.isDown = true;
|
|
2147
|
+
}
|
|
2148
|
+
this.touches.set(e.pointerId, { clientX: e.clientX, clientY: e.clientY });
|
|
1546
2149
|
};
|
|
1547
|
-
this.onUp = () => {
|
|
1548
|
-
|
|
2150
|
+
this.onUp = (e) => {
|
|
2151
|
+
if (!readButtons(e)) {
|
|
2152
|
+
this.isDown = false;
|
|
2153
|
+
if (e.button === 2) this.rightDown = false;
|
|
2154
|
+
else if (e.button === 1) this.middleDown = false;
|
|
2155
|
+
else {
|
|
2156
|
+
this.rightDown = false;
|
|
2157
|
+
this.middleDown = false;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
this.touches.delete(e?.pointerId);
|
|
1549
2161
|
};
|
|
2162
|
+
this.onCtx = (e) => e.preventDefault();
|
|
1550
2163
|
this.el?.addEventListener("pointermove", this.onMove);
|
|
1551
2164
|
this.el?.addEventListener("pointerdown", this.onDown);
|
|
2165
|
+
this.el?.addEventListener("pointerup", this.onUp);
|
|
2166
|
+
this.el?.addEventListener("pointercancel", this.onUp);
|
|
2167
|
+
if (!opts.contextMenu) this.el?.addEventListener("contextmenu", this.onCtx);
|
|
1552
2168
|
globalThis.addEventListener?.("pointerup", this.onUp);
|
|
1553
2169
|
globalThis.addEventListener?.("pointercancel", this.onUp);
|
|
1554
2170
|
}
|
|
@@ -1557,9 +2173,28 @@ var PointerSource = class {
|
|
|
1557
2173
|
const p = this.target.toDesign?.(this.clientX, this.clientY) ?? { x: 0, y: 0 };
|
|
1558
2174
|
return { x: p.x, y: p.y, down: this.isDown, active: this.seen };
|
|
1559
2175
|
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Every live (pressed) touch in DESIGN space, ordered by ascending id so the
|
|
2178
|
+
* array is stable to iterate. Empty when nothing is pressed. This is the
|
|
2179
|
+
* multitouch channel — a dual-stick game reads `left = touches[0]`, etc., or
|
|
2180
|
+
* partitions by screen half.
|
|
2181
|
+
*/
|
|
2182
|
+
readAll() {
|
|
2183
|
+
const ids = [...this.touches.keys()].sort((a, b) => a - b);
|
|
2184
|
+
return ids.map((id) => {
|
|
2185
|
+
const t = this.touches.get(id);
|
|
2186
|
+
const p = this.target.toDesign?.(t.clientX, t.clientY) ?? { x: 0, y: 0 };
|
|
2187
|
+
return { x: p.x, y: p.y, down: true, active: true, id };
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
1560
2190
|
/**
|
|
1561
2191
|
* Write the current sample into an InputState's axes as pointer.x / pointer.y /
|
|
1562
|
-
* pointer.down
|
|
2192
|
+
* pointer.down, plus pointer.right / pointer.middle (0/1) for the secondary
|
|
2193
|
+
* buttons. Call once per step (the browser driver does this before advance).
|
|
2194
|
+
*
|
|
2195
|
+
* When a keyboard is wired (PointerSourceOptions.keyboard), buttons are ALSO
|
|
2196
|
+
* held as actions `mouse.left` / `mouse.right` / `mouse.middle` so they flow
|
|
2197
|
+
* through the same deterministic actionsDown log as keys.
|
|
1563
2198
|
*/
|
|
1564
2199
|
sample(input) {
|
|
1565
2200
|
const r = this.read();
|
|
@@ -1567,15 +2202,110 @@ var PointerSource = class {
|
|
|
1567
2202
|
input.axes.set("pointer.x", q(r.x));
|
|
1568
2203
|
input.axes.set("pointer.y", q(r.y));
|
|
1569
2204
|
input.axes.set("pointer.down", r.down ? 1 : 0);
|
|
2205
|
+
input.axes.set("pointer.right", this.rightDown ? 1 : 0);
|
|
2206
|
+
input.axes.set("pointer.middle", this.middleDown ? 1 : 0);
|
|
2207
|
+
if (this.keyboard) {
|
|
2208
|
+
input.declareActions(MOUSE_ACTIONS);
|
|
2209
|
+
this.keyboard.setHeld("mouse.left", this.isDown);
|
|
2210
|
+
this.keyboard.setHeld("mouse.right", this.rightDown);
|
|
2211
|
+
this.keyboard.setHeld("mouse.middle", this.middleDown);
|
|
2212
|
+
}
|
|
1570
2213
|
}
|
|
1571
2214
|
dispose() {
|
|
2215
|
+
if (this.keyboard) for (const a of MOUSE_ACTIONS) this.keyboard.releaseHeld(a);
|
|
1572
2216
|
this.el?.removeEventListener("pointermove", this.onMove);
|
|
1573
2217
|
this.el?.removeEventListener("pointerdown", this.onDown);
|
|
2218
|
+
this.el?.removeEventListener("pointerup", this.onUp);
|
|
2219
|
+
this.el?.removeEventListener("pointercancel", this.onUp);
|
|
2220
|
+
this.el?.removeEventListener("contextmenu", this.onCtx);
|
|
1574
2221
|
globalThis.removeEventListener?.("pointerup", this.onUp);
|
|
1575
2222
|
globalThis.removeEventListener?.("pointercancel", this.onUp);
|
|
1576
2223
|
}
|
|
1577
2224
|
};
|
|
1578
2225
|
|
|
2226
|
+
// src/input/gamepad.ts
|
|
2227
|
+
var GamepadSource = class {
|
|
2228
|
+
index;
|
|
2229
|
+
stickBuckets;
|
|
2230
|
+
triggerBuckets;
|
|
2231
|
+
deadzone;
|
|
2232
|
+
buttonMap;
|
|
2233
|
+
keyboard;
|
|
2234
|
+
/** Track which buttons were pressed last frame to detect edge transitions. */
|
|
2235
|
+
prevPressed = /* @__PURE__ */ new Map();
|
|
2236
|
+
constructor(opts = {}) {
|
|
2237
|
+
this.index = opts.index ?? 0;
|
|
2238
|
+
this.stickBuckets = opts.stickBuckets ?? 128;
|
|
2239
|
+
this.triggerBuckets = opts.triggerBuckets ?? 64;
|
|
2240
|
+
this.deadzone = opts.deadzone ?? 0.12;
|
|
2241
|
+
this.buttonMap = opts.buttonMap ?? DEFAULT_GAMEPAD_MAP;
|
|
2242
|
+
this.keyboard = opts.keyboard;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Write the current gamepad state into InputState axes (quantized). Call once
|
|
2246
|
+
* per fixed step, before world.advance() — the browser driver does this via
|
|
2247
|
+
* the `sources` option in RunOptions. See also: runBrowser, GameHandle.
|
|
2248
|
+
*/
|
|
2249
|
+
sample(input) {
|
|
2250
|
+
const pad2 = navigator.getGamepads?.()[this.index] ?? null;
|
|
2251
|
+
if (!pad2) return;
|
|
2252
|
+
const applyDeadzone = (x, y) => {
|
|
2253
|
+
const mag = dhypot(x, y);
|
|
2254
|
+
if (mag < this.deadzone) return { x: 0, y: 0 };
|
|
2255
|
+
const scale = (mag - this.deadzone) / (1 - this.deadzone);
|
|
2256
|
+
const clampedMag = Math.min(scale, 1);
|
|
2257
|
+
return { x: x / mag * clampedMag, y: y / mag * clampedMag };
|
|
2258
|
+
};
|
|
2259
|
+
const ls = applyDeadzone(pad2.axes[0] ?? 0, pad2.axes[1] ?? 0);
|
|
2260
|
+
const rs = applyDeadzone(pad2.axes[2] ?? 0, pad2.axes[3] ?? 0);
|
|
2261
|
+
const qs = (v) => snapAxis(v, this.stickBuckets, -1, 1);
|
|
2262
|
+
const qt = (v) => snapAxis(v, this.triggerBuckets, 0, 1);
|
|
2263
|
+
input.axes.set("gamepad.lx", qs(ls.x));
|
|
2264
|
+
input.axes.set("gamepad.ly", qs(ls.y));
|
|
2265
|
+
input.axes.set("gamepad.rx", qs(rs.x));
|
|
2266
|
+
input.axes.set("gamepad.ry", qs(rs.y));
|
|
2267
|
+
input.axes.set("gamepad.lt", qt(pad2.buttons[6]?.value ?? 0));
|
|
2268
|
+
input.axes.set("gamepad.rt", qt(pad2.buttons[7]?.value ?? 0));
|
|
2269
|
+
const du = pad2.buttons[12]?.pressed ? 1 : 0;
|
|
2270
|
+
const dd = pad2.buttons[13]?.pressed ? 1 : 0;
|
|
2271
|
+
const dl = pad2.buttons[14]?.pressed ? 1 : 0;
|
|
2272
|
+
const dr = pad2.buttons[15]?.pressed ? 1 : 0;
|
|
2273
|
+
input.axes.set("gamepad.dpad.x", dr - dl);
|
|
2274
|
+
input.axes.set("gamepad.dpad.y", dd - du);
|
|
2275
|
+
if (this.keyboard) {
|
|
2276
|
+
for (const [btnStr, action] of Object.entries(this.buttonMap)) {
|
|
2277
|
+
const idx = Number(btnStr);
|
|
2278
|
+
const pressed = pad2.buttons[idx]?.pressed ?? false;
|
|
2279
|
+
this.keyboard.setHeld(action, pressed);
|
|
2280
|
+
this.prevPressed.set(idx, pressed);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Release all held actions and clean up. Call when removing the source or
|
|
2286
|
+
* switching gamepads.
|
|
2287
|
+
*/
|
|
2288
|
+
dispose() {
|
|
2289
|
+
if (this.keyboard) {
|
|
2290
|
+
for (const action of Object.values(this.buttonMap)) {
|
|
2291
|
+
this.keyboard.releaseHeld(action);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
this.prevPressed.clear();
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
var DEFAULT_GAMEPAD_MAP = {
|
|
2298
|
+
0: "confirm",
|
|
2299
|
+
1: "cancel",
|
|
2300
|
+
2: "action2",
|
|
2301
|
+
3: "action",
|
|
2302
|
+
9: "confirm",
|
|
2303
|
+
12: "up",
|
|
2304
|
+
13: "down",
|
|
2305
|
+
14: "left",
|
|
2306
|
+
15: "right"
|
|
2307
|
+
};
|
|
2308
|
+
|
|
1579
2309
|
// src/physics/tilemap.ts
|
|
1580
2310
|
var TILE = {
|
|
1581
2311
|
EMPTY: 0,
|
|
@@ -2903,7 +3633,7 @@ function rayCastRigid(rw, x0, y0, x1, y1, mask = 65535) {
|
|
|
2903
3633
|
|
|
2904
3634
|
// src/render/commands.ts
|
|
2905
3635
|
function sortCommands(cmds) {
|
|
2906
|
-
return cmds.map((c, i) => [c, i]).sort((a, b) => a[0].z - b[0].z || a[1] - b[1]).map(([c]) => c);
|
|
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);
|
|
2907
3637
|
}
|
|
2908
3638
|
|
|
2909
3639
|
// src/render/paint.ts
|
|
@@ -2946,6 +3676,10 @@ function shapeBBox(c) {
|
|
|
2946
3676
|
return { x: c.x, y: c.y, w: c.w, h: c.h };
|
|
2947
3677
|
case "circle":
|
|
2948
3678
|
return { x: c.cx - c.radius, y: c.cy - c.radius, w: c.radius * 2, h: c.radius * 2 };
|
|
3679
|
+
case "ellipse":
|
|
3680
|
+
return { x: c.cx - c.rx, y: c.cy - c.ry, w: c.rx * 2, h: c.ry * 2 };
|
|
3681
|
+
case "arc":
|
|
3682
|
+
return { x: c.cx - c.radius, y: c.cy - c.radius, w: c.radius * 2, h: c.radius * 2 };
|
|
2949
3683
|
case "poly": {
|
|
2950
3684
|
if (c.points.length < 2) return null;
|
|
2951
3685
|
let minX = c.points[0];
|
|
@@ -2964,6 +3698,39 @@ function shapeBBox(c) {
|
|
|
2964
3698
|
return null;
|
|
2965
3699
|
}
|
|
2966
3700
|
}
|
|
3701
|
+
function invalidCommandReason(c) {
|
|
3702
|
+
switch (c.kind) {
|
|
3703
|
+
case "rect":
|
|
3704
|
+
if (!Number.isFinite(c.x) || !Number.isFinite(c.y) || !Number.isFinite(c.w) || !Number.isFinite(c.h))
|
|
3705
|
+
return "non-finite x/y/w/h";
|
|
3706
|
+
return null;
|
|
3707
|
+
case "circle":
|
|
3708
|
+
if (!Number.isFinite(c.cx) || !Number.isFinite(c.cy)) return "non-finite center";
|
|
3709
|
+
if (!Number.isFinite(c.radius)) return "non-finite radius";
|
|
3710
|
+
if (c.radius < 0) return "negative radius";
|
|
3711
|
+
return null;
|
|
3712
|
+
case "ellipse":
|
|
3713
|
+
if (!Number.isFinite(c.cx) || !Number.isFinite(c.cy)) return "non-finite center";
|
|
3714
|
+
if (!Number.isFinite(c.rx) || !Number.isFinite(c.ry)) return "non-finite rx/ry";
|
|
3715
|
+
if (c.rx < 0 || c.ry < 0) return "negative rx/ry";
|
|
3716
|
+
return null;
|
|
3717
|
+
case "arc":
|
|
3718
|
+
if (!Number.isFinite(c.cx) || !Number.isFinite(c.cy)) return "non-finite center";
|
|
3719
|
+
if (!Number.isFinite(c.radius)) return "non-finite radius";
|
|
3720
|
+
if (c.radius < 0) return "negative radius";
|
|
3721
|
+
if (!Number.isFinite(c.start) || !Number.isFinite(c.end)) return "non-finite start/end";
|
|
3722
|
+
return null;
|
|
3723
|
+
default:
|
|
3724
|
+
return null;
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
var warnedCommands = /* @__PURE__ */ new Set();
|
|
3728
|
+
function warnCommandOnce(kind, reason, detail) {
|
|
3729
|
+
const key = `${kind}:${reason}`;
|
|
3730
|
+
if (warnedCommands.has(key)) return;
|
|
3731
|
+
warnedCommands.add(key);
|
|
3732
|
+
console.warn(`hayao/render: skipped '${kind}' command \u2014 ${reason}`, detail);
|
|
3733
|
+
}
|
|
2967
3734
|
function canvasGradient(ctx, g, bbox) {
|
|
2968
3735
|
const px = (u) => bbox.x + u * bbox.w;
|
|
2969
3736
|
const py = (v) => bbox.y + v * bbox.h;
|
|
@@ -2988,6 +3755,16 @@ function clientToDesign(rect, width, height, clientX, clientY) {
|
|
|
2988
3755
|
y: (clientY - rect.top - offsetY) / scale
|
|
2989
3756
|
};
|
|
2990
3757
|
}
|
|
3758
|
+
function fitViewport(rect, width, height) {
|
|
3759
|
+
const scale = Math.min(rect.width / width, rect.height / height) || 1;
|
|
3760
|
+
return {
|
|
3761
|
+
x: (rect.width - width * scale) / 2,
|
|
3762
|
+
y: (rect.height - height * scale) / 2,
|
|
3763
|
+
width: width * scale,
|
|
3764
|
+
height: height * scale,
|
|
3765
|
+
scale
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
2991
3768
|
|
|
2992
3769
|
// src/render/svgString.ts
|
|
2993
3770
|
var n2 = (v) => Number.isInteger(v) ? String(v) : (Math.round(v * 1e3) / 1e3).toString();
|
|
@@ -3001,6 +3778,7 @@ function paintAttrs(p, fillOverride, filterId) {
|
|
|
3001
3778
|
parts.push(`stroke="${p.stroke}"`);
|
|
3002
3779
|
parts.push(`stroke-width="${n2(p.strokeWidth ?? 1)}"`);
|
|
3003
3780
|
if (p.round) parts.push('stroke-linejoin="round" stroke-linecap="round"');
|
|
3781
|
+
if (p.lineDash && p.lineDash.length) parts.push(`stroke-dasharray="${p.lineDash.map(n2).join(" ")}"`);
|
|
3004
3782
|
}
|
|
3005
3783
|
if (p.opacity !== void 0 && p.opacity !== 1) parts.push(`opacity="${n2(p.opacity)}"`);
|
|
3006
3784
|
if (filterId) parts.push(`filter="url(#${filterId})"`);
|
|
@@ -3009,6 +3787,24 @@ function paintAttrs(p, fillOverride, filterId) {
|
|
|
3009
3787
|
function escapeText(s) {
|
|
3010
3788
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3011
3789
|
}
|
|
3790
|
+
function arcPathData(c) {
|
|
3791
|
+
const r = c.radius;
|
|
3792
|
+
const x0 = c.cx + dcos(c.start) * r;
|
|
3793
|
+
const y0 = c.cy + dsin(c.start) * r;
|
|
3794
|
+
const raw = c.end - c.start;
|
|
3795
|
+
let sweep = (raw % TAU + TAU) % TAU;
|
|
3796
|
+
if (sweep === 0 && raw !== 0) sweep = TAU;
|
|
3797
|
+
if (sweep >= TAU - 1e-9) {
|
|
3798
|
+
const xh = c.cx + dcos(c.start + Math.PI) * r;
|
|
3799
|
+
const yh = c.cy + dsin(c.start + Math.PI) * r;
|
|
3800
|
+
return `M ${n2(x0)} ${n2(y0)} A ${n2(r)} ${n2(r)} 0 1 1 ${n2(xh)} ${n2(yh)} A ${n2(r)} ${n2(r)} 0 1 1 ${n2(x0)} ${n2(y0)} Z`;
|
|
3801
|
+
}
|
|
3802
|
+
const x1 = c.cx + dcos(c.start + sweep) * r;
|
|
3803
|
+
const y1 = c.cy + dsin(c.start + sweep) * r;
|
|
3804
|
+
const large = sweep > Math.PI ? 1 : 0;
|
|
3805
|
+
const a = `A ${n2(r)} ${n2(r)} 0 ${large} 1 ${n2(x1)} ${n2(y1)}`;
|
|
3806
|
+
return c.sector ? `M ${n2(c.cx)} ${n2(c.cy)} L ${n2(x0)} ${n2(y0)} ${a} Z` : `M ${n2(x0)} ${n2(y0)} ${a}`;
|
|
3807
|
+
}
|
|
3012
3808
|
function commandToSVG(c, defs, idBase) {
|
|
3013
3809
|
const tf = `transform="${matrix(c.transform)}"`;
|
|
3014
3810
|
let fillOverride;
|
|
@@ -3028,6 +3824,10 @@ function commandToSVG(c, defs, idBase) {
|
|
|
3028
3824
|
return `<rect x="${n2(c.x)}" y="${n2(c.y)}" width="${n2(c.w)}" height="${n2(c.h)}"${c.r ? ` rx="${n2(c.r)}"` : ""} ${tf} ${paint}/>`;
|
|
3029
3825
|
case "circle":
|
|
3030
3826
|
return `<circle cx="${n2(c.cx)}" cy="${n2(c.cy)}" r="${n2(c.radius)}" ${tf} ${paint}/>`;
|
|
3827
|
+
case "ellipse":
|
|
3828
|
+
return `<ellipse cx="${n2(c.cx)}" cy="${n2(c.cy)}" rx="${n2(c.rx)}" ry="${n2(c.ry)}" ${tf} ${paint}/>`;
|
|
3829
|
+
case "arc":
|
|
3830
|
+
return `<path d="${arcPathData(c)}" ${tf} ${paint}/>`;
|
|
3031
3831
|
case "poly": {
|
|
3032
3832
|
const pts = [];
|
|
3033
3833
|
for (let i = 0; i < c.points.length; i += 2) pts.push(`${n2(c.points[i])},${n2(c.points[i + 1])}`);
|
|
@@ -3054,7 +3854,14 @@ function commandToSVG(c, defs, idBase) {
|
|
|
3054
3854
|
}
|
|
3055
3855
|
function commandsToSVGInner(commands, idPrefix = "h") {
|
|
3056
3856
|
const defs = [];
|
|
3057
|
-
const body = sortCommands(commands).map((c, i) =>
|
|
3857
|
+
const body = sortCommands(commands).map((c, i) => {
|
|
3858
|
+
const bad = invalidCommandReason(c);
|
|
3859
|
+
if (bad) {
|
|
3860
|
+
warnCommandOnce(c.kind, bad, c);
|
|
3861
|
+
return "";
|
|
3862
|
+
}
|
|
3863
|
+
return commandToSVG(c, defs, `${idPrefix}${i}`);
|
|
3864
|
+
}).join("");
|
|
3058
3865
|
return (defs.length ? `<defs>${defs.join("")}</defs>` : "") + body;
|
|
3059
3866
|
}
|
|
3060
3867
|
function renderToSVGString(commands, width, height, background = "#ffffff") {
|
|
@@ -3107,11 +3914,152 @@ var SvgRenderer = class {
|
|
|
3107
3914
|
toDesign(clientX, clientY) {
|
|
3108
3915
|
return clientToDesign(this.svg.getBoundingClientRect(), this.width, this.height, clientX, clientY);
|
|
3109
3916
|
}
|
|
3917
|
+
viewport() {
|
|
3918
|
+
return fitViewport(this.svg.getBoundingClientRect(), this.width, this.height);
|
|
3919
|
+
}
|
|
3110
3920
|
dispose() {
|
|
3111
3921
|
this.svg.remove();
|
|
3112
3922
|
}
|
|
3113
3923
|
};
|
|
3114
3924
|
|
|
3925
|
+
// src/render/canvas2d-core.ts
|
|
3926
|
+
function fillFor(ctx, c) {
|
|
3927
|
+
const p = c;
|
|
3928
|
+
if (p.gradient) {
|
|
3929
|
+
const bbox = shapeBBox(c);
|
|
3930
|
+
if (bbox && bbox.w > 0 && bbox.h > 0) return canvasGradient(ctx, p.gradient, bbox);
|
|
3931
|
+
const stops = p.gradient.stops;
|
|
3932
|
+
return stops.length ? stops[stops.length - 1].color : p.fill;
|
|
3933
|
+
}
|
|
3934
|
+
return p.fill;
|
|
3935
|
+
}
|
|
3936
|
+
function applyStroke(ctx, c) {
|
|
3937
|
+
const fill = fillFor(ctx, c);
|
|
3938
|
+
if (fill && fill !== "none") {
|
|
3939
|
+
ctx.fillStyle = fill;
|
|
3940
|
+
ctx.fill();
|
|
3941
|
+
}
|
|
3942
|
+
if (c.stroke) {
|
|
3943
|
+
ctx.strokeStyle = c.stroke;
|
|
3944
|
+
ctx.lineWidth = c.strokeWidth ?? 1;
|
|
3945
|
+
if (c.round) {
|
|
3946
|
+
ctx.lineJoin = "round";
|
|
3947
|
+
ctx.lineCap = "round";
|
|
3948
|
+
}
|
|
3949
|
+
ctx.stroke();
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
function roundRectPath(ctx, x, y, w, h, r) {
|
|
3953
|
+
const rr = Math.min(r, w / 2, h / 2);
|
|
3954
|
+
ctx.moveTo(x + rr, y);
|
|
3955
|
+
ctx.arcTo(x + w, y, x + w, y + h, rr);
|
|
3956
|
+
ctx.arcTo(x + w, y + h, x, y + h, rr);
|
|
3957
|
+
ctx.arcTo(x, y + h, x, y, rr);
|
|
3958
|
+
ctx.arcTo(x, y, x + w, y, rr);
|
|
3959
|
+
ctx.closePath();
|
|
3960
|
+
}
|
|
3961
|
+
function paintCommand(ctx, c) {
|
|
3962
|
+
switch (c.kind) {
|
|
3963
|
+
case "rect":
|
|
3964
|
+
ctx.beginPath();
|
|
3965
|
+
if (c.r) roundRectPath(ctx, c.x, c.y, c.w, c.h, c.r);
|
|
3966
|
+
else ctx.rect(c.x, c.y, c.w, c.h);
|
|
3967
|
+
applyStroke(ctx, c);
|
|
3968
|
+
break;
|
|
3969
|
+
case "circle":
|
|
3970
|
+
ctx.beginPath();
|
|
3971
|
+
ctx.arc(c.cx, c.cy, c.radius, 0, Math.PI * 2);
|
|
3972
|
+
applyStroke(ctx, c);
|
|
3973
|
+
break;
|
|
3974
|
+
case "ellipse":
|
|
3975
|
+
ctx.beginPath();
|
|
3976
|
+
ctx.ellipse(c.cx, c.cy, c.rx, c.ry, 0, 0, Math.PI * 2);
|
|
3977
|
+
applyStroke(ctx, c);
|
|
3978
|
+
break;
|
|
3979
|
+
case "arc":
|
|
3980
|
+
ctx.beginPath();
|
|
3981
|
+
if (c.sector) ctx.moveTo(c.cx, c.cy);
|
|
3982
|
+
ctx.arc(c.cx, c.cy, c.radius, c.start, c.end);
|
|
3983
|
+
if (c.sector) ctx.closePath();
|
|
3984
|
+
applyStroke(ctx, c);
|
|
3985
|
+
break;
|
|
3986
|
+
case "poly":
|
|
3987
|
+
ctx.beginPath();
|
|
3988
|
+
for (let i = 0; i < c.points.length; i += 2) {
|
|
3989
|
+
if (i === 0) ctx.moveTo(c.points[i], c.points[i + 1]);
|
|
3990
|
+
else ctx.lineTo(c.points[i], c.points[i + 1]);
|
|
3991
|
+
}
|
|
3992
|
+
if (c.closed) ctx.closePath();
|
|
3993
|
+
applyStroke(ctx, c);
|
|
3994
|
+
break;
|
|
3995
|
+
case "path": {
|
|
3996
|
+
const p = new Path2D(c.d);
|
|
3997
|
+
const fill = fillFor(ctx, c);
|
|
3998
|
+
if (fill && fill !== "none") {
|
|
3999
|
+
ctx.fillStyle = fill;
|
|
4000
|
+
ctx.fill(p);
|
|
4001
|
+
}
|
|
4002
|
+
if (c.stroke) {
|
|
4003
|
+
ctx.strokeStyle = c.stroke;
|
|
4004
|
+
ctx.lineWidth = c.strokeWidth ?? 1;
|
|
4005
|
+
if (c.round) {
|
|
4006
|
+
ctx.lineJoin = "round";
|
|
4007
|
+
ctx.lineCap = "round";
|
|
4008
|
+
}
|
|
4009
|
+
ctx.stroke(p);
|
|
4010
|
+
}
|
|
4011
|
+
break;
|
|
4012
|
+
}
|
|
4013
|
+
case "text":
|
|
4014
|
+
ctx.font = `${c.weight ?? 400} ${c.size}px ${c.font ?? "sans-serif"}`;
|
|
4015
|
+
ctx.textAlign = c.align ?? "left";
|
|
4016
|
+
ctx.textBaseline = "middle";
|
|
4017
|
+
if (c.stroke) {
|
|
4018
|
+
ctx.strokeStyle = c.stroke;
|
|
4019
|
+
ctx.lineWidth = c.strokeWidth ?? 1;
|
|
4020
|
+
ctx.lineJoin = "round";
|
|
4021
|
+
ctx.lineCap = "round";
|
|
4022
|
+
ctx.strokeText(c.text, c.x, c.y);
|
|
4023
|
+
}
|
|
4024
|
+
ctx.fillStyle = c.fill ?? "#000";
|
|
4025
|
+
ctx.fillText(c.text, c.x, c.y);
|
|
4026
|
+
break;
|
|
4027
|
+
case "image":
|
|
4028
|
+
break;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
function drawToCanvas2D(ctx, commands, width, height, background, scale) {
|
|
4032
|
+
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
4033
|
+
ctx.fillStyle = background;
|
|
4034
|
+
ctx.fillRect(0, 0, width, height);
|
|
4035
|
+
for (const c of sortCommands(commands)) {
|
|
4036
|
+
const bad = invalidCommandReason(c);
|
|
4037
|
+
if (bad) {
|
|
4038
|
+
warnCommandOnce(c.kind, bad, c);
|
|
4039
|
+
continue;
|
|
4040
|
+
}
|
|
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
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
|
|
3115
4063
|
// src/render/canvas.ts
|
|
3116
4064
|
var Canvas2DRenderer = class {
|
|
3117
4065
|
width;
|
|
@@ -3144,120 +4092,7 @@ var Canvas2DRenderer = class {
|
|
|
3144
4092
|
this.canvas.height = Math.round(this.height * this.dpr);
|
|
3145
4093
|
}
|
|
3146
4094
|
draw(commands) {
|
|
3147
|
-
|
|
3148
|
-
ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
3149
|
-
ctx.fillStyle = this.background;
|
|
3150
|
-
ctx.fillRect(0, 0, this.width, this.height);
|
|
3151
|
-
for (const c of sortCommands(commands)) {
|
|
3152
|
-
ctx.save();
|
|
3153
|
-
const t = c.transform;
|
|
3154
|
-
ctx.transform(t.a, t.b, t.c, t.d, t.e, t.f);
|
|
3155
|
-
ctx.globalAlpha = c.opacity ?? 1;
|
|
3156
|
-
const sh = c.shadow;
|
|
3157
|
-
if (sh) {
|
|
3158
|
-
ctx.shadowColor = sh.color;
|
|
3159
|
-
ctx.shadowBlur = sh.blur;
|
|
3160
|
-
ctx.shadowOffsetX = sh.dx ?? 0;
|
|
3161
|
-
ctx.shadowOffsetY = sh.dy ?? 0;
|
|
3162
|
-
}
|
|
3163
|
-
this.paint(ctx, c);
|
|
3164
|
-
ctx.restore();
|
|
3165
|
-
}
|
|
3166
|
-
}
|
|
3167
|
-
/** Resolve a fill: a gradient (mapped to the shape's bbox) or a solid color. */
|
|
3168
|
-
fillFor(ctx, c) {
|
|
3169
|
-
const p = c;
|
|
3170
|
-
if (p.gradient) {
|
|
3171
|
-
const bbox = shapeBBox(c);
|
|
3172
|
-
if (bbox && bbox.w > 0 && bbox.h > 0) return canvasGradient(ctx, p.gradient, bbox);
|
|
3173
|
-
const stops = p.gradient.stops;
|
|
3174
|
-
return stops.length ? stops[stops.length - 1].color : p.fill;
|
|
3175
|
-
}
|
|
3176
|
-
return p.fill;
|
|
3177
|
-
}
|
|
3178
|
-
stroke(ctx, c) {
|
|
3179
|
-
const fill = this.fillFor(ctx, c);
|
|
3180
|
-
if (fill && fill !== "none") {
|
|
3181
|
-
ctx.fillStyle = fill;
|
|
3182
|
-
ctx.fill();
|
|
3183
|
-
}
|
|
3184
|
-
if (c.stroke) {
|
|
3185
|
-
ctx.strokeStyle = c.stroke;
|
|
3186
|
-
ctx.lineWidth = c.strokeWidth ?? 1;
|
|
3187
|
-
if (c.round) {
|
|
3188
|
-
ctx.lineJoin = "round";
|
|
3189
|
-
ctx.lineCap = "round";
|
|
3190
|
-
}
|
|
3191
|
-
ctx.stroke();
|
|
3192
|
-
}
|
|
3193
|
-
}
|
|
3194
|
-
paint(ctx, c) {
|
|
3195
|
-
switch (c.kind) {
|
|
3196
|
-
case "rect":
|
|
3197
|
-
ctx.beginPath();
|
|
3198
|
-
if (c.r) this.roundRect(ctx, c.x, c.y, c.w, c.h, c.r);
|
|
3199
|
-
else ctx.rect(c.x, c.y, c.w, c.h);
|
|
3200
|
-
this.stroke(ctx, c);
|
|
3201
|
-
break;
|
|
3202
|
-
case "circle":
|
|
3203
|
-
ctx.beginPath();
|
|
3204
|
-
ctx.arc(c.cx, c.cy, c.radius, 0, Math.PI * 2);
|
|
3205
|
-
this.stroke(ctx, c);
|
|
3206
|
-
break;
|
|
3207
|
-
case "poly":
|
|
3208
|
-
ctx.beginPath();
|
|
3209
|
-
for (let i = 0; i < c.points.length; i += 2) {
|
|
3210
|
-
if (i === 0) ctx.moveTo(c.points[i], c.points[i + 1]);
|
|
3211
|
-
else ctx.lineTo(c.points[i], c.points[i + 1]);
|
|
3212
|
-
}
|
|
3213
|
-
if (c.closed) ctx.closePath();
|
|
3214
|
-
this.stroke(ctx, c);
|
|
3215
|
-
break;
|
|
3216
|
-
case "path":
|
|
3217
|
-
{
|
|
3218
|
-
const p = new Path2D(c.d);
|
|
3219
|
-
const fill = this.fillFor(ctx, c);
|
|
3220
|
-
if (fill && fill !== "none") {
|
|
3221
|
-
ctx.fillStyle = fill;
|
|
3222
|
-
ctx.fill(p);
|
|
3223
|
-
}
|
|
3224
|
-
if (c.stroke) {
|
|
3225
|
-
ctx.strokeStyle = c.stroke;
|
|
3226
|
-
ctx.lineWidth = c.strokeWidth ?? 1;
|
|
3227
|
-
if (c.round) {
|
|
3228
|
-
ctx.lineJoin = "round";
|
|
3229
|
-
ctx.lineCap = "round";
|
|
3230
|
-
}
|
|
3231
|
-
ctx.stroke(p);
|
|
3232
|
-
}
|
|
3233
|
-
}
|
|
3234
|
-
break;
|
|
3235
|
-
case "text":
|
|
3236
|
-
ctx.font = `${c.weight ?? 400} ${c.size}px ${c.font ?? "sans-serif"}`;
|
|
3237
|
-
ctx.textAlign = c.align ?? "left";
|
|
3238
|
-
ctx.textBaseline = "middle";
|
|
3239
|
-
if (c.stroke) {
|
|
3240
|
-
ctx.strokeStyle = c.stroke;
|
|
3241
|
-
ctx.lineWidth = c.strokeWidth ?? 1;
|
|
3242
|
-
ctx.lineJoin = "round";
|
|
3243
|
-
ctx.lineCap = "round";
|
|
3244
|
-
ctx.strokeText(c.text, c.x, c.y);
|
|
3245
|
-
}
|
|
3246
|
-
ctx.fillStyle = c.fill ?? "#000";
|
|
3247
|
-
ctx.fillText(c.text, c.x, c.y);
|
|
3248
|
-
break;
|
|
3249
|
-
case "image":
|
|
3250
|
-
break;
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
roundRect(ctx, x, y, w, h, r) {
|
|
3254
|
-
const rr = Math.min(r, w / 2, h / 2);
|
|
3255
|
-
ctx.moveTo(x + rr, y);
|
|
3256
|
-
ctx.arcTo(x + w, y, x + w, y + h, rr);
|
|
3257
|
-
ctx.arcTo(x + w, y + h, x, y + h, rr);
|
|
3258
|
-
ctx.arcTo(x, y + h, x, y, rr);
|
|
3259
|
-
ctx.arcTo(x, y, x + w, y, rr);
|
|
3260
|
-
ctx.closePath();
|
|
4095
|
+
drawToCanvas2D(this.ctx, commands, this.width, this.height, this.background, this.dpr);
|
|
3261
4096
|
}
|
|
3262
4097
|
get element() {
|
|
3263
4098
|
return this.canvas;
|
|
@@ -3266,11 +4101,517 @@ var Canvas2DRenderer = class {
|
|
|
3266
4101
|
toDesign(clientX, clientY) {
|
|
3267
4102
|
return clientToDesign(this.canvas.getBoundingClientRect(), this.width, this.height, clientX, clientY);
|
|
3268
4103
|
}
|
|
4104
|
+
viewport() {
|
|
4105
|
+
return fitViewport(this.canvas.getBoundingClientRect(), this.width, this.height);
|
|
4106
|
+
}
|
|
3269
4107
|
dispose() {
|
|
3270
4108
|
this.canvas.remove();
|
|
3271
4109
|
}
|
|
3272
4110
|
};
|
|
3273
4111
|
|
|
4112
|
+
// src/render/webgl.ts
|
|
4113
|
+
var VERT_SRC = (
|
|
4114
|
+
/* glsl */
|
|
4115
|
+
`#version 300 es
|
|
4116
|
+
out vec2 v_uv;
|
|
4117
|
+
void main() {
|
|
4118
|
+
const vec2 CORNERS[4] = vec2[](
|
|
4119
|
+
vec2(-1.0, -1.0), vec2(1.0, -1.0),
|
|
4120
|
+
vec2(-1.0, 1.0), vec2(1.0, 1.0)
|
|
4121
|
+
);
|
|
4122
|
+
const vec2 UVS[4] = vec2[](
|
|
4123
|
+
vec2(0.0, 1.0), vec2(1.0, 1.0),
|
|
4124
|
+
vec2(0.0, 0.0), vec2(1.0, 0.0)
|
|
4125
|
+
);
|
|
4126
|
+
v_uv = UVS[gl_VertexID];
|
|
4127
|
+
gl_Position = vec4(CORNERS[gl_VertexID], 0.0, 1.0);
|
|
4128
|
+
}`
|
|
4129
|
+
);
|
|
4130
|
+
var PASSTHROUGH_FRAG = (
|
|
4131
|
+
/* glsl */
|
|
4132
|
+
`#version 300 es
|
|
4133
|
+
precision mediump float;
|
|
4134
|
+
in vec2 v_uv;
|
|
4135
|
+
uniform sampler2D u_scene;
|
|
4136
|
+
uniform sampler2D u_prev;
|
|
4137
|
+
out vec4 fragColor;
|
|
4138
|
+
void main() { fragColor = texture(u_prev, v_uv); }`
|
|
4139
|
+
);
|
|
4140
|
+
var WEBGL_EFFECTS = {
|
|
4141
|
+
/** Passthrough — no effect. */
|
|
4142
|
+
passthrough: PASSTHROUGH_FRAG,
|
|
4143
|
+
/** Pixelate: blockSize in pixels (default 4). setUniform('u_block', N) */
|
|
4144
|
+
pixelate: (
|
|
4145
|
+
/* glsl */
|
|
4146
|
+
`#version 300 es
|
|
4147
|
+
precision mediump float;
|
|
4148
|
+
in vec2 v_uv;
|
|
4149
|
+
uniform sampler2D u_prev;
|
|
4150
|
+
uniform vec2 u_resolution;
|
|
4151
|
+
uniform float u_block;
|
|
4152
|
+
out vec4 fragColor;
|
|
4153
|
+
void main() {
|
|
4154
|
+
float block = max(1.0, u_block > 0.0 ? u_block : 4.0);
|
|
4155
|
+
vec2 px = floor(v_uv * u_resolution / block) * block / u_resolution;
|
|
4156
|
+
fragColor = texture(u_prev, px);
|
|
4157
|
+
}`
|
|
4158
|
+
),
|
|
4159
|
+
/** Vignette: darkens edges. setUniform('u_vignette', 0.0..1.0, default 0.4) */
|
|
4160
|
+
vignette: (
|
|
4161
|
+
/* glsl */
|
|
4162
|
+
`#version 300 es
|
|
4163
|
+
precision mediump float;
|
|
4164
|
+
in vec2 v_uv;
|
|
4165
|
+
uniform sampler2D u_prev;
|
|
4166
|
+
uniform float u_vignette;
|
|
4167
|
+
out vec4 fragColor;
|
|
4168
|
+
void main() {
|
|
4169
|
+
vec4 col = texture(u_prev, v_uv);
|
|
4170
|
+
vec2 uv = v_uv - 0.5;
|
|
4171
|
+
float d = dot(uv, uv);
|
|
4172
|
+
float strength = u_vignette > 0.0 ? u_vignette : 0.4;
|
|
4173
|
+
fragColor = col * (1.0 - d * strength * 4.0);
|
|
4174
|
+
}`
|
|
4175
|
+
),
|
|
4176
|
+
/** CRT scanlines + slight barrel distortion. u_time drives the scan roll. */
|
|
4177
|
+
crt: (
|
|
4178
|
+
/* glsl */
|
|
4179
|
+
`#version 300 es
|
|
4180
|
+
precision mediump float;
|
|
4181
|
+
in vec2 v_uv;
|
|
4182
|
+
uniform sampler2D u_prev;
|
|
4183
|
+
uniform vec2 u_resolution;
|
|
4184
|
+
uniform float u_time;
|
|
4185
|
+
uniform float u_crt_lines; // scanline density, default 600
|
|
4186
|
+
uniform float u_crt_warp; // barrel strength, default 0.08
|
|
4187
|
+
out vec4 fragColor;
|
|
4188
|
+
void main() {
|
|
4189
|
+
float lines = u_crt_lines > 0.0 ? u_crt_lines : 600.0;
|
|
4190
|
+
float warp = u_crt_warp > 0.0 ? u_crt_warp : 0.08;
|
|
4191
|
+
// barrel distortion
|
|
4192
|
+
vec2 uv = v_uv - 0.5;
|
|
4193
|
+
float r2 = dot(uv, uv);
|
|
4194
|
+
uv *= 1.0 + warp * r2;
|
|
4195
|
+
uv += 0.5;
|
|
4196
|
+
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
|
|
4197
|
+
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
|
4198
|
+
return;
|
|
4199
|
+
}
|
|
4200
|
+
vec4 col = texture(u_prev, uv);
|
|
4201
|
+
// rolling scanlines
|
|
4202
|
+
float scan = sin((uv.y * lines + u_time * 40.0) * 3.14159) * 0.5 + 0.5;
|
|
4203
|
+
col.rgb *= 0.75 + 0.25 * scan;
|
|
4204
|
+
// slight green phosphor tint
|
|
4205
|
+
col.rgb = mix(col.rgb, col.rgb * vec3(0.9, 1.05, 0.85), 0.3);
|
|
4206
|
+
fragColor = col;
|
|
4207
|
+
}`
|
|
4208
|
+
),
|
|
4209
|
+
/** Chromatic aberration / RGB fringe. setUniform('u_aberration', 0..0.01, default 0.004) */
|
|
4210
|
+
chromaticAberration: (
|
|
4211
|
+
/* glsl */
|
|
4212
|
+
`#version 300 es
|
|
4213
|
+
precision mediump float;
|
|
4214
|
+
in vec2 v_uv;
|
|
4215
|
+
uniform sampler2D u_prev;
|
|
4216
|
+
uniform float u_aberration;
|
|
4217
|
+
out vec4 fragColor;
|
|
4218
|
+
void main() {
|
|
4219
|
+
float amt = u_aberration > 0.0 ? u_aberration : 0.004;
|
|
4220
|
+
vec2 dir = v_uv - 0.5;
|
|
4221
|
+
float r = texture(u_prev, v_uv + dir * amt * 2.0).r;
|
|
4222
|
+
float g = texture(u_prev, v_uv).g;
|
|
4223
|
+
float b = texture(u_prev, v_uv - dir * amt * 2.0).b;
|
|
4224
|
+
fragColor = vec4(r, g, b, 1.0);
|
|
4225
|
+
}`
|
|
4226
|
+
),
|
|
4227
|
+
/** Palette-shift / hue rotation. u_hue_shift in radians, driven by u_time for animation. */
|
|
4228
|
+
hueRotate: (
|
|
4229
|
+
/* glsl */
|
|
4230
|
+
`#version 300 es
|
|
4231
|
+
precision mediump float;
|
|
4232
|
+
in vec2 v_uv;
|
|
4233
|
+
uniform sampler2D u_prev;
|
|
4234
|
+
uniform float u_hue_shift; // radians; if 0 uses u_time
|
|
4235
|
+
uniform float u_time;
|
|
4236
|
+
out vec4 fragColor;
|
|
4237
|
+
void main() {
|
|
4238
|
+
float angle = u_hue_shift != 0.0 ? u_hue_shift : u_time * 0.5;
|
|
4239
|
+
vec4 col = texture(u_prev, v_uv);
|
|
4240
|
+
// Hue rotate via Rodrigues in YIQ-ish space
|
|
4241
|
+
float c = cos(angle), s = sin(angle);
|
|
4242
|
+
mat3 m = mat3(
|
|
4243
|
+
0.299+0.701*c+0.168*s, 0.587-0.587*c+0.330*s, 0.114-0.114*c-0.497*s,
|
|
4244
|
+
0.299-0.299*c-0.328*s, 0.587+0.413*c+0.035*s, 0.114-0.114*c+0.292*s,
|
|
4245
|
+
0.299-0.300*c+1.250*s, 0.587-0.588*c-1.050*s, 0.114+0.886*c-0.203*s
|
|
4246
|
+
);
|
|
4247
|
+
fragColor = vec4(m * col.rgb, col.a);
|
|
4248
|
+
}`
|
|
4249
|
+
),
|
|
4250
|
+
/** Wave distortion (heat haze, underwater). setUniform('u_wave_amp', default 0.003) */
|
|
4251
|
+
wave: (
|
|
4252
|
+
/* glsl */
|
|
4253
|
+
`#version 300 es
|
|
4254
|
+
precision mediump float;
|
|
4255
|
+
in vec2 v_uv;
|
|
4256
|
+
uniform sampler2D u_prev;
|
|
4257
|
+
uniform float u_time;
|
|
4258
|
+
uniform float u_wave_amp;
|
|
4259
|
+
uniform float u_wave_freq;
|
|
4260
|
+
out vec4 fragColor;
|
|
4261
|
+
void main() {
|
|
4262
|
+
float amp = u_wave_amp > 0.0 ? u_wave_amp : 0.003;
|
|
4263
|
+
float freq = u_wave_freq > 0.0 ? u_wave_freq : 8.0;
|
|
4264
|
+
vec2 uv = v_uv;
|
|
4265
|
+
uv.x += sin(uv.y * freq + u_time * 2.5) * amp;
|
|
4266
|
+
uv.y += cos(uv.x * freq + u_time * 2.5) * amp;
|
|
4267
|
+
fragColor = texture(u_prev, clamp(uv, 0.0, 1.0));
|
|
4268
|
+
}`
|
|
4269
|
+
),
|
|
4270
|
+
// ── Multi-pass bloom building blocks ───────────────────────────────────────
|
|
4271
|
+
// Use with setPipeline([brightpass, hblur, vblur, bloomComposite]).
|
|
4272
|
+
/** Bloom pass 1/4: extract bright pixels above threshold (default 0.6). */
|
|
4273
|
+
bloomBrightpass: (
|
|
4274
|
+
/* glsl */
|
|
4275
|
+
`#version 300 es
|
|
4276
|
+
precision mediump float;
|
|
4277
|
+
in vec2 v_uv;
|
|
4278
|
+
uniform sampler2D u_prev;
|
|
4279
|
+
uniform float u_bloom_threshold;
|
|
4280
|
+
out vec4 fragColor;
|
|
4281
|
+
void main() {
|
|
4282
|
+
vec4 col = texture(u_prev, v_uv);
|
|
4283
|
+
float lum = dot(col.rgb, vec3(0.2126, 0.7152, 0.0722));
|
|
4284
|
+
float thresh = u_bloom_threshold > 0.0 ? u_bloom_threshold : 0.55;
|
|
4285
|
+
float weight = smoothstep(thresh - 0.05, thresh + 0.05, lum);
|
|
4286
|
+
fragColor = col * weight;
|
|
4287
|
+
}`
|
|
4288
|
+
),
|
|
4289
|
+
/** Bloom pass 2/4: horizontal gaussian blur (13-tap). */
|
|
4290
|
+
bloomHBlur: (
|
|
4291
|
+
/* glsl */
|
|
4292
|
+
`#version 300 es
|
|
4293
|
+
precision mediump float;
|
|
4294
|
+
in vec2 v_uv;
|
|
4295
|
+
uniform sampler2D u_prev;
|
|
4296
|
+
uniform vec2 u_resolution;
|
|
4297
|
+
uniform float u_bloom_spread;
|
|
4298
|
+
out vec4 fragColor;
|
|
4299
|
+
void main() {
|
|
4300
|
+
float spread = u_bloom_spread > 0.0 ? u_bloom_spread : 1.0;
|
|
4301
|
+
vec2 texel = vec2(spread / u_resolution.x, 0.0);
|
|
4302
|
+
const float W[7] = float[](0.0625, 0.125, 0.1875, 0.25, 0.1875, 0.125, 0.0625);
|
|
4303
|
+
vec4 sum = vec4(0.0);
|
|
4304
|
+
for (int i = 0; i < 7; i++)
|
|
4305
|
+
sum += texture(u_prev, clamp(v_uv + float(i-3) * texel, 0.0, 1.0)) * W[i];
|
|
4306
|
+
fragColor = sum;
|
|
4307
|
+
}`
|
|
4308
|
+
),
|
|
4309
|
+
/** Bloom pass 3/4: vertical gaussian blur (13-tap). */
|
|
4310
|
+
bloomVBlur: (
|
|
4311
|
+
/* glsl */
|
|
4312
|
+
`#version 300 es
|
|
4313
|
+
precision mediump float;
|
|
4314
|
+
in vec2 v_uv;
|
|
4315
|
+
uniform sampler2D u_prev;
|
|
4316
|
+
uniform vec2 u_resolution;
|
|
4317
|
+
uniform float u_bloom_spread;
|
|
4318
|
+
out vec4 fragColor;
|
|
4319
|
+
void main() {
|
|
4320
|
+
float spread = u_bloom_spread > 0.0 ? u_bloom_spread : 1.0;
|
|
4321
|
+
vec2 texel = vec2(0.0, spread / u_resolution.y);
|
|
4322
|
+
const float W[7] = float[](0.0625, 0.125, 0.1875, 0.25, 0.1875, 0.125, 0.0625);
|
|
4323
|
+
vec4 sum = vec4(0.0);
|
|
4324
|
+
for (int i = 0; i < 7; i++)
|
|
4325
|
+
sum += texture(u_prev, clamp(v_uv + float(i-3) * texel, 0.0, 1.0)) * W[i];
|
|
4326
|
+
fragColor = sum;
|
|
4327
|
+
}`
|
|
4328
|
+
),
|
|
4329
|
+
/** Bloom pass 4/4: composite original + blurred bloom. u_bloom_intensity controls strength. */
|
|
4330
|
+
bloomComposite: (
|
|
4331
|
+
/* glsl */
|
|
4332
|
+
`#version 300 es
|
|
4333
|
+
precision mediump float;
|
|
4334
|
+
in vec2 v_uv;
|
|
4335
|
+
uniform sampler2D u_scene;
|
|
4336
|
+
uniform sampler2D u_prev;
|
|
4337
|
+
uniform float u_bloom_intensity;
|
|
4338
|
+
out vec4 fragColor;
|
|
4339
|
+
void main() {
|
|
4340
|
+
vec4 scene = texture(u_scene, v_uv);
|
|
4341
|
+
vec4 bloom = texture(u_prev, v_uv);
|
|
4342
|
+
float intensity = u_bloom_intensity > 0.0 ? u_bloom_intensity : 0.8;
|
|
4343
|
+
fragColor = vec4(scene.rgb + bloom.rgb * intensity, scene.a);
|
|
4344
|
+
}`
|
|
4345
|
+
)
|
|
4346
|
+
};
|
|
4347
|
+
var BLOOM_PIPELINE = [
|
|
4348
|
+
{ shader: WEBGL_EFFECTS.bloomBrightpass },
|
|
4349
|
+
{ shader: WEBGL_EFFECTS.bloomHBlur },
|
|
4350
|
+
{ shader: WEBGL_EFFECTS.bloomVBlur },
|
|
4351
|
+
{ shader: WEBGL_EFFECTS.bloomComposite }
|
|
4352
|
+
];
|
|
4353
|
+
function compileShader(gl, type, src) {
|
|
4354
|
+
const sh = gl.createShader(type);
|
|
4355
|
+
if (!sh) throw new Error("hayao/webgl: createShader failed");
|
|
4356
|
+
gl.shaderSource(sh, src);
|
|
4357
|
+
gl.compileShader(sh);
|
|
4358
|
+
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
4359
|
+
const log = gl.getShaderInfoLog(sh);
|
|
4360
|
+
gl.deleteShader(sh);
|
|
4361
|
+
throw new Error(`hayao/webgl: shader compile error
|
|
4362
|
+
${log}`);
|
|
4363
|
+
}
|
|
4364
|
+
return sh;
|
|
4365
|
+
}
|
|
4366
|
+
function buildProgram(gl, fragSrc) {
|
|
4367
|
+
const vert = compileShader(gl, gl.VERTEX_SHADER, VERT_SRC);
|
|
4368
|
+
const frag = compileShader(gl, gl.FRAGMENT_SHADER, fragSrc);
|
|
4369
|
+
const prog = gl.createProgram();
|
|
4370
|
+
if (!prog) throw new Error("hayao/webgl: createProgram failed");
|
|
4371
|
+
gl.attachShader(prog, vert);
|
|
4372
|
+
gl.attachShader(prog, frag);
|
|
4373
|
+
gl.linkProgram(prog);
|
|
4374
|
+
gl.deleteShader(vert);
|
|
4375
|
+
gl.deleteShader(frag);
|
|
4376
|
+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
4377
|
+
const log = gl.getProgramInfoLog(prog);
|
|
4378
|
+
gl.deleteProgram(prog);
|
|
4379
|
+
throw new Error(`hayao/webgl: program link error
|
|
4380
|
+
${log}`);
|
|
4381
|
+
}
|
|
4382
|
+
return prog;
|
|
4383
|
+
}
|
|
4384
|
+
function applyUniform(gl, prog, name, value) {
|
|
4385
|
+
const loc = gl.getUniformLocation(prog, name);
|
|
4386
|
+
if (loc === null) return;
|
|
4387
|
+
if (typeof value === "number") {
|
|
4388
|
+
gl.uniform1f(loc, value);
|
|
4389
|
+
} else if (value.length === 2) {
|
|
4390
|
+
gl.uniform2f(loc, value[0], value[1]);
|
|
4391
|
+
} else if (value.length === 3) {
|
|
4392
|
+
gl.uniform3f(loc, value[0], value[1], value[2]);
|
|
4393
|
+
} else {
|
|
4394
|
+
gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
function makeFboSlot(gl, w, h) {
|
|
4398
|
+
const tex = gl.createTexture();
|
|
4399
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
4400
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
4401
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
4402
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
4403
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
4404
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
4405
|
+
const fbo = gl.createFramebuffer();
|
|
4406
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
4407
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
4408
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
4409
|
+
return { fbo, tex };
|
|
4410
|
+
}
|
|
4411
|
+
var WebGL2Renderer = class {
|
|
4412
|
+
width;
|
|
4413
|
+
height;
|
|
4414
|
+
background;
|
|
4415
|
+
glCanvas;
|
|
4416
|
+
gl;
|
|
4417
|
+
offscreen;
|
|
4418
|
+
ctx2d;
|
|
4419
|
+
dpr = 1;
|
|
4420
|
+
/** Original rasterized scene texture. */
|
|
4421
|
+
sceneTex;
|
|
4422
|
+
/** Intermediate pass framebuffers (one per non-final pass). */
|
|
4423
|
+
fbos = [];
|
|
4424
|
+
/** Compiled programs, one per pass. */
|
|
4425
|
+
programs = [];
|
|
4426
|
+
/** Per-pass uniform maps (merged with global uniforms). */
|
|
4427
|
+
passUniforms = [];
|
|
4428
|
+
/** Empty VAO for gl_VertexID-only draws. */
|
|
4429
|
+
vao;
|
|
4430
|
+
/** Frames drawn, driving u_time at a nominal 60 fps (cosmetic, but deterministic). */
|
|
4431
|
+
framesDrawn = 0;
|
|
4432
|
+
/** Global custom uniforms applied to every pass. */
|
|
4433
|
+
uniforms = /* @__PURE__ */ new Map();
|
|
4434
|
+
constructor(config) {
|
|
4435
|
+
this.width = config.width;
|
|
4436
|
+
this.height = config.height;
|
|
4437
|
+
this.background = config.background ?? "#ffffff";
|
|
4438
|
+
this.glCanvas = document.createElement("canvas");
|
|
4439
|
+
this.glCanvas.style.width = "100%";
|
|
4440
|
+
this.glCanvas.style.height = "100%";
|
|
4441
|
+
this.glCanvas.style.objectFit = "contain";
|
|
4442
|
+
this.glCanvas.style.display = "block";
|
|
4443
|
+
const gl = this.glCanvas.getContext("webgl2", { alpha: false, antialias: false, preserveDrawingBuffer: false });
|
|
4444
|
+
if (!gl) throw new Error("hayao/webgl: WebGL2 unavailable \u2014 fall back to Canvas2DRenderer");
|
|
4445
|
+
this.gl = gl;
|
|
4446
|
+
this.offscreen = document.createElement("canvas");
|
|
4447
|
+
const ctx2d = this.offscreen.getContext("2d");
|
|
4448
|
+
if (!ctx2d) throw new Error("hayao/webgl: Canvas2D unavailable for rasterization");
|
|
4449
|
+
this.ctx2d = ctx2d;
|
|
4450
|
+
this.resize();
|
|
4451
|
+
const vao = gl.createVertexArray();
|
|
4452
|
+
if (!vao) throw new Error("hayao/webgl: createVertexArray failed");
|
|
4453
|
+
this.vao = vao;
|
|
4454
|
+
const tex = gl.createTexture();
|
|
4455
|
+
if (!tex) throw new Error("hayao/webgl: createTexture failed");
|
|
4456
|
+
this.sceneTex = tex;
|
|
4457
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
4458
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
4459
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
4460
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
4461
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
4462
|
+
this.buildPipeline([{ shader: config.postProcess ?? PASSTHROUGH_FRAG }]);
|
|
4463
|
+
}
|
|
4464
|
+
mount(parent) {
|
|
4465
|
+
parent.appendChild(this.glCanvas);
|
|
4466
|
+
this.resize();
|
|
4467
|
+
}
|
|
4468
|
+
resize() {
|
|
4469
|
+
this.dpr = Math.min(3, globalThis.devicePixelRatio || 1);
|
|
4470
|
+
const w = Math.round(this.width * this.dpr);
|
|
4471
|
+
const h = Math.round(this.height * this.dpr);
|
|
4472
|
+
this.glCanvas.width = w;
|
|
4473
|
+
this.glCanvas.height = h;
|
|
4474
|
+
this.offscreen.width = w;
|
|
4475
|
+
this.offscreen.height = h;
|
|
4476
|
+
this.gl.viewport(0, 0, w, h);
|
|
4477
|
+
for (const slot of this.fbos) {
|
|
4478
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, slot.tex);
|
|
4479
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, w, h, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null);
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
buildPipeline(passes) {
|
|
4483
|
+
const gl = this.gl;
|
|
4484
|
+
const w = this.glCanvas.width || Math.round(this.width * this.dpr);
|
|
4485
|
+
const h = this.glCanvas.height || Math.round(this.height * this.dpr);
|
|
4486
|
+
for (const p of this.programs) gl.deleteProgram(p);
|
|
4487
|
+
for (const s of this.fbos) {
|
|
4488
|
+
gl.deleteFramebuffer(s.fbo);
|
|
4489
|
+
gl.deleteTexture(s.tex);
|
|
4490
|
+
}
|
|
4491
|
+
this.programs = [];
|
|
4492
|
+
this.fbos = [];
|
|
4493
|
+
this.passUniforms = [];
|
|
4494
|
+
for (const pass of passes) {
|
|
4495
|
+
this.programs.push(buildProgram(gl, pass.shader));
|
|
4496
|
+
this.passUniforms.push(pass.uniforms ?? {});
|
|
4497
|
+
}
|
|
4498
|
+
for (let i = 0; i < passes.length - 1; i++) {
|
|
4499
|
+
this.fbos.push(makeFboSlot(gl, w, h));
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
draw(commands) {
|
|
4503
|
+
const gl = this.gl;
|
|
4504
|
+
const n5 = this.programs.length;
|
|
4505
|
+
const t = this.framesDrawn++ / 60;
|
|
4506
|
+
const w = this.glCanvas.width;
|
|
4507
|
+
const h = this.glCanvas.height;
|
|
4508
|
+
drawToCanvas2D(this.ctx2d, commands, this.width, this.height, this.background, this.dpr);
|
|
4509
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
4510
|
+
gl.bindTexture(gl.TEXTURE_2D, this.sceneTex);
|
|
4511
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.offscreen);
|
|
4512
|
+
gl.bindVertexArray(this.vao);
|
|
4513
|
+
for (let i = 0; i < n5; i++) {
|
|
4514
|
+
const prog = this.programs[i];
|
|
4515
|
+
const isLast = i === n5 - 1;
|
|
4516
|
+
if (isLast) {
|
|
4517
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
4518
|
+
gl.viewport(0, 0, w, h);
|
|
4519
|
+
} else {
|
|
4520
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbos[i].fbo);
|
|
4521
|
+
gl.viewport(0, 0, w, h);
|
|
4522
|
+
}
|
|
4523
|
+
gl.useProgram(prog);
|
|
4524
|
+
gl.uniform1i(gl.getUniformLocation(prog, "u_scene"), 0);
|
|
4525
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
4526
|
+
if (i === 0) {
|
|
4527
|
+
gl.bindTexture(gl.TEXTURE_2D, this.sceneTex);
|
|
4528
|
+
} else {
|
|
4529
|
+
gl.bindTexture(gl.TEXTURE_2D, this.fbos[i - 1].tex);
|
|
4530
|
+
}
|
|
4531
|
+
gl.uniform1i(gl.getUniformLocation(prog, "u_prev"), 1);
|
|
4532
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
4533
|
+
gl.uniform1f(gl.getUniformLocation(prog, "u_time"), t);
|
|
4534
|
+
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), w, h);
|
|
4535
|
+
for (const [name, val] of this.uniforms) applyUniform(gl, prog, name, val);
|
|
4536
|
+
for (const [name, val] of Object.entries(this.passUniforms[i])) applyUniform(gl, prog, name, val);
|
|
4537
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
// ── Public control API ────────────────────────────────────────────────────
|
|
4541
|
+
/**
|
|
4542
|
+
* Set a custom uniform applied to every pass. Useful for knob-driven effect
|
|
4543
|
+
* parameters (intensity, scale, colour) without recompiling the shader.
|
|
4544
|
+
* The value is set on every draw() until cleared or overridden.
|
|
4545
|
+
*
|
|
4546
|
+
* ```ts
|
|
4547
|
+
* renderer.setUniform('u_vignette', 0.5);
|
|
4548
|
+
* renderer.setUniform('u_bloom_intensity', [1.0, 0.8]);
|
|
4549
|
+
* ```
|
|
4550
|
+
*/
|
|
4551
|
+
setUniform(name, value) {
|
|
4552
|
+
this.uniforms.set(name, value);
|
|
4553
|
+
return this;
|
|
4554
|
+
}
|
|
4555
|
+
/** Remove a custom uniform. */
|
|
4556
|
+
clearUniform(name) {
|
|
4557
|
+
this.uniforms.delete(name);
|
|
4558
|
+
return this;
|
|
4559
|
+
}
|
|
4560
|
+
/**
|
|
4561
|
+
* Replace the post-processing pipeline. Accepts an array of passes; each
|
|
4562
|
+
* pass reads `u_prev` (previous output) and `u_scene` (original frame).
|
|
4563
|
+
* Throws on shader compile error — previous pipeline stays active.
|
|
4564
|
+
*
|
|
4565
|
+
* For single-pass effects, prefer `setPostProcess(shader)`. For the standard
|
|
4566
|
+
* bloom setup, use `setPipeline(BLOOM_PIPELINE)`.
|
|
4567
|
+
*
|
|
4568
|
+
* ```ts
|
|
4569
|
+
* renderer.setPipeline([
|
|
4570
|
+
* { shader: WEBGL_EFFECTS.bloomBrightpass },
|
|
4571
|
+
* { shader: WEBGL_EFFECTS.bloomHBlur },
|
|
4572
|
+
* { shader: WEBGL_EFFECTS.bloomVBlur },
|
|
4573
|
+
* { shader: WEBGL_EFFECTS.bloomComposite },
|
|
4574
|
+
* ]);
|
|
4575
|
+
* renderer.setUniform('u_bloom_intensity', 1.0);
|
|
4576
|
+
* ```
|
|
4577
|
+
*/
|
|
4578
|
+
setPipeline(passes) {
|
|
4579
|
+
this.buildPipeline(passes.length ? passes : [{ shader: PASSTHROUGH_FRAG }]);
|
|
4580
|
+
return this;
|
|
4581
|
+
}
|
|
4582
|
+
/**
|
|
4583
|
+
* Convenience: set a single-pass post-processing shader.
|
|
4584
|
+
* Throws on compile error — previous effect stays active.
|
|
4585
|
+
*/
|
|
4586
|
+
setPostProcess(fragmentShader) {
|
|
4587
|
+
return this.setPipeline([{ shader: fragmentShader }]);
|
|
4588
|
+
}
|
|
4589
|
+
/** Restore passthrough (no post-processing). */
|
|
4590
|
+
clearPostProcess() {
|
|
4591
|
+
return this.setPostProcess(PASSTHROUGH_FRAG);
|
|
4592
|
+
}
|
|
4593
|
+
get element() {
|
|
4594
|
+
return this.glCanvas;
|
|
4595
|
+
}
|
|
4596
|
+
toDesign(clientX, clientY) {
|
|
4597
|
+
return clientToDesign(this.glCanvas.getBoundingClientRect(), this.width, this.height, clientX, clientY);
|
|
4598
|
+
}
|
|
4599
|
+
viewport() {
|
|
4600
|
+
return fitViewport(this.glCanvas.getBoundingClientRect(), this.width, this.height);
|
|
4601
|
+
}
|
|
4602
|
+
dispose() {
|
|
4603
|
+
const gl = this.gl;
|
|
4604
|
+
gl.deleteTexture(this.sceneTex);
|
|
4605
|
+
for (const p of this.programs) gl.deleteProgram(p);
|
|
4606
|
+
for (const s of this.fbos) {
|
|
4607
|
+
gl.deleteFramebuffer(s.fbo);
|
|
4608
|
+
gl.deleteTexture(s.tex);
|
|
4609
|
+
}
|
|
4610
|
+
gl.deleteVertexArray(this.vao);
|
|
4611
|
+
this.glCanvas.remove();
|
|
4612
|
+
}
|
|
4613
|
+
};
|
|
4614
|
+
|
|
3274
4615
|
// src/render/headless.ts
|
|
3275
4616
|
var HeadlessRenderer = class {
|
|
3276
4617
|
width;
|
|
@@ -4541,6 +5882,7 @@ function resolve(s) {
|
|
|
4541
5882
|
pitchJumpTime: s.pitchJumpTime ?? 0,
|
|
4542
5883
|
vibrato: s.vibrato ?? 0,
|
|
4543
5884
|
vibratoFreq: s.vibratoFreq ?? 6,
|
|
5885
|
+
repeat: Math.max(0, s.repeat ?? 0),
|
|
4544
5886
|
detune: s.detune ?? 0,
|
|
4545
5887
|
sub: Math.max(0, s.sub ?? 0),
|
|
4546
5888
|
noise: s.noise ?? 0,
|
|
@@ -4601,9 +5943,10 @@ function renderSound(spec, opts = {}) {
|
|
|
4601
5943
|
let env = adsr(t, r);
|
|
4602
5944
|
if (r.punch > 0) env *= 1 + r.punch * Math.max(0, 1 - t / Math.max(1e-4, r.attack + r.decay));
|
|
4603
5945
|
if (r.tremolo > 0) env *= 1 - r.tremolo * 0.5 * (1 - dsin(TAU * r.tremoloFreq * t));
|
|
4604
|
-
const
|
|
5946
|
+
const pt = r.repeat > 0 ? t - Math.floor(t / r.repeat) * r.repeat : t;
|
|
5947
|
+
const prog = body > 0 ? pt / body : 0;
|
|
4605
5948
|
let f = r.freq * semis(r.slide * prog + r.slideAccel * prog * prog);
|
|
4606
|
-
if (r.pitchJump !== 0 &&
|
|
5949
|
+
if (r.pitchJump !== 0 && pt >= r.pitchJumpTime) f *= semis(r.pitchJump);
|
|
4607
5950
|
if (r.vibrato > 0) f *= semis(r.vibrato * dsin(TAU * r.vibratoFreq * t));
|
|
4608
5951
|
if (r.fm > 0) f += r.fm * dsin(TAU * r.fmFreq * t);
|
|
4609
5952
|
if (f < 0) f = 0;
|
|
@@ -5052,33 +6395,56 @@ var INSTRUMENTS = {
|
|
|
5052
6395
|
// src/audio/audio.ts
|
|
5053
6396
|
var DEFAULT_VOLUMES = { master: 0.7, music: 0.6, sfx: 0.8, muted: false };
|
|
5054
6397
|
var AudioBus = class {
|
|
5055
|
-
|
|
6398
|
+
_ctx = null;
|
|
5056
6399
|
master = null;
|
|
5057
6400
|
musicGain = null;
|
|
5058
6401
|
sfxGain = null;
|
|
5059
6402
|
vol = { ...DEFAULT_VOLUMES };
|
|
5060
6403
|
padOn = false;
|
|
6404
|
+
/**
|
|
6405
|
+
* The live AudioContext (null until `start()` unlocks audio). External
|
|
6406
|
+
* synths being ported in (bundled ZzFX/ZzFXM, hand-rolled Web Audio) should
|
|
6407
|
+
* create their sources ON this context — never their own — so they inherit
|
|
6408
|
+
* the autoplay unlock the user gesture already paid for.
|
|
6409
|
+
*/
|
|
6410
|
+
get ctx() {
|
|
6411
|
+
return this._ctx;
|
|
6412
|
+
}
|
|
6413
|
+
/**
|
|
6414
|
+
* The SFX mix bus (null until `start()`). Connect external one-shot sources
|
|
6415
|
+
* here instead of `ctx.destination` so master/sfx volume and mute apply.
|
|
6416
|
+
*/
|
|
6417
|
+
get sfxBus() {
|
|
6418
|
+
return this.sfxGain;
|
|
6419
|
+
}
|
|
6420
|
+
/**
|
|
6421
|
+
* The music mix bus (null until `start()`). Connect external music sources
|
|
6422
|
+
* (e.g. a ZzFXM-rendered buffer) here so master/music volume and mute apply.
|
|
6423
|
+
*/
|
|
6424
|
+
get musicBus() {
|
|
6425
|
+
return this.musicGain;
|
|
6426
|
+
}
|
|
5061
6427
|
get available() {
|
|
5062
6428
|
return typeof globalThis.AudioContext !== "undefined" || "webkitAudioContext" in globalThis;
|
|
5063
6429
|
}
|
|
5064
6430
|
get started() {
|
|
5065
|
-
return !!this.
|
|
6431
|
+
return !!this._ctx;
|
|
5066
6432
|
}
|
|
5067
6433
|
/** Must be called from a user gesture (browser autoplay policy). */
|
|
5068
6434
|
start() {
|
|
5069
|
-
if (this.
|
|
5070
|
-
void this.
|
|
6435
|
+
if (this._ctx) {
|
|
6436
|
+
void this._ctx.resume();
|
|
5071
6437
|
return;
|
|
5072
6438
|
}
|
|
5073
6439
|
if (!this.available) return;
|
|
5074
6440
|
const Ctx = globalThis.AudioContext || globalThis.webkitAudioContext;
|
|
5075
|
-
this.
|
|
5076
|
-
this.master = this.
|
|
5077
|
-
this.musicGain = this.
|
|
5078
|
-
this.sfxGain = this.
|
|
6441
|
+
this._ctx = new Ctx();
|
|
6442
|
+
this.master = this._ctx.createGain();
|
|
6443
|
+
this.musicGain = this._ctx.createGain();
|
|
6444
|
+
this.sfxGain = this._ctx.createGain();
|
|
5079
6445
|
this.musicGain.connect(this.master);
|
|
5080
6446
|
this.sfxGain.connect(this.master);
|
|
5081
|
-
this.master.connect(this.
|
|
6447
|
+
this.master.connect(this._ctx.destination);
|
|
5082
6448
|
this.applyVolumes();
|
|
5083
6449
|
}
|
|
5084
6450
|
setVolumes(v) {
|
|
@@ -5089,27 +6455,27 @@ var AudioBus = class {
|
|
|
5089
6455
|
return { ...this.vol };
|
|
5090
6456
|
}
|
|
5091
6457
|
applyVolumes() {
|
|
5092
|
-
if (!this.
|
|
5093
|
-
const t = this.
|
|
6458
|
+
if (!this._ctx || !this.master || !this.musicGain || !this.sfxGain) return;
|
|
6459
|
+
const t = this._ctx.currentTime;
|
|
5094
6460
|
this.master.gain.setTargetAtTime(this.vol.muted ? 0 : this.vol.master, t, 0.04);
|
|
5095
6461
|
this.musicGain.gain.setTargetAtTime(this.vol.music * 0.5, t, 0.04);
|
|
5096
6462
|
this.sfxGain.gain.setTargetAtTime(this.vol.sfx, t, 0.04);
|
|
5097
6463
|
}
|
|
5098
6464
|
/** Play a single tone (no-op if audio unstarted). */
|
|
5099
6465
|
tone(spec) {
|
|
5100
|
-
if (!this.
|
|
6466
|
+
if (!this._ctx || !this.sfxGain) return;
|
|
5101
6467
|
const { freq, duration, type = "sine", gain = 0.2, delay = 0, pan } = spec;
|
|
5102
|
-
const t = this.
|
|
5103
|
-
const osc = this.
|
|
6468
|
+
const t = this._ctx.currentTime + delay;
|
|
6469
|
+
const osc = this._ctx.createOscillator();
|
|
5104
6470
|
osc.type = type;
|
|
5105
6471
|
osc.frequency.value = freq;
|
|
5106
|
-
const g = this.
|
|
6472
|
+
const g = this._ctx.createGain();
|
|
5107
6473
|
g.gain.setValueAtTime(0, t);
|
|
5108
6474
|
g.gain.linearRampToValueAtTime(gain, t + 8e-3);
|
|
5109
6475
|
g.gain.exponentialRampToValueAtTime(1e-4, t + duration);
|
|
5110
6476
|
osc.connect(g);
|
|
5111
|
-
if (pan !== void 0 && typeof this.
|
|
5112
|
-
const p = this.
|
|
6477
|
+
if (pan !== void 0 && typeof this._ctx.createStereoPanner === "function") {
|
|
6478
|
+
const p = this._ctx.createStereoPanner();
|
|
5113
6479
|
p.pan.value = Math.max(-1, Math.min(1, pan));
|
|
5114
6480
|
g.connect(p);
|
|
5115
6481
|
p.connect(this.sfxGain);
|
|
@@ -5139,24 +6505,24 @@ var AudioBus = class {
|
|
|
5139
6505
|
* No-op without an AudioContext.
|
|
5140
6506
|
*/
|
|
5141
6507
|
playSpec(spec, opts = {}) {
|
|
5142
|
-
if (!this.
|
|
5143
|
-
const sig = renderSound(spec, { sampleRate: this.
|
|
5144
|
-
const buf = this.
|
|
6508
|
+
if (!this._ctx || !this.sfxGain) return;
|
|
6509
|
+
const sig = renderSound(spec, { sampleRate: this._ctx.sampleRate });
|
|
6510
|
+
const buf = this._ctx.createBuffer(1, sig.length, this._ctx.sampleRate);
|
|
5145
6511
|
buf.copyToChannel(new Float32Array(sig), 0);
|
|
5146
|
-
const src = this.
|
|
6512
|
+
const src = this._ctx.createBufferSource();
|
|
5147
6513
|
src.buffer = buf;
|
|
5148
|
-
const g = this.
|
|
6514
|
+
const g = this._ctx.createGain();
|
|
5149
6515
|
g.gain.value = opts.gain ?? 1;
|
|
5150
6516
|
src.connect(g);
|
|
5151
|
-
if (opts.pan !== void 0 && typeof this.
|
|
5152
|
-
const p = this.
|
|
6517
|
+
if (opts.pan !== void 0 && typeof this._ctx.createStereoPanner === "function") {
|
|
6518
|
+
const p = this._ctx.createStereoPanner();
|
|
5153
6519
|
p.pan.value = Math.max(-1, Math.min(1, opts.pan));
|
|
5154
6520
|
g.connect(p);
|
|
5155
6521
|
p.connect(this.sfxGain);
|
|
5156
6522
|
} else {
|
|
5157
6523
|
g.connect(this.sfxGain);
|
|
5158
6524
|
}
|
|
5159
|
-
src.start(this.
|
|
6525
|
+
src.start(this._ctx.currentTime + (opts.when ?? 0));
|
|
5160
6526
|
}
|
|
5161
6527
|
/**
|
|
5162
6528
|
* Render a Song offline and play it on the music bus. Returns a stop handle.
|
|
@@ -5164,13 +6530,13 @@ var AudioBus = class {
|
|
|
5164
6530
|
* an AudioContext.
|
|
5165
6531
|
*/
|
|
5166
6532
|
playSong(song, opts = {}) {
|
|
5167
|
-
if (!this.
|
|
6533
|
+
if (!this._ctx || !this.musicGain) return () => {
|
|
5168
6534
|
};
|
|
5169
|
-
const mix2 = renderSong(song, { sampleRate: this.
|
|
5170
|
-
const buf = this.
|
|
6535
|
+
const mix2 = renderSong(song, { sampleRate: this._ctx.sampleRate });
|
|
6536
|
+
const buf = this._ctx.createBuffer(2, mix2.left.length, this._ctx.sampleRate);
|
|
5171
6537
|
buf.copyToChannel(new Float32Array(mix2.left), 0);
|
|
5172
6538
|
buf.copyToChannel(new Float32Array(mix2.right), 1);
|
|
5173
|
-
const src = this.
|
|
6539
|
+
const src = this._ctx.createBufferSource();
|
|
5174
6540
|
src.buffer = buf;
|
|
5175
6541
|
if (opts.loop) {
|
|
5176
6542
|
src.loop = true;
|
|
@@ -5201,26 +6567,26 @@ var AudioBus = class {
|
|
|
5201
6567
|
}
|
|
5202
6568
|
/** Start a soft evolving ambient pad on the music bus. */
|
|
5203
6569
|
startAmbient(root = 110, voices = [1, 1.5, 2, 2.5]) {
|
|
5204
|
-
if (!this.
|
|
6570
|
+
if (!this._ctx || !this.musicGain || this.padOn) return;
|
|
5205
6571
|
this.padOn = true;
|
|
5206
|
-
const filter = this.
|
|
6572
|
+
const filter = this._ctx.createBiquadFilter();
|
|
5207
6573
|
filter.type = "lowpass";
|
|
5208
6574
|
filter.frequency.value = 900;
|
|
5209
6575
|
filter.connect(this.musicGain);
|
|
5210
|
-
const bus = this.
|
|
6576
|
+
const bus = this._ctx.createGain();
|
|
5211
6577
|
bus.gain.value = 0;
|
|
5212
6578
|
bus.connect(filter);
|
|
5213
6579
|
for (const mult of voices) {
|
|
5214
|
-
const osc = this.
|
|
6580
|
+
const osc = this._ctx.createOscillator();
|
|
5215
6581
|
osc.type = "triangle";
|
|
5216
6582
|
osc.frequency.value = root * mult;
|
|
5217
|
-
const g = this.
|
|
6583
|
+
const g = this._ctx.createGain();
|
|
5218
6584
|
g.gain.value = 0.1 / voices.length;
|
|
5219
6585
|
osc.connect(g);
|
|
5220
6586
|
g.connect(bus);
|
|
5221
6587
|
osc.start();
|
|
5222
6588
|
}
|
|
5223
|
-
bus.gain.setTargetAtTime(0.9, this.
|
|
6589
|
+
bus.gain.setTargetAtTime(0.9, this._ctx.currentTime, 3);
|
|
5224
6590
|
}
|
|
5225
6591
|
};
|
|
5226
6592
|
var audio = new AudioBus();
|
|
@@ -5944,7 +7310,7 @@ function melancholicPiano() {
|
|
|
5944
7310
|
}
|
|
5945
7311
|
function epicOrchestral() {
|
|
5946
7312
|
const key = noteToMidi("D3");
|
|
5947
|
-
const
|
|
7313
|
+
const all2 = voiceLead(progression(key, "minor", ["i", "VI", "III", "VII", "VI", "VII", "i", "i"]));
|
|
5948
7314
|
const scale = scaleMidis(noteToMidi("D4"), "minor", 3);
|
|
5949
7315
|
const deg = (i) => scale[(i % scale.length + scale.length) % scale.length];
|
|
5950
7316
|
const strings = {
|
|
@@ -5952,7 +7318,7 @@ function epicOrchestral() {
|
|
|
5952
7318
|
instrument: { ...INSTRUMENTS.strings, volume: 0.24 },
|
|
5953
7319
|
gain: 0.7,
|
|
5954
7320
|
pan: -0.15,
|
|
5955
|
-
patterns:
|
|
7321
|
+
patterns: all2.map((c) => arp([c[0], c[1], c[2], c[0] + 12], 8, 0.5, 0.75)),
|
|
5956
7322
|
sequence: [0, 1, 2, 3, 4, 5, 6, 7]
|
|
5957
7323
|
};
|
|
5958
7324
|
const themeBars = [
|
|
@@ -5980,7 +7346,7 @@ function epicOrchestral() {
|
|
|
5980
7346
|
instrument: { ...INSTRUMENTS.choir, volume: 0.18 },
|
|
5981
7347
|
gain: 0.5,
|
|
5982
7348
|
pan: 0.05,
|
|
5983
|
-
patterns:
|
|
7349
|
+
patterns: all2.map((c) => [n3([c[2] + 12, c[0] + 24], 4, 0.6)]),
|
|
5984
7350
|
sequence: [0, 1, 2, 3, 4, 5, 6, 7]
|
|
5985
7351
|
};
|
|
5986
7352
|
const timp = {
|
|
@@ -5996,7 +7362,7 @@ function epicOrchestral() {
|
|
|
5996
7362
|
gain: 0.32,
|
|
5997
7363
|
pan: 0.25,
|
|
5998
7364
|
// sparkles in only from bar 3, with the theme
|
|
5999
|
-
patterns: [restBar, ...
|
|
7365
|
+
patterns: [restBar, ...all2.map((c) => [rest(1), n3(c[2] + 24, 1, 0.55), rest(1), n3(c[1] + 24, 1, 0.5)])],
|
|
6000
7366
|
sequence: [0, 0, 3, 4, 5, 6, 7, 8]
|
|
6001
7367
|
};
|
|
6002
7368
|
return { bpm: 88, tracks: [timp, strings, brass, choir, glock], humanize: 0.14, velBrightness: 0.5, reverb: { wet: 0.32, roomSize: 0.9, damp: 0.4 }, master: { lowCut: 0.4, presence: 0.32, air: 0.14 } };
|
|
@@ -6051,6 +7417,83 @@ function genre(id) {
|
|
|
6051
7417
|
return GENRES.find((g) => g.id === id);
|
|
6052
7418
|
}
|
|
6053
7419
|
|
|
7420
|
+
// src/audio/zzfx.ts
|
|
7421
|
+
var SHAPES = ["sine", "triangle", "saw", "square", "noise"];
|
|
7422
|
+
var warned = /* @__PURE__ */ new Set();
|
|
7423
|
+
function warnOnce(param, msg) {
|
|
7424
|
+
if (warned.has(param)) return;
|
|
7425
|
+
warned.add(param);
|
|
7426
|
+
console.warn(`[hayao] specFromZzfx: ${param} ${msg}`);
|
|
7427
|
+
}
|
|
7428
|
+
function resetZzfxWarnings() {
|
|
7429
|
+
warned.clear();
|
|
7430
|
+
}
|
|
7431
|
+
function specFromZzfx(p) {
|
|
7432
|
+
const volume = p[0] ?? 1;
|
|
7433
|
+
const randomness = p[1] ?? 0.05;
|
|
7434
|
+
const frequency = p[2] ?? 220;
|
|
7435
|
+
const attack = p[3] ?? 0;
|
|
7436
|
+
const sustain = p[4] ?? 0;
|
|
7437
|
+
const release = p[5] ?? 0.1;
|
|
7438
|
+
const shape = p[6] ?? 0;
|
|
7439
|
+
const shapeCurve = p[7] ?? 1;
|
|
7440
|
+
const slide = p[8] ?? 0;
|
|
7441
|
+
const deltaSlide = p[9] ?? 0;
|
|
7442
|
+
const pitchJump = p[10] ?? 0;
|
|
7443
|
+
const pitchJumpTime = p[11] ?? 0;
|
|
7444
|
+
const repeatTime = p[12] ?? 0;
|
|
7445
|
+
const noise = p[13] ?? 0;
|
|
7446
|
+
const modulation = p[14] ?? 0;
|
|
7447
|
+
const bitCrush = p[15] ?? 0;
|
|
7448
|
+
const delay = p[16] ?? 0;
|
|
7449
|
+
const sustainVolume = p[17] ?? 1;
|
|
7450
|
+
const decay = p[18] ?? 0;
|
|
7451
|
+
const tremolo = p[19] ?? 0;
|
|
7452
|
+
const filter = p[20] ?? 0;
|
|
7453
|
+
const body = attack + decay + sustain + release;
|
|
7454
|
+
const spec = {
|
|
7455
|
+
freq: frequency,
|
|
7456
|
+
volume,
|
|
7457
|
+
attack,
|
|
7458
|
+
decay,
|
|
7459
|
+
sustain,
|
|
7460
|
+
release,
|
|
7461
|
+
sustainLevel: sustainVolume
|
|
7462
|
+
};
|
|
7463
|
+
const shapeIdx = Math.max(0, Math.min(SHAPES.length - 1, Math.round(shape)));
|
|
7464
|
+
spec.wave = SHAPES[shapeIdx];
|
|
7465
|
+
if (Math.round(shape) === 3) warnOnce("shape=3 (tan)", "has no hayao wave; approximated as a hard square.");
|
|
7466
|
+
if (shapeCurve !== 1) spec.shapeCurve = shapeCurve;
|
|
7467
|
+
if (randomness !== 0) warnOnce("randomness", "is dropped: hayao specs are deterministic (same spec \u2192 same samples).");
|
|
7468
|
+
if (slide !== 0) spec.slide = slide * body;
|
|
7469
|
+
if (deltaSlide !== 0) spec.slideAccel = deltaSlide * body * body / 2;
|
|
7470
|
+
if (pitchJump !== 0) {
|
|
7471
|
+
const target = frequency + pitchJump;
|
|
7472
|
+
if (target > 0 && frequency > 0) {
|
|
7473
|
+
spec.pitchJump = 12 * dlog2(target / frequency);
|
|
7474
|
+
spec.pitchJumpTime = pitchJumpTime;
|
|
7475
|
+
} else {
|
|
7476
|
+
warnOnce("pitchJump", "would drive frequency to \u2264 0 Hz; dropped.");
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
if (repeatTime > 0) spec.repeat = repeatTime;
|
|
7480
|
+
if (noise !== 0) spec.noise = Math.max(0, Math.min(1, noise));
|
|
7481
|
+
if (modulation !== 0) {
|
|
7482
|
+
warnOnce("modulation", "is approximated as vibrato (pitch LFO at |modulation| Hz).");
|
|
7483
|
+
spec.vibrato = 0.6;
|
|
7484
|
+
spec.vibratoFreq = Math.abs(modulation);
|
|
7485
|
+
}
|
|
7486
|
+
if (bitCrush > 0) spec.bitCrush = Math.max(1, Math.round(bitCrush * 100));
|
|
7487
|
+
if (tremolo !== 0) spec.tremolo = Math.max(0, Math.min(1, tremolo));
|
|
7488
|
+
if (delay > 0) {
|
|
7489
|
+
spec.delay = delay;
|
|
7490
|
+
spec.delayFeedback = 0.5;
|
|
7491
|
+
}
|
|
7492
|
+
if (filter > 0) spec.lowpass = filter;
|
|
7493
|
+
else if (filter < 0) spec.highpass = -filter;
|
|
7494
|
+
return spec;
|
|
7495
|
+
}
|
|
7496
|
+
|
|
6054
7497
|
// src/audio/album.ts
|
|
6055
7498
|
var n4 = (pitch, beats, vel = 1) => ({ pitch, beats, vel });
|
|
6056
7499
|
var rest2 = (beats) => ({ pitch: null, beats });
|
|
@@ -6635,6 +8078,163 @@ var Shell = class {
|
|
|
6635
8078
|
}
|
|
6636
8079
|
};
|
|
6637
8080
|
|
|
8081
|
+
// src/ui/touch.ts
|
|
8082
|
+
var STYLE_ID2 = "hayao-touch-style";
|
|
8083
|
+
var CSS2 = `
|
|
8084
|
+
.hy-touch{position:absolute;inset:0;z-index:40;pointer-events:none;touch-action:none;-webkit-user-select:none;user-select:none}
|
|
8085
|
+
.hy-stick{position:absolute;bottom:6%;width:120px;height:120px;border-radius:50%;pointer-events:auto;background:rgba(20,16,10,.16);border:2px solid rgba(250,244,230,.35);box-shadow:inset 0 0 20px rgba(0,0,0,.12)}
|
|
8086
|
+
.hy-stick.left{left:5%}
|
|
8087
|
+
.hy-stick.right{right:5%}
|
|
8088
|
+
.hy-knob{position:absolute;left:50%;top:50%;width:52px;height:52px;margin:-26px 0 0 -26px;border-radius:50%;background:rgba(250,244,230,.6);border:2px solid rgba(20,16,10,.25);transition:transform .04s linear}
|
|
8089
|
+
.hy-btns{position:absolute;right:5%;bottom:26%;display:flex;flex-direction:column-reverse;gap:12px;pointer-events:none}
|
|
8090
|
+
.hy-btn{pointer-events:auto;width:66px;height:66px;border-radius:50%;font:600 15px/1 var(--hy-serif,Georgia,serif);color:#fbf6ea;background:var(--hy-accent,#a11d3a);border:2px solid rgba(250,244,230,.4);box-shadow:0 4px 12px rgba(30,20,10,.3);display:grid;place-items:center;text-transform:capitalize}
|
|
8091
|
+
.hy-btn:active,.hy-btn.on{transform:translateY(2px);filter:brightness(1.15)}
|
|
8092
|
+
`;
|
|
8093
|
+
function ensureStyle2() {
|
|
8094
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
8095
|
+
const s = document.createElement("style");
|
|
8096
|
+
s.id = STYLE_ID2;
|
|
8097
|
+
s.textContent = CSS2;
|
|
8098
|
+
document.head.appendChild(s);
|
|
8099
|
+
}
|
|
8100
|
+
function normDirs(s) {
|
|
8101
|
+
if (!s.dirs) return void 0;
|
|
8102
|
+
return Array.isArray(s.dirs) ? { up: s.dirs[0], down: s.dirs[1], left: s.dirs[2], right: s.dirs[3] } : s.dirs;
|
|
8103
|
+
}
|
|
8104
|
+
var TouchControls = class {
|
|
8105
|
+
handle;
|
|
8106
|
+
root;
|
|
8107
|
+
disposers = [];
|
|
8108
|
+
/** Every action this control set can hold — released together on dispose/reset. */
|
|
8109
|
+
owned = /* @__PURE__ */ new Set();
|
|
8110
|
+
constructor(handle, layout) {
|
|
8111
|
+
this.handle = handle;
|
|
8112
|
+
ensureStyle2();
|
|
8113
|
+
const coarse = typeof matchMedia !== "undefined" && matchMedia("(pointer: coarse)").matches;
|
|
8114
|
+
const mount = handle.canvas?.parentElement ?? void 0;
|
|
8115
|
+
this.root = document.createElement("div");
|
|
8116
|
+
this.root.className = "hy-touch";
|
|
8117
|
+
if (mount && (layout.touchOnly === false || coarse)) mount.appendChild(this.root);
|
|
8118
|
+
const left = normStick(layout.left);
|
|
8119
|
+
const right = normStick(layout.right);
|
|
8120
|
+
if (left) this.addStick("left", left);
|
|
8121
|
+
if (right) this.addStick("right", right);
|
|
8122
|
+
if (layout.buttons?.length) this.addButtons(layout.buttons);
|
|
8123
|
+
}
|
|
8124
|
+
addStick(side, spec) {
|
|
8125
|
+
const base = document.createElement("div");
|
|
8126
|
+
base.className = `hy-stick ${side}`;
|
|
8127
|
+
const knob2 = document.createElement("div");
|
|
8128
|
+
knob2.className = "hy-knob";
|
|
8129
|
+
base.appendChild(knob2);
|
|
8130
|
+
this.root.appendChild(base);
|
|
8131
|
+
const dirs = normDirs(spec);
|
|
8132
|
+
const prefix = spec.prefix;
|
|
8133
|
+
const buckets = spec.buckets ?? 32;
|
|
8134
|
+
const dead = spec.deadzone ?? 0.28;
|
|
8135
|
+
let activeId = null;
|
|
8136
|
+
const setAxes = (nx, ny) => {
|
|
8137
|
+
if (!prefix) return;
|
|
8138
|
+
const axes = this.handle.world.input.axes;
|
|
8139
|
+
axes.set(`${prefix}x`, snapAxis(nx, buckets));
|
|
8140
|
+
axes.set(`${prefix}y`, snapAxis(ny, buckets));
|
|
8141
|
+
axes.set(`${prefix}angle`, nx === 0 && ny === 0 ? 0 : quantizeAngle(datan2(ny, nx), buckets));
|
|
8142
|
+
};
|
|
8143
|
+
const release = () => {
|
|
8144
|
+
if (dirs) for (const a of [dirs.up, dirs.down, dirs.left, dirs.right]) this.hold(a, false);
|
|
8145
|
+
setAxes(0, 0);
|
|
8146
|
+
knob2.style.transform = "translate(0,0)";
|
|
8147
|
+
activeId = null;
|
|
8148
|
+
};
|
|
8149
|
+
const apply = (clientX, clientY) => {
|
|
8150
|
+
const r = base.getBoundingClientRect();
|
|
8151
|
+
const rad = r.width / 2;
|
|
8152
|
+
let dx = (clientX - (r.left + rad)) / rad;
|
|
8153
|
+
let dy = (clientY - (r.top + rad)) / rad;
|
|
8154
|
+
const m = dhypot(dx, dy);
|
|
8155
|
+
if (m > 1) {
|
|
8156
|
+
dx /= m;
|
|
8157
|
+
dy /= m;
|
|
8158
|
+
}
|
|
8159
|
+
knob2.style.transform = `translate(${dx * rad * 0.6}px,${dy * rad * 0.6}px)`;
|
|
8160
|
+
const mag = dhypot(dx, dy);
|
|
8161
|
+
if (dirs) {
|
|
8162
|
+
this.hold(dirs.left, dx < -dead);
|
|
8163
|
+
this.hold(dirs.right, dx > dead);
|
|
8164
|
+
this.hold(dirs.up, dy < -dead);
|
|
8165
|
+
this.hold(dirs.down, dy > dead);
|
|
8166
|
+
}
|
|
8167
|
+
setAxes(mag < dead ? 0 : dx, mag < dead ? 0 : dy);
|
|
8168
|
+
};
|
|
8169
|
+
const onDown = (e) => {
|
|
8170
|
+
if (activeId !== null) return;
|
|
8171
|
+
activeId = e.pointerId;
|
|
8172
|
+
base.setPointerCapture?.(e.pointerId);
|
|
8173
|
+
apply(e.clientX, e.clientY);
|
|
8174
|
+
e.preventDefault();
|
|
8175
|
+
};
|
|
8176
|
+
const onMove = (e) => {
|
|
8177
|
+
if (e.pointerId === activeId) apply(e.clientX, e.clientY);
|
|
8178
|
+
};
|
|
8179
|
+
const onUp = (e) => {
|
|
8180
|
+
if (e.pointerId === activeId) release();
|
|
8181
|
+
};
|
|
8182
|
+
base.addEventListener("pointerdown", onDown);
|
|
8183
|
+
base.addEventListener("pointermove", onMove);
|
|
8184
|
+
base.addEventListener("pointerup", onUp);
|
|
8185
|
+
base.addEventListener("pointercancel", onUp);
|
|
8186
|
+
this.disposers.push(() => {
|
|
8187
|
+
release();
|
|
8188
|
+
base.remove();
|
|
8189
|
+
});
|
|
8190
|
+
}
|
|
8191
|
+
addButtons(buttons) {
|
|
8192
|
+
const wrap = document.createElement("div");
|
|
8193
|
+
wrap.className = "hy-btns";
|
|
8194
|
+
this.root.appendChild(wrap);
|
|
8195
|
+
for (const b of buttons) {
|
|
8196
|
+
const el = document.createElement("button");
|
|
8197
|
+
el.className = "hy-btn";
|
|
8198
|
+
el.textContent = b.label ?? b.action;
|
|
8199
|
+
wrap.appendChild(el);
|
|
8200
|
+
const hold2 = b.hold ?? true;
|
|
8201
|
+
const down = (e) => {
|
|
8202
|
+
el.classList.add("on");
|
|
8203
|
+
if (hold2) this.hold(b.action, true);
|
|
8204
|
+
else this.handle.input.press(b.action);
|
|
8205
|
+
el.setPointerCapture?.(e.pointerId);
|
|
8206
|
+
e.preventDefault();
|
|
8207
|
+
};
|
|
8208
|
+
const up = () => {
|
|
8209
|
+
el.classList.remove("on");
|
|
8210
|
+
if (hold2) this.hold(b.action, false);
|
|
8211
|
+
};
|
|
8212
|
+
el.addEventListener("pointerdown", down);
|
|
8213
|
+
el.addEventListener("pointerup", up);
|
|
8214
|
+
el.addEventListener("pointercancel", up);
|
|
8215
|
+
this.disposers.push(() => {
|
|
8216
|
+
up();
|
|
8217
|
+
el.remove();
|
|
8218
|
+
});
|
|
8219
|
+
}
|
|
8220
|
+
this.disposers.push(() => wrap.remove());
|
|
8221
|
+
}
|
|
8222
|
+
hold(action, on) {
|
|
8223
|
+
this.handle.input.setHeld(action, on);
|
|
8224
|
+
if (on) this.owned.add(action);
|
|
8225
|
+
}
|
|
8226
|
+
dispose() {
|
|
8227
|
+
for (const d of this.disposers.splice(0)) d();
|
|
8228
|
+
for (const a of this.owned) this.handle.input.setHeld(a, false);
|
|
8229
|
+
this.owned.clear();
|
|
8230
|
+
this.root.remove();
|
|
8231
|
+
}
|
|
8232
|
+
};
|
|
8233
|
+
function normStick(s) {
|
|
8234
|
+
if (!s) return void 0;
|
|
8235
|
+
return Array.isArray(s) ? { dirs: s } : s;
|
|
8236
|
+
}
|
|
8237
|
+
|
|
6638
8238
|
// src/ui/transition.ts
|
|
6639
8239
|
var BAYER4 = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
|
|
6640
8240
|
var smoothstep2 = (t) => t * t * (3 - 2 * t);
|
|
@@ -6862,7 +8462,7 @@ function replay(makeWorld, log) {
|
|
|
6862
8462
|
const world = makeWorld();
|
|
6863
8463
|
const hashes = [];
|
|
6864
8464
|
for (let i = 0; i < log.frames.length; i++) {
|
|
6865
|
-
world.step(frameActions(log, i));
|
|
8465
|
+
world.step(frameActions(log, i), frameAxes(log, i));
|
|
6866
8466
|
hashes.push(world.hash());
|
|
6867
8467
|
}
|
|
6868
8468
|
return { finalHash: world.hash(), hashes };
|
|
@@ -6895,13 +8495,13 @@ function assertDeterministic(makeWorld, log) {
|
|
|
6895
8495
|
}
|
|
6896
8496
|
function assertSnapshotStable(makeWorld, log, warmup) {
|
|
6897
8497
|
const original = makeWorld();
|
|
6898
|
-
for (let i = 0; i < warmup; i++) original.step(frameActions(log, i));
|
|
8498
|
+
for (let i = 0; i < warmup; i++) original.step(frameActions(log, i), frameAxes(log, i));
|
|
6899
8499
|
const snap = original.snapshot();
|
|
6900
8500
|
const restored = makeWorld();
|
|
6901
8501
|
restored.restore(snap);
|
|
6902
8502
|
for (let i = warmup; i < log.frames.length; i++) {
|
|
6903
|
-
original.step(frameActions(log, i));
|
|
6904
|
-
restored.step(frameActions(log, i));
|
|
8503
|
+
original.step(frameActions(log, i), frameAxes(log, i));
|
|
8504
|
+
restored.step(frameActions(log, i), frameAxes(log, i));
|
|
6905
8505
|
}
|
|
6906
8506
|
const hashA = original.hash();
|
|
6907
8507
|
const hashB = restored.hash();
|
|
@@ -7572,12 +9172,29 @@ var World = class {
|
|
|
7572
9172
|
state = {};
|
|
7573
9173
|
width;
|
|
7574
9174
|
height;
|
|
9175
|
+
/**
|
|
9176
|
+
* Pause switch. While true, only `pauseMode: 'always'` subtrees update —
|
|
9177
|
+
* rendering, input sampling, and the clock keep ticking, so a pause menu
|
|
9178
|
+
* (an 'always' subtree) stays live. Sim state: hashed + snapshotted, but
|
|
9179
|
+
* only when true, so pinned hashes of unpaused games are unchanged.
|
|
9180
|
+
*/
|
|
9181
|
+
paused = false;
|
|
9182
|
+
/**
|
|
9183
|
+
* Sim-time multiplier: the tree receives `clock.dt * timeScale` (slow-mo /
|
|
9184
|
+
* fast-forward), while `'always'` subtrees get the unscaled dt so UI keeps
|
|
9185
|
+
* realtime feel. The clock itself ticks normally. Hashed/snapshotted only
|
|
9186
|
+
* when ≠ 1.
|
|
9187
|
+
*/
|
|
9188
|
+
timeScale = 1;
|
|
7575
9189
|
root;
|
|
7576
9190
|
activeCamera = null;
|
|
7577
9191
|
seed;
|
|
7578
9192
|
freeQueue = [];
|
|
7579
9193
|
started = false;
|
|
7580
9194
|
tuningValues;
|
|
9195
|
+
guardDeterminism;
|
|
9196
|
+
warnedClamp = false;
|
|
9197
|
+
warnedNondet = false;
|
|
7581
9198
|
constructor(config = {}) {
|
|
7582
9199
|
this.seed = config.seed ?? 1;
|
|
7583
9200
|
this.rng = new Rng(this.seed);
|
|
@@ -7585,6 +9202,7 @@ var World = class {
|
|
|
7585
9202
|
this.width = config.width ?? 1280;
|
|
7586
9203
|
this.height = config.height ?? 720;
|
|
7587
9204
|
this.tuningValues = { ...config.tuning ?? {} };
|
|
9205
|
+
this.guardDeterminism = config.guardDeterminism ?? false;
|
|
7588
9206
|
this.root = new Node({ name: "root" });
|
|
7589
9207
|
}
|
|
7590
9208
|
/**
|
|
@@ -7597,6 +9215,18 @@ var World = class {
|
|
|
7597
9215
|
if (v === void 0) throw new Error(`tune('${key}'): no such knob declared in the game's tuning spec`);
|
|
7598
9216
|
return v;
|
|
7599
9217
|
}
|
|
9218
|
+
/**
|
|
9219
|
+
* Read a shared resource by key. Throws on a missing key (listing what IS
|
|
9220
|
+
* available) — a typo here would otherwise silently read `undefined` into
|
|
9221
|
+
* the sim, exactly like an undeclared tuning knob.
|
|
9222
|
+
*/
|
|
9223
|
+
resource(key) {
|
|
9224
|
+
if (!this.resources.has(key)) {
|
|
9225
|
+
const available = [...this.resources.keys()].join(", ") || "(none)";
|
|
9226
|
+
throw new Error(`resource('${key}'): not set. Available: ${available}`);
|
|
9227
|
+
}
|
|
9228
|
+
return this.resources.get(key);
|
|
9229
|
+
}
|
|
7600
9230
|
get time() {
|
|
7601
9231
|
return this.clock.simTimeSec;
|
|
7602
9232
|
}
|
|
@@ -7622,12 +9252,51 @@ var World = class {
|
|
|
7622
9252
|
* Advance exactly one fixed step with the given actions held down.
|
|
7623
9253
|
* This is THE deterministic transition — call it from Node or the browser loop.
|
|
7624
9254
|
*/
|
|
7625
|
-
step(actionsDown = []) {
|
|
9255
|
+
step(actionsDown = [], axes) {
|
|
9256
|
+
if (typeof actionsDown === "string" || typeof actionsDown[Symbol.iterator] !== "function") {
|
|
9257
|
+
throw new TypeError(`step(actionsDown): pass an array/iterable of action names, got ${typeof actionsDown === "string" ? `the string '${actionsDown}'` : typeof actionsDown}`);
|
|
9258
|
+
}
|
|
7626
9259
|
this.ensureStarted();
|
|
7627
|
-
this.
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
9260
|
+
const unguard = this.guardDeterminism ? this.installGuard() : null;
|
|
9261
|
+
try {
|
|
9262
|
+
this.input.beginFrame(actionsDown, axes);
|
|
9263
|
+
this.root.updateTree(this.clock.dt * this.timeScale, this.clock.dt, "pausable", this.paused);
|
|
9264
|
+
this.flushFree();
|
|
9265
|
+
this.clock.tick();
|
|
9266
|
+
} finally {
|
|
9267
|
+
unguard?.();
|
|
9268
|
+
}
|
|
9269
|
+
}
|
|
9270
|
+
/**
|
|
9271
|
+
* Wrap `Math.random`/`Date.now` for the duration of a step so a stray call
|
|
9272
|
+
* inside the sim warns ONCE per world (with a stack hint) — the values still
|
|
9273
|
+
* flow through, so nothing breaks mid-frame. Returns the restore function;
|
|
9274
|
+
* properly nested re-entry (a world stepped inside another's step) is safe
|
|
9275
|
+
* because each install restores exactly what it replaced.
|
|
9276
|
+
*/
|
|
9277
|
+
installGuard() {
|
|
9278
|
+
const mathObj = globalThis.Math;
|
|
9279
|
+
const realRandom = mathObj.random;
|
|
9280
|
+
const realNow = globalThis.Date.now;
|
|
9281
|
+
const warn = (what) => {
|
|
9282
|
+
if (this.warnedNondet) return;
|
|
9283
|
+
this.warnedNondet = true;
|
|
9284
|
+
const stack = new Error().stack?.split("\n").slice(2, 5).join("\n") ?? "";
|
|
9285
|
+
console.warn(`hayao: ${what} called during step() \u2014 nondeterministic. Use world.rng / ctx.time instead.
|
|
9286
|
+
${stack}`);
|
|
9287
|
+
};
|
|
9288
|
+
mathObj.random = () => {
|
|
9289
|
+
warn("Math.random");
|
|
9290
|
+
return realRandom.call(mathObj);
|
|
9291
|
+
};
|
|
9292
|
+
globalThis.Date.now = () => {
|
|
9293
|
+
warn("Date.now");
|
|
9294
|
+
return realNow.call(globalThis.Date);
|
|
9295
|
+
};
|
|
9296
|
+
return () => {
|
|
9297
|
+
mathObj.random = realRandom;
|
|
9298
|
+
globalThis.Date.now = realNow;
|
|
9299
|
+
};
|
|
7631
9300
|
}
|
|
7632
9301
|
/**
|
|
7633
9302
|
* Feed REAL elapsed ms; runs 0+ fixed steps. Returns steps run. This is the
|
|
@@ -7636,9 +9305,13 @@ var World = class {
|
|
|
7636
9305
|
* not 72. For headless tests/harnesses that want to fast-forward an exact
|
|
7637
9306
|
* number of steps, use `runSteps(n)` or `step()` — never `advance` with a big ms.
|
|
7638
9307
|
*/
|
|
7639
|
-
advance(realMs, actionsDown = []) {
|
|
9308
|
+
advance(realMs, actionsDown = [], axes) {
|
|
9309
|
+
if (realMs > this.clock.maxFrameMs && !this.warnedClamp) {
|
|
9310
|
+
this.warnedClamp = true;
|
|
9311
|
+
console.warn(`hayao: advance(${realMs}) exceeds maxFrameMs (${this.clock.maxFrameMs}) and is clamped \u2014 a realtime driver must never spiral. To fast-forward an exact number of steps, use runSteps(n).`);
|
|
9312
|
+
}
|
|
7640
9313
|
const steps = this.clock.advance(realMs);
|
|
7641
|
-
for (let i = 0; i < steps; i++) this.step(actionsDown);
|
|
9314
|
+
for (let i = 0; i < steps; i++) this.step(actionsDown, axes);
|
|
7642
9315
|
return steps;
|
|
7643
9316
|
}
|
|
7644
9317
|
/**
|
|
@@ -7646,9 +9319,16 @@ var World = class {
|
|
|
7646
9319
|
* headless drivers (unlike `advance`, no realtime clamp). Pass `actionsFor(i)`
|
|
7647
9320
|
* to script the input per step; omit it to hold nothing down.
|
|
7648
9321
|
*/
|
|
7649
|
-
runSteps(n5, actionsFor) {
|
|
7650
|
-
for (let i = 0; i < n5; i++) this.step(actionsFor ? actionsFor(i) : []);
|
|
9322
|
+
runSteps(n5, actionsFor, axesFor) {
|
|
9323
|
+
for (let i = 0; i < n5; i++) this.step(actionsFor ? actionsFor(i) : [], axesFor?.(i));
|
|
7651
9324
|
}
|
|
9325
|
+
/**
|
|
9326
|
+
* Run pending frees NOW instead of at the end of the step. `free()` is
|
|
9327
|
+
* deferred (safe during iteration), which means freed nodes survive until
|
|
9328
|
+
* the step ends — during a scene swap that lets old nodes contaminate the
|
|
9329
|
+
* first frame of the new scene. Pattern: `oldRoot.free(); world.flushFree();
|
|
9330
|
+
* buildNewScene()`. Called automatically at the end of every step.
|
|
9331
|
+
*/
|
|
7652
9332
|
flushFree() {
|
|
7653
9333
|
if (this.freeQueue.length === 0) return;
|
|
7654
9334
|
const q = this.freeQueue;
|
|
@@ -7659,6 +9339,45 @@ var World = class {
|
|
|
7659
9339
|
if (this.activeCamera === node) this.activeCamera = null;
|
|
7660
9340
|
}
|
|
7661
9341
|
}
|
|
9342
|
+
// ── Tree inspection ──────────────────────────────────────────
|
|
9343
|
+
/** Visit every live node depth-first (root first, child-index order). */
|
|
9344
|
+
walk(fn) {
|
|
9345
|
+
const visit = (n5) => {
|
|
9346
|
+
fn(n5);
|
|
9347
|
+
for (const c of n5.children) visit(c);
|
|
9348
|
+
};
|
|
9349
|
+
visit(this.root);
|
|
9350
|
+
}
|
|
9351
|
+
/** Total live nodes in the tree (root included). */
|
|
9352
|
+
get nodeCount() {
|
|
9353
|
+
let n5 = 0;
|
|
9354
|
+
this.walk(() => n5++);
|
|
9355
|
+
return n5;
|
|
9356
|
+
}
|
|
9357
|
+
/**
|
|
9358
|
+
* One indented line per node — `name (type) [flags] @x,y` — for quick tree
|
|
9359
|
+
* audits in a REPL or test failure message.
|
|
9360
|
+
*/
|
|
9361
|
+
debugTree() {
|
|
9362
|
+
const lines = [];
|
|
9363
|
+
const visit = (n5, depth) => {
|
|
9364
|
+
const flags = [];
|
|
9365
|
+
if (n5.cosmetic) flags.push("cosmetic");
|
|
9366
|
+
if (n5.screenSpace) flags.push("screenSpace");
|
|
9367
|
+
if (n5.pauseMode !== "inherit") flags.push(n5.pauseMode);
|
|
9368
|
+
if (!n5.visible) flags.push("!visible");
|
|
9369
|
+
lines.push(`${" ".repeat(depth)}${n5.name} (${n5.type})${flags.length > 0 ? ` [${flags.join(" ")}]` : ""} @${n5.pos.x},${n5.pos.y}`);
|
|
9370
|
+
for (const c of n5.children) visit(c, depth + 1);
|
|
9371
|
+
};
|
|
9372
|
+
visit(this.root, 0);
|
|
9373
|
+
return lines.join("\n");
|
|
9374
|
+
}
|
|
9375
|
+
/** The active camera's world position + zoom, or null (see WorldContext.camera). */
|
|
9376
|
+
camera() {
|
|
9377
|
+
if (!this.activeCamera) return null;
|
|
9378
|
+
const wt = this.activeCamera.worldTransform();
|
|
9379
|
+
return { pos: { x: wt.e, y: wt.f }, zoom: this.activeCamera.zoom };
|
|
9380
|
+
}
|
|
7662
9381
|
// ── Rendering ────────────────────────────────────────────────
|
|
7663
9382
|
/** The view transform (inverse of the active camera), mapping world → screen. */
|
|
7664
9383
|
viewTransform() {
|
|
@@ -7703,7 +9422,10 @@ var World = class {
|
|
|
7703
9422
|
state: this.state,
|
|
7704
9423
|
tree: this.root.serialize(),
|
|
7705
9424
|
// Only when declared, so games without tuning keep their pinned hashes.
|
|
7706
|
-
...Object.keys(this.tuningValues).length > 0 ? { tuning: this.tuningValues } : {}
|
|
9425
|
+
...Object.keys(this.tuningValues).length > 0 ? { tuning: this.tuningValues } : {},
|
|
9426
|
+
// Same guard: only when non-default, so pre-pause pinned hashes survive.
|
|
9427
|
+
...this.paused ? { paused: true } : {},
|
|
9428
|
+
...this.timeScale !== 1 ? { timeScale: this.timeScale } : {}
|
|
7707
9429
|
});
|
|
7708
9430
|
}
|
|
7709
9431
|
/** Compact snapshot for undo/time-travel and saves. */
|
|
@@ -7715,7 +9437,10 @@ var World = class {
|
|
|
7715
9437
|
input: this.input.getState(),
|
|
7716
9438
|
state: structuredClone(this.state),
|
|
7717
9439
|
tree: this.root.serialize(),
|
|
7718
|
-
tuning: { ...this.tuningValues }
|
|
9440
|
+
tuning: { ...this.tuningValues },
|
|
9441
|
+
// Only when non-default, so pre-pause snapshots stay byte-identical.
|
|
9442
|
+
...this.paused ? { paused: true } : {},
|
|
9443
|
+
...this.timeScale !== 1 ? { timeScale: this.timeScale } : {}
|
|
7719
9444
|
};
|
|
7720
9445
|
}
|
|
7721
9446
|
/** Restore a snapshot. Rebuilds the tree from data (behaviors are re-attached by scene code). */
|
|
@@ -7726,6 +9451,8 @@ var World = class {
|
|
|
7726
9451
|
this.input.setState(snap.input);
|
|
7727
9452
|
this.state = structuredClone(snap.state);
|
|
7728
9453
|
if (snap.tuning) this.tuningValues = { ...snap.tuning };
|
|
9454
|
+
this.paused = snap.paused ?? false;
|
|
9455
|
+
this.timeScale = snap.timeScale ?? 1;
|
|
7729
9456
|
resetNodeIds(1e6);
|
|
7730
9457
|
this.setRoot(deserializeNode(snap.tree));
|
|
7731
9458
|
this.ensureStarted();
|
|
@@ -7990,6 +9717,245 @@ function analyzePlaytest(def, session, opts = {}) {
|
|
|
7990
9717
|
return report;
|
|
7991
9718
|
}
|
|
7992
9719
|
|
|
9720
|
+
// src/app/browser.ts
|
|
9721
|
+
function splashCommands(cfg, def, width, height) {
|
|
9722
|
+
const bg = cfg.palette?.bg ?? "#141821";
|
|
9723
|
+
const fg = cfg.palette?.fg ?? (relLuminance(bg) > 0.4 ? "#14171f" : "#f4efe3");
|
|
9724
|
+
const title = cfg.title ?? def.title;
|
|
9725
|
+
const t = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
9726
|
+
return [
|
|
9727
|
+
{ kind: "rect", x: 0, y: 0, w: width, h: height, fill: bg, transform: t, z: 0 },
|
|
9728
|
+
{ kind: "text", text: title, x: width / 2, y: height / 2, size: Math.round(height * 0.06), align: "center", weight: 700, fill: fg, transform: t, z: 1 },
|
|
9729
|
+
{ kind: "text", text: "loading\u2026", x: width / 2, y: height / 2 + height * 0.08, size: Math.round(height * 0.03), align: "center", fill: fg, opacity: 0.7, transform: t, z: 1 }
|
|
9730
|
+
];
|
|
9731
|
+
}
|
|
9732
|
+
function runBrowser(def, mount, opts = {}) {
|
|
9733
|
+
const width = def.width ?? 1280;
|
|
9734
|
+
const height = def.height ?? 720;
|
|
9735
|
+
const background = def.background ?? "#f3ecdb";
|
|
9736
|
+
let world = createWorld(def, opts.world);
|
|
9737
|
+
const renderer = opts.renderer === "canvas" ? new Canvas2DRenderer({ width, height, background }) : opts.renderer === "webgl" ? new WebGL2Renderer({ width, height, background }) : new SvgRenderer({ width, height, background });
|
|
9738
|
+
mount.style.position = mount.style.position || "relative";
|
|
9739
|
+
renderer.mount?.(mount);
|
|
9740
|
+
setOverlayHost(mount);
|
|
9741
|
+
const input = new KeyboardSource(def.inputMap ?? {}, document);
|
|
9742
|
+
const pointer = new PointerSource(renderer, { keyboard: input });
|
|
9743
|
+
const extraSources = [...opts.sources ?? []];
|
|
9744
|
+
const capture = isCaptureMode();
|
|
9745
|
+
const declareInputs = () => world.input.declareActions(input.actionNames());
|
|
9746
|
+
declareInputs();
|
|
9747
|
+
const startAudio = () => {
|
|
9748
|
+
audio.start();
|
|
9749
|
+
const s = settings.get();
|
|
9750
|
+
audio.setVolumes(s);
|
|
9751
|
+
window.removeEventListener("pointerdown", startAudio);
|
|
9752
|
+
window.removeEventListener("keydown", startAudio);
|
|
9753
|
+
};
|
|
9754
|
+
window.addEventListener("pointerdown", startAudio);
|
|
9755
|
+
window.addEventListener("keydown", startAudio);
|
|
9756
|
+
const renderFrame = () => renderer.draw(world.render());
|
|
9757
|
+
const restart = () => {
|
|
9758
|
+
world = createWorld(def, opts.world);
|
|
9759
|
+
declareInputs();
|
|
9760
|
+
renderFrame();
|
|
9761
|
+
};
|
|
9762
|
+
const shell = opts.shell === false ? null : new Shell({
|
|
9763
|
+
title: def.title,
|
|
9764
|
+
onRestart: opts.onRestart ?? restart,
|
|
9765
|
+
onPause: (paused) => opts.onPause?.(paused)
|
|
9766
|
+
});
|
|
9767
|
+
const splashCfg = def.splash === false ? null : def.splash ?? {};
|
|
9768
|
+
const splash = splashCfg && !capture ? splashCommands(splashCfg, def, width, height) : null;
|
|
9769
|
+
let booting = !capture;
|
|
9770
|
+
const readyCbs = [];
|
|
9771
|
+
let resolveReady;
|
|
9772
|
+
const ready = new Promise((r) => resolveReady = r);
|
|
9773
|
+
const bootStart = performance.now();
|
|
9774
|
+
let last = performance.now();
|
|
9775
|
+
const finishBoot = () => {
|
|
9776
|
+
if (!booting) return;
|
|
9777
|
+
booting = false;
|
|
9778
|
+
last = performance.now();
|
|
9779
|
+
renderFrame();
|
|
9780
|
+
resolveReady();
|
|
9781
|
+
for (const cb of readyCbs.splice(0)) cb();
|
|
9782
|
+
};
|
|
9783
|
+
const perFrame = (dtMs) => {
|
|
9784
|
+
if (!capture && !shell?.isPaused && !opts.isHeld?.()) {
|
|
9785
|
+
pointer.sample(world.input);
|
|
9786
|
+
for (const s of extraSources) s.sample(world.input);
|
|
9787
|
+
const actions = input.currentActions();
|
|
9788
|
+
const steps = world.advance(dtMs, actions);
|
|
9789
|
+
if (steps > 0) {
|
|
9790
|
+
input.clearPressed();
|
|
9791
|
+
opts.onAdvance?.(world, steps, actions);
|
|
9792
|
+
}
|
|
9793
|
+
}
|
|
9794
|
+
renderFrame();
|
|
9795
|
+
};
|
|
9796
|
+
let raf = 0;
|
|
9797
|
+
let timer;
|
|
9798
|
+
let pendingCb = null;
|
|
9799
|
+
let stopped = false;
|
|
9800
|
+
const HIDDEN_TICK_MS = 16;
|
|
9801
|
+
const clearScheduled = () => {
|
|
9802
|
+
if (raf !== 0) {
|
|
9803
|
+
cancelAnimationFrame(raf);
|
|
9804
|
+
raf = 0;
|
|
9805
|
+
}
|
|
9806
|
+
if (timer !== void 0) {
|
|
9807
|
+
clearTimeout(timer);
|
|
9808
|
+
timer = void 0;
|
|
9809
|
+
}
|
|
9810
|
+
};
|
|
9811
|
+
const schedule = (cb) => {
|
|
9812
|
+
if (stopped) return;
|
|
9813
|
+
pendingCb = cb;
|
|
9814
|
+
if (document.hidden) {
|
|
9815
|
+
timer = setTimeout(() => {
|
|
9816
|
+
timer = void 0;
|
|
9817
|
+
pendingCb = null;
|
|
9818
|
+
cb(performance.now());
|
|
9819
|
+
}, HIDDEN_TICK_MS);
|
|
9820
|
+
} else {
|
|
9821
|
+
raf = requestAnimationFrame((now) => {
|
|
9822
|
+
raf = 0;
|
|
9823
|
+
pendingCb = null;
|
|
9824
|
+
cb(now);
|
|
9825
|
+
});
|
|
9826
|
+
}
|
|
9827
|
+
};
|
|
9828
|
+
const onVisibility = () => {
|
|
9829
|
+
const cb = pendingCb;
|
|
9830
|
+
if (cb === null) return;
|
|
9831
|
+
clearScheduled();
|
|
9832
|
+
pendingCb = null;
|
|
9833
|
+
schedule(cb);
|
|
9834
|
+
};
|
|
9835
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
9836
|
+
const loop = (now) => {
|
|
9837
|
+
if (booting) {
|
|
9838
|
+
if (splash) renderer.draw(splash);
|
|
9839
|
+
schedule(loop);
|
|
9840
|
+
return;
|
|
9841
|
+
}
|
|
9842
|
+
const dt = now - last;
|
|
9843
|
+
last = now;
|
|
9844
|
+
perFrame(dt);
|
|
9845
|
+
schedule(loop);
|
|
9846
|
+
};
|
|
9847
|
+
schedule(loop);
|
|
9848
|
+
if (booting) {
|
|
9849
|
+
const minDur = splashCfg?.minDurationMs ?? 0;
|
|
9850
|
+
const preload = def.preload ? Promise.resolve().then(() => def.preload(world)) : Promise.resolve();
|
|
9851
|
+
const holdSplash = new Promise((r) => setTimeout(r, Math.max(0, minDur - (performance.now() - bootStart))));
|
|
9852
|
+
Promise.all([preload, holdSplash]).then(finishBoot).catch((err) => {
|
|
9853
|
+
console.error("hayao: preload failed \u2014", err);
|
|
9854
|
+
finishBoot();
|
|
9855
|
+
});
|
|
9856
|
+
} else {
|
|
9857
|
+
resolveReady();
|
|
9858
|
+
}
|
|
9859
|
+
const handle = {
|
|
9860
|
+
get world() {
|
|
9861
|
+
return world;
|
|
9862
|
+
},
|
|
9863
|
+
renderer,
|
|
9864
|
+
input,
|
|
9865
|
+
pointer,
|
|
9866
|
+
canvas: renderer.element,
|
|
9867
|
+
toDesign: (clientX, clientY) => renderer.toDesign?.(clientX, clientY) ?? { x: clientX, y: clientY },
|
|
9868
|
+
viewport: () => renderer.viewport?.(),
|
|
9869
|
+
ready,
|
|
9870
|
+
onReady(cb) {
|
|
9871
|
+
if (booting) readyCbs.push(cb);
|
|
9872
|
+
else cb();
|
|
9873
|
+
},
|
|
9874
|
+
addSource(source) {
|
|
9875
|
+
extraSources.push(source);
|
|
9876
|
+
return () => {
|
|
9877
|
+
const i = extraSources.indexOf(source);
|
|
9878
|
+
if (i !== -1) extraSources.splice(i, 1);
|
|
9879
|
+
source.dispose?.();
|
|
9880
|
+
};
|
|
9881
|
+
},
|
|
9882
|
+
tick(dtMs) {
|
|
9883
|
+
perFrame(dtMs ?? world.clock.stepMs);
|
|
9884
|
+
},
|
|
9885
|
+
stop() {
|
|
9886
|
+
stopped = true;
|
|
9887
|
+
pendingCb = null;
|
|
9888
|
+
clearScheduled();
|
|
9889
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
9890
|
+
input.dispose();
|
|
9891
|
+
pointer.dispose();
|
|
9892
|
+
for (const s of extraSources) s.dispose?.();
|
|
9893
|
+
extraSources.length = 0;
|
|
9894
|
+
shell?.dispose();
|
|
9895
|
+
renderer.dispose?.();
|
|
9896
|
+
},
|
|
9897
|
+
restart
|
|
9898
|
+
};
|
|
9899
|
+
if (capture) {
|
|
9900
|
+
installCapture({
|
|
9901
|
+
get world() {
|
|
9902
|
+
return world;
|
|
9903
|
+
},
|
|
9904
|
+
stepOnce: (actions = []) => {
|
|
9905
|
+
world.step(actions);
|
|
9906
|
+
renderFrame();
|
|
9907
|
+
},
|
|
9908
|
+
renderSVG: () => renderToSVGString(world.render(), width, height, background),
|
|
9909
|
+
setPaused: () => {
|
|
9910
|
+
}
|
|
9911
|
+
});
|
|
9912
|
+
}
|
|
9913
|
+
return handle;
|
|
9914
|
+
}
|
|
9915
|
+
|
|
9916
|
+
// src/verify/dom.ts
|
|
9917
|
+
function synthPointer(type, x, y, id) {
|
|
9918
|
+
const e = new Event(type, { bubbles: true });
|
|
9919
|
+
e.clientX = x;
|
|
9920
|
+
e.clientY = y;
|
|
9921
|
+
e.pointerId = id;
|
|
9922
|
+
e.button = 0;
|
|
9923
|
+
return e;
|
|
9924
|
+
}
|
|
9925
|
+
function bootDom(def, mount) {
|
|
9926
|
+
if (typeof document === "undefined") {
|
|
9927
|
+
throw new Error("verify/dom: no DOM found \u2014 set the test's environment to jsdom (// @vitest-environment jsdom).");
|
|
9928
|
+
}
|
|
9929
|
+
const owned = !mount;
|
|
9930
|
+
const host2 = mount ?? document.createElement("div");
|
|
9931
|
+
if (owned) document.body.appendChild(host2);
|
|
9932
|
+
const handle = runBrowser(def, host2, { shell: false, isHeld: () => true });
|
|
9933
|
+
const el = handle.canvas;
|
|
9934
|
+
return {
|
|
9935
|
+
handle,
|
|
9936
|
+
touchDown(x, y, id = 1) {
|
|
9937
|
+
el?.dispatchEvent(synthPointer("pointerdown", x, y, id));
|
|
9938
|
+
},
|
|
9939
|
+
touchMove(x, y, id = 1) {
|
|
9940
|
+
el?.dispatchEvent(synthPointer("pointermove", x, y, id));
|
|
9941
|
+
},
|
|
9942
|
+
touchUp(id = 1) {
|
|
9943
|
+
globalThis.dispatchEvent?.(synthPointer("pointerup", 0, 0, id));
|
|
9944
|
+
},
|
|
9945
|
+
step(n5 = 1, axes) {
|
|
9946
|
+
for (let i = 0; i < n5; i++) {
|
|
9947
|
+
handle.pointer.sample(handle.world.input);
|
|
9948
|
+
handle.world.step(handle.input.currentActions(), axes);
|
|
9949
|
+
handle.input.clearPressed();
|
|
9950
|
+
}
|
|
9951
|
+
},
|
|
9952
|
+
dispose() {
|
|
9953
|
+
handle.stop();
|
|
9954
|
+
if (owned) host2.remove();
|
|
9955
|
+
}
|
|
9956
|
+
};
|
|
9957
|
+
}
|
|
9958
|
+
|
|
7993
9959
|
// src/logic/fsm.ts
|
|
7994
9960
|
var Fsm = class {
|
|
7995
9961
|
constructor(states, transitions, initial, ctx) {
|
|
@@ -8094,6 +10060,155 @@ var PhaseClock = class {
|
|
|
8094
10060
|
}
|
|
8095
10061
|
};
|
|
8096
10062
|
|
|
10063
|
+
// src/logic/coroutine.ts
|
|
10064
|
+
function sleep(seconds) {
|
|
10065
|
+
return { kind: "sleep", left: seconds };
|
|
10066
|
+
}
|
|
10067
|
+
function waitFor(cond) {
|
|
10068
|
+
return { kind: "cond", cond };
|
|
10069
|
+
}
|
|
10070
|
+
function nextStep() {
|
|
10071
|
+
return { kind: "next" };
|
|
10072
|
+
}
|
|
10073
|
+
function race(...waits) {
|
|
10074
|
+
return { kind: "race", waits };
|
|
10075
|
+
}
|
|
10076
|
+
function all(...waits) {
|
|
10077
|
+
return { kind: "all", waits };
|
|
10078
|
+
}
|
|
10079
|
+
var EPS2 = 1e-9;
|
|
10080
|
+
function advanceWait(w, dt) {
|
|
10081
|
+
if (isHandle(w)) return w.done ? 0 : -1;
|
|
10082
|
+
switch (w.kind) {
|
|
10083
|
+
case "sleep":
|
|
10084
|
+
w.left -= dt;
|
|
10085
|
+
return w.left <= EPS2 ? 0 : -1;
|
|
10086
|
+
case "cond":
|
|
10087
|
+
if (!w.met && w.cond()) w.met = true;
|
|
10088
|
+
return w.met ? 0 : -1;
|
|
10089
|
+
case "next":
|
|
10090
|
+
w.passed = true;
|
|
10091
|
+
return 0;
|
|
10092
|
+
case "race": {
|
|
10093
|
+
let winner = -1;
|
|
10094
|
+
for (let i = 0; i < w.waits.length; i++) {
|
|
10095
|
+
if (advanceWait(w.waits[i], dt) >= 0 && winner < 0) winner = i;
|
|
10096
|
+
}
|
|
10097
|
+
return winner;
|
|
10098
|
+
}
|
|
10099
|
+
case "all": {
|
|
10100
|
+
let done = true;
|
|
10101
|
+
for (const inner of w.waits) {
|
|
10102
|
+
if (!isDone(inner)) {
|
|
10103
|
+
if (advanceWait(inner, dt) < 0) done = false;
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10106
|
+
return done ? 0 : -1;
|
|
10107
|
+
}
|
|
10108
|
+
}
|
|
10109
|
+
}
|
|
10110
|
+
function isDone(w) {
|
|
10111
|
+
if (isHandle(w)) return w.done;
|
|
10112
|
+
switch (w.kind) {
|
|
10113
|
+
case "sleep":
|
|
10114
|
+
return w.left <= EPS2;
|
|
10115
|
+
case "cond":
|
|
10116
|
+
return w.met === true;
|
|
10117
|
+
case "next":
|
|
10118
|
+
return w.passed === true;
|
|
10119
|
+
case "race":
|
|
10120
|
+
return w.waits.some(isDone);
|
|
10121
|
+
case "all":
|
|
10122
|
+
return w.waits.every(isDone);
|
|
10123
|
+
}
|
|
10124
|
+
}
|
|
10125
|
+
function isHandle(w) {
|
|
10126
|
+
return w instanceof Runner;
|
|
10127
|
+
}
|
|
10128
|
+
var Runner = class {
|
|
10129
|
+
name;
|
|
10130
|
+
done = false;
|
|
10131
|
+
/** null until the first resume (which runs the body to its first yield). */
|
|
10132
|
+
wait = null;
|
|
10133
|
+
gen;
|
|
10134
|
+
constructor(factory, name) {
|
|
10135
|
+
this.name = name;
|
|
10136
|
+
this.gen = factory();
|
|
10137
|
+
}
|
|
10138
|
+
/** Resume the generator with a value; a throw kills only this runner. */
|
|
10139
|
+
resume(value) {
|
|
10140
|
+
try {
|
|
10141
|
+
const it = this.gen.next(value);
|
|
10142
|
+
if (it.done) this.done = true;
|
|
10143
|
+
else this.wait = it.value;
|
|
10144
|
+
} catch (err) {
|
|
10145
|
+
this.done = true;
|
|
10146
|
+
console.warn(`[hayao] coroutine "${this.name}" threw and was stopped:`, err);
|
|
10147
|
+
}
|
|
10148
|
+
}
|
|
10149
|
+
stop() {
|
|
10150
|
+
if (this.done) return;
|
|
10151
|
+
this.done = true;
|
|
10152
|
+
try {
|
|
10153
|
+
this.gen.return(void 0);
|
|
10154
|
+
} catch {
|
|
10155
|
+
}
|
|
10156
|
+
}
|
|
10157
|
+
};
|
|
10158
|
+
var Coroutines = class {
|
|
10159
|
+
runners = [];
|
|
10160
|
+
/** Started since the last step boundary; adopted at the top of the next step. */
|
|
10161
|
+
pending = [];
|
|
10162
|
+
nextId = 0;
|
|
10163
|
+
/**
|
|
10164
|
+
* Register a coroutine. Its body runs (up to the first yield) on the NEXT
|
|
10165
|
+
* `step()` — never immediately, and never mid-step. Pass a `name` so a
|
|
10166
|
+
* thrown generator warns something better than "co3".
|
|
10167
|
+
*/
|
|
10168
|
+
start(gen, name) {
|
|
10169
|
+
const r = new Runner(gen, name ?? `co${this.nextId}`);
|
|
10170
|
+
this.nextId++;
|
|
10171
|
+
this.pending.push(r);
|
|
10172
|
+
return r;
|
|
10173
|
+
}
|
|
10174
|
+
/** Advance every live runner by one fixed step, in insertion order. */
|
|
10175
|
+
step(dt) {
|
|
10176
|
+
if (this.pending.length > 0) {
|
|
10177
|
+
this.runners.push(...this.pending);
|
|
10178
|
+
this.pending.length = 0;
|
|
10179
|
+
}
|
|
10180
|
+
for (const r of this.runners) {
|
|
10181
|
+
if (r.done) continue;
|
|
10182
|
+
if (r.wait === null) {
|
|
10183
|
+
r.resume(0);
|
|
10184
|
+
continue;
|
|
10185
|
+
}
|
|
10186
|
+
const result = advanceWait(r.wait, dt);
|
|
10187
|
+
if (result >= 0) {
|
|
10188
|
+
r.wait = null;
|
|
10189
|
+
r.resume(result);
|
|
10190
|
+
}
|
|
10191
|
+
}
|
|
10192
|
+
let write = 0;
|
|
10193
|
+
for (const r of this.runners) if (!r.done) this.runners[write++] = r;
|
|
10194
|
+
this.runners.length = write;
|
|
10195
|
+
}
|
|
10196
|
+
/** Stop every runner (including ones not yet adopted). */
|
|
10197
|
+
stopAll() {
|
|
10198
|
+
for (const r of this.runners) r.stop();
|
|
10199
|
+
for (const r of this.pending) r.stop();
|
|
10200
|
+
this.runners.length = 0;
|
|
10201
|
+
this.pending.length = 0;
|
|
10202
|
+
}
|
|
10203
|
+
/** Live runner count (started-but-not-yet-stepped ones included). */
|
|
10204
|
+
get active() {
|
|
10205
|
+
let n5 = 0;
|
|
10206
|
+
for (const r of this.runners) if (!r.done) n5++;
|
|
10207
|
+
for (const r of this.pending) if (!r.done) n5++;
|
|
10208
|
+
return n5;
|
|
10209
|
+
}
|
|
10210
|
+
};
|
|
10211
|
+
|
|
8097
10212
|
// src/logic/random.ts
|
|
8098
10213
|
function totalWeight(weights) {
|
|
8099
10214
|
let sum = 0;
|
|
@@ -10189,138 +12304,6 @@ function runBrowserNet(def, mount, opts) {
|
|
|
10189
12304
|
};
|
|
10190
12305
|
}
|
|
10191
12306
|
|
|
10192
|
-
// src/app/browser.ts
|
|
10193
|
-
function splashCommands(cfg, def, width, height) {
|
|
10194
|
-
const bg = cfg.palette?.bg ?? "#141821";
|
|
10195
|
-
const fg = cfg.palette?.fg ?? (relLuminance(bg) > 0.4 ? "#14171f" : "#f4efe3");
|
|
10196
|
-
const title = cfg.title ?? def.title;
|
|
10197
|
-
const t = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
10198
|
-
return [
|
|
10199
|
-
{ kind: "rect", x: 0, y: 0, w: width, h: height, fill: bg, transform: t, z: 0 },
|
|
10200
|
-
{ kind: "text", text: title, x: width / 2, y: height / 2, size: Math.round(height * 0.06), align: "center", weight: 700, fill: fg, transform: t, z: 1 },
|
|
10201
|
-
{ kind: "text", text: "loading\u2026", x: width / 2, y: height / 2 + height * 0.08, size: Math.round(height * 0.03), align: "center", fill: fg, opacity: 0.7, transform: t, z: 1 }
|
|
10202
|
-
];
|
|
10203
|
-
}
|
|
10204
|
-
function runBrowser(def, mount, opts = {}) {
|
|
10205
|
-
const width = def.width ?? 1280;
|
|
10206
|
-
const height = def.height ?? 720;
|
|
10207
|
-
const background = def.background ?? "#f3ecdb";
|
|
10208
|
-
let world = createWorld(def, opts.world);
|
|
10209
|
-
const renderer = opts.renderer === "canvas" ? new Canvas2DRenderer({ width, height, background }) : new SvgRenderer({ width, height, background });
|
|
10210
|
-
mount.style.position = mount.style.position || "relative";
|
|
10211
|
-
renderer.mount?.(mount);
|
|
10212
|
-
setOverlayHost(mount);
|
|
10213
|
-
const input = new KeyboardSource(def.inputMap ?? {}, document);
|
|
10214
|
-
const pointer = new PointerSource(renderer);
|
|
10215
|
-
const capture = isCaptureMode();
|
|
10216
|
-
const startAudio = () => {
|
|
10217
|
-
audio.start();
|
|
10218
|
-
const s = settings.get();
|
|
10219
|
-
audio.setVolumes(s);
|
|
10220
|
-
window.removeEventListener("pointerdown", startAudio);
|
|
10221
|
-
window.removeEventListener("keydown", startAudio);
|
|
10222
|
-
};
|
|
10223
|
-
window.addEventListener("pointerdown", startAudio);
|
|
10224
|
-
window.addEventListener("keydown", startAudio);
|
|
10225
|
-
const renderFrame = () => renderer.draw(world.render());
|
|
10226
|
-
const restart = () => {
|
|
10227
|
-
world = createWorld(def, opts.world);
|
|
10228
|
-
renderFrame();
|
|
10229
|
-
};
|
|
10230
|
-
const shell = opts.shell === false ? null : new Shell({
|
|
10231
|
-
title: def.title,
|
|
10232
|
-
onRestart: opts.onRestart ?? restart,
|
|
10233
|
-
onPause: (paused) => opts.onPause?.(paused)
|
|
10234
|
-
});
|
|
10235
|
-
const splashCfg = def.splash === false ? null : def.splash ?? {};
|
|
10236
|
-
const splash = splashCfg && !capture ? splashCommands(splashCfg, def, width, height) : null;
|
|
10237
|
-
let booting = !capture;
|
|
10238
|
-
const readyCbs = [];
|
|
10239
|
-
let resolveReady;
|
|
10240
|
-
const ready = new Promise((r) => resolveReady = r);
|
|
10241
|
-
const bootStart = performance.now();
|
|
10242
|
-
let raf = 0;
|
|
10243
|
-
let last = performance.now();
|
|
10244
|
-
const finishBoot = () => {
|
|
10245
|
-
if (!booting) return;
|
|
10246
|
-
booting = false;
|
|
10247
|
-
last = performance.now();
|
|
10248
|
-
renderFrame();
|
|
10249
|
-
resolveReady();
|
|
10250
|
-
for (const cb of readyCbs.splice(0)) cb();
|
|
10251
|
-
};
|
|
10252
|
-
const loop = (now) => {
|
|
10253
|
-
if (booting) {
|
|
10254
|
-
if (splash) renderer.draw(splash);
|
|
10255
|
-
raf = requestAnimationFrame(loop);
|
|
10256
|
-
return;
|
|
10257
|
-
}
|
|
10258
|
-
const dt = now - last;
|
|
10259
|
-
last = now;
|
|
10260
|
-
if (!capture && !shell?.isPaused && !opts.isHeld?.()) {
|
|
10261
|
-
pointer.sample(world.input);
|
|
10262
|
-
const actions = input.currentActions();
|
|
10263
|
-
const steps = world.advance(dt, actions);
|
|
10264
|
-
if (steps > 0) {
|
|
10265
|
-
input.clearPressed();
|
|
10266
|
-
opts.onAdvance?.(world, steps, actions);
|
|
10267
|
-
}
|
|
10268
|
-
}
|
|
10269
|
-
renderFrame();
|
|
10270
|
-
raf = requestAnimationFrame(loop);
|
|
10271
|
-
};
|
|
10272
|
-
raf = requestAnimationFrame(loop);
|
|
10273
|
-
if (booting) {
|
|
10274
|
-
const minDur = splashCfg?.minDurationMs ?? 0;
|
|
10275
|
-
const preload = def.preload ? Promise.resolve().then(() => def.preload(world)) : Promise.resolve();
|
|
10276
|
-
const holdSplash = new Promise((r) => setTimeout(r, Math.max(0, minDur - (performance.now() - bootStart))));
|
|
10277
|
-
Promise.all([preload, holdSplash]).then(finishBoot).catch((err) => {
|
|
10278
|
-
console.error("hayao: preload failed \u2014", err);
|
|
10279
|
-
finishBoot();
|
|
10280
|
-
});
|
|
10281
|
-
} else {
|
|
10282
|
-
resolveReady();
|
|
10283
|
-
}
|
|
10284
|
-
const handle = {
|
|
10285
|
-
get world() {
|
|
10286
|
-
return world;
|
|
10287
|
-
},
|
|
10288
|
-
renderer,
|
|
10289
|
-
input,
|
|
10290
|
-
pointer,
|
|
10291
|
-
canvas: renderer.element,
|
|
10292
|
-
toDesign: (clientX, clientY) => renderer.toDesign?.(clientX, clientY) ?? { x: clientX, y: clientY },
|
|
10293
|
-
ready,
|
|
10294
|
-
onReady(cb) {
|
|
10295
|
-
if (booting) readyCbs.push(cb);
|
|
10296
|
-
else cb();
|
|
10297
|
-
},
|
|
10298
|
-
stop() {
|
|
10299
|
-
cancelAnimationFrame(raf);
|
|
10300
|
-
input.dispose();
|
|
10301
|
-
pointer.dispose();
|
|
10302
|
-
shell?.dispose();
|
|
10303
|
-
renderer.dispose?.();
|
|
10304
|
-
},
|
|
10305
|
-
restart
|
|
10306
|
-
};
|
|
10307
|
-
if (capture) {
|
|
10308
|
-
installCapture({
|
|
10309
|
-
get world() {
|
|
10310
|
-
return world;
|
|
10311
|
-
},
|
|
10312
|
-
stepOnce: (actions = []) => {
|
|
10313
|
-
world.step(actions);
|
|
10314
|
-
renderFrame();
|
|
10315
|
-
},
|
|
10316
|
-
renderSVG: () => renderToSVGString(world.render(), width, height, background),
|
|
10317
|
-
setPaused: () => {
|
|
10318
|
-
}
|
|
10319
|
-
});
|
|
10320
|
-
}
|
|
10321
|
-
return handle;
|
|
10322
|
-
}
|
|
10323
|
-
|
|
10324
12307
|
// src/studio/record.ts
|
|
10325
12308
|
var counter = 0;
|
|
10326
12309
|
function sessionId(game, startedAt) {
|
|
@@ -10774,13 +12757,14 @@ function runStudio(baseDef, mount, opts = {}) {
|
|
|
10774
12757
|
}
|
|
10775
12758
|
|
|
10776
12759
|
// src/index.ts
|
|
10777
|
-
var VERSION = "0.
|
|
12760
|
+
var VERSION = "0.4.0";
|
|
10778
12761
|
export {
|
|
10779
12762
|
ALBUM,
|
|
10780
12763
|
AMBIENT_PRESETS,
|
|
10781
12764
|
AmbientField,
|
|
10782
12765
|
AnimationPlayer,
|
|
10783
12766
|
AudioBus,
|
|
12767
|
+
BLOOM_PIPELINE,
|
|
10784
12768
|
BitmapText,
|
|
10785
12769
|
BroadcastChannelTransport,
|
|
10786
12770
|
Camera2D,
|
|
@@ -10788,11 +12772,14 @@ export {
|
|
|
10788
12772
|
Canvas2DRenderer,
|
|
10789
12773
|
CinematicPlayer,
|
|
10790
12774
|
Clock,
|
|
12775
|
+
Coroutines,
|
|
12776
|
+
DEFAULT_GAMEPAD_MAP,
|
|
10791
12777
|
DEFAULT_INPUT_MAP,
|
|
10792
12778
|
DEFAULT_PLATFORMER,
|
|
10793
12779
|
DEFAULT_SESSION_CONFIG,
|
|
10794
12780
|
DEFAULT_TILE_CHARS,
|
|
10795
12781
|
DUSK,
|
|
12782
|
+
DepthSort,
|
|
10796
12783
|
EASINGS,
|
|
10797
12784
|
Edge,
|
|
10798
12785
|
EventBus,
|
|
@@ -10802,12 +12789,14 @@ export {
|
|
|
10802
12789
|
Fsm,
|
|
10803
12790
|
GENRES,
|
|
10804
12791
|
GENRE_PROFILES,
|
|
12792
|
+
GamepadSource,
|
|
10805
12793
|
HeadlessRenderer,
|
|
10806
12794
|
IDENTITY,
|
|
10807
12795
|
INSTRUMENTS,
|
|
10808
12796
|
InputBuffer,
|
|
10809
12797
|
InputRecorder,
|
|
10810
12798
|
InputState,
|
|
12799
|
+
IsoPrism,
|
|
10811
12800
|
KENTO,
|
|
10812
12801
|
KeyboardSource,
|
|
10813
12802
|
LocalStorageAdapter,
|
|
@@ -10816,6 +12805,7 @@ export {
|
|
|
10816
12805
|
LoopbackTransport,
|
|
10817
12806
|
LootTable,
|
|
10818
12807
|
MEADOW,
|
|
12808
|
+
MOUSE_ACTIONS,
|
|
10819
12809
|
MemoryStorage,
|
|
10820
12810
|
NEIGHBORS_4,
|
|
10821
12811
|
NEIGHBORS_8,
|
|
@@ -10857,9 +12847,13 @@ export {
|
|
|
10857
12847
|
Text,
|
|
10858
12848
|
TextureSprite,
|
|
10859
12849
|
Timer,
|
|
12850
|
+
TouchControls,
|
|
10860
12851
|
UndoStack,
|
|
10861
12852
|
VERSION,
|
|
12853
|
+
VerletChain,
|
|
12854
|
+
WEBGL_EFFECTS,
|
|
10862
12855
|
WangFrame,
|
|
12856
|
+
WebGL2Renderer,
|
|
10863
12857
|
World,
|
|
10864
12858
|
abilitiesOf,
|
|
10865
12859
|
addBody,
|
|
@@ -10867,6 +12861,7 @@ export {
|
|
|
10867
12861
|
addRevoluteJoint,
|
|
10868
12862
|
addTransition,
|
|
10869
12863
|
albumTrack,
|
|
12864
|
+
all,
|
|
10870
12865
|
analyzePlaytest,
|
|
10871
12866
|
applyImpulse,
|
|
10872
12867
|
applyReverb,
|
|
@@ -10888,6 +12883,7 @@ export {
|
|
|
10888
12883
|
blobPath,
|
|
10889
12884
|
bodyAABB,
|
|
10890
12885
|
bodyContains,
|
|
12886
|
+
bootDom,
|
|
10891
12887
|
cadenceResolves,
|
|
10892
12888
|
cameraIssues,
|
|
10893
12889
|
canvasGradient,
|
|
@@ -10931,6 +12927,7 @@ export {
|
|
|
10931
12927
|
dexp,
|
|
10932
12928
|
dexp2,
|
|
10933
12929
|
dhypot,
|
|
12930
|
+
diamondPoints,
|
|
10934
12931
|
diatonicChord,
|
|
10935
12932
|
diffLevels,
|
|
10936
12933
|
distanceGain,
|
|
@@ -10953,11 +12950,13 @@ export {
|
|
|
10953
12950
|
fft,
|
|
10954
12951
|
findSoftlocks,
|
|
10955
12952
|
firstFrame,
|
|
12953
|
+
fitViewport,
|
|
10956
12954
|
floodFill,
|
|
10957
12955
|
fluxEnvelope,
|
|
10958
12956
|
forgivenessIssues,
|
|
10959
12957
|
fractalNoise,
|
|
10960
12958
|
frameActions,
|
|
12959
|
+
frameAxes,
|
|
10961
12960
|
gameInputMap,
|
|
10962
12961
|
generateCave,
|
|
10963
12962
|
generateDungeon,
|
|
@@ -10975,6 +12974,7 @@ export {
|
|
|
10975
12974
|
gridSet,
|
|
10976
12975
|
gridToTilemap,
|
|
10977
12976
|
groundAt,
|
|
12977
|
+
hashNoise,
|
|
10978
12978
|
hashString,
|
|
10979
12979
|
hashValue,
|
|
10980
12980
|
hexToHsl,
|
|
@@ -10988,10 +12988,12 @@ export {
|
|
|
10988
12988
|
inputDensity,
|
|
10989
12989
|
installCapture,
|
|
10990
12990
|
invLerp,
|
|
12991
|
+
invalidCommandReason,
|
|
10991
12992
|
invertTransform,
|
|
10992
12993
|
isCaptureMode,
|
|
10993
12994
|
isGround,
|
|
10994
12995
|
isMonotonic,
|
|
12996
|
+
iso,
|
|
10995
12997
|
joinRoom,
|
|
10996
12998
|
jumpAirtime,
|
|
10997
12999
|
jumpDistance,
|
|
@@ -11036,6 +13038,7 @@ export {
|
|
|
11036
13038
|
moveRect,
|
|
11037
13039
|
mutateColor,
|
|
11038
13040
|
netMessage,
|
|
13041
|
+
nextStep,
|
|
11039
13042
|
nineSlice,
|
|
11040
13043
|
normalize,
|
|
11041
13044
|
noteToMidi,
|
|
@@ -11064,6 +13067,8 @@ export {
|
|
|
11064
13067
|
proveCompletable,
|
|
11065
13068
|
proveFullCompletion,
|
|
11066
13069
|
pump,
|
|
13070
|
+
quantizeAngle,
|
|
13071
|
+
race,
|
|
11067
13072
|
rad2deg,
|
|
11068
13073
|
radialGradient,
|
|
11069
13074
|
rampIssues,
|
|
@@ -11077,6 +13082,7 @@ export {
|
|
|
11077
13082
|
rectContains,
|
|
11078
13083
|
rectsOverlap,
|
|
11079
13084
|
registerNode,
|
|
13085
|
+
regularPolyPoints,
|
|
11080
13086
|
regularPolygon,
|
|
11081
13087
|
relLuminance,
|
|
11082
13088
|
remap,
|
|
@@ -11090,6 +13096,7 @@ export {
|
|
|
11090
13096
|
replay,
|
|
11091
13097
|
replaySession,
|
|
11092
13098
|
resetNodeIds,
|
|
13099
|
+
resetZzfxWarnings,
|
|
11093
13100
|
resolveTuning,
|
|
11094
13101
|
rigidStep,
|
|
11095
13102
|
rleDecode,
|
|
@@ -11118,14 +13125,17 @@ export {
|
|
|
11118
13125
|
setOverlayHost,
|
|
11119
13126
|
setScreenObserver,
|
|
11120
13127
|
settings,
|
|
13128
|
+
shadeHex,
|
|
11121
13129
|
shadowDef,
|
|
11122
13130
|
shapeBBox,
|
|
11123
13131
|
shapeBox,
|
|
11124
13132
|
showScreen,
|
|
11125
13133
|
signalHash,
|
|
13134
|
+
sleep,
|
|
11126
13135
|
smoothClosedPath,
|
|
11127
13136
|
smoothOpenPath,
|
|
11128
13137
|
smoothstep,
|
|
13138
|
+
snapAxis,
|
|
11129
13139
|
softClip,
|
|
11130
13140
|
softClipInPlace,
|
|
11131
13141
|
solidNeighbours,
|
|
@@ -11135,6 +13145,7 @@ export {
|
|
|
11135
13145
|
songDuration,
|
|
11136
13146
|
sortCommands,
|
|
11137
13147
|
spatialMix,
|
|
13148
|
+
specFromZzfx,
|
|
11138
13149
|
spectralCentroid,
|
|
11139
13150
|
spring,
|
|
11140
13151
|
springStep,
|
|
@@ -11170,8 +13181,10 @@ export {
|
|
|
11170
13181
|
vscale,
|
|
11171
13182
|
vsub,
|
|
11172
13183
|
wait,
|
|
13184
|
+
waitFor,
|
|
11173
13185
|
wakeBody,
|
|
11174
13186
|
wangTile,
|
|
13187
|
+
warnCommandOnce,
|
|
11175
13188
|
weatherEnvelope,
|
|
11176
13189
|
weightedIndex,
|
|
11177
13190
|
weightedPick,
|