@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,931 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GCanvas Fractal Demo
|
|
3
|
+
*
|
|
4
|
+
* This demo showcases the creation of various fractals using the GCanvas engine.
|
|
5
|
+
* It implements multiple fractal algorithms and provides interactive controls.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
Easing,
|
|
10
|
+
FPSCounter,
|
|
11
|
+
Fractals,
|
|
12
|
+
Game,
|
|
13
|
+
HorizontalLayout,
|
|
14
|
+
ImageGo,
|
|
15
|
+
Painter,
|
|
16
|
+
Position,
|
|
17
|
+
Scene,
|
|
18
|
+
TaskManager,
|
|
19
|
+
Text,
|
|
20
|
+
} from "../../src/index";
|
|
21
|
+
|
|
22
|
+
export class FractalDemo extends Game {
|
|
23
|
+
constructor(canvas) {
|
|
24
|
+
super(canvas);
|
|
25
|
+
this.enableFluidSize();
|
|
26
|
+
// Settings for fractal rendering
|
|
27
|
+
this.settings = {
|
|
28
|
+
type: Fractals.types.MANDELBROT,
|
|
29
|
+
iterations: 32,
|
|
30
|
+
colorScheme: Fractals.colors.FUTURISTIC,
|
|
31
|
+
hueShift: 0,
|
|
32
|
+
zoom: 1,
|
|
33
|
+
offsetX: 0,
|
|
34
|
+
offsetY: 0,
|
|
35
|
+
animating: false,
|
|
36
|
+
typeIndex: 0,
|
|
37
|
+
};
|
|
38
|
+
// Target values for smooth interpolation
|
|
39
|
+
this.target = {
|
|
40
|
+
zoom: 1,
|
|
41
|
+
offsetX: 0,
|
|
42
|
+
offsetY: 0
|
|
43
|
+
};
|
|
44
|
+
// Pan velocity for momentum
|
|
45
|
+
this.panVelocity = { x: 0, y: 0 };
|
|
46
|
+
this.panDamping = 0.92;
|
|
47
|
+
// Easing speed (higher = faster convergence)
|
|
48
|
+
this.easeSpeed = 8;
|
|
49
|
+
this.baseIterations = 32;
|
|
50
|
+
// Render throttling
|
|
51
|
+
this.lastRenderTime = 0;
|
|
52
|
+
this.minRenderInterval = 50; // ms between renders during interaction
|
|
53
|
+
this.isInteracting = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
init() {
|
|
57
|
+
super.init();
|
|
58
|
+
|
|
59
|
+
// Create scenes
|
|
60
|
+
this.mainScene = new Scene(this, {
|
|
61
|
+
anchor: Position.CENTER,
|
|
62
|
+
debug: true
|
|
63
|
+
});
|
|
64
|
+
this.mainScene.width = this.width;
|
|
65
|
+
this.mainScene.height = this.height;
|
|
66
|
+
this.ui = new Scene(this, { debug: true, anchor: Position.CENTER });
|
|
67
|
+
this.pipeline.add(this.mainScene);
|
|
68
|
+
this.pipeline.add(this.ui);
|
|
69
|
+
|
|
70
|
+
// Add fractal renderer - create it at full size initially
|
|
71
|
+
this.fractal = new FractalRenderer(
|
|
72
|
+
this,
|
|
73
|
+
this.mainScene.width,
|
|
74
|
+
this.mainScene.height,
|
|
75
|
+
{
|
|
76
|
+
debug: true,
|
|
77
|
+
debugColor: "white",
|
|
78
|
+
width: this.mainScene.width,
|
|
79
|
+
height: this.mainScene.height,
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
this.mainScene.add(this.fractal);
|
|
83
|
+
// Add UI controls
|
|
84
|
+
this.setupUI();
|
|
85
|
+
// Add FPS counter
|
|
86
|
+
this.ui.add(
|
|
87
|
+
new FPSCounter(this, {
|
|
88
|
+
anchor: "bottom-right",
|
|
89
|
+
color: "#0f0",
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
// Set up interactive controls
|
|
93
|
+
this.setupInteraction();
|
|
94
|
+
this.onResize();
|
|
95
|
+
// Initial render
|
|
96
|
+
this.updateFractalSettings();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onResize() {
|
|
100
|
+
if (this.mainScene) {
|
|
101
|
+
// Full width/height for mobile-friendly display
|
|
102
|
+
const newWidth = this.width;
|
|
103
|
+
const newHeight = this.height;
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
this.mainScene.width !== newWidth ||
|
|
107
|
+
this.mainScene.height !== newHeight
|
|
108
|
+
) {
|
|
109
|
+
this.mainScene.width = newWidth;
|
|
110
|
+
this.mainScene.height = newHeight;
|
|
111
|
+
this.ui.width = newWidth;
|
|
112
|
+
this.ui.height = newHeight;
|
|
113
|
+
// Resize fractal renderer with a small delay to prevent multiple resizes
|
|
114
|
+
if (!this.resizeTimeout) {
|
|
115
|
+
this.resizeTimeout = setTimeout(() => {
|
|
116
|
+
// Use actual dimensions, capped for performance on large screens
|
|
117
|
+
const renderWidth = Math.min(newWidth, 1200);
|
|
118
|
+
const renderHeight = Math.min(newHeight, 900);
|
|
119
|
+
this.fractal.resize(renderWidth, renderHeight);
|
|
120
|
+
this.resizeTimeout = null;
|
|
121
|
+
}, 100);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
update(dt) {
|
|
128
|
+
super.update(dt);
|
|
129
|
+
|
|
130
|
+
let needsRender = false;
|
|
131
|
+
const wasAnimating = this.wasAnimating || false;
|
|
132
|
+
|
|
133
|
+
// Apply pan momentum when not dragging
|
|
134
|
+
if (!this.settings.isDragging && (Math.abs(this.panVelocity.x) > 0.0001 || Math.abs(this.panVelocity.y) > 0.0001)) {
|
|
135
|
+
const scaleFactor = 0.005 / this.settings.zoom;
|
|
136
|
+
this.target.offsetX -= this.panVelocity.x * scaleFactor;
|
|
137
|
+
this.target.offsetY -= this.panVelocity.y * scaleFactor;
|
|
138
|
+
this.panVelocity.x *= this.panDamping;
|
|
139
|
+
this.panVelocity.y *= this.panDamping;
|
|
140
|
+
needsRender = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Smooth interpolation toward target values using Easing.lerp
|
|
144
|
+
const lerpFactor = 1 - Math.exp(-this.easeSpeed * dt);
|
|
145
|
+
const zoomDiff = Math.abs(this.settings.zoom - this.target.zoom);
|
|
146
|
+
const offsetXDiff = Math.abs(this.settings.offsetX - this.target.offsetX);
|
|
147
|
+
const offsetYDiff = Math.abs(this.settings.offsetY - this.target.offsetY);
|
|
148
|
+
|
|
149
|
+
if (zoomDiff > 0.001 || offsetXDiff > 0.00001 || offsetYDiff > 0.00001) {
|
|
150
|
+
this.settings.zoom = Easing.lerp(this.settings.zoom, this.target.zoom, lerpFactor);
|
|
151
|
+
this.settings.offsetX = Easing.lerp(this.settings.offsetX, this.target.offsetX, lerpFactor);
|
|
152
|
+
this.settings.offsetY = Easing.lerp(this.settings.offsetY, this.target.offsetY, lerpFactor);
|
|
153
|
+
needsRender = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle animation if enabled
|
|
157
|
+
if (this.settings.animating) {
|
|
158
|
+
this.settings.hueShift = (this.settings.hueShift + dt * 30) % 360;
|
|
159
|
+
this.target.zoom = this.target.zoom * (1 + dt * 0.08);
|
|
160
|
+
needsRender = true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.wasAnimating = needsRender;
|
|
164
|
+
|
|
165
|
+
// Throttle renders during animation, but always render when settled
|
|
166
|
+
if (needsRender) {
|
|
167
|
+
const now = performance.now();
|
|
168
|
+
if (now - this.lastRenderTime > this.minRenderInterval) {
|
|
169
|
+
this.lastRenderTime = now;
|
|
170
|
+
this.updateFractalSettings(true); // Preview mode
|
|
171
|
+
}
|
|
172
|
+
} else if (wasAnimating) {
|
|
173
|
+
// Animation just settled - final full-quality render
|
|
174
|
+
this.updateFractalSettings(false);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
updateFractalSettings(preview = false) {
|
|
179
|
+
// Use lower iterations for preview during interaction
|
|
180
|
+
const iterations = preview
|
|
181
|
+
? Math.max(16, Math.floor(this.baseIterations * 0.4))
|
|
182
|
+
: this.baseIterations;
|
|
183
|
+
|
|
184
|
+
// Update renderer settings - it will render asynchronously
|
|
185
|
+
this.fractal.updateSettings({
|
|
186
|
+
type: this.settings.type,
|
|
187
|
+
iterations: iterations,
|
|
188
|
+
colorScheme: this.settings.colorScheme,
|
|
189
|
+
hueShift: this.settings.hueShift,
|
|
190
|
+
zoom: this.settings.zoom,
|
|
191
|
+
offsetX: this.settings.offsetX,
|
|
192
|
+
offsetY: this.settings.offsetY,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
setupUI() {
|
|
197
|
+
// Responsive sizing based on screen width
|
|
198
|
+
const isMobile = this.width < 600;
|
|
199
|
+
const btnHeight = isMobile ? 28 : 30;
|
|
200
|
+
const btnSpacing = isMobile ? 4 : 10;
|
|
201
|
+
const fontSize = isMobile ? "11px" : "13px";
|
|
202
|
+
|
|
203
|
+
// Create UI layout
|
|
204
|
+
const controlsLayout = new HorizontalLayout(this, {
|
|
205
|
+
anchor: Position.BOTTOM_LEFT,
|
|
206
|
+
spacing: btnSpacing,
|
|
207
|
+
padding: isMobile ? 4 : 10,
|
|
208
|
+
height: btnHeight + 10,
|
|
209
|
+
width: this.width,
|
|
210
|
+
debug: false,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Fractal type selector
|
|
214
|
+
const typeBtn = new Button(this, {
|
|
215
|
+
text: isMobile ? this.settings.type.toUpperCase() : `Type: ${this.settings.type.toUpperCase()}`,
|
|
216
|
+
width: isMobile ? 90 : 180,
|
|
217
|
+
height: btnHeight,
|
|
218
|
+
fontSize,
|
|
219
|
+
onClick: () => this.cycleFractalType(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Iteration controller
|
|
223
|
+
const iterBtn = new Button(this, {
|
|
224
|
+
text: isMobile ? `${this.settings.iterations}` : `Iter: ${this.settings.iterations}`,
|
|
225
|
+
width: isMobile ? 45 : 100,
|
|
226
|
+
height: btnHeight,
|
|
227
|
+
fontSize,
|
|
228
|
+
onClick: () => this.cycleIterations(),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Color mode selector
|
|
232
|
+
const colorBtn = new Button(this, {
|
|
233
|
+
text: isMobile ? "Color" : `Color: ${this.settings.colorScheme}`,
|
|
234
|
+
width: isMobile ? 50 : 150,
|
|
235
|
+
height: btnHeight,
|
|
236
|
+
fontSize,
|
|
237
|
+
onClick: () => this.cycleColorScheme(),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Animate button
|
|
241
|
+
const animateBtn = new Button(this, {
|
|
242
|
+
text: isMobile ? "Anim" : "Animate",
|
|
243
|
+
width: isMobile ? 45 : 80,
|
|
244
|
+
height: btnHeight,
|
|
245
|
+
fontSize,
|
|
246
|
+
onClick: () => {
|
|
247
|
+
this.settings.animating = !this.settings.animating;
|
|
248
|
+
animateBtn.text = this.settings.animating ? "Stop" : (isMobile ? "Anim" : "Animate");
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Zoom in button
|
|
253
|
+
const zoomInBtn = new Button(this, {
|
|
254
|
+
text: "+",
|
|
255
|
+
width: isMobile ? 32 : 50,
|
|
256
|
+
height: btnHeight,
|
|
257
|
+
fontSize,
|
|
258
|
+
onClick: () => this.zoomIn(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Zoom out button
|
|
262
|
+
const zoomOutBtn = new Button(this, {
|
|
263
|
+
text: "-",
|
|
264
|
+
width: isMobile ? 32 : 50,
|
|
265
|
+
height: btnHeight,
|
|
266
|
+
fontSize,
|
|
267
|
+
onClick: () => this.zoomOut(),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Add all buttons to layout
|
|
271
|
+
controlsLayout.add(typeBtn);
|
|
272
|
+
controlsLayout.add(iterBtn);
|
|
273
|
+
controlsLayout.add(colorBtn);
|
|
274
|
+
controlsLayout.add(animateBtn);
|
|
275
|
+
controlsLayout.add(zoomInBtn);
|
|
276
|
+
controlsLayout.add(zoomOutBtn);
|
|
277
|
+
|
|
278
|
+
// Store references to UI elements
|
|
279
|
+
this.controls = {
|
|
280
|
+
typeBtn,
|
|
281
|
+
iterBtn,
|
|
282
|
+
colorBtn,
|
|
283
|
+
animateBtn,
|
|
284
|
+
layout: controlsLayout,
|
|
285
|
+
isMobile,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
this.controls.typeBtn.type = 0;
|
|
289
|
+
|
|
290
|
+
// Add layout to UI
|
|
291
|
+
this.ui.add(controlsLayout);
|
|
292
|
+
|
|
293
|
+
// Add title (hide on mobile)
|
|
294
|
+
if (!isMobile) {
|
|
295
|
+
const title = new Text(this, "GCanvas Fractal Explorer", {
|
|
296
|
+
font: "bold 24px Arial",
|
|
297
|
+
color: "#fff",
|
|
298
|
+
align: "center",
|
|
299
|
+
baseline: "middle",
|
|
300
|
+
width: 10,
|
|
301
|
+
anchor: Position.BOTTOM_RIGHT,
|
|
302
|
+
anchorRelative: this.ui,
|
|
303
|
+
});
|
|
304
|
+
this.ui.add(title);
|
|
305
|
+
}
|
|
306
|
+
this.ui.update(); //force update to the anchor
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
resetView() {
|
|
310
|
+
// Set targets - smooth interpolation happens in update()
|
|
311
|
+
this.target.zoom = 1;
|
|
312
|
+
this.target.offsetX = 0;
|
|
313
|
+
this.target.offsetY = 0;
|
|
314
|
+
this.panVelocity.x = 0;
|
|
315
|
+
this.panVelocity.y = 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
setupInteraction() {
|
|
319
|
+
// Throttle function to limit calls
|
|
320
|
+
const throttle = (func, limit) => {
|
|
321
|
+
let inThrottle;
|
|
322
|
+
return function () {
|
|
323
|
+
const args = arguments;
|
|
324
|
+
const context = this;
|
|
325
|
+
if (!inThrottle) {
|
|
326
|
+
func.apply(context, args);
|
|
327
|
+
inThrottle = true;
|
|
328
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Mouse down for panning
|
|
334
|
+
this.canvas.addEventListener("mousedown", (e) => {
|
|
335
|
+
this.settings.isDragging = true;
|
|
336
|
+
this.settings.lastX = e.offsetX;
|
|
337
|
+
this.settings.lastY = e.offsetY;
|
|
338
|
+
this.panVelocity.x = 0;
|
|
339
|
+
this.panVelocity.y = 0;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Mouse move for panning - visual transform only (no re-render)
|
|
343
|
+
this.canvas.addEventListener(
|
|
344
|
+
"mousemove",
|
|
345
|
+
throttle((e) => {
|
|
346
|
+
if (this.settings.isDragging) {
|
|
347
|
+
const dx = e.offsetX - this.settings.lastX;
|
|
348
|
+
const dy = e.offsetY - this.settings.lastY;
|
|
349
|
+
|
|
350
|
+
// Track velocity for momentum
|
|
351
|
+
this.panVelocity.x = dx * 0.8 + this.panVelocity.x * 0.2;
|
|
352
|
+
this.panVelocity.y = dy * 0.8 + this.panVelocity.y * 0.2;
|
|
353
|
+
|
|
354
|
+
// Update offset values
|
|
355
|
+
const scaleFactor = 0.005 / this.settings.zoom;
|
|
356
|
+
const deltaX = dx * scaleFactor;
|
|
357
|
+
const deltaY = dy * scaleFactor;
|
|
358
|
+
this.settings.offsetX -= deltaX;
|
|
359
|
+
this.settings.offsetY -= deltaY;
|
|
360
|
+
this.target.offsetX -= deltaX;
|
|
361
|
+
this.target.offsetY -= deltaY;
|
|
362
|
+
|
|
363
|
+
this.settings.lastX = e.offsetX;
|
|
364
|
+
this.settings.lastY = e.offsetY;
|
|
365
|
+
|
|
366
|
+
// Throttled preview render during drag
|
|
367
|
+
const now = performance.now();
|
|
368
|
+
if (now - this.lastRenderTime > this.minRenderInterval) {
|
|
369
|
+
this.lastRenderTime = now;
|
|
370
|
+
this.updateFractalSettings(true);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}, 16)
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Mouse up to end panning - full quality render
|
|
377
|
+
this.canvas.addEventListener("mouseup", () => {
|
|
378
|
+
this.settings.isDragging = false;
|
|
379
|
+
// If no significant momentum, re-render immediately at full quality
|
|
380
|
+
if (Math.abs(this.panVelocity.x) < 0.5 && Math.abs(this.panVelocity.y) < 0.5) {
|
|
381
|
+
this.panVelocity.x = 0;
|
|
382
|
+
this.panVelocity.y = 0;
|
|
383
|
+
this.updateFractalSettings(false);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Mouse wheel for zooming - target-based smooth easing
|
|
388
|
+
this.canvas.addEventListener(
|
|
389
|
+
"wheel",
|
|
390
|
+
(e) => {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
|
|
393
|
+
// Calculate zoom factor
|
|
394
|
+
const zoomFactor = e.deltaY < 0 ? 1.25 : 0.8;
|
|
395
|
+
|
|
396
|
+
// Get mouse position in canvas
|
|
397
|
+
const mouseX = e.offsetX;
|
|
398
|
+
const mouseY = e.offsetY;
|
|
399
|
+
|
|
400
|
+
// Calculate normalized position (0-1)
|
|
401
|
+
const normalizedX = mouseX / this.width;
|
|
402
|
+
const normalizedY = mouseY / this.height;
|
|
403
|
+
|
|
404
|
+
// Map to complex plane coordinates using TARGET values for accumulation
|
|
405
|
+
const planeWidth = 3.5 / this.target.zoom;
|
|
406
|
+
const planeHeight = 3.0 / this.target.zoom;
|
|
407
|
+
const planeX = -2.5 / this.target.zoom + this.target.offsetX;
|
|
408
|
+
const planeY = -1.5 / this.target.zoom + this.target.offsetY;
|
|
409
|
+
|
|
410
|
+
// Find the point we're zooming in on
|
|
411
|
+
const pointX = planeX + normalizedX * planeWidth;
|
|
412
|
+
const pointY = planeY + normalizedY * planeHeight;
|
|
413
|
+
|
|
414
|
+
// Calculate new target zoom and offset
|
|
415
|
+
const newZoom = this.target.zoom * zoomFactor;
|
|
416
|
+
const newPlaneWidth = 3.5 / newZoom;
|
|
417
|
+
const newPlaneHeight = 3.0 / newZoom;
|
|
418
|
+
const newPlaneX = pointX - normalizedX * newPlaneWidth;
|
|
419
|
+
const newPlaneY = pointY - normalizedY * newPlaneHeight;
|
|
420
|
+
|
|
421
|
+
// Update target values - smooth interpolation happens in update()
|
|
422
|
+
this.target.zoom = newZoom;
|
|
423
|
+
this.target.offsetX = newPlaneX + 2.5 / newZoom;
|
|
424
|
+
this.target.offsetY = newPlaneY + 1.5 / newZoom;
|
|
425
|
+
},
|
|
426
|
+
{ passive: false }
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Touch support for mobile
|
|
430
|
+
let lastTouchX = 0;
|
|
431
|
+
let lastTouchY = 0;
|
|
432
|
+
let lastPinchDist = 0;
|
|
433
|
+
|
|
434
|
+
this.canvas.addEventListener("touchstart", (e) => {
|
|
435
|
+
if (e.touches.length === 1) {
|
|
436
|
+
lastTouchX = e.touches[0].clientX;
|
|
437
|
+
lastTouchY = e.touches[0].clientY;
|
|
438
|
+
this.settings.isDragging = true;
|
|
439
|
+
this.panVelocity.x = 0;
|
|
440
|
+
this.panVelocity.y = 0;
|
|
441
|
+
} else if (e.touches.length === 2) {
|
|
442
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
443
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
444
|
+
lastPinchDist = Math.sqrt(dx * dx + dy * dy);
|
|
445
|
+
this.settings.isDragging = false;
|
|
446
|
+
}
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
}, { passive: false });
|
|
449
|
+
|
|
450
|
+
this.canvas.addEventListener("touchmove", throttle((e) => {
|
|
451
|
+
if (e.touches.length === 1 && this.settings.isDragging) {
|
|
452
|
+
const dx = e.touches[0].clientX - lastTouchX;
|
|
453
|
+
const dy = e.touches[0].clientY - lastTouchY;
|
|
454
|
+
|
|
455
|
+
this.panVelocity.x = dx * 0.8 + this.panVelocity.x * 0.2;
|
|
456
|
+
this.panVelocity.y = dy * 0.8 + this.panVelocity.y * 0.2;
|
|
457
|
+
|
|
458
|
+
const scaleFactor = 0.005 / this.settings.zoom;
|
|
459
|
+
const deltaX = dx * scaleFactor;
|
|
460
|
+
const deltaY = dy * scaleFactor;
|
|
461
|
+
this.settings.offsetX -= deltaX;
|
|
462
|
+
this.settings.offsetY -= deltaY;
|
|
463
|
+
this.target.offsetX -= deltaX;
|
|
464
|
+
this.target.offsetY -= deltaY;
|
|
465
|
+
|
|
466
|
+
lastTouchX = e.touches[0].clientX;
|
|
467
|
+
lastTouchY = e.touches[0].clientY;
|
|
468
|
+
|
|
469
|
+
// Throttled preview render during drag
|
|
470
|
+
const now = performance.now();
|
|
471
|
+
if (now - this.lastRenderTime > this.minRenderInterval) {
|
|
472
|
+
this.lastRenderTime = now;
|
|
473
|
+
this.updateFractalSettings(true);
|
|
474
|
+
}
|
|
475
|
+
} else if (e.touches.length === 2) {
|
|
476
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
477
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
478
|
+
const pinchDist = Math.sqrt(dx * dx + dy * dy);
|
|
479
|
+
|
|
480
|
+
if (lastPinchDist > 0) {
|
|
481
|
+
const zoomFactor = pinchDist / lastPinchDist;
|
|
482
|
+
// Direct zoom for responsive feel during pinch
|
|
483
|
+
this.settings.zoom *= zoomFactor;
|
|
484
|
+
this.target.zoom *= zoomFactor;
|
|
485
|
+
// Throttled preview render during pinch
|
|
486
|
+
const now = performance.now();
|
|
487
|
+
if (now - this.lastRenderTime > this.minRenderInterval) {
|
|
488
|
+
this.lastRenderTime = now;
|
|
489
|
+
this.updateFractalSettings(true);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
lastPinchDist = pinchDist;
|
|
494
|
+
}
|
|
495
|
+
e.preventDefault();
|
|
496
|
+
}, 16), { passive: false });
|
|
497
|
+
|
|
498
|
+
this.canvas.addEventListener("touchend", () => {
|
|
499
|
+
this.settings.isDragging = false;
|
|
500
|
+
lastPinchDist = 0;
|
|
501
|
+
// If no significant momentum, re-render immediately at full quality
|
|
502
|
+
if (Math.abs(this.panVelocity.x) < 0.5 && Math.abs(this.panVelocity.y) < 0.5) {
|
|
503
|
+
this.panVelocity.x = 0;
|
|
504
|
+
this.panVelocity.y = 0;
|
|
505
|
+
this.updateFractalSettings(false);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
cycleFractalType() {
|
|
511
|
+
const types = Object.values(Fractals.types);
|
|
512
|
+
this.settings.typeIndex = (this.settings.typeIndex + 1) % types.length;
|
|
513
|
+
this.settings.type = types[this.settings.typeIndex];
|
|
514
|
+
this.controls.typeBtn.text = this.controls.isMobile
|
|
515
|
+
? this.settings.type.toUpperCase()
|
|
516
|
+
: `Type: ${this.settings.type.toUpperCase()}`;
|
|
517
|
+
// Tweaking default params for selected type
|
|
518
|
+
if (this.settings.type === Fractals.types.KOCH) {
|
|
519
|
+
this.settings.iterations = 3;
|
|
520
|
+
this.settings.colorScheme = Fractals.colors.TOPOGRAPHIC;
|
|
521
|
+
} else if (this.settings.type === Fractals.types.BARNSEY_FERN) {
|
|
522
|
+
this.settings.colorScheme = Fractals.colors.ELECTRIC;
|
|
523
|
+
} else if (this.settings.type === Fractals.types.LYAPUNOV) {
|
|
524
|
+
this.settings.colorScheme = Fractals.colors.RAINBOW;
|
|
525
|
+
this.settings.iterations = 8;
|
|
526
|
+
} else if (
|
|
527
|
+
this.settings.type === Fractals.types.MANDELBROT ||
|
|
528
|
+
this.settings.type == Fractals.types.JULIA ||
|
|
529
|
+
this.settings.type == Fractals.types.TRICORN ||
|
|
530
|
+
this.settings.type == Fractals.types.PHOENIX
|
|
531
|
+
) {
|
|
532
|
+
this.settings.iterations = 32;
|
|
533
|
+
this.settings.colorScheme = Fractals.colors.FUTURISTIC;
|
|
534
|
+
} else if (this.settings.type === Fractals.types.NEWTON) {
|
|
535
|
+
this.settings.iterations = 32;
|
|
536
|
+
this.settings.colorScheme = Fractals.colors.ELECTRIC;
|
|
537
|
+
} else if (
|
|
538
|
+
this.settings.type === Fractals.types.SIERPINSKI ||
|
|
539
|
+
this.settings.type === Fractals.types.SCARPET
|
|
540
|
+
) {
|
|
541
|
+
this.settings.colorScheme = Fractals.colors.ELECTRIC;
|
|
542
|
+
this.settings.iterations = 4;
|
|
543
|
+
}
|
|
544
|
+
this.baseIterations = this.settings.iterations;
|
|
545
|
+
this.controls.iterBtn.text = this.controls.isMobile
|
|
546
|
+
? `${this.settings.iterations}`
|
|
547
|
+
: `Iter: ${this.settings.iterations}`;
|
|
548
|
+
this.controls.colorBtn.text = this.controls.isMobile
|
|
549
|
+
? "Color"
|
|
550
|
+
: `Color: ${this.settings.colorScheme}`;
|
|
551
|
+
this.resetView();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
cycleIterations() {
|
|
555
|
+
let iterationPresets;
|
|
556
|
+
if (
|
|
557
|
+
this.settings.type === Fractals.types.SIERPINSKI ||
|
|
558
|
+
this.settings.type === Fractals.types.SCARPET
|
|
559
|
+
) {
|
|
560
|
+
iterationPresets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25];
|
|
561
|
+
} else if (
|
|
562
|
+
this.settings.type === Fractals.types.MANDELBROT ||
|
|
563
|
+
this.settings.type === Fractals.types.TRICORN ||
|
|
564
|
+
this.settings.type === Fractals.types.PHOENIX ||
|
|
565
|
+
this.settings.type === Fractals.types.JULIA
|
|
566
|
+
) {
|
|
567
|
+
iterationPresets = [3, 4, 5, 8, 32, 50, 100, 200, 500, 1000];
|
|
568
|
+
} else if (this.settings.type === Fractals.types.KOCH) {
|
|
569
|
+
iterationPresets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
570
|
+
} else {
|
|
571
|
+
iterationPresets = [3, 4, 5, 8, 32, 50, 100];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const currentIndex = iterationPresets.indexOf(this.settings.iterations);
|
|
575
|
+
const nextIndex = (currentIndex + 1) % iterationPresets.length;
|
|
576
|
+
|
|
577
|
+
this.settings.iterations = iterationPresets[nextIndex];
|
|
578
|
+
this.baseIterations = this.settings.iterations;
|
|
579
|
+
this.controls.iterBtn.text = this.controls.isMobile
|
|
580
|
+
? `${this.settings.iterations}`
|
|
581
|
+
: `Iter: ${this.settings.iterations}`;
|
|
582
|
+
|
|
583
|
+
// Update with new iterations
|
|
584
|
+
this.updateFractalSettings();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
cycleColorScheme() {
|
|
588
|
+
let schemes = Object.values(Fractals.colors);
|
|
589
|
+
if (this.settings.type === Fractals.types.LYAPUNOV) {
|
|
590
|
+
schemes = [
|
|
591
|
+
Fractals.colors.RAINBOW,
|
|
592
|
+
Fractals.colors.ELECTRIC,
|
|
593
|
+
Fractals.colors.HISTORIC,
|
|
594
|
+
Fractals.colors.GRAYSCALE,
|
|
595
|
+
Fractals.colors.BINARY,
|
|
596
|
+
];
|
|
597
|
+
} else if (
|
|
598
|
+
this.settings.type === Fractals.types.SIERPINSKI ||
|
|
599
|
+
this.settings.type === Fractals.types.SCARPET
|
|
600
|
+
) {
|
|
601
|
+
schemes = [
|
|
602
|
+
Fractals.colors.ELECTRIC,
|
|
603
|
+
Fractals.colors.TOPOGRAPHIC,
|
|
604
|
+
Fractals.colors.OCEAN,
|
|
605
|
+
Fractals.colors.GRAYSCALE,
|
|
606
|
+
Fractals.colors.BINARY,
|
|
607
|
+
];
|
|
608
|
+
} else if (this.settings.type === Fractals.types.BARNSEY_FERN) {
|
|
609
|
+
schemes = [
|
|
610
|
+
Fractals.colors.ELECTRIC,
|
|
611
|
+
Fractals.colors.RAINBOW,
|
|
612
|
+
Fractals.colors.GRAYSCALE,
|
|
613
|
+
Fractals.colors.BINARY,
|
|
614
|
+
];
|
|
615
|
+
} else if (this.settings.type === Fractals.types.NEWTON) {
|
|
616
|
+
schemes = [
|
|
617
|
+
Fractals.colors.ELECTRIC,
|
|
618
|
+
Fractals.colors.OCEAN,
|
|
619
|
+
Fractals.colors.BINARY,
|
|
620
|
+
Fractals.colors.FUTURISTIC,
|
|
621
|
+
Fractals.colors.HISTORIC,
|
|
622
|
+
Fractals.colors.RAINBOW,
|
|
623
|
+
Fractals.colors.TOPOGRAPHIC,
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
const currentIndex = schemes.indexOf(this.settings.colorScheme);
|
|
627
|
+
const nextIndex = (currentIndex + 1) % schemes.length;
|
|
628
|
+
this.settings.colorScheme = schemes[nextIndex];
|
|
629
|
+
this.controls.colorBtn.text = this.controls.isMobile
|
|
630
|
+
? "Color"
|
|
631
|
+
: `Color: ${this.settings.colorScheme}`;
|
|
632
|
+
|
|
633
|
+
// Update with new color scheme
|
|
634
|
+
this.updateFractalSettings();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
zoomIn() {
|
|
638
|
+
this.target.zoom = this.target.zoom * 1.5;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
zoomOut() {
|
|
642
|
+
this.target.zoom = this.target.zoom / 1.5;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
render() {
|
|
646
|
+
super.render();
|
|
647
|
+
//if(this.fractal.active) this.fractal.active = false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
//
|
|
651
|
+
class FractalRenderer extends ImageGo {
|
|
652
|
+
/**
|
|
653
|
+
* Create a new fractal renderer with double buffering
|
|
654
|
+
*
|
|
655
|
+
* @param {Game} game - Game instance
|
|
656
|
+
* @param {number} width - Canvas width
|
|
657
|
+
* @param {number} height - Canvas height
|
|
658
|
+
* @param {Object} options - Additional options
|
|
659
|
+
*/
|
|
660
|
+
constructor(game, width, height, options = {}) {
|
|
661
|
+
// Create a blank ImageData for initial display
|
|
662
|
+
const initialData = Painter.img.createImageData(width, height);
|
|
663
|
+
|
|
664
|
+
// Initialize with the blank data
|
|
665
|
+
super(game, initialData, options);
|
|
666
|
+
|
|
667
|
+
this.width = width;
|
|
668
|
+
this.height = height;
|
|
669
|
+
|
|
670
|
+
// Store the current fractal data
|
|
671
|
+
this.fractalData = null;
|
|
672
|
+
|
|
673
|
+
// Flag to indicate rendering is in progress
|
|
674
|
+
this.isRendering = false;
|
|
675
|
+
|
|
676
|
+
// Default settings
|
|
677
|
+
this.settings = {
|
|
678
|
+
type: Fractals.types.MANDELBROT,
|
|
679
|
+
iterations: 50,
|
|
680
|
+
colorScheme: "rainbow",
|
|
681
|
+
hueShift: 0,
|
|
682
|
+
zoom: 1,
|
|
683
|
+
offsetX: 0,
|
|
684
|
+
offsetY: 0,
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// Settings that were used for the current display
|
|
688
|
+
this.currentSettings = { ...this.settings };
|
|
689
|
+
|
|
690
|
+
// Create a second ImageData as a working buffer
|
|
691
|
+
this.workingBuffer = Painter.img.createImageData(width, height);
|
|
692
|
+
|
|
693
|
+
// Request initial render
|
|
694
|
+
this.pendingRender = true;
|
|
695
|
+
// Initialize task manager
|
|
696
|
+
this.taskManager = new TaskManager("./js/fractalworker.js");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Update renderer size
|
|
701
|
+
*
|
|
702
|
+
* @param {number} width - New width
|
|
703
|
+
* @param {number} height - New height
|
|
704
|
+
*/
|
|
705
|
+
resize(width, height) {
|
|
706
|
+
this.width = width;
|
|
707
|
+
this.height = height;
|
|
708
|
+
|
|
709
|
+
// Create new working buffer with new dimensions
|
|
710
|
+
this.workingBuffer = Painter.img.createImageData(width, height);
|
|
711
|
+
|
|
712
|
+
// Reset main buffer
|
|
713
|
+
this.reset();
|
|
714
|
+
|
|
715
|
+
// Request a re-render with new dimensions
|
|
716
|
+
this.pendingRender = true;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Update fractal settings and queue a re-render
|
|
721
|
+
*
|
|
722
|
+
* @param {Object} settings - New settings
|
|
723
|
+
*/
|
|
724
|
+
updateSettings(settings) {
|
|
725
|
+
const oldSettings = { ...this.settings };
|
|
726
|
+
this.settings = { ...this.settings, ...settings };
|
|
727
|
+
|
|
728
|
+
// Only request a render if settings actually changed
|
|
729
|
+
if (JSON.stringify(oldSettings) !== JSON.stringify(this.settings)) {
|
|
730
|
+
this.pendingRender = true;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Check if settings have changed from what's currently displayed
|
|
736
|
+
*/
|
|
737
|
+
settingsChanged() {
|
|
738
|
+
return (
|
|
739
|
+
JSON.stringify(this.settings) !== JSON.stringify(this.currentSettings)
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async renderFractal() {
|
|
744
|
+
this.isRendering = true;
|
|
745
|
+
const renderSettings = { ...this.settings };
|
|
746
|
+
try {
|
|
747
|
+
// Step 1: Generate raw fractal data
|
|
748
|
+
const fractalData = this.generateFractalData(renderSettings);
|
|
749
|
+
//console.log("fractalData", fractalData);
|
|
750
|
+
const result = await this.taskManager.runTask("generateFractal", {
|
|
751
|
+
width: this.width,
|
|
752
|
+
height: this.height,
|
|
753
|
+
type: renderSettings.type,
|
|
754
|
+
iterations: renderSettings.iterations,
|
|
755
|
+
zoom: renderSettings.zoom,
|
|
756
|
+
offsetX: renderSettings.offsetX,
|
|
757
|
+
offsetY: renderSettings.offsetY,
|
|
758
|
+
fractalFunction: fractalData.fractalFunction,
|
|
759
|
+
args: fractalData.args,
|
|
760
|
+
colorFunction: Fractals.applyColorScheme.toString(),
|
|
761
|
+
colorArgs: [
|
|
762
|
+
renderSettings.colorScheme,
|
|
763
|
+
renderSettings.iterations,
|
|
764
|
+
renderSettings.hueShift,
|
|
765
|
+
],
|
|
766
|
+
});
|
|
767
|
+
//console.log("worker result", result);
|
|
768
|
+
// Apply the result to our image
|
|
769
|
+
this.shape.bitmap = result.image;
|
|
770
|
+
// Update current settings
|
|
771
|
+
this.currentSettings = { ...renderSettings };
|
|
772
|
+
} catch (error) {
|
|
773
|
+
console.error("Error rendering fractal:", error);
|
|
774
|
+
} finally {
|
|
775
|
+
this.isRendering = false;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
update(dt) {
|
|
780
|
+
if (this.pendingRender && !this.isRendering && this.active) {
|
|
781
|
+
this.pendingRender = false;
|
|
782
|
+
this.renderFractal();
|
|
783
|
+
}
|
|
784
|
+
super.update(dt);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
generateFractalData(settings) {
|
|
788
|
+
const { type, iterations, zoom, offsetX, offsetY } = settings;
|
|
789
|
+
|
|
790
|
+
switch (type) {
|
|
791
|
+
case "pythagorasTree":
|
|
792
|
+
return {
|
|
793
|
+
fractalFunction: Fractals.pythagorasTree.toString(),
|
|
794
|
+
args: [
|
|
795
|
+
iterations,
|
|
796
|
+
-1.5 / zoom + offsetX,
|
|
797
|
+
1 / zoom + offsetX,
|
|
798
|
+
-1.5 / zoom + offsetY,
|
|
799
|
+
1.5 / zoom + offsetY,
|
|
800
|
+
],
|
|
801
|
+
};
|
|
802
|
+
case "mandelbrot":
|
|
803
|
+
return {
|
|
804
|
+
fractalFunction: Fractals.mandelbrot.toString(),
|
|
805
|
+
args: [
|
|
806
|
+
iterations,
|
|
807
|
+
-2.5 / zoom + offsetX,
|
|
808
|
+
1 / zoom + offsetX,
|
|
809
|
+
-1.5 / zoom + offsetY,
|
|
810
|
+
1.5 / zoom + offsetY,
|
|
811
|
+
],
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
case "julia":
|
|
815
|
+
return {
|
|
816
|
+
fractalFunction: Fractals.julia.toString(),
|
|
817
|
+
args: [
|
|
818
|
+
iterations,
|
|
819
|
+
-0.7, // cReal
|
|
820
|
+
0.27, // cImag
|
|
821
|
+
zoom,
|
|
822
|
+
offsetX,
|
|
823
|
+
offsetY,
|
|
824
|
+
],
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
case "newton":
|
|
828
|
+
return {
|
|
829
|
+
fractalFunction: Fractals.newton.toString(),
|
|
830
|
+
args: [
|
|
831
|
+
iterations,
|
|
832
|
+
0.000001,
|
|
833
|
+
(-1 / zoom) * 2 + offsetX,
|
|
834
|
+
(1 / zoom) * 2 + offsetX,
|
|
835
|
+
(-1 / zoom) * 2 + offsetY,
|
|
836
|
+
(1 / zoom) * 2 + offsetY,
|
|
837
|
+
],
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
case "sierpinski":
|
|
841
|
+
return {
|
|
842
|
+
fractalFunction: Fractals.sierpinski.toString(),
|
|
843
|
+
args: [
|
|
844
|
+
Math.min(50, iterations),
|
|
845
|
+
(-16 / zoom) * 2 + offsetX * 10,
|
|
846
|
+
(16 / zoom) * 2 + offsetX * 10,
|
|
847
|
+
(-16 / zoom) * 2 + offsetY * 10,
|
|
848
|
+
(16 / zoom) * 2 + offsetY * 10,
|
|
849
|
+
],
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
case "sierpinskiCarpet":
|
|
853
|
+
const f = 3 / zoom;
|
|
854
|
+
return {
|
|
855
|
+
fractalFunction: Fractals.sierpinskiCarpet.toString(),
|
|
856
|
+
args: [
|
|
857
|
+
Math.min(50, iterations),
|
|
858
|
+
-f * 2 + offsetX + 2,
|
|
859
|
+
f * 2 + offsetX + 2,
|
|
860
|
+
-f * 2 + offsetY + 2,
|
|
861
|
+
f * 2 + offsetY + 2,
|
|
862
|
+
],
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
case "barnsleyFern":
|
|
866
|
+
return {
|
|
867
|
+
fractalFunction: Fractals.barnsleyFern.toString(),
|
|
868
|
+
args: [iterations * 1000],
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
case "lyapunov":
|
|
872
|
+
return {
|
|
873
|
+
fractalFunction: Fractals.lyapunov.toString(),
|
|
874
|
+
args: [
|
|
875
|
+
iterations,
|
|
876
|
+
"ABAB",
|
|
877
|
+
(-1 / zoom) * 2 + offsetX,
|
|
878
|
+
(1 / zoom) * 2 + offsetX,
|
|
879
|
+
(-1 / zoom) * 2 + offsetY,
|
|
880
|
+
(1 / zoom) * 2 + offsetY,
|
|
881
|
+
1,
|
|
882
|
+
iterations,
|
|
883
|
+
],
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
case "tricorn":
|
|
887
|
+
return {
|
|
888
|
+
fractalFunction: Fractals.tricorn.toString(),
|
|
889
|
+
args: [
|
|
890
|
+
iterations,
|
|
891
|
+
-2.5 / zoom + offsetX,
|
|
892
|
+
1.5 / zoom + offsetX,
|
|
893
|
+
-1.5 / zoom + offsetY,
|
|
894
|
+
1.5 / zoom + offsetY,
|
|
895
|
+
],
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
case "phoenix":
|
|
899
|
+
return {
|
|
900
|
+
fractalFunction: Fractals.phoenix.toString(),
|
|
901
|
+
args: [
|
|
902
|
+
iterations,
|
|
903
|
+
0.8,
|
|
904
|
+
0.3,
|
|
905
|
+
-2 / zoom + offsetX - 1,
|
|
906
|
+
2 / zoom + offsetX,
|
|
907
|
+
-2 / zoom + offsetY,
|
|
908
|
+
2 / zoom + offsetY,
|
|
909
|
+
],
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
case "koch":
|
|
913
|
+
return {
|
|
914
|
+
fractalFunction: Fractals.koch.toString(),
|
|
915
|
+
args: [
|
|
916
|
+
iterations,
|
|
917
|
+
(-1 / zoom) * 2 + offsetX,
|
|
918
|
+
(1 / zoom) * 2 + offsetX,
|
|
919
|
+
(-1 / zoom) * 2 + offsetY + 1,
|
|
920
|
+
(1 / zoom) * 2 + offsetY + 1,
|
|
921
|
+
],
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
default:
|
|
925
|
+
return {
|
|
926
|
+
fractalFunction: Fractals.mandelbrot.toString(),
|
|
927
|
+
args: [iterations],
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|