@guinetik/gcanvas 1.0.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/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- package/vitest.config.js +13 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BezierShape,
|
|
3
|
+
Circle,
|
|
4
|
+
FPSCounter,
|
|
5
|
+
Game,
|
|
6
|
+
GameObject,
|
|
7
|
+
Tween,
|
|
8
|
+
Painter,
|
|
9
|
+
Scene,
|
|
10
|
+
Easing,
|
|
11
|
+
} from "../../src/index";
|
|
12
|
+
|
|
13
|
+
class MyGame extends Game {
|
|
14
|
+
constructor(canvas) {
|
|
15
|
+
super(canvas);
|
|
16
|
+
this.enableFluidSize();
|
|
17
|
+
this.backgroundColor = "black";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
init() {
|
|
21
|
+
super.init();
|
|
22
|
+
// Set up scenes
|
|
23
|
+
this.scene = new Scene(this);
|
|
24
|
+
this.ui = new Scene(this);
|
|
25
|
+
this.pipeline.add(this.scene); // game layer
|
|
26
|
+
this.pipeline.add(this.ui); // UI layer
|
|
27
|
+
|
|
28
|
+
// Add signature animation
|
|
29
|
+
this.signature = new SignatureAnimation(this, {
|
|
30
|
+
debug: true,
|
|
31
|
+
anchor: "center",
|
|
32
|
+
});
|
|
33
|
+
this.signature.width = 400;
|
|
34
|
+
this.signature.height = 150;
|
|
35
|
+
this.scene.add(this.signature);
|
|
36
|
+
|
|
37
|
+
// Add FPS counter in the UI scene
|
|
38
|
+
this.ui.add(new FPSCounter(this, { anchor: "bottom-right" }));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Signature Animation - A progressive bezier curve animation
|
|
43
|
+
class SignatureAnimation extends GameObject {
|
|
44
|
+
constructor(game, options = {}) {
|
|
45
|
+
super(game, options);
|
|
46
|
+
|
|
47
|
+
// The signature path - represents a cursive signature
|
|
48
|
+
this.signaturePath = [
|
|
49
|
+
// First part - the cursive letter (stylized name)
|
|
50
|
+
["M", -200, 20],
|
|
51
|
+
["C", -180, -40, -160, 40, -140, 10],
|
|
52
|
+
["C", -120, -30, -100, 40, -80, 0],
|
|
53
|
+
["C", -60, -40, -40, 40, -20, 10],
|
|
54
|
+
["C", 0, -20, 20, -30, 40, 10],
|
|
55
|
+
["C", 60, 40, 80, 40, 100, 20],
|
|
56
|
+
["C", 120, 0, 140, -10, 160, 10],
|
|
57
|
+
["C", 180, 30, 200, 30, 220, 10],
|
|
58
|
+
|
|
59
|
+
// The underline swoop
|
|
60
|
+
["M", -180, 40],
|
|
61
|
+
["C", -100, 60, 100, 80, 250, 30],
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Initialize state
|
|
65
|
+
this.progress = 0;
|
|
66
|
+
this.speed = 0.3; // Speed of animation
|
|
67
|
+
this.complete = false;
|
|
68
|
+
|
|
69
|
+
// Create visible bezier shape for the signature
|
|
70
|
+
this.signature = new BezierShape(
|
|
71
|
+
[], // Start with empty path
|
|
72
|
+
{
|
|
73
|
+
stroke: "#fff",
|
|
74
|
+
lineWidth: 3,
|
|
75
|
+
color: null,
|
|
76
|
+
debug: true,
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Create a circle to represent the pen tip
|
|
81
|
+
this.penTip = new Circle(8, {
|
|
82
|
+
x: game.width / 2,
|
|
83
|
+
y: game.height / 2,
|
|
84
|
+
color: "#4f8",
|
|
85
|
+
shadowColor: "rgba(64, 255, 128, 0.8)",
|
|
86
|
+
shadowBlur: 15,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Canvas click handler to restart animation
|
|
90
|
+
game.canvas.addEventListener("click", () => this.restart());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Restart the animation
|
|
94
|
+
restart() {
|
|
95
|
+
this.progress = 0;
|
|
96
|
+
this.complete = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Calculate the current point along a particular bezier curve segment
|
|
100
|
+
getBezierPoint(segment, t) {
|
|
101
|
+
if (segment[0] === "M") {
|
|
102
|
+
// For move commands, just return the point
|
|
103
|
+
return { x: segment[1], y: segment[2] };
|
|
104
|
+
} else if (segment[0] === "C") {
|
|
105
|
+
// For Cubic Bezier curves, calculate the point at t
|
|
106
|
+
const startX = this.prevX || 0;
|
|
107
|
+
const startY = this.prevY || 0;
|
|
108
|
+
const cp1x = segment[1];
|
|
109
|
+
const cp1y = segment[2];
|
|
110
|
+
const cp2x = segment[3];
|
|
111
|
+
const cp2y = segment[4];
|
|
112
|
+
const endX = segment[5];
|
|
113
|
+
const endY = segment[6];
|
|
114
|
+
|
|
115
|
+
// Cubic Bezier formula
|
|
116
|
+
const x =
|
|
117
|
+
Math.pow(1 - t, 3) * startX +
|
|
118
|
+
3 * Math.pow(1 - t, 2) * t * cp1x +
|
|
119
|
+
3 * (1 - t) * Math.pow(t, 2) * cp2x +
|
|
120
|
+
Math.pow(t, 3) * endX;
|
|
121
|
+
|
|
122
|
+
const y =
|
|
123
|
+
Math.pow(1 - t, 3) * startY +
|
|
124
|
+
3 * Math.pow(1 - t, 2) * t * cp1y +
|
|
125
|
+
3 * (1 - t) * Math.pow(t, 2) * cp2y +
|
|
126
|
+
Math.pow(t, 3) * endY;
|
|
127
|
+
|
|
128
|
+
return { x, y };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { x: 0, y: 0 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Get a subset of the path up to the current progress
|
|
135
|
+
getPartialPath() {
|
|
136
|
+
const result = [];
|
|
137
|
+
let totalSegments = this.signaturePath.length;
|
|
138
|
+
let segmentIndex = Math.floor(this.progress * totalSegments);
|
|
139
|
+
let segmentProgress = (this.progress * totalSegments) % 1;
|
|
140
|
+
|
|
141
|
+
// Add all completed segments
|
|
142
|
+
for (let i = 0; i < segmentIndex; i++) {
|
|
143
|
+
result.push([...this.signaturePath[i]]);
|
|
144
|
+
|
|
145
|
+
// Keep track of the last point for calculating bezier curves
|
|
146
|
+
if (this.signaturePath[i][0] === "M") {
|
|
147
|
+
this.prevX = this.signaturePath[i][1];
|
|
148
|
+
this.prevY = this.signaturePath[i][2];
|
|
149
|
+
} else if (this.signaturePath[i][0] === "C") {
|
|
150
|
+
this.prevX = this.signaturePath[i][5];
|
|
151
|
+
this.prevY = this.signaturePath[i][6];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Add the current segment with partial progress
|
|
156
|
+
if (segmentIndex < totalSegments) {
|
|
157
|
+
const currentSegment = this.signaturePath[segmentIndex];
|
|
158
|
+
|
|
159
|
+
if (currentSegment[0] === "M") {
|
|
160
|
+
// For move commands, add the full command
|
|
161
|
+
result.push([...currentSegment]);
|
|
162
|
+
this.prevX = currentSegment[1];
|
|
163
|
+
this.prevY = currentSegment[2];
|
|
164
|
+
|
|
165
|
+
// Position pen tip at the move point
|
|
166
|
+
this.penTipPos = {
|
|
167
|
+
x: currentSegment[1],
|
|
168
|
+
y: currentSegment[2],
|
|
169
|
+
};
|
|
170
|
+
} else if (currentSegment[0] === "C") {
|
|
171
|
+
// For bezier curves, calculate the partial command
|
|
172
|
+
const point = this.getBezierPoint(currentSegment, segmentProgress);
|
|
173
|
+
|
|
174
|
+
// Add a partial curve to the result
|
|
175
|
+
result.push([
|
|
176
|
+
"C",
|
|
177
|
+
currentSegment[1],
|
|
178
|
+
currentSegment[2],
|
|
179
|
+
currentSegment[3],
|
|
180
|
+
currentSegment[4],
|
|
181
|
+
point.x,
|
|
182
|
+
point.y,
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
// Position pen tip at the end of the partial curve
|
|
186
|
+
this.penTipPos = point;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
update(dt) {
|
|
194
|
+
// Update progress if animation not complete
|
|
195
|
+
if (!this.complete) {
|
|
196
|
+
this.progress += dt * this.speed;
|
|
197
|
+
|
|
198
|
+
if (this.progress >= 1) {
|
|
199
|
+
this.progress = 1;
|
|
200
|
+
this.complete = true;
|
|
201
|
+
}
|
|
202
|
+
// Calculate partial path based on current progress
|
|
203
|
+
this.currentPath = this.getPartialPath();
|
|
204
|
+
// Update signature path
|
|
205
|
+
this.signature.path = this.currentPath;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Add gentle bouncing motion when complete
|
|
209
|
+
if (this.complete) {
|
|
210
|
+
const time = performance.now() / 1000;
|
|
211
|
+
this.signature.y = Math.sin(time * 2) * 5;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Update pen tip position
|
|
215
|
+
if (this.penTipPos) {
|
|
216
|
+
this.penTip.x = this.penTipPos.x;
|
|
217
|
+
this.penTip.y = this.penTipPos.y;
|
|
218
|
+
}
|
|
219
|
+
super.update(dt);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
draw() {
|
|
223
|
+
super.draw();
|
|
224
|
+
// Draw signature
|
|
225
|
+
this.signature.render();
|
|
226
|
+
// Draw pen tip if animation is not complete
|
|
227
|
+
if (!this.complete) {
|
|
228
|
+
this.penTip.render();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
render() {
|
|
233
|
+
super.render();
|
|
234
|
+
Painter.text.setFont("18px monospace");
|
|
235
|
+
Painter.text.setTextAlign("center");
|
|
236
|
+
Painter.text.setTextBaseline("bottom");
|
|
237
|
+
Painter.text.fillText("Click anywhere to restart the signature animation", this.game.width / 2, this.game.height - 40, "#4f8");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export { MyGame };
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AccretionDisk - Manages disk particles and formation animation
|
|
3
|
+
*
|
|
4
|
+
* Handles particle creation, formation state transitions,
|
|
5
|
+
* camera-space projection with gravitational lensing, and Doppler effects.
|
|
6
|
+
*/
|
|
7
|
+
import { GameObject, Easing } from "../../../src/index.js";
|
|
8
|
+
import { Particle } from "./particle.js";
|
|
9
|
+
|
|
10
|
+
// Formation source configuration
|
|
11
|
+
const INFALL_SOURCE_ANGLE = Math.PI * 1.25; // Top-right corner
|
|
12
|
+
const INFALL_STREAM_WIDTH = 0.12;
|
|
13
|
+
const INFALL_SPIRAL_TURNS = 1.5;
|
|
14
|
+
|
|
15
|
+
export class AccretionDisk extends GameObject {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Game} game - Game instance
|
|
18
|
+
* @param {Object} options
|
|
19
|
+
* @param {Camera3D} options.camera - Camera for projection
|
|
20
|
+
* @param {StateMachine} options.formationFSM - Formation state machine
|
|
21
|
+
* @param {number} options.baseScale - Base scale for sizing
|
|
22
|
+
* @param {number} options.bhRadius - Black hole radius
|
|
23
|
+
* @param {number} options.diskInner - Inner disk radius
|
|
24
|
+
* @param {number} options.diskOuter - Outer disk radius
|
|
25
|
+
* @param {number} [options.particleCount=2500] - Number of particles
|
|
26
|
+
* @param {number} [options.diskTilt=0] - Disk tilt in radians
|
|
27
|
+
* @param {Object} [options.colors] - Color configuration
|
|
28
|
+
*/
|
|
29
|
+
constructor(game, options = {}) {
|
|
30
|
+
super(game, options);
|
|
31
|
+
|
|
32
|
+
this.camera = options.camera;
|
|
33
|
+
this.formationFSM = options.formationFSM;
|
|
34
|
+
|
|
35
|
+
// Sizing (updated from main demo on resize)
|
|
36
|
+
this.baseScale = options.baseScale ?? 500;
|
|
37
|
+
this.bhRadius = options.bhRadius ?? 40;
|
|
38
|
+
this.diskInner = options.diskInner ?? 60;
|
|
39
|
+
this.diskOuter = options.diskOuter ?? 175;
|
|
40
|
+
this.diskTilt = options.diskTilt ?? 0;
|
|
41
|
+
|
|
42
|
+
// Particle configuration
|
|
43
|
+
this.particleCount = options.particleCount ?? 2500;
|
|
44
|
+
this.particles = [];
|
|
45
|
+
|
|
46
|
+
// Colors for temperature gradient
|
|
47
|
+
this.colors = options.colors ?? {
|
|
48
|
+
inner: [255, 250, 220], // White-hot
|
|
49
|
+
mid: [255, 160, 50], // Orange
|
|
50
|
+
outer: [180, 40, 40], // Deep red
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Consumption tracking
|
|
54
|
+
this.particlesConsumed = 0;
|
|
55
|
+
this.totalParticleMass = this.particleCount;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Update sizing when window resizes.
|
|
60
|
+
*/
|
|
61
|
+
updateSizing(baseScale, bhRadius, diskInner, diskOuter) {
|
|
62
|
+
this.baseScale = baseScale;
|
|
63
|
+
this.bhRadius = bhRadius;
|
|
64
|
+
this.diskInner = diskInner;
|
|
65
|
+
this.diskOuter = diskOuter;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get temperature-based color for a particle at radius r.
|
|
70
|
+
*/
|
|
71
|
+
getHeatColor(r) {
|
|
72
|
+
const t = (r - this.diskInner) / (this.diskOuter - this.diskInner);
|
|
73
|
+
const c1 = t < 0.3 ? this.colors.inner : this.colors.mid;
|
|
74
|
+
const c2 = t < 0.3 ? this.colors.mid : this.colors.outer;
|
|
75
|
+
const mix = t < 0.3 ? t / 0.3 : (t - 0.3) / 0.7;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
r: c1[0] + (c2[0] - c1[0]) * mix,
|
|
79
|
+
g: c1[1] + (c2[1] - c1[1]) * mix,
|
|
80
|
+
b: c1[2] + (c2[2] - c1[2]) * mix,
|
|
81
|
+
a: 1 - t,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Initialize particles at their final disk positions.
|
|
87
|
+
* Used for initial load and resize.
|
|
88
|
+
*/
|
|
89
|
+
initParticles() {
|
|
90
|
+
this.particles = [];
|
|
91
|
+
for (let i = 0; i < this.particleCount; i++) {
|
|
92
|
+
const angle = Math.random() * Math.PI * 2;
|
|
93
|
+
const t = Math.random();
|
|
94
|
+
// Bias toward inner (hotter) region
|
|
95
|
+
const r = this.diskInner + t * t * (this.diskOuter - this.diskInner);
|
|
96
|
+
|
|
97
|
+
const speed = (1 / Math.sqrt(r)) * 600; // Keplerian
|
|
98
|
+
const yOffset = (Math.random() - 0.5) * this.baseScale * 0.006;
|
|
99
|
+
const baseColor = this.getHeatColor(r);
|
|
100
|
+
|
|
101
|
+
this.particles.push(
|
|
102
|
+
Particle.createForDisk(angle, r, yOffset, speed, baseColor),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reset particles for infall animation.
|
|
109
|
+
* Sets up continuous stream from top-right corner.
|
|
110
|
+
*/
|
|
111
|
+
initParticlesForInfall() {
|
|
112
|
+
this.particlesConsumed = 0;
|
|
113
|
+
this.totalParticleMass = this.particles.length;
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < this.particles.length; i++) {
|
|
116
|
+
const p = this.particles[i];
|
|
117
|
+
|
|
118
|
+
// Stream offset - like beads on a string
|
|
119
|
+
p.streamOffset = i / this.particles.length;
|
|
120
|
+
|
|
121
|
+
// Start position: far off-screen
|
|
122
|
+
const angleOffset = (Math.random() - 0.5) * INFALL_STREAM_WIDTH;
|
|
123
|
+
p.startAngle = INFALL_SOURCE_ANGLE + angleOffset;
|
|
124
|
+
p.startDistance =
|
|
125
|
+
this.baseScale * 0.9 + Math.random() * this.baseScale * 0.3;
|
|
126
|
+
p.startYOffset = (Math.random() - 0.5) * this.baseScale * 0.08;
|
|
127
|
+
|
|
128
|
+
// ~40% fall into black hole, ~60% form the disk
|
|
129
|
+
p.willFallIn = Math.random() < 0.4;
|
|
130
|
+
p.consumed = false;
|
|
131
|
+
|
|
132
|
+
if (p.willFallIn) {
|
|
133
|
+
p.targetDistance = 0;
|
|
134
|
+
p.spiralTurns = INFALL_SPIRAL_TURNS * (1.5 + Math.random() * 0.5);
|
|
135
|
+
} else {
|
|
136
|
+
p.targetAngle = Math.random() * Math.PI * 2;
|
|
137
|
+
const t = Math.random();
|
|
138
|
+
p.targetDistance =
|
|
139
|
+
this.diskInner + t * t * (this.diskOuter - this.diskInner);
|
|
140
|
+
p.spiralTurns =
|
|
141
|
+
INFALL_SPIRAL_TURNS +
|
|
142
|
+
(p.targetAngle - INFALL_SOURCE_ANGLE) / (Math.PI * 2);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
p.targetYOffset = (Math.random() - 0.5) * this.baseScale * 0.006;
|
|
146
|
+
|
|
147
|
+
// Initialize to start position
|
|
148
|
+
p.angle = p.startAngle;
|
|
149
|
+
p.distance = p.startDistance;
|
|
150
|
+
p.yOffset = p.startYOffset;
|
|
151
|
+
|
|
152
|
+
// Color based on final position
|
|
153
|
+
p.baseColor = p.willFallIn
|
|
154
|
+
? { r: 255, g: 200, b: 150, a: 1 }
|
|
155
|
+
: this.getHeatColor(p.targetDistance);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Normalize angle to [-PI, PI] range.
|
|
161
|
+
*/
|
|
162
|
+
normalizeAngle(angle) {
|
|
163
|
+
while (angle > Math.PI) angle -= Math.PI * 2;
|
|
164
|
+
while (angle < -Math.PI) angle += Math.PI * 2;
|
|
165
|
+
return angle;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
update(dt) {
|
|
169
|
+
super.update(dt);
|
|
170
|
+
this.updateParticleFormation(dt);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Update particle positions based on formation state.
|
|
175
|
+
*/
|
|
176
|
+
updateParticleFormation(dt) {
|
|
177
|
+
const state = this.formationFSM.state;
|
|
178
|
+
const progress = this.formationFSM.progress;
|
|
179
|
+
|
|
180
|
+
for (const p of this.particles) {
|
|
181
|
+
if (p.consumed) continue;
|
|
182
|
+
|
|
183
|
+
if (state === "infall") {
|
|
184
|
+
this.updateInfall(p, progress);
|
|
185
|
+
} else if (state === "collapse") {
|
|
186
|
+
this.updateCollapse(p, progress, dt);
|
|
187
|
+
} else if (state === "circularize") {
|
|
188
|
+
this.updateCircularize(p, progress, dt);
|
|
189
|
+
} else if (state === "stable") {
|
|
190
|
+
this.updateStable(p, dt);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
updateInfall(p, progress) {
|
|
196
|
+
const particleProgress = progress + p.streamOffset * 0.5;
|
|
197
|
+
const t = Math.min(1, Easing.easeInQuad(Math.max(0, particleProgress)));
|
|
198
|
+
|
|
199
|
+
// Spiral inward
|
|
200
|
+
const spiralAngle = p.startAngle + p.spiralTurns * Math.PI * 2 * t;
|
|
201
|
+
p.angle = spiralAngle;
|
|
202
|
+
|
|
203
|
+
const targetDist = p.willFallIn ? 0 : this.bhRadius * 2;
|
|
204
|
+
p.distance = Easing.lerp(p.startDistance, targetDist, t);
|
|
205
|
+
p.yOffset = Easing.lerp(p.startYOffset, 0, t);
|
|
206
|
+
|
|
207
|
+
// Check if consumed
|
|
208
|
+
if (p.willFallIn && p.distance < this.bhRadius * 0.5) {
|
|
209
|
+
p.consumed = true;
|
|
210
|
+
this.particlesConsumed++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
updateCollapse(p, progress, dt) {
|
|
215
|
+
// Delay particle spreading so black hole grows first
|
|
216
|
+
// Particles wait until 20% into collapse before spreading
|
|
217
|
+
const delayedProgress = Math.max(0, (progress - 0.2) / 0.8);
|
|
218
|
+
const t = Easing.easeOutQuad(delayedProgress);
|
|
219
|
+
|
|
220
|
+
if (p.willFallIn) {
|
|
221
|
+
// Falling particles spiral in immediately
|
|
222
|
+
const fallT = Easing.easeInQuad(progress);
|
|
223
|
+
p.distance = Easing.lerp(p.distance, 0, fallT * 0.7);
|
|
224
|
+
p.angle += p.spiralTurns * 0.1 * (1 - progress);
|
|
225
|
+
|
|
226
|
+
if (p.distance < this.bhRadius * 0.5) {
|
|
227
|
+
p.consumed = true;
|
|
228
|
+
this.particlesConsumed++;
|
|
229
|
+
}
|
|
230
|
+
} else if (delayedProgress > 0) {
|
|
231
|
+
// Disk particles spread slowly - reduced factors for gradual expansion
|
|
232
|
+
p.distance = Easing.lerp(p.distance, p.targetDistance, t * 0.3);
|
|
233
|
+
p.angle = Easing.lerp(p.angle, p.targetAngle, t * 0.2);
|
|
234
|
+
p.yOffset = Easing.lerp(p.yOffset, p.targetYOffset, t * 0.2);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
updateCircularize(p, progress, dt) {
|
|
239
|
+
const t = Easing.easeOutCubic(progress) * p.circularizeSpeed;
|
|
240
|
+
const clampedT = Math.min(1, t);
|
|
241
|
+
|
|
242
|
+
p.distance = Easing.lerp(p.distance, p.targetDistance, clampedT * 0.15);
|
|
243
|
+
p.yOffset = Easing.lerp(p.yOffset, p.targetYOffset, clampedT * 0.2);
|
|
244
|
+
|
|
245
|
+
// Start orbital motion
|
|
246
|
+
const orbitSpeed = p.speed * dt * 0.01 * clampedT;
|
|
247
|
+
p.angle += orbitSpeed;
|
|
248
|
+
|
|
249
|
+
// Drift toward target angle
|
|
250
|
+
const angleDiff = this.normalizeAngle(p.targetAngle - p.angle);
|
|
251
|
+
p.angle += angleDiff * 0.03 * clampedT;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
updateStable(p, dt) {
|
|
255
|
+
// Occasionally a particle loses angular momentum and falls in
|
|
256
|
+
if (
|
|
257
|
+
!p.isFalling &&
|
|
258
|
+
p.distance < this.diskInner * 1.5 &&
|
|
259
|
+
Math.random() < 0.0001
|
|
260
|
+
) {
|
|
261
|
+
p.isFalling = true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (p.isFalling) {
|
|
265
|
+
p.distance *= 0.985;
|
|
266
|
+
p.angle += p.speed * dt * 0.03;
|
|
267
|
+
p.yOffset *= 0.95;
|
|
268
|
+
|
|
269
|
+
if (p.distance < this.bhRadius * 0.5) {
|
|
270
|
+
p.consumed = true;
|
|
271
|
+
this.particlesConsumed++;
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
// Normal Keplerian orbits
|
|
275
|
+
p.angle += p.speed * dt * 0.01;
|
|
276
|
+
p.distance = Easing.lerp(p.distance, p.targetDistance, 0.02);
|
|
277
|
+
p.yOffset = Easing.lerp(p.yOffset, p.targetYOffset, 0.02);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Build render list with projected particles.
|
|
283
|
+
* Returns array of render items for depth sorting.
|
|
284
|
+
*/
|
|
285
|
+
buildRenderList() {
|
|
286
|
+
const state = this.formationFSM.state;
|
|
287
|
+
const renderList = [];
|
|
288
|
+
|
|
289
|
+
const diskAlpha =
|
|
290
|
+
state === "infall" || state === "collapse"
|
|
291
|
+
? 0.9
|
|
292
|
+
: state === "stable"
|
|
293
|
+
? 1
|
|
294
|
+
: Math.max(0.5, this.formationFSM.progress);
|
|
295
|
+
|
|
296
|
+
// Calculate lensing strength based on formation state
|
|
297
|
+
let lensingStrength = 0;
|
|
298
|
+
if (state === "stable") {
|
|
299
|
+
lensingStrength = 1;
|
|
300
|
+
} else if (state === "circularize") {
|
|
301
|
+
lensingStrength =
|
|
302
|
+
0.4 + Easing.easeOutCubic(this.formationFSM.progress) * 0.6;
|
|
303
|
+
} else if (state === "collapse") {
|
|
304
|
+
lensingStrength = Easing.easeInQuad(this.formationFSM.progress) * 0.4;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const cosTilt = Math.cos(this.diskTilt);
|
|
308
|
+
const sinTilt = Math.sin(this.diskTilt);
|
|
309
|
+
|
|
310
|
+
for (const p of this.particles) {
|
|
311
|
+
if (p.consumed) continue;
|
|
312
|
+
|
|
313
|
+
// World coordinates (flat disk)
|
|
314
|
+
let x = Math.cos(p.angle) * p.distance;
|
|
315
|
+
let z = Math.sin(p.angle) * p.distance;
|
|
316
|
+
let y = p.yOffset;
|
|
317
|
+
|
|
318
|
+
// Apply disk tilt
|
|
319
|
+
const yTilted = y * cosTilt - z * sinTilt;
|
|
320
|
+
const zTilted = y * sinTilt + z * cosTilt;
|
|
321
|
+
y = yTilted;
|
|
322
|
+
z = zTilted;
|
|
323
|
+
|
|
324
|
+
// Transform to camera space
|
|
325
|
+
const cosY = Math.cos(this.camera.rotationY);
|
|
326
|
+
const sinY = Math.sin(this.camera.rotationY);
|
|
327
|
+
let xCam = x * cosY - z * sinY;
|
|
328
|
+
let zCam = x * sinY + z * cosY;
|
|
329
|
+
|
|
330
|
+
const cosX = Math.cos(this.camera.rotationX);
|
|
331
|
+
const sinX = Math.sin(this.camera.rotationX);
|
|
332
|
+
let yCam = y * cosX - zCam * sinX;
|
|
333
|
+
zCam = y * sinX + zCam * cosX;
|
|
334
|
+
|
|
335
|
+
// Apply gravitational lensing
|
|
336
|
+
if (lensingStrength > 0 && zCam > 0) {
|
|
337
|
+
const currentR = Math.sqrt(xCam * xCam + yCam * yCam);
|
|
338
|
+
const ringRadius = this.bhRadius * 1.3;
|
|
339
|
+
const lensFactor = Math.exp(-currentR / (this.bhRadius * 1.5));
|
|
340
|
+
const warp = lensFactor * 1.2 * lensingStrength;
|
|
341
|
+
|
|
342
|
+
if (currentR > 0) {
|
|
343
|
+
const ratio = (currentR + ringRadius * warp) / currentR;
|
|
344
|
+
xCam *= ratio;
|
|
345
|
+
yCam *= ratio;
|
|
346
|
+
} else {
|
|
347
|
+
yCam = ringRadius * lensingStrength;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Perspective projection
|
|
352
|
+
const perspectiveScale =
|
|
353
|
+
this.camera.perspective / (this.camera.perspective + zCam);
|
|
354
|
+
const screenX = xCam * perspectiveScale;
|
|
355
|
+
const screenY = yCam * perspectiveScale;
|
|
356
|
+
|
|
357
|
+
if (zCam < -this.camera.perspective + 10) continue;
|
|
358
|
+
|
|
359
|
+
// Doppler effect
|
|
360
|
+
const velocityDir = Math.cos(p.angle + this.camera.rotationY);
|
|
361
|
+
const doppler = 1 + velocityDir * 0.4;
|
|
362
|
+
|
|
363
|
+
renderList.push({
|
|
364
|
+
type: "particle",
|
|
365
|
+
z: zCam,
|
|
366
|
+
x: screenX,
|
|
367
|
+
y: screenY,
|
|
368
|
+
scale: perspectiveScale,
|
|
369
|
+
color: p.baseColor,
|
|
370
|
+
doppler: doppler,
|
|
371
|
+
diskAlpha: diskAlpha,
|
|
372
|
+
isFalling: p.isFalling || p.willFallIn,
|
|
373
|
+
horizonProximity: p.distance / this.bhRadius,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return renderList;
|
|
378
|
+
}
|
|
379
|
+
}
|