@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,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Circle,
|
|
3
|
+
Easing,
|
|
4
|
+
FPSCounter,
|
|
5
|
+
Game,
|
|
6
|
+
GameObject,
|
|
7
|
+
Motion,
|
|
8
|
+
Painter,
|
|
9
|
+
Scene,
|
|
10
|
+
SVGShape,
|
|
11
|
+
Tween,
|
|
12
|
+
} from "../../src/index";
|
|
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
|
+
console.groupCollapsed("init");
|
|
24
|
+
this.scene = new Scene(this, { debug: true, debugColor: "#0f0", anchor: "center" });
|
|
25
|
+
this.ui = new Scene(this, { debug: true, debugColor: "#0f0", anchor: "center" });
|
|
26
|
+
this.pipeline.add(this.scene); // game layer
|
|
27
|
+
this.pipeline.add(this.ui); // UI layer
|
|
28
|
+
console.groupEnd();
|
|
29
|
+
// Add SVG path animation
|
|
30
|
+
console.groupCollapsed("add SVGPathAnimation");
|
|
31
|
+
const svg = new SVGPathAnimation(this, {
|
|
32
|
+
width: 210,
|
|
33
|
+
height: 250,
|
|
34
|
+
offsetX: -70,
|
|
35
|
+
offsetY: -35,
|
|
36
|
+
path: "M 0 30.276 L 0 9.358 L 0 0.845 L 17.139 0.845 L 17.139 -5.247 L 5.189 -5.247 L 5.189 -19.273 L 0 -19.273 L 0 -4.975 L 0 0.845 L -8.618 0.845 L -25.071 0.845 L -25.071 9.757 L -7.593 9.757 L -7.593 30.276 L 0 30.276 Z",
|
|
37
|
+
});
|
|
38
|
+
this.scene.add(svg);
|
|
39
|
+
console.groupEnd();
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
console.groupCollapsed("add SVGPathAnimation");
|
|
42
|
+
this.scene.add(
|
|
43
|
+
new SVGPathAnimation(this, {
|
|
44
|
+
width: 210,
|
|
45
|
+
height: 250,
|
|
46
|
+
offsetX: 70,
|
|
47
|
+
offsetY: 35,
|
|
48
|
+
path: "M -0.003 20.33 L -0.003 6.031 L -0.003 0.211 L 25.068 0.211 L 25.068 -8.702 L 7.59 -8.702 L 7.59 -29.22 L -0.003 -29.22 L -0.003 -8.303 L -0.003 0.211 L -17.141 0.211 L -17.141 6.304 L -5.194 6.304 L -5.194 20.33 L -0.003 20.33 Z",
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
console.groupEnd();
|
|
52
|
+
}, 200);
|
|
53
|
+
// Add FPS counter in the UI scene
|
|
54
|
+
console.groupCollapsed("add FPSCounter");
|
|
55
|
+
this.ui.add(new FPSCounter(this, { anchor: "bottom-right" }));
|
|
56
|
+
console.groupEnd();
|
|
57
|
+
this.glow = Painter.effects.createGlow('rgba(0, 255, 0, 1)', 100, {
|
|
58
|
+
pulseSpeed: 1,
|
|
59
|
+
pulseMin: 0,
|
|
60
|
+
pulseMax: 50,
|
|
61
|
+
colorShift: 0.5
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
update(dt) {
|
|
66
|
+
this.scene.width = this.width - 20;
|
|
67
|
+
this.scene.height = this.height - 20;
|
|
68
|
+
this.glow.update({ pulseSpeed: 1 });
|
|
69
|
+
super.update(dt);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
render() {
|
|
73
|
+
super.render();
|
|
74
|
+
// Instructions text
|
|
75
|
+
Painter.text.setFont("18px monospace");
|
|
76
|
+
Painter.text.setTextAlign("center");
|
|
77
|
+
Painter.text.setTextBaseline("bottom");
|
|
78
|
+
Painter.text.fillText(
|
|
79
|
+
"Click anywhere to restart the SVG path animation",
|
|
80
|
+
this.width / 2,
|
|
81
|
+
this.height - 100,
|
|
82
|
+
"#0f0"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// SVG Path Animation - An animated SVG path drawing
|
|
88
|
+
class SVGPathAnimation extends GameObject {
|
|
89
|
+
constructor(game, options = {}) {
|
|
90
|
+
super(game, options);
|
|
91
|
+
// My Logo as an SVG
|
|
92
|
+
//
|
|
93
|
+
//
|
|
94
|
+
this.offsetX = options.offsetX ?? 0;
|
|
95
|
+
this.offsetY = options.offsetY ?? 0;
|
|
96
|
+
this.animTime = 0;
|
|
97
|
+
const path =
|
|
98
|
+
options.path ??
|
|
99
|
+
"M 50,10 L 50,40 L 20,40 L 20,60 L 50,60 L 50,90 L 70,90 L 70,60 L 100,60 L 100,40 L 70,40 L 70,10 Z";
|
|
100
|
+
// Initialize state
|
|
101
|
+
this.progress = 0;
|
|
102
|
+
this.speed = 0.6; // Speed of animation
|
|
103
|
+
this.complete = false;
|
|
104
|
+
// Create SVG shape with initial 0 progress
|
|
105
|
+
this.svgShape = new SVGShape(path, {
|
|
106
|
+
stroke: "#0f0", // Green color
|
|
107
|
+
lineWidth: 3,
|
|
108
|
+
color: "rgba(0, 255, 0, 0.1)",
|
|
109
|
+
scale: 5,
|
|
110
|
+
animationProgress: 1,
|
|
111
|
+
// debug:true,
|
|
112
|
+
//debugColor:"yellow",
|
|
113
|
+
x: options.offsetX ?? 0,
|
|
114
|
+
y: options.offsetY ?? 0,
|
|
115
|
+
width: 210,
|
|
116
|
+
height: 250,
|
|
117
|
+
|
|
118
|
+
});
|
|
119
|
+
// Create a circle to represent the drawing point
|
|
120
|
+
this.drawingPoint = new Circle(6, {
|
|
121
|
+
x: 0,
|
|
122
|
+
y: 0,
|
|
123
|
+
color: "#fff",
|
|
124
|
+
shadowColor: "rgba(0, 255, 0, 1)",
|
|
125
|
+
shadowBlur: 15,
|
|
126
|
+
shadowOffsetX: 0,
|
|
127
|
+
shadowOffsetY: 0,
|
|
128
|
+
});
|
|
129
|
+
// Canvas click handler to restart animation
|
|
130
|
+
game.canvas.addEventListener("click", () => this.restart());
|
|
131
|
+
console.log("SVGPathAnimation", this.x, this.y);
|
|
132
|
+
this.jittery = Math.random() * 0.2 + 0.2;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Restart the animation
|
|
136
|
+
restart() {
|
|
137
|
+
this.progress = 0;
|
|
138
|
+
this.complete = false;
|
|
139
|
+
this.x = 0;
|
|
140
|
+
this.y = 0;
|
|
141
|
+
this.animTime = 0;
|
|
142
|
+
this.jittery = Math.random() * 0.2 + 0.2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
update(dt) {
|
|
146
|
+
//console.log(this.x, this.y);
|
|
147
|
+
// Update progress if animation not complete
|
|
148
|
+
if (!this.complete) {
|
|
149
|
+
this.progress += dt * this.speed;
|
|
150
|
+
if (this.progress >= 1) {
|
|
151
|
+
this.progress = 1;
|
|
152
|
+
this.complete = true;
|
|
153
|
+
this.floatState = null;
|
|
154
|
+
}
|
|
155
|
+
// Apply easing for more natural movement
|
|
156
|
+
const easedProgress = Easing.easeInOutQuad(this.progress);
|
|
157
|
+
// Update SVG shape animation progress
|
|
158
|
+
this.svgShape.setAnimationProgress(easedProgress);
|
|
159
|
+
}
|
|
160
|
+
let x = 0;
|
|
161
|
+
let y = 0;
|
|
162
|
+
// Add gentle bouncing motion when complete
|
|
163
|
+
if (this.complete) {
|
|
164
|
+
this.animTime = this.complete ? (this.animTime ?? 0) + (dt) : 0;
|
|
165
|
+
const floatResult = Motion.float(
|
|
166
|
+
{x:-5,y:-55},
|
|
167
|
+
this.animTime, // elapsed time
|
|
168
|
+
1, // duration (seconds per full loop)
|
|
169
|
+
1, // speed multiplier
|
|
170
|
+
this.jittery,
|
|
171
|
+
50, // radius
|
|
172
|
+
true, // loop
|
|
173
|
+
Easing.easeInOutSine, // optional easing
|
|
174
|
+
{},
|
|
175
|
+
this.floatState // persistent state
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
this.floatState = floatResult.state;
|
|
179
|
+
x = floatResult.x;
|
|
180
|
+
y = floatResult.y;
|
|
181
|
+
this.drawingPoint.visible = false;
|
|
182
|
+
} else {
|
|
183
|
+
// Show the drawing point during animation
|
|
184
|
+
this.drawingPoint.visible = true;
|
|
185
|
+
// Update drawing point position to follow the current path position
|
|
186
|
+
const currentPoint = this.svgShape.getCurrentPoint();
|
|
187
|
+
this.drawingPoint.x = currentPoint.x + this.offsetX;
|
|
188
|
+
this.drawingPoint.y = currentPoint.y + this.offsetY;
|
|
189
|
+
}
|
|
190
|
+
this.x = x;
|
|
191
|
+
this.y = y;
|
|
192
|
+
super.update(dt);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
draw() {
|
|
196
|
+
super.draw();
|
|
197
|
+
// Draw SVG path
|
|
198
|
+
this.svgShape.render();
|
|
199
|
+
// Draw drawing point
|
|
200
|
+
this.drawingPoint.render();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { MyGame };
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { GameObject, Painter, Tweenetik, Easing } from "../../../src/index.js";
|
|
2
|
+
import { keplerianOmega } from "../../../src/math/orbital.js";
|
|
3
|
+
import { CONFIG } from "./config.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AccretionDisk - Keplerian particle disk with gravitational lensing
|
|
7
|
+
*
|
|
8
|
+
* Uses the same proven lensing formula as demos/js/blackhole.js:
|
|
9
|
+
* - Single-pass lensing that pushes particles outward
|
|
10
|
+
* - Einstein ring forms naturally from disk geometry
|
|
11
|
+
* - Doppler beaming for brightness variation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const DISK_CONFIG = {
|
|
15
|
+
// Orbital bounds (multiplier of BH radius)
|
|
16
|
+
innerRadiusMultiplier: 1.5,
|
|
17
|
+
outerRadiusMultiplier: 9.0, // Wide disk with margin from screen edges
|
|
18
|
+
|
|
19
|
+
// Particle properties
|
|
20
|
+
maxParticles: 4000,
|
|
21
|
+
particleLifetime: 80,
|
|
22
|
+
spawnRate: 50,
|
|
23
|
+
|
|
24
|
+
// Orbital physics
|
|
25
|
+
baseOrbitalSpeed: 0.8,
|
|
26
|
+
|
|
27
|
+
// Decay mechanics
|
|
28
|
+
decayChanceBase: 0.0002,
|
|
29
|
+
decaySpeedFactor: 0.995,
|
|
30
|
+
|
|
31
|
+
// Disk geometry - thin disk with some spread
|
|
32
|
+
diskThickness: 0.006,
|
|
33
|
+
|
|
34
|
+
// Lensing - pushes particles outward to form Einstein ring
|
|
35
|
+
ringRadiusFactor: 1.8, // Higher = more margin between BH and ring
|
|
36
|
+
lensingFalloff: 1.8, // Slightly wider falloff
|
|
37
|
+
|
|
38
|
+
// Visual - heat gradient (white-hot inner to deep red outer)
|
|
39
|
+
colorHot: { r: 255, g: 250, b: 220 }, // Inner (white-hot)
|
|
40
|
+
colorMid: { r: 255, g: 160, b: 50 }, // Mid (orange)
|
|
41
|
+
colorCool: { r: 180, g: 40, b: 40 }, // Outer (deep red)
|
|
42
|
+
|
|
43
|
+
sizeMin: 1,
|
|
44
|
+
sizeMax: 2.5,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export class AccretionDisk extends GameObject {
|
|
48
|
+
constructor(game, options = {}) {
|
|
49
|
+
super(game, options);
|
|
50
|
+
|
|
51
|
+
this.camera = options.camera;
|
|
52
|
+
this.bhRadius = options.bhRadius ?? 50;
|
|
53
|
+
this.bhMass = options.bhMass ?? CONFIG.blackHole.initialMass;
|
|
54
|
+
|
|
55
|
+
// Disk bounds scale with BH radius
|
|
56
|
+
this.innerRadius = this.bhRadius * DISK_CONFIG.innerRadiusMultiplier;
|
|
57
|
+
this.outerRadius = this.bhRadius * DISK_CONFIG.outerRadiusMultiplier;
|
|
58
|
+
|
|
59
|
+
// State
|
|
60
|
+
this.active = false;
|
|
61
|
+
this.lensingStrength = 0; // Ramps up during activation
|
|
62
|
+
this.scale = 0; // For expand-from-BH animation
|
|
63
|
+
|
|
64
|
+
// Callback when particle falls into BH
|
|
65
|
+
this.onParticleConsumed = options.onParticleConsumed ?? null;
|
|
66
|
+
|
|
67
|
+
// Particle array
|
|
68
|
+
this.particles = [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Activate disk with expand-from-center animation
|
|
73
|
+
*/
|
|
74
|
+
activate() {
|
|
75
|
+
if (this.active) return;
|
|
76
|
+
this.active = true;
|
|
77
|
+
this.scale = 0.3; // Start partially expanded so it's visible immediately
|
|
78
|
+
this.lensingStrength = 0;
|
|
79
|
+
// Expansion from BH center - 2 seconds (was 4, felt too slow)
|
|
80
|
+
Tweenetik.to(this, { scale: 1 }, 2.0, Easing.easeOutQuart);
|
|
81
|
+
// Lensing ramps up alongside scale
|
|
82
|
+
Tweenetik.to(this, { lensingStrength: 1 }, 2.5, Easing.easeOutQuad);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
init() {
|
|
86
|
+
this.particles = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get heat-based color for particle at given radius
|
|
91
|
+
*/
|
|
92
|
+
getHeatColor(distance) {
|
|
93
|
+
const t = (distance - this.innerRadius) / (this.outerRadius - this.innerRadius);
|
|
94
|
+
|
|
95
|
+
let r, g, b;
|
|
96
|
+
if (t < 0.5) {
|
|
97
|
+
// Inner half: hot -> mid
|
|
98
|
+
const t2 = t * 2;
|
|
99
|
+
r = DISK_CONFIG.colorHot.r + (DISK_CONFIG.colorMid.r - DISK_CONFIG.colorHot.r) * t2;
|
|
100
|
+
g = DISK_CONFIG.colorHot.g + (DISK_CONFIG.colorMid.g - DISK_CONFIG.colorHot.g) * t2;
|
|
101
|
+
b = DISK_CONFIG.colorHot.b + (DISK_CONFIG.colorMid.b - DISK_CONFIG.colorHot.b) * t2;
|
|
102
|
+
} else {
|
|
103
|
+
// Outer half: mid -> cool
|
|
104
|
+
const t2 = (t - 0.5) * 2;
|
|
105
|
+
r = DISK_CONFIG.colorMid.r + (DISK_CONFIG.colorCool.r - DISK_CONFIG.colorMid.r) * t2;
|
|
106
|
+
g = DISK_CONFIG.colorMid.g + (DISK_CONFIG.colorCool.g - DISK_CONFIG.colorMid.g) * t2;
|
|
107
|
+
b = DISK_CONFIG.colorMid.b + (DISK_CONFIG.colorCool.b - DISK_CONFIG.colorMid.b) * t2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Spawn a new particle at random position in disk
|
|
115
|
+
*/
|
|
116
|
+
spawnParticle() {
|
|
117
|
+
if (this.particles.length >= DISK_CONFIG.maxParticles) return;
|
|
118
|
+
|
|
119
|
+
// Balanced distribution with slight inner bias for lensing visibility
|
|
120
|
+
// Lower power = more particles near inner edge (where lensing is strongest)
|
|
121
|
+
const t = Math.pow(Math.random(), 0.6);
|
|
122
|
+
const distance = this.innerRadius + (this.outerRadius - this.innerRadius) * t;
|
|
123
|
+
|
|
124
|
+
const angle = Math.random() * Math.PI * 2;
|
|
125
|
+
|
|
126
|
+
// Keplerian orbital speed
|
|
127
|
+
const speed = keplerianOmega(distance, this.bhMass, DISK_CONFIG.baseOrbitalSpeed, this.outerRadius);
|
|
128
|
+
|
|
129
|
+
// Small vertical offset for thin disk
|
|
130
|
+
const baseScale = this.game.baseScale ?? Math.min(this.game.width, this.game.height);
|
|
131
|
+
const yOffset = (Math.random() - 0.5) * baseScale * DISK_CONFIG.diskThickness;
|
|
132
|
+
|
|
133
|
+
this.particles.push({
|
|
134
|
+
angle,
|
|
135
|
+
distance,
|
|
136
|
+
yOffset,
|
|
137
|
+
speed,
|
|
138
|
+
// Small random initial age prevents batch death
|
|
139
|
+
age: Math.random() * DISK_CONFIG.particleLifetime * 0.1, // Only 10% spread
|
|
140
|
+
isFalling: false,
|
|
141
|
+
size: DISK_CONFIG.sizeMin + Math.random() * (DISK_CONFIG.sizeMax - DISK_CONFIG.sizeMin),
|
|
142
|
+
baseColor: this.getHeatColor(distance),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Capture a particle from the tidal stream
|
|
148
|
+
* Converts Cartesian stream particle to polar disk orbit
|
|
149
|
+
*/
|
|
150
|
+
captureParticle(streamParticle) {
|
|
151
|
+
if (this.particles.length >= DISK_CONFIG.maxParticles) return;
|
|
152
|
+
|
|
153
|
+
const x = streamParticle.x;
|
|
154
|
+
const z = streamParticle.z;
|
|
155
|
+
const dist = Math.sqrt(x * x + z * z);
|
|
156
|
+
|
|
157
|
+
// Skip if outside disk bounds
|
|
158
|
+
if (dist < this.innerRadius || dist > this.outerRadius) return;
|
|
159
|
+
|
|
160
|
+
const angle = Math.atan2(z, x);
|
|
161
|
+
|
|
162
|
+
// Calculate tangential velocity from stream particle
|
|
163
|
+
const vx = streamParticle.vx ?? 0;
|
|
164
|
+
const vz = streamParticle.vz ?? 0;
|
|
165
|
+
const tangentVx = -z / dist;
|
|
166
|
+
const tangentVz = x / dist;
|
|
167
|
+
const tangentSpeed = vx * tangentVx + vz * tangentVz;
|
|
168
|
+
|
|
169
|
+
// Convert to angular velocity
|
|
170
|
+
const angularVelocity = Math.abs(tangentSpeed) / dist;
|
|
171
|
+
|
|
172
|
+
// Target Keplerian speed
|
|
173
|
+
const keplerianSpeed = keplerianOmega(dist, this.bhMass, DISK_CONFIG.baseOrbitalSpeed, this.outerRadius);
|
|
174
|
+
|
|
175
|
+
// Blend toward Keplerian (captured particles circularize)
|
|
176
|
+
const blendedSpeed = (angularVelocity + keplerianSpeed) / 2;
|
|
177
|
+
|
|
178
|
+
this.particles.push({
|
|
179
|
+
angle,
|
|
180
|
+
distance: dist,
|
|
181
|
+
yOffset: streamParticle.y ?? 0,
|
|
182
|
+
speed: blendedSpeed,
|
|
183
|
+
age: 0,
|
|
184
|
+
isFalling: false,
|
|
185
|
+
size: streamParticle.size ?? (DISK_CONFIG.sizeMin + Math.random() * (DISK_CONFIG.sizeMax - DISK_CONFIG.sizeMin)),
|
|
186
|
+
baseColor: this.getHeatColor(dist),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if particle should begin decay spiral
|
|
192
|
+
*/
|
|
193
|
+
checkDecay(p) {
|
|
194
|
+
// Higher decay chance near ISCO (innermost stable circular orbit)
|
|
195
|
+
const iscoProximity = (p.distance - this.innerRadius) / (this.outerRadius - this.innerRadius);
|
|
196
|
+
const ageDecayFactor = Math.min(1, p.age / DISK_CONFIG.particleLifetime);
|
|
197
|
+
|
|
198
|
+
// Particles near inner edge or old ones are more likely to fall
|
|
199
|
+
const decayChance = DISK_CONFIG.decayChanceBase *
|
|
200
|
+
(1 + 3 * (1 - iscoProximity)) *
|
|
201
|
+
(1 + ageDecayFactor);
|
|
202
|
+
|
|
203
|
+
if (Math.random() < decayChance) {
|
|
204
|
+
p.isFalling = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
update(dt) {
|
|
209
|
+
super.update(dt);
|
|
210
|
+
|
|
211
|
+
// Spawn new particles when active
|
|
212
|
+
if (this.active && this.particles.length < DISK_CONFIG.maxParticles) {
|
|
213
|
+
for (let i = 0; i < DISK_CONFIG.spawnRate; i++) {
|
|
214
|
+
this.spawnParticle();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update particles
|
|
219
|
+
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
220
|
+
const p = this.particles[i];
|
|
221
|
+
p.age += dt;
|
|
222
|
+
|
|
223
|
+
// Remove old particles
|
|
224
|
+
if (p.age > DISK_CONFIG.particleLifetime) {
|
|
225
|
+
this.particles.splice(i, 1);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (p.isFalling) {
|
|
230
|
+
// Spiral inward - exponential decay
|
|
231
|
+
p.distance *= DISK_CONFIG.decaySpeedFactor;
|
|
232
|
+
p.angle += p.speed * dt * 2; // Accelerate as falls
|
|
233
|
+
p.yOffset *= 0.95; // Flatten toward equator
|
|
234
|
+
|
|
235
|
+
// Consumed by black hole
|
|
236
|
+
if (p.distance < this.bhRadius * 0.5) {
|
|
237
|
+
this.particles.splice(i, 1);
|
|
238
|
+
if (this.onParticleConsumed) {
|
|
239
|
+
this.onParticleConsumed();
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
// Normal Keplerian orbit
|
|
245
|
+
p.angle += p.speed * dt;
|
|
246
|
+
|
|
247
|
+
// Check for decay
|
|
248
|
+
this.checkDecay(p);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build render list with camera-space lensing
|
|
255
|
+
* Uses the proven formula from demos/js/blackhole.js
|
|
256
|
+
*/
|
|
257
|
+
buildRenderList() {
|
|
258
|
+
const renderList = [];
|
|
259
|
+
if (!this.camera || this.particles.length === 0) return renderList;
|
|
260
|
+
|
|
261
|
+
const lensingStrength = this.lensingStrength;
|
|
262
|
+
|
|
263
|
+
for (const p of this.particles) {
|
|
264
|
+
// World coordinates (flat disk in x-z plane)
|
|
265
|
+
const scaledDist = p.distance * this.scale;
|
|
266
|
+
let x = Math.cos(p.angle) * scaledDist;
|
|
267
|
+
let y = p.yOffset * this.scale;
|
|
268
|
+
let z = Math.sin(p.angle) * scaledDist;
|
|
269
|
+
|
|
270
|
+
// Transform to camera space
|
|
271
|
+
const cosY = Math.cos(this.camera.rotationY);
|
|
272
|
+
const sinY = Math.sin(this.camera.rotationY);
|
|
273
|
+
let xCam = x * cosY - z * sinY;
|
|
274
|
+
let zCam = x * sinY + z * cosY;
|
|
275
|
+
|
|
276
|
+
const cosX = Math.cos(this.camera.rotationX);
|
|
277
|
+
const sinX = Math.sin(this.camera.rotationX);
|
|
278
|
+
let yCam = y * cosX - zCam * sinX;
|
|
279
|
+
zCam = y * sinX + zCam * cosX;
|
|
280
|
+
|
|
281
|
+
// === GRAVITATIONAL LENSING (from blackhole.js) ===
|
|
282
|
+
// Only affects particles behind the BH (zCam > 0)
|
|
283
|
+
if (lensingStrength > 0 && zCam > 0) {
|
|
284
|
+
const currentR = Math.sqrt(xCam * xCam + yCam * yCam);
|
|
285
|
+
const ringRadius = this.bhRadius * DISK_CONFIG.ringRadiusFactor;
|
|
286
|
+
const lensFactor = Math.exp(-currentR / (this.bhRadius * DISK_CONFIG.lensingFalloff));
|
|
287
|
+
const warp = lensFactor * 1.2 * lensingStrength;
|
|
288
|
+
|
|
289
|
+
if (currentR > 0) {
|
|
290
|
+
const ratio = (currentR + ringRadius * warp) / currentR;
|
|
291
|
+
xCam *= ratio;
|
|
292
|
+
yCam *= ratio;
|
|
293
|
+
} else {
|
|
294
|
+
yCam = ringRadius * lensingStrength;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Perspective projection
|
|
299
|
+
const perspectiveScale = this.camera.perspective / (this.camera.perspective + zCam);
|
|
300
|
+
const screenX = xCam * perspectiveScale;
|
|
301
|
+
const screenY = yCam * perspectiveScale;
|
|
302
|
+
|
|
303
|
+
// Skip particles behind camera
|
|
304
|
+
if (zCam < -this.camera.perspective + 10) continue;
|
|
305
|
+
|
|
306
|
+
// Doppler beaming - approaching side brighter
|
|
307
|
+
const velocityDir = Math.cos(p.angle + this.camera.rotationY);
|
|
308
|
+
const doppler = 1 + velocityDir * 0.4;
|
|
309
|
+
|
|
310
|
+
// Age-based fade
|
|
311
|
+
const ageRatio = p.age / DISK_CONFIG.particleLifetime;
|
|
312
|
+
const alpha = Math.max(0.3, 1 - Math.pow(ageRatio, 2.5));
|
|
313
|
+
|
|
314
|
+
// Color (redshift for falling particles)
|
|
315
|
+
let color = p.baseColor;
|
|
316
|
+
if (p.isFalling) {
|
|
317
|
+
const fallProgress = 1 - (p.distance / this.innerRadius);
|
|
318
|
+
color = {
|
|
319
|
+
r: Math.round(p.baseColor.r * (1 - fallProgress * 0.5)),
|
|
320
|
+
g: Math.round(p.baseColor.g * (1 - fallProgress * 0.7)),
|
|
321
|
+
b: Math.round(p.baseColor.b * (1 - fallProgress * 0.3)),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
renderList.push({
|
|
326
|
+
x: screenX,
|
|
327
|
+
y: screenY,
|
|
328
|
+
z: zCam,
|
|
329
|
+
scale: perspectiveScale,
|
|
330
|
+
color,
|
|
331
|
+
doppler,
|
|
332
|
+
alpha,
|
|
333
|
+
size: p.size,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Sort back to front for proper blending
|
|
338
|
+
renderList.sort((a, b) => b.z - a.z);
|
|
339
|
+
return renderList;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Clear all particles
|
|
344
|
+
*/
|
|
345
|
+
clear() {
|
|
346
|
+
this.particles = [];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Update BH radius - also updates disk bounds since they scale with BH
|
|
351
|
+
* Particles inside the event horizon are consumed; others remain in place
|
|
352
|
+
* and will naturally be replaced by new spawns at correct radii
|
|
353
|
+
*/
|
|
354
|
+
updateBHRadius(radius) {
|
|
355
|
+
this.bhRadius = radius;
|
|
356
|
+
// Disk bounds scale with BH radius
|
|
357
|
+
this.innerRadius = this.bhRadius * DISK_CONFIG.innerRadiusMultiplier;
|
|
358
|
+
this.outerRadius = this.bhRadius * DISK_CONFIG.outerRadiusMultiplier;
|
|
359
|
+
|
|
360
|
+
// Consume particles swallowed by event horizon (same threshold as update loop)
|
|
361
|
+
const consumeRadius = this.bhRadius * 0.5;
|
|
362
|
+
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
363
|
+
const p = this.particles[i];
|
|
364
|
+
|
|
365
|
+
if (p.distance < consumeRadius) {
|
|
366
|
+
this.particles.splice(i, 1);
|
|
367
|
+
if (this.onParticleConsumed) {
|
|
368
|
+
this.onParticleConsumed();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
render() {
|
|
375
|
+
super.render();
|
|
376
|
+
|
|
377
|
+
if (!this.active || !this.camera || this.particles.length === 0) return;
|
|
378
|
+
|
|
379
|
+
const cx = this.game.width / 2;
|
|
380
|
+
const cy = this.game.height / 2;
|
|
381
|
+
const baseScale = this.game.baseScale ?? Math.min(this.game.width, this.game.height);
|
|
382
|
+
const renderList = this.buildRenderList();
|
|
383
|
+
|
|
384
|
+
Painter.useCtx((ctx) => {
|
|
385
|
+
// Reset transform (bypass Scene3D transforms)
|
|
386
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
387
|
+
|
|
388
|
+
for (const item of renderList) {
|
|
389
|
+
const { r, g, b } = item.color;
|
|
390
|
+
const size = baseScale * 0.003 * item.scale;
|
|
391
|
+
if (size < 0.1) continue;
|
|
392
|
+
|
|
393
|
+
// Apply Doppler brightness
|
|
394
|
+
const dr = Math.min(255, Math.round(r * item.doppler));
|
|
395
|
+
const dg = Math.min(255, Math.round(g * item.doppler * 0.95));
|
|
396
|
+
const db = Math.min(255, Math.round(b * item.doppler * 0.9));
|
|
397
|
+
|
|
398
|
+
const finalAlpha = Math.max(0, Math.min(1, item.alpha * item.doppler));
|
|
399
|
+
|
|
400
|
+
// Core particle
|
|
401
|
+
ctx.fillStyle = `rgba(${dr}, ${dg}, ${db}, ${finalAlpha})`;
|
|
402
|
+
ctx.beginPath();
|
|
403
|
+
ctx.arc(cx + item.x, cy + item.y, size / 2, 0, Math.PI * 2);
|
|
404
|
+
ctx.fill();
|
|
405
|
+
|
|
406
|
+
// Additive glow for bright/close particles (from blackhole.js)
|
|
407
|
+
if (item.doppler > 1.1 && item.alpha > 0.5) {
|
|
408
|
+
ctx.globalCompositeOperation = "screen";
|
|
409
|
+
ctx.fillStyle = `rgba(${dr}, ${dg}, ${db}, ${finalAlpha * 0.4})`;
|
|
410
|
+
ctx.beginPath();
|
|
411
|
+
ctx.arc(cx + item.x, cy + item.y, size, 0, Math.PI * 2);
|
|
412
|
+
ctx.fill();
|
|
413
|
+
ctx.globalCompositeOperation = "source-over";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|