@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,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root Dance - Bhaskara Formula Visualization
|
|
3
|
+
*
|
|
4
|
+
* A generative art demo visualizing the quadratic formula (Bhaskara).
|
|
5
|
+
* Two particles represent the roots x₁ and x₂ of ax² + bx + c = 0.
|
|
6
|
+
* Watch them dance as coefficients animate, merge when Δ = 0,
|
|
7
|
+
* and spiral into the complex plane when roots become imaginary.
|
|
8
|
+
*/
|
|
9
|
+
import { Game, Painter } from "../../src/index.js";
|
|
10
|
+
import { GameObject } from "../../src/game/objects/go.js";
|
|
11
|
+
import { Rectangle } from "../../src/shapes/rect.js";
|
|
12
|
+
import { TextShape } from "../../src/shapes/text.js";
|
|
13
|
+
import { Position } from "../../src/util/position.js";
|
|
14
|
+
import { Synth } from "../../src/sound/synth.js";
|
|
15
|
+
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
// Coefficient ranges for animation
|
|
18
|
+
aRange: [0.5, 2],
|
|
19
|
+
bRange: [-3, 3],
|
|
20
|
+
cRange: [-2, 2],
|
|
21
|
+
aSpeed: 0.3,
|
|
22
|
+
bSpeed: 0.5,
|
|
23
|
+
cSpeed: 0.4,
|
|
24
|
+
|
|
25
|
+
// Particles
|
|
26
|
+
trailLength: 60,
|
|
27
|
+
particleRadius: 8,
|
|
28
|
+
glowLayers: 4,
|
|
29
|
+
glowExpansion: 4,
|
|
30
|
+
|
|
31
|
+
// Coordinate mapping
|
|
32
|
+
rootScale: 75,
|
|
33
|
+
complexScale: 50,
|
|
34
|
+
|
|
35
|
+
// Effects
|
|
36
|
+
mergeThreshold: 0.3,
|
|
37
|
+
mergeFlashDuration: 0.5,
|
|
38
|
+
spiralSpeed: 2.0,
|
|
39
|
+
|
|
40
|
+
// Mouse influence strength
|
|
41
|
+
mouseInfluence: 1.5,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Famous quadratic equations with assigned colors (HSL hues) and percussion
|
|
45
|
+
const EQUATIONS = [
|
|
46
|
+
{ name: "easeInOutQuad", a: 2, b: -4, c: 1, hue: 45, perc: "kick" }, // Orange - 2t²-4t+1=0
|
|
47
|
+
{ name: "Imaginary Unit", a: 1, b: 0, c: 1, hue: 280, perc: "sweep" }, // Purple - ±i (usually complex)
|
|
48
|
+
{ name: "Mandelbrot Origin", a: 1, b: 0, c: 0, hue: 160, perc: "hihat" }, // Teal - c=0 center
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* FormulaPanelGO - A GameObject that displays all 3 equations with color-coded headers
|
|
53
|
+
*/
|
|
54
|
+
class FormulaPanelGO extends GameObject {
|
|
55
|
+
constructor(game, options = {}) {
|
|
56
|
+
const panelWidth = 260;
|
|
57
|
+
const rowHeight = 48;
|
|
58
|
+
const panelHeight = rowHeight * EQUATIONS.length + 16;
|
|
59
|
+
|
|
60
|
+
super(game, {
|
|
61
|
+
...options,
|
|
62
|
+
width: panelWidth,
|
|
63
|
+
height: panelHeight,
|
|
64
|
+
anchor: Position.BOTTOM_LEFT,
|
|
65
|
+
anchorMargin: 20,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.panelWidth = panelWidth;
|
|
69
|
+
this.panelHeight = panelHeight;
|
|
70
|
+
this.rowHeight = rowHeight;
|
|
71
|
+
|
|
72
|
+
// Create shapes
|
|
73
|
+
this.bgRect = new Rectangle({
|
|
74
|
+
width: panelWidth,
|
|
75
|
+
height: panelHeight,
|
|
76
|
+
color: "rgba(0, 0, 0, 0.6)",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Create text shapes for each equation
|
|
80
|
+
this.equationRows = EQUATIONS.map((eq) => ({
|
|
81
|
+
nameText: new TextShape(`${eq.name} [${eq.perc}]`, {
|
|
82
|
+
font: "bold 11px monospace",
|
|
83
|
+
color: `hsl(${eq.hue}, 80%, 65%)`,
|
|
84
|
+
align: "left",
|
|
85
|
+
baseline: "top",
|
|
86
|
+
}),
|
|
87
|
+
formulaText: new TextShape("0.00x² + 0.00x + 0.00 = 0", {
|
|
88
|
+
font: "11px monospace",
|
|
89
|
+
color: "#ccc",
|
|
90
|
+
align: "left",
|
|
91
|
+
baseline: "top",
|
|
92
|
+
}),
|
|
93
|
+
rootsText: new TextShape("x₁ = 0.00, x₂ = 0.00", {
|
|
94
|
+
font: "10px monospace",
|
|
95
|
+
color: "#888",
|
|
96
|
+
align: "left",
|
|
97
|
+
baseline: "top",
|
|
98
|
+
}),
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setEquationValues(index, a, b, c, discriminant, x1, x2, isComplex) {
|
|
103
|
+
if (index < 0 || index >= this.equationRows.length) return;
|
|
104
|
+
|
|
105
|
+
const row = this.equationRows[index];
|
|
106
|
+
|
|
107
|
+
// Update formula text
|
|
108
|
+
const aStr = a.toFixed(2);
|
|
109
|
+
const bStr = b >= 0 ? `+ ${b.toFixed(2)}` : `- ${Math.abs(b).toFixed(2)}`;
|
|
110
|
+
const cStr = c >= 0 ? `+ ${c.toFixed(2)}` : `- ${Math.abs(c).toFixed(2)}`;
|
|
111
|
+
row.formulaText.text = `${aStr}x² ${bStr}x ${cStr} = 0`;
|
|
112
|
+
|
|
113
|
+
// Update root values text
|
|
114
|
+
if (isComplex) {
|
|
115
|
+
const r = x1.real.toFixed(2);
|
|
116
|
+
const i = x1.imag.toFixed(2);
|
|
117
|
+
row.rootsText.text = `x = ${r} ± ${i}i`;
|
|
118
|
+
row.rootsText.color = "#a78bfa"; // Purple for complex
|
|
119
|
+
} else {
|
|
120
|
+
row.rootsText.text = `x₁ = ${x1.toFixed(2)}, x₂ = ${x2.toFixed(2)}`;
|
|
121
|
+
row.rootsText.color = "#888";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
draw() {
|
|
126
|
+
super.draw();
|
|
127
|
+
|
|
128
|
+
// Draw background
|
|
129
|
+
this.bgRect.render();
|
|
130
|
+
|
|
131
|
+
// Draw each equation row
|
|
132
|
+
const left = -this.panelWidth / 2 + 12;
|
|
133
|
+
const startTop = -this.panelHeight / 2 + 10;
|
|
134
|
+
|
|
135
|
+
this.equationRows.forEach((row, i) => {
|
|
136
|
+
const rowTop = startTop + i * this.rowHeight;
|
|
137
|
+
|
|
138
|
+
Painter.save();
|
|
139
|
+
Painter.translate(left, rowTop);
|
|
140
|
+
row.nameText.render();
|
|
141
|
+
Painter.restore();
|
|
142
|
+
|
|
143
|
+
Painter.save();
|
|
144
|
+
Painter.translate(left, rowTop + 14);
|
|
145
|
+
row.formulaText.render();
|
|
146
|
+
Painter.restore();
|
|
147
|
+
|
|
148
|
+
Painter.save();
|
|
149
|
+
Painter.translate(left, rowTop + 28);
|
|
150
|
+
row.rootsText.render();
|
|
151
|
+
Painter.restore();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export class BaskaraDemo extends Game {
|
|
157
|
+
constructor(canvas) {
|
|
158
|
+
super(canvas);
|
|
159
|
+
this.backgroundColor = "#0a0a12";
|
|
160
|
+
this.enableFluidSize();
|
|
161
|
+
this.time = 0;
|
|
162
|
+
|
|
163
|
+
// Mouse influence
|
|
164
|
+
this.mouseActive = false;
|
|
165
|
+
this.mouseX = 0;
|
|
166
|
+
this.mouseY = 0;
|
|
167
|
+
this.mouseInactiveTime = 0;
|
|
168
|
+
this.resumeDelay = 1.5; // seconds before animation resumes
|
|
169
|
+
this.setupMouseTracking();
|
|
170
|
+
|
|
171
|
+
// Initialize state for each equation
|
|
172
|
+
this.equations = EQUATIONS.map((eq, i) => ({
|
|
173
|
+
// Base equation info
|
|
174
|
+
name: eq.name,
|
|
175
|
+
baseA: eq.a,
|
|
176
|
+
baseB: eq.b,
|
|
177
|
+
baseC: eq.c,
|
|
178
|
+
hue: eq.hue,
|
|
179
|
+
|
|
180
|
+
// Current animated coefficients
|
|
181
|
+
a: eq.a,
|
|
182
|
+
b: eq.b,
|
|
183
|
+
c: eq.c,
|
|
184
|
+
discriminant: 0,
|
|
185
|
+
|
|
186
|
+
// Root values
|
|
187
|
+
x1: 0,
|
|
188
|
+
x2: 0,
|
|
189
|
+
isComplex: false,
|
|
190
|
+
|
|
191
|
+
// Root screen positions
|
|
192
|
+
root1: { x: 0, y: 0 },
|
|
193
|
+
root2: { x: 0, y: 0 },
|
|
194
|
+
|
|
195
|
+
// Trails
|
|
196
|
+
trail1: [],
|
|
197
|
+
trail2: [],
|
|
198
|
+
|
|
199
|
+
// Merge effect
|
|
200
|
+
merging: false,
|
|
201
|
+
mergeTime: 0,
|
|
202
|
+
|
|
203
|
+
// Phase offsets for unique animation per equation
|
|
204
|
+
phaseOffset: i * 1.2,
|
|
205
|
+
|
|
206
|
+
// Track previous root values for percussion triggers
|
|
207
|
+
prevX1: 0,
|
|
208
|
+
prevX2: 0,
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
// Percussion timing - steady beat system
|
|
212
|
+
this.bpm = 120;
|
|
213
|
+
this.beatInterval = 60 / this.bpm; // seconds per beat
|
|
214
|
+
this.lastBeatTime = 0;
|
|
215
|
+
this.beatCount = 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
init() {
|
|
219
|
+
super.init();
|
|
220
|
+
this.createFormulaPanel();
|
|
221
|
+
this.initSound();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
initSound() {
|
|
225
|
+
this.soundEnabled = false;
|
|
226
|
+
|
|
227
|
+
// Initialize on first click (proper user gesture)
|
|
228
|
+
const initAudio = () => {
|
|
229
|
+
if (!Synth.isInitialized) {
|
|
230
|
+
Synth.init({ masterVolume: 0.3 });
|
|
231
|
+
}
|
|
232
|
+
Synth.resume();
|
|
233
|
+
this.soundEnabled = true;
|
|
234
|
+
this.canvas.removeEventListener("click", initAudio);
|
|
235
|
+
};
|
|
236
|
+
this.canvas.addEventListener("click", initAudio);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
playComplexSound(eq) {
|
|
240
|
+
if (!this.soundEnabled) return;
|
|
241
|
+
|
|
242
|
+
// Musical scale (pentatonic) - maps nicely to any value
|
|
243
|
+
const pentatonic = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25];
|
|
244
|
+
|
|
245
|
+
// Use real part to pick base note, imaginary magnitude for octave shift
|
|
246
|
+
const realPart = eq.isComplex ? eq.x1.real : 0;
|
|
247
|
+
const imagMag = eq.isComplex ? Math.abs(eq.x1.imag) : 0;
|
|
248
|
+
|
|
249
|
+
// Map real part to scale index
|
|
250
|
+
const noteIndex = Math.abs(Math.floor((realPart + 3) * 1.2)) % pentatonic.length;
|
|
251
|
+
const baseFreq = pentatonic[noteIndex];
|
|
252
|
+
|
|
253
|
+
// Imaginary magnitude shifts octave (higher = further from real axis)
|
|
254
|
+
const octaveShift = 1 + imagMag * 0.5;
|
|
255
|
+
const freq = baseFreq * octaveShift;
|
|
256
|
+
|
|
257
|
+
Synth.osc.sweep(freq, freq * 1.3, 0.35, {
|
|
258
|
+
type: "sine",
|
|
259
|
+
volume: 0.12,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
playRealSound(eq) {
|
|
264
|
+
if (!this.soundEnabled) return;
|
|
265
|
+
|
|
266
|
+
// Musical scale for return sound
|
|
267
|
+
const pentatonic = [196.00, 220.00, 261.63, 293.66, 329.63, 392.00];
|
|
268
|
+
|
|
269
|
+
// Use root spread to pick note
|
|
270
|
+
const spread = Math.abs(eq.x1 - eq.x2);
|
|
271
|
+
const noteIndex = Math.floor(spread * 1.5) % pentatonic.length;
|
|
272
|
+
const baseFreq = pentatonic[noteIndex];
|
|
273
|
+
|
|
274
|
+
Synth.osc.sweep(baseFreq * 1.5, baseFreq, 0.3, {
|
|
275
|
+
type: "triangle",
|
|
276
|
+
volume: 0.1,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Percussion sounds for real root movement
|
|
281
|
+
playKick() {
|
|
282
|
+
if (!this.soundEnabled) return;
|
|
283
|
+
const ctx = Synth.ctx;
|
|
284
|
+
const now = ctx.currentTime;
|
|
285
|
+
|
|
286
|
+
// Kick = low sine with pitch drop
|
|
287
|
+
const osc = ctx.createOscillator();
|
|
288
|
+
const gain = ctx.createGain();
|
|
289
|
+
osc.type = "sine";
|
|
290
|
+
osc.frequency.setValueAtTime(150, now);
|
|
291
|
+
osc.frequency.exponentialRampToValueAtTime(40, now + 0.1);
|
|
292
|
+
gain.gain.setValueAtTime(0.3, now);
|
|
293
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
|
|
294
|
+
osc.connect(gain);
|
|
295
|
+
gain.connect(Synth.master);
|
|
296
|
+
osc.start(now);
|
|
297
|
+
osc.stop(now + 0.2);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
playHihat() {
|
|
301
|
+
if (!this.soundEnabled) return;
|
|
302
|
+
const ctx = Synth.ctx;
|
|
303
|
+
const now = ctx.currentTime;
|
|
304
|
+
|
|
305
|
+
// Hihat = filtered noise burst
|
|
306
|
+
const noise = ctx.createBufferSource();
|
|
307
|
+
const buffer = ctx.createBuffer(1, ctx.sampleRate * 0.05, ctx.sampleRate);
|
|
308
|
+
const data = buffer.getChannelData(0);
|
|
309
|
+
for (let i = 0; i < data.length; i++) {
|
|
310
|
+
data[i] = Math.random() * 2 - 1;
|
|
311
|
+
}
|
|
312
|
+
noise.buffer = buffer;
|
|
313
|
+
|
|
314
|
+
const filter = ctx.createBiquadFilter();
|
|
315
|
+
filter.type = "highpass";
|
|
316
|
+
filter.frequency.value = 7000;
|
|
317
|
+
|
|
318
|
+
const gain = ctx.createGain();
|
|
319
|
+
gain.gain.setValueAtTime(0.15, now);
|
|
320
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
|
|
321
|
+
|
|
322
|
+
noise.connect(filter);
|
|
323
|
+
filter.connect(gain);
|
|
324
|
+
gain.connect(Synth.master);
|
|
325
|
+
noise.start(now);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
playTick(pitch = 1) {
|
|
329
|
+
if (!this.soundEnabled) return;
|
|
330
|
+
const ctx = Synth.ctx;
|
|
331
|
+
const now = ctx.currentTime;
|
|
332
|
+
|
|
333
|
+
// Tick = short high sine blip
|
|
334
|
+
const osc = ctx.createOscillator();
|
|
335
|
+
const gain = ctx.createGain();
|
|
336
|
+
osc.type = "sine";
|
|
337
|
+
osc.frequency.value = 800 * pitch;
|
|
338
|
+
gain.gain.setValueAtTime(0.1, now);
|
|
339
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.03);
|
|
340
|
+
osc.connect(gain);
|
|
341
|
+
gain.connect(Synth.master);
|
|
342
|
+
osc.start(now);
|
|
343
|
+
osc.stop(now + 0.03);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
checkPercussion(eq, t) {
|
|
347
|
+
// Update previous values for any position-based logic
|
|
348
|
+
eq.prevX1 = eq.x1;
|
|
349
|
+
eq.prevX2 = eq.x2;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Called once per frame to handle steady beat
|
|
353
|
+
updateBeat(t) {
|
|
354
|
+
if (!this.soundEnabled) return;
|
|
355
|
+
|
|
356
|
+
// Check if it's time for a new beat
|
|
357
|
+
if (t - this.lastBeatTime >= this.beatInterval) {
|
|
358
|
+
this.lastBeatTime = t;
|
|
359
|
+
this.beatCount++;
|
|
360
|
+
|
|
361
|
+
const beatInBar = this.beatCount % 4;
|
|
362
|
+
|
|
363
|
+
// Each equation plays its percussion on different beats when real
|
|
364
|
+
this.equations.forEach((eq, i) => {
|
|
365
|
+
if (eq.isComplex) return; // Skip complex roots
|
|
366
|
+
|
|
367
|
+
// Stagger beats: eq0 on beat 0, eq1 on beat 1, eq2 on beat 2
|
|
368
|
+
const eqBeat = i % 4;
|
|
369
|
+
|
|
370
|
+
if (beatInBar === eqBeat || (beatInBar === (eqBeat + 2) % 4)) {
|
|
371
|
+
// Play this equation's assigned percussion
|
|
372
|
+
const pitch = 0.7 + Math.abs(eq.x1) * 0.15; // Pitch tied to root position
|
|
373
|
+
|
|
374
|
+
switch (EQUATIONS[i].perc) {
|
|
375
|
+
case "kick":
|
|
376
|
+
this.playKick();
|
|
377
|
+
break;
|
|
378
|
+
case "hihat":
|
|
379
|
+
this.playHihat();
|
|
380
|
+
break;
|
|
381
|
+
case "sweep":
|
|
382
|
+
// Short tick for sweep equations when real
|
|
383
|
+
this.playTick(pitch);
|
|
384
|
+
break;
|
|
385
|
+
default:
|
|
386
|
+
this.playTick(pitch);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
createFormulaPanel() {
|
|
394
|
+
this.formulaPanel = new FormulaPanelGO(this, { name: "formulaPanel" });
|
|
395
|
+
this.pipeline.add(this.formulaPanel);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
setupMouseTracking() {
|
|
399
|
+
this.canvas.addEventListener("mousemove", (e) => {
|
|
400
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
401
|
+
// Normalize to -1 to 1 range from canvas center
|
|
402
|
+
this.mouseX = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
|
|
403
|
+
this.mouseY = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
|
|
404
|
+
this.mouseActive = true;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
this.canvas.addEventListener("mouseleave", () => {
|
|
408
|
+
this.mouseActive = false;
|
|
409
|
+
this.mouseInactiveTime = this.time;
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
this.canvas.addEventListener("mousemove", () => {
|
|
413
|
+
this.mouseInactiveTime = this.time; // Reset timer on any movement
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
update(dt) {
|
|
418
|
+
super.update(dt);
|
|
419
|
+
this.time += dt;
|
|
420
|
+
|
|
421
|
+
const t = this.time;
|
|
422
|
+
|
|
423
|
+
// Steady beat - always playing when roots are real
|
|
424
|
+
this.updateBeat(t);
|
|
425
|
+
|
|
426
|
+
const lerpSpeed = 3;
|
|
427
|
+
const centerX = this.canvas.width / 2;
|
|
428
|
+
const centerY = this.canvas.height / 2;
|
|
429
|
+
|
|
430
|
+
// Check if we should resume autonomous animation
|
|
431
|
+
const timeSinceInactive = t - this.mouseInactiveTime;
|
|
432
|
+
const shouldAnimate = !this.mouseActive && timeSinceInactive > this.resumeDelay;
|
|
433
|
+
|
|
434
|
+
// Update each equation - roots at their true mathematical positions
|
|
435
|
+
this.equations.forEach((eq, index) => {
|
|
436
|
+
// Phase offset gives each equation unique timing
|
|
437
|
+
const phase = t + eq.phaseOffset;
|
|
438
|
+
|
|
439
|
+
if (this.mouseActive) {
|
|
440
|
+
// Mouse X influences b, Mouse Y influences c
|
|
441
|
+
eq.b = eq.baseB + this.mouseX * 4;
|
|
442
|
+
eq.c = eq.baseC + this.mouseY * 3;
|
|
443
|
+
} else if (shouldAnimate) {
|
|
444
|
+
// Animate coefficients around base values - roots follow the math
|
|
445
|
+
const animB = eq.baseB + Math.sin(phase * 0.4) * 2.5;
|
|
446
|
+
const animC = eq.baseC + Math.sin(phase * 0.25 + index) * 2;
|
|
447
|
+
const blendSpeed = 2 * dt;
|
|
448
|
+
eq.b += (animB - eq.b) * blendSpeed;
|
|
449
|
+
eq.c += (animC - eq.c) * blendSpeed;
|
|
450
|
+
}
|
|
451
|
+
// else: hold at current values during delay
|
|
452
|
+
|
|
453
|
+
eq.a = eq.baseA;
|
|
454
|
+
|
|
455
|
+
// Calculate discriminant
|
|
456
|
+
eq.discriminant = eq.b * eq.b - 4 * eq.a * eq.c;
|
|
457
|
+
|
|
458
|
+
// Check merge state
|
|
459
|
+
const wasMerging = eq.merging;
|
|
460
|
+
eq.merging = Math.abs(eq.discriminant) < CONFIG.mergeThreshold;
|
|
461
|
+
if (eq.merging && !wasMerging) {
|
|
462
|
+
eq.mergeTime = this.time;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Calculate roots
|
|
466
|
+
const twoA = 2 * eq.a;
|
|
467
|
+
const negB = -eq.b;
|
|
468
|
+
|
|
469
|
+
const wasComplex = eq.isComplex;
|
|
470
|
+
|
|
471
|
+
if (eq.discriminant >= 0) {
|
|
472
|
+
// Real roots - on the X axis (Y = 0)
|
|
473
|
+
const sqrtD = Math.sqrt(eq.discriminant);
|
|
474
|
+
eq.x1 = (negB + sqrtD) / twoA;
|
|
475
|
+
eq.x2 = (negB - sqrtD) / twoA;
|
|
476
|
+
|
|
477
|
+
eq.root1.x = centerX + eq.x1 * CONFIG.rootScale;
|
|
478
|
+
eq.root1.y = centerY; // Y = 0 (on real axis)
|
|
479
|
+
eq.root2.x = centerX + eq.x2 * CONFIG.rootScale;
|
|
480
|
+
eq.root2.y = centerY; // Y = 0 (on real axis)
|
|
481
|
+
|
|
482
|
+
eq.isComplex = false;
|
|
483
|
+
|
|
484
|
+
// Sound: returned to real axis
|
|
485
|
+
if (wasComplex) {
|
|
486
|
+
this.playRealSound(eq);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Percussion: trigger when roots cross integer gridlines
|
|
490
|
+
this.checkPercussion(eq, t);
|
|
491
|
+
} else {
|
|
492
|
+
// Complex roots - positioned in complex plane (Y ≠ 0)
|
|
493
|
+
const realPart = negB / twoA;
|
|
494
|
+
const imagPart = Math.sqrt(-eq.discriminant) / twoA;
|
|
495
|
+
|
|
496
|
+
// Root positions ARE their complex values: x + yi
|
|
497
|
+
eq.root1.x = centerX + realPart * CONFIG.rootScale;
|
|
498
|
+
eq.root1.y = centerY - imagPart * CONFIG.complexScale; // -imagPart because Y grows downward
|
|
499
|
+
eq.root2.x = centerX + realPart * CONFIG.rootScale;
|
|
500
|
+
eq.root2.y = centerY + imagPart * CONFIG.complexScale; // conjugate (opposite imaginary)
|
|
501
|
+
|
|
502
|
+
eq.x1 = { real: realPart, imag: imagPart };
|
|
503
|
+
eq.x2 = { real: realPart, imag: -imagPart };
|
|
504
|
+
eq.isComplex = true;
|
|
505
|
+
|
|
506
|
+
// Sound: entered complex plane
|
|
507
|
+
if (!wasComplex) {
|
|
508
|
+
this.playComplexSound(eq);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Update trails
|
|
513
|
+
eq.trail1.unshift({ x: eq.root1.x, y: eq.root1.y });
|
|
514
|
+
eq.trail2.unshift({ x: eq.root2.x, y: eq.root2.y });
|
|
515
|
+
|
|
516
|
+
if (eq.trail1.length > CONFIG.trailLength) {
|
|
517
|
+
eq.trail1.pop();
|
|
518
|
+
eq.trail2.pop();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Update formula panel for this equation
|
|
522
|
+
if (this.formulaPanel) {
|
|
523
|
+
this.formulaPanel.setEquationValues(
|
|
524
|
+
index, eq.a, eq.b, eq.c,
|
|
525
|
+
eq.discriminant, eq.x1, eq.x2, eq.isComplex
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
render() {
|
|
532
|
+
super.render();
|
|
533
|
+
Painter.useCtx((ctx) => {
|
|
534
|
+
// Draw coordinate axes
|
|
535
|
+
this.drawAxes(ctx);
|
|
536
|
+
|
|
537
|
+
// Draw all equations
|
|
538
|
+
this.equations.forEach((eq) => {
|
|
539
|
+
// Draw subtle parabola curve first (behind everything)
|
|
540
|
+
this.drawParabola(ctx, eq);
|
|
541
|
+
|
|
542
|
+
// Draw trails
|
|
543
|
+
this.drawTrails(ctx, eq);
|
|
544
|
+
|
|
545
|
+
// Draw particles with glow
|
|
546
|
+
this.drawParticles(ctx, eq);
|
|
547
|
+
|
|
548
|
+
// Draw merge effect
|
|
549
|
+
if (eq.merging) {
|
|
550
|
+
this.drawMergeEffect(ctx, eq);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
drawParabola(ctx, eq) {
|
|
557
|
+
// Only show parabola when roots are real (crosses the axis)
|
|
558
|
+
if (eq.isComplex) return;
|
|
559
|
+
|
|
560
|
+
const centerX = this.canvas.width / 2;
|
|
561
|
+
const centerY = this.canvas.height / 2;
|
|
562
|
+
const scale = CONFIG.rootScale;
|
|
563
|
+
|
|
564
|
+
// Draw parabola y = ax² + bx + c
|
|
565
|
+
ctx.strokeStyle = `hsla(${eq.hue}, 60%, 50%, 0.2)`;
|
|
566
|
+
ctx.lineWidth = 2;
|
|
567
|
+
ctx.beginPath();
|
|
568
|
+
|
|
569
|
+
const xRange = 5; // How far left/right to draw
|
|
570
|
+
const steps = 100;
|
|
571
|
+
|
|
572
|
+
for (let i = 0; i <= steps; i++) {
|
|
573
|
+
const x = -xRange + (i / steps) * xRange * 2;
|
|
574
|
+
const y = eq.a * x * x + eq.b * x + eq.c;
|
|
575
|
+
|
|
576
|
+
const screenX = centerX + x * scale;
|
|
577
|
+
const screenY = centerY - y * scale; // Flip Y for screen coords
|
|
578
|
+
|
|
579
|
+
if (i === 0) {
|
|
580
|
+
ctx.moveTo(screenX, screenY);
|
|
581
|
+
} else {
|
|
582
|
+
ctx.lineTo(screenX, screenY);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
ctx.stroke();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
drawAxes(ctx) {
|
|
590
|
+
const centerX = this.canvas.width / 2;
|
|
591
|
+
const centerY = this.canvas.height / 2;
|
|
592
|
+
|
|
593
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.1)";
|
|
594
|
+
ctx.lineWidth = 1;
|
|
595
|
+
|
|
596
|
+
// X-axis (real)
|
|
597
|
+
ctx.beginPath();
|
|
598
|
+
ctx.moveTo(0, centerY);
|
|
599
|
+
ctx.lineTo(this.canvas.width, centerY);
|
|
600
|
+
ctx.stroke();
|
|
601
|
+
|
|
602
|
+
// Y-axis (imaginary)
|
|
603
|
+
ctx.beginPath();
|
|
604
|
+
ctx.moveTo(centerX, 0);
|
|
605
|
+
ctx.lineTo(centerX, this.canvas.height);
|
|
606
|
+
ctx.stroke();
|
|
607
|
+
|
|
608
|
+
// Axis labels
|
|
609
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.25)";
|
|
610
|
+
ctx.font = "12px monospace";
|
|
611
|
+
ctx.textAlign = "left";
|
|
612
|
+
ctx.fillText("Re", this.canvas.width - 30, centerY - 10);
|
|
613
|
+
ctx.fillText("Im", centerX + 10, 20);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
drawTrails(ctx, eq) {
|
|
617
|
+
const hue = eq.hue;
|
|
618
|
+
|
|
619
|
+
for (let i = 0; i < eq.trail1.length - 1; i++) {
|
|
620
|
+
const alpha = (1 - i / eq.trail1.length) * 0.5;
|
|
621
|
+
const width = (1 - i / eq.trail1.length) * 3 + 1;
|
|
622
|
+
|
|
623
|
+
ctx.strokeStyle = `hsla(${hue}, 80%, 60%, ${alpha})`;
|
|
624
|
+
ctx.lineWidth = width;
|
|
625
|
+
ctx.lineCap = "round";
|
|
626
|
+
|
|
627
|
+
// Trail 1
|
|
628
|
+
ctx.beginPath();
|
|
629
|
+
ctx.moveTo(eq.trail1[i].x, eq.trail1[i].y);
|
|
630
|
+
ctx.lineTo(eq.trail1[i + 1].x, eq.trail1[i + 1].y);
|
|
631
|
+
ctx.stroke();
|
|
632
|
+
|
|
633
|
+
// Trail 2
|
|
634
|
+
ctx.beginPath();
|
|
635
|
+
ctx.moveTo(eq.trail2[i].x, eq.trail2[i].y);
|
|
636
|
+
ctx.lineTo(eq.trail2[i + 1].x, eq.trail2[i + 1].y);
|
|
637
|
+
ctx.stroke();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
drawParticles(ctx, eq) {
|
|
642
|
+
const hue = eq.hue;
|
|
643
|
+
const radius = CONFIG.particleRadius;
|
|
644
|
+
|
|
645
|
+
// Draw glow layers (outer to inner)
|
|
646
|
+
for (let layer = CONFIG.glowLayers; layer >= 0; layer--) {
|
|
647
|
+
const layerRadius = radius + layer * CONFIG.glowExpansion;
|
|
648
|
+
const alpha = 0.12 * (1 - layer / CONFIG.glowLayers);
|
|
649
|
+
|
|
650
|
+
ctx.fillStyle = `hsla(${hue}, 90%, 65%, ${alpha})`;
|
|
651
|
+
|
|
652
|
+
ctx.beginPath();
|
|
653
|
+
ctx.arc(eq.root1.x, eq.root1.y, layerRadius, 0, Math.PI * 2);
|
|
654
|
+
ctx.fill();
|
|
655
|
+
|
|
656
|
+
ctx.beginPath();
|
|
657
|
+
ctx.arc(eq.root2.x, eq.root2.y, layerRadius, 0, Math.PI * 2);
|
|
658
|
+
ctx.fill();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Draw core
|
|
662
|
+
ctx.fillStyle = `hsl(${hue}, 100%, 75%)`;
|
|
663
|
+
ctx.beginPath();
|
|
664
|
+
ctx.arc(eq.root1.x, eq.root1.y, radius * 0.7, 0, Math.PI * 2);
|
|
665
|
+
ctx.fill();
|
|
666
|
+
|
|
667
|
+
ctx.beginPath();
|
|
668
|
+
ctx.arc(eq.root2.x, eq.root2.y, radius * 0.7, 0, Math.PI * 2);
|
|
669
|
+
ctx.fill();
|
|
670
|
+
|
|
671
|
+
// Bright center
|
|
672
|
+
ctx.fillStyle = `hsl(${hue}, 50%, 95%)`;
|
|
673
|
+
ctx.beginPath();
|
|
674
|
+
ctx.arc(eq.root1.x, eq.root1.y, radius * 0.3, 0, Math.PI * 2);
|
|
675
|
+
ctx.fill();
|
|
676
|
+
|
|
677
|
+
ctx.beginPath();
|
|
678
|
+
ctx.arc(eq.root2.x, eq.root2.y, radius * 0.3, 0, Math.PI * 2);
|
|
679
|
+
ctx.fill();
|
|
680
|
+
|
|
681
|
+
// Labels above particles with equation color
|
|
682
|
+
const labelOffset = radius + 18;
|
|
683
|
+
ctx.textAlign = "center";
|
|
684
|
+
ctx.textBaseline = "bottom";
|
|
685
|
+
|
|
686
|
+
// x₁ label
|
|
687
|
+
ctx.font = "bold 11px monospace";
|
|
688
|
+
ctx.fillStyle = `hsl(${hue}, 80%, 75%)`;
|
|
689
|
+
ctx.fillText("x₁", eq.root1.x, eq.root1.y - labelOffset);
|
|
690
|
+
|
|
691
|
+
// x₁ value
|
|
692
|
+
ctx.font = "9px monospace";
|
|
693
|
+
ctx.fillStyle = "#777";
|
|
694
|
+
if (eq.isComplex) {
|
|
695
|
+
const sign = eq.x1.imag >= 0 ? "+" : "";
|
|
696
|
+
ctx.fillText(`${eq.x1.real.toFixed(1)}${sign}${eq.x1.imag.toFixed(1)}i`, eq.root1.x, eq.root1.y - labelOffset + 12);
|
|
697
|
+
} else {
|
|
698
|
+
ctx.fillText(eq.x1.toFixed(2), eq.root1.x, eq.root1.y - labelOffset + 12);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// x₂ label
|
|
702
|
+
ctx.font = "bold 11px monospace";
|
|
703
|
+
ctx.fillStyle = `hsl(${hue}, 80%, 75%)`;
|
|
704
|
+
ctx.fillText("x₂", eq.root2.x, eq.root2.y - labelOffset);
|
|
705
|
+
|
|
706
|
+
// x₂ value
|
|
707
|
+
ctx.font = "9px monospace";
|
|
708
|
+
ctx.fillStyle = "#777";
|
|
709
|
+
if (eq.isComplex) {
|
|
710
|
+
const sign = eq.x2.imag >= 0 ? "+" : "";
|
|
711
|
+
ctx.fillText(`${eq.x2.real.toFixed(1)}${sign}${eq.x2.imag.toFixed(1)}i`, eq.root2.x, eq.root2.y - labelOffset + 12);
|
|
712
|
+
} else {
|
|
713
|
+
ctx.fillText(eq.x2.toFixed(2), eq.root2.x, eq.root2.y - labelOffset + 12);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
drawMergeEffect(ctx, eq) {
|
|
718
|
+
const elapsed = this.time - eq.mergeTime;
|
|
719
|
+
const progress = Math.min(elapsed / CONFIG.mergeFlashDuration, 1);
|
|
720
|
+
|
|
721
|
+
const midX = (eq.root1.x + eq.root2.x) / 2;
|
|
722
|
+
const midY = (eq.root1.y + eq.root2.y) / 2;
|
|
723
|
+
|
|
724
|
+
const ringRadius = 15 + progress * 40;
|
|
725
|
+
const alpha = (1 - progress) * 0.5;
|
|
726
|
+
|
|
727
|
+
ctx.strokeStyle = `hsla(${eq.hue}, 100%, 70%, ${alpha})`;
|
|
728
|
+
ctx.lineWidth = 2 * (1 - progress) + 1;
|
|
729
|
+
ctx.beginPath();
|
|
730
|
+
ctx.arc(midX, midY, ringRadius, 0, Math.PI * 2);
|
|
731
|
+
ctx.stroke();
|
|
732
|
+
|
|
733
|
+
// Inner glow
|
|
734
|
+
const innerAlpha = (1 - progress) * 0.3;
|
|
735
|
+
const innerGradient = ctx.createRadialGradient(midX, midY, 0, midX, midY, ringRadius);
|
|
736
|
+
innerGradient.addColorStop(0, `hsla(${eq.hue}, 100%, 80%, ${innerAlpha})`);
|
|
737
|
+
innerGradient.addColorStop(1, `hsla(${eq.hue}, 100%, 80%, 0)`);
|
|
738
|
+
ctx.fillStyle = innerGradient;
|
|
739
|
+
ctx.beginPath();
|
|
740
|
+
ctx.arc(midX, midY, ringRadius, 0, Math.PI * 2);
|
|
741
|
+
ctx.fill();
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Export as MyGame for backwards compatibility with HTML
|
|
746
|
+
export function MyGame(canvas) {
|
|
747
|
+
const demo = new BaskaraDemo(canvas);
|
|
748
|
+
return {
|
|
749
|
+
start: () => demo.start()
|
|
750
|
+
};
|
|
751
|
+
}
|