@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,1271 @@
|
|
|
1
|
+
import { Complex } from "./complex";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fractals class
|
|
5
|
+
*
|
|
6
|
+
* Pure mathematical functions for generating fractal data structures.
|
|
7
|
+
* These functions perform the calculations without any rendering concerns.
|
|
8
|
+
*/
|
|
9
|
+
export class Fractals {
|
|
10
|
+
/**
|
|
11
|
+
* Types of fractals
|
|
12
|
+
*/
|
|
13
|
+
static types = {
|
|
14
|
+
MANDELBROT: "mandelbrot",
|
|
15
|
+
TRICORN: "tricorn",
|
|
16
|
+
PHOENIX: "phoenix",
|
|
17
|
+
JULIA: "julia",
|
|
18
|
+
SIERPINSKI: "sierpinski",
|
|
19
|
+
SCARPET: "sierpinskiCarpet",
|
|
20
|
+
BARNSEY_FERN: "barnsleyFern",
|
|
21
|
+
KOCH: "koch",
|
|
22
|
+
PYTHAGORAS_TREE: "pythagorasTree",
|
|
23
|
+
NEWTON: "newton",
|
|
24
|
+
LYAPUNOV: "lyapunov",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
static colors = {
|
|
28
|
+
FUTURISTIC: "futuristic",
|
|
29
|
+
RAINBOW: "rainbow",
|
|
30
|
+
GRAYSCALE: "grayscale",
|
|
31
|
+
TOPOGRAPHIC: "topographic",
|
|
32
|
+
FIRE: "fire",
|
|
33
|
+
OCEAN: "ocean",
|
|
34
|
+
ELECTRIC: "electric",
|
|
35
|
+
BINARY: "binary",
|
|
36
|
+
HISTORIC: "historic",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Apply a color scheme to raw fractal data
|
|
41
|
+
*
|
|
42
|
+
* @param {Uint8Array} fractalData - Raw fractal data
|
|
43
|
+
* @param {ImageData} imageData - Image data to apply color scheme to
|
|
44
|
+
* @param {string} colorScheme - Color scheme to apply
|
|
45
|
+
* @param {number} iterations - Number of iterations
|
|
46
|
+
* @param {number} hueShift - Hue shift
|
|
47
|
+
* @param {function} hslToRgb - Function to convert HSL to RGB
|
|
48
|
+
* @returns {Uint8Array} Array containing color values for each pixel
|
|
49
|
+
*/
|
|
50
|
+
static applyColorScheme(
|
|
51
|
+
fractalData,
|
|
52
|
+
imageData,
|
|
53
|
+
colorScheme,
|
|
54
|
+
iterations,
|
|
55
|
+
hueShift,
|
|
56
|
+
hslToRgb
|
|
57
|
+
) {
|
|
58
|
+
const data = imageData?.data || [];
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < fractalData.length; i++) {
|
|
61
|
+
const pixelValue = fractalData[i];
|
|
62
|
+
const pixelIndex = i * 4; // RGBA = 4 bytes per pixel
|
|
63
|
+
|
|
64
|
+
// Different coloring based on the scheme
|
|
65
|
+
switch (colorScheme) {
|
|
66
|
+
case "futuristic":
|
|
67
|
+
{
|
|
68
|
+
const normalizedValue = fractalData[i] / 10; // 0-1 range
|
|
69
|
+
|
|
70
|
+
// Deep, almost black base
|
|
71
|
+
const darkBase = {
|
|
72
|
+
r: 0, // Minimal red
|
|
73
|
+
g: 5, // Extremely low green
|
|
74
|
+
b: 10, // Very dark blue-green
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Subtle, deep green highlight
|
|
78
|
+
const deepGreenHighlight = {
|
|
79
|
+
r: 0, // No red
|
|
80
|
+
g: 30, // Low green
|
|
81
|
+
b: 20, // Slight blue-green tint
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// More selective highlighting with softer transition
|
|
85
|
+
if (normalizedValue > 0.7) {
|
|
86
|
+
// Higher threshold for highlights
|
|
87
|
+
const t = (normalizedValue - 0.7) * 3.33; // Steeper, more selective falloff
|
|
88
|
+
data[pixelIndex] = Math.floor(
|
|
89
|
+
darkBase.r * (1 - t) + deepGreenHighlight.r * t
|
|
90
|
+
);
|
|
91
|
+
data[pixelIndex + 1] = Math.floor(
|
|
92
|
+
darkBase.g * (1 - t) + deepGreenHighlight.g * t
|
|
93
|
+
);
|
|
94
|
+
data[pixelIndex + 2] = Math.floor(
|
|
95
|
+
darkBase.b * (1 - t) + deepGreenHighlight.b * t
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
// Maintain very dark base
|
|
99
|
+
else {
|
|
100
|
+
const t = normalizedValue * 1.43; // Adjusted to keep base extremely dark
|
|
101
|
+
data[pixelIndex] = Math.floor(darkBase.r * t);
|
|
102
|
+
data[pixelIndex + 1] = Math.floor(darkBase.g * t);
|
|
103
|
+
data[pixelIndex + 2] = Math.floor(darkBase.b * t);
|
|
104
|
+
}
|
|
105
|
+
data[pixelIndex + 3] = 255;
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
case "rainbow":
|
|
109
|
+
{
|
|
110
|
+
if (pixelValue === 0) {
|
|
111
|
+
// Points inside the set (black)
|
|
112
|
+
data[pixelIndex] = 0;
|
|
113
|
+
data[pixelIndex + 1] = 0;
|
|
114
|
+
data[pixelIndex + 2] = 0;
|
|
115
|
+
data[pixelIndex + 3] = 255;
|
|
116
|
+
} else {
|
|
117
|
+
// Map iteration count to hue
|
|
118
|
+
const hue = (pixelValue * 10 + hueShift) % 360;
|
|
119
|
+
const [r, g, b] = hslToRgb(hue, 0.8, 0.5);
|
|
120
|
+
data[pixelIndex] = r;
|
|
121
|
+
data[pixelIndex + 1] = g;
|
|
122
|
+
data[pixelIndex + 2] = b;
|
|
123
|
+
data[pixelIndex + 3] = 255;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
case "grayscale":
|
|
128
|
+
{
|
|
129
|
+
// Simple grayscale mapping
|
|
130
|
+
const gray =
|
|
131
|
+
pixelValue === 0 ? 0 : 255 - (pixelValue * 255) / iterations;
|
|
132
|
+
data[pixelIndex] = gray;
|
|
133
|
+
data[pixelIndex + 1] = gray;
|
|
134
|
+
data[pixelIndex + 2] = gray;
|
|
135
|
+
data[pixelIndex + 3] = 255;
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case "binary":
|
|
139
|
+
{
|
|
140
|
+
// For Sierpinski fractals - binary coloring
|
|
141
|
+
if (pixelValue !== 0) {
|
|
142
|
+
data[pixelIndex] = 0;
|
|
143
|
+
data[pixelIndex + 1] = 0;
|
|
144
|
+
data[pixelIndex + 2] = 0;
|
|
145
|
+
} else {
|
|
146
|
+
data[pixelIndex] = 255;
|
|
147
|
+
data[pixelIndex + 1] = 255;
|
|
148
|
+
data[pixelIndex + 2] = 255;
|
|
149
|
+
}
|
|
150
|
+
data[pixelIndex + 3] = 255;
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
/**
|
|
154
|
+
* Fire Palette (Heatmap Style)
|
|
155
|
+
*/
|
|
156
|
+
case "fire":
|
|
157
|
+
{
|
|
158
|
+
if (pixelValue == 0) {
|
|
159
|
+
// Points inside the set (black)
|
|
160
|
+
data[pixelIndex] = 0;
|
|
161
|
+
data[pixelIndex + 1] = 0;
|
|
162
|
+
data[pixelIndex + 2] = 0;
|
|
163
|
+
} else {
|
|
164
|
+
// Fire-like gradient: black -> red -> orange -> yellow -> white
|
|
165
|
+
const t = pixelValue / iterations;
|
|
166
|
+
if (t < 0.3) {
|
|
167
|
+
const v = t / 0.3;
|
|
168
|
+
data[pixelIndex] = Math.floor(255 * v);
|
|
169
|
+
data[pixelIndex + 1] = 0;
|
|
170
|
+
data[pixelIndex + 2] = 0;
|
|
171
|
+
} else if (t < 0.6) {
|
|
172
|
+
const v = (t - 0.3) / 0.3;
|
|
173
|
+
data[pixelIndex] = 255;
|
|
174
|
+
data[pixelIndex + 1] = Math.floor(165 * v);
|
|
175
|
+
data[pixelIndex + 2] = 0;
|
|
176
|
+
} else if (t < 0.9) {
|
|
177
|
+
const v = (t - 0.6) / 0.3;
|
|
178
|
+
data[pixelIndex] = 255;
|
|
179
|
+
data[pixelIndex + 1] = 165 + Math.floor(90 * v);
|
|
180
|
+
data[pixelIndex + 2] = Math.floor(255 * v);
|
|
181
|
+
} else {
|
|
182
|
+
const v = (t - 0.9) / 0.1;
|
|
183
|
+
data[pixelIndex] = 255;
|
|
184
|
+
data[pixelIndex + 1] = 255;
|
|
185
|
+
data[pixelIndex + 2] = 255;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
data[pixelIndex + 3] = 255;
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
/**
|
|
192
|
+
* Ocean Palette (Cool Blues/Greens)
|
|
193
|
+
*/
|
|
194
|
+
case "ocean":
|
|
195
|
+
{
|
|
196
|
+
if (pixelValue === 0) {
|
|
197
|
+
// Deep ocean color for set interior
|
|
198
|
+
data[pixelIndex] = 0;
|
|
199
|
+
data[pixelIndex + 1] = 20;
|
|
200
|
+
data[pixelIndex + 2] = 50;
|
|
201
|
+
} else {
|
|
202
|
+
// Ocean gradient: dark blue -> cyan -> white
|
|
203
|
+
const t = pixelValue / iterations;
|
|
204
|
+
data[pixelIndex] = Math.floor(10 + 50 * t);
|
|
205
|
+
data[pixelIndex + 1] = Math.floor(50 + 150 * t);
|
|
206
|
+
data[pixelIndex + 2] = Math.floor(100 + 155 * t);
|
|
207
|
+
}
|
|
208
|
+
data[pixelIndex + 3] = 255;
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
/**
|
|
212
|
+
* Electric Palette (80's inspired neon colors)
|
|
213
|
+
*/
|
|
214
|
+
case "electric":
|
|
215
|
+
{
|
|
216
|
+
if (pixelValue === 0) {
|
|
217
|
+
// Black background
|
|
218
|
+
data[pixelIndex] = 0;
|
|
219
|
+
data[pixelIndex + 1] = 0;
|
|
220
|
+
data[pixelIndex + 2] = 0;
|
|
221
|
+
} else {
|
|
222
|
+
// Vibrant neon colors
|
|
223
|
+
const phase = (pixelValue + hueShift) % 3;
|
|
224
|
+
const t = (pixelValue % 20) / 20;
|
|
225
|
+
|
|
226
|
+
if (phase === 0) {
|
|
227
|
+
data[pixelIndex] = Math.floor(
|
|
228
|
+
255 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2))
|
|
229
|
+
);
|
|
230
|
+
data[pixelIndex + 1] = Math.floor(128 * t);
|
|
231
|
+
data[pixelIndex + 2] = Math.floor(255 * t);
|
|
232
|
+
} else if (phase === 1) {
|
|
233
|
+
data[pixelIndex] = Math.floor(255 * t);
|
|
234
|
+
data[pixelIndex + 1] = Math.floor(
|
|
235
|
+
255 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2))
|
|
236
|
+
);
|
|
237
|
+
data[pixelIndex + 2] = Math.floor(128 * t);
|
|
238
|
+
} else {
|
|
239
|
+
data[pixelIndex] = Math.floor(128 * t);
|
|
240
|
+
data[pixelIndex + 1] = Math.floor(255 * t);
|
|
241
|
+
data[pixelIndex + 2] = Math.floor(
|
|
242
|
+
255 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2))
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
data[pixelIndex + 3] = 255;
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case "topographic":
|
|
250
|
+
{
|
|
251
|
+
if (pixelValue === 0) {
|
|
252
|
+
// Deep ocean
|
|
253
|
+
data[pixelIndex] = 5;
|
|
254
|
+
data[pixelIndex + 1] = 15;
|
|
255
|
+
data[pixelIndex + 2] = 30;
|
|
256
|
+
} else {
|
|
257
|
+
// Elevation-based colors
|
|
258
|
+
const elevation = pixelValue / iterations;
|
|
259
|
+
|
|
260
|
+
if (elevation < 0.1) {
|
|
261
|
+
// Water
|
|
262
|
+
const depth = elevation / 0.1;
|
|
263
|
+
data[pixelIndex] = Math.floor(5 + 20 * depth);
|
|
264
|
+
data[pixelIndex + 1] = Math.floor(15 + 40 * depth);
|
|
265
|
+
data[pixelIndex + 2] = Math.floor(30 + 50 * depth);
|
|
266
|
+
} else if (elevation < 0.3) {
|
|
267
|
+
// Sand/beach
|
|
268
|
+
const t = (elevation - 0.1) / 0.2;
|
|
269
|
+
data[pixelIndex] = Math.floor(210 + 45 * t);
|
|
270
|
+
data[pixelIndex + 1] = Math.floor(180 + 40 * t);
|
|
271
|
+
data[pixelIndex + 2] = Math.floor(140 + 30 * t);
|
|
272
|
+
} else if (elevation < 0.7) {
|
|
273
|
+
// Vegetation
|
|
274
|
+
const t = (elevation - 0.3) / 0.4;
|
|
275
|
+
data[pixelIndex] = Math.floor(50 * (1 - t));
|
|
276
|
+
data[pixelIndex + 1] = Math.floor(100 + 80 * t);
|
|
277
|
+
data[pixelIndex + 2] = Math.floor(50 * (1 - t));
|
|
278
|
+
} else {
|
|
279
|
+
// Mountain/snow
|
|
280
|
+
const t = (elevation - 0.7) / 0.3;
|
|
281
|
+
data[pixelIndex] = Math.floor(150 + 105 * t);
|
|
282
|
+
data[pixelIndex + 1] = Math.floor(150 + 105 * t);
|
|
283
|
+
data[pixelIndex + 2] = Math.floor(150 + 105 * t);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
data[pixelIndex + 3] = 255;
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
/**
|
|
290
|
+
* Historical Palette (Classic Fractint Colors)
|
|
291
|
+
*/
|
|
292
|
+
case "historic":
|
|
293
|
+
default: {
|
|
294
|
+
if (pixelValue === 0) {
|
|
295
|
+
data[pixelIndex] = 0;
|
|
296
|
+
data[pixelIndex + 1] = 0;
|
|
297
|
+
data[pixelIndex + 2] = 0;
|
|
298
|
+
} else {
|
|
299
|
+
// Classic Fractint color cycling
|
|
300
|
+
const cycle = 64; // Classic cycle length
|
|
301
|
+
const pos = (pixelValue + hueShift) % cycle;
|
|
302
|
+
|
|
303
|
+
if (pos < 16) {
|
|
304
|
+
data[pixelIndex] = pos * 16;
|
|
305
|
+
data[pixelIndex + 1] = 0;
|
|
306
|
+
data[pixelIndex + 2] = 0;
|
|
307
|
+
} else if (pos < 32) {
|
|
308
|
+
data[pixelIndex] = 255;
|
|
309
|
+
data[pixelIndex + 1] = (pos - 16) * 16;
|
|
310
|
+
data[pixelIndex + 2] = 0;
|
|
311
|
+
} else if (pos < 48) {
|
|
312
|
+
data[pixelIndex] = 255 - (pos - 32) * 16;
|
|
313
|
+
data[pixelIndex + 1] = 255;
|
|
314
|
+
data[pixelIndex + 2] = 0;
|
|
315
|
+
} else {
|
|
316
|
+
data[pixelIndex] = 0;
|
|
317
|
+
data[pixelIndex + 1] = 255 - (pos - 48) * 16;
|
|
318
|
+
data[pixelIndex + 2] = (pos - 48) * 16;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
data[pixelIndex + 3] = 255;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return imageData != null ? imageData : data;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Generates a Pythagoras tree fractal
|
|
331
|
+
*
|
|
332
|
+
* @param {number} width - Image width
|
|
333
|
+
* @param {number} height - Image height
|
|
334
|
+
* @param {number} maxIterations - Maximum iterations for tree depth
|
|
335
|
+
* @param {number} xMin - Minimum X coordinate in viewing plane
|
|
336
|
+
* @param {number} xMax - Maximum X coordinate in viewing plane
|
|
337
|
+
* @param {number} yMin - Minimum Y coordinate in viewing plane
|
|
338
|
+
* @param {number} yMax - Maximum Y coordinate in viewing plane
|
|
339
|
+
* @returns {Uint8Array} Array containing pixel values for the Pythagoras tree
|
|
340
|
+
*/
|
|
341
|
+
static pythagorasTree(
|
|
342
|
+
width,
|
|
343
|
+
height,
|
|
344
|
+
maxIterations = 10,
|
|
345
|
+
xMin = -2,
|
|
346
|
+
xMax = 2,
|
|
347
|
+
yMin = -0.5,
|
|
348
|
+
yMax = 3.5
|
|
349
|
+
) {
|
|
350
|
+
// Create data array
|
|
351
|
+
const data = new Uint8Array(width * height);
|
|
352
|
+
|
|
353
|
+
// World to screen coordinate conversion
|
|
354
|
+
const mapX = (x) => Math.floor(((x - xMin) * width) / (xMax - xMin));
|
|
355
|
+
const mapY = (y) => Math.floor(((y - yMin) * height) / (yMax - yMin));
|
|
356
|
+
|
|
357
|
+
// Basic line drawing
|
|
358
|
+
const drawLine = (x0, y0, x1, y1) => {
|
|
359
|
+
const sx = mapX(x0);
|
|
360
|
+
const sy = mapY(y0);
|
|
361
|
+
const ex = mapX(x1);
|
|
362
|
+
const ey = mapY(y1);
|
|
363
|
+
|
|
364
|
+
// Bresenham's algorithm
|
|
365
|
+
let x = sx,
|
|
366
|
+
y = sy;
|
|
367
|
+
const dx = Math.abs(ex - sx),
|
|
368
|
+
dy = Math.abs(ey - sy);
|
|
369
|
+
const sx1 = sx < ex ? 1 : -1,
|
|
370
|
+
sy1 = sy < ey ? 1 : -1;
|
|
371
|
+
let err = dx - dy;
|
|
372
|
+
|
|
373
|
+
while (true) {
|
|
374
|
+
if (x >= 0 && x < width && y >= 0 && y < height) {
|
|
375
|
+
data[y * width + x] = 255;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (x === ex && y === ey) break;
|
|
379
|
+
const e2 = 2 * err;
|
|
380
|
+
if (e2 > -dy) {
|
|
381
|
+
err -= dy;
|
|
382
|
+
x += sx1;
|
|
383
|
+
}
|
|
384
|
+
if (e2 < dx) {
|
|
385
|
+
err += dx;
|
|
386
|
+
y += sy1;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// Fill a square (used for the tree blocks)
|
|
392
|
+
const drawSquare = (x1, y1, x2, y2, x3, y3, x4, y4) => {
|
|
393
|
+
drawLine(x1, y1, x2, y2);
|
|
394
|
+
drawLine(x2, y2, x3, y3);
|
|
395
|
+
drawLine(x3, y3, x4, y4);
|
|
396
|
+
drawLine(x4, y4, x1, y1);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Recursively draw the Pythagoras tree
|
|
400
|
+
const drawTree = (x1, y1, x2, y2, depth) => {
|
|
401
|
+
if (depth <= 0) return;
|
|
402
|
+
|
|
403
|
+
// Calculate the square coordinates
|
|
404
|
+
const dx = x2 - x1;
|
|
405
|
+
const dy = y2 - y1;
|
|
406
|
+
|
|
407
|
+
// Find the perpendicular direction to form a square
|
|
408
|
+
// Important change: we're now creating a square that goes UP
|
|
409
|
+
// from the base line, not down
|
|
410
|
+
const x3 = x2 + dy;
|
|
411
|
+
const y3 = y2 - dx;
|
|
412
|
+
const x4 = x1 + dy;
|
|
413
|
+
const y4 = y1 - dx;
|
|
414
|
+
|
|
415
|
+
// Draw the square
|
|
416
|
+
drawSquare(x1, y1, x2, y2, x3, y3, x4, y4);
|
|
417
|
+
|
|
418
|
+
// Angle for the branches - can be adjusted for different tree shapes
|
|
419
|
+
const angle = Math.PI / 4; // 45 degrees
|
|
420
|
+
|
|
421
|
+
// Calculate the position for the right branch (rotated by angle)
|
|
422
|
+
const rightLen = Math.sqrt(dx * dx + dy * dy) * 0.7; // Smaller scale for branches
|
|
423
|
+
const rightDx = rightLen * Math.cos(Math.atan2(dy, dx) - angle);
|
|
424
|
+
const rightDy = rightLen * Math.sin(Math.atan2(dy, dx) - angle);
|
|
425
|
+
|
|
426
|
+
// Calculate the position for the left branch (rotated by -angle)
|
|
427
|
+
const leftLen = Math.sqrt(dx * dx + dy * dy) * 0.7;
|
|
428
|
+
const leftDx = leftLen * Math.cos(Math.atan2(dy, dx) + angle);
|
|
429
|
+
const leftDy = leftLen * Math.sin(Math.atan2(dy, dx) + angle);
|
|
430
|
+
|
|
431
|
+
// Start positions for branches are the top of the current square
|
|
432
|
+
const startRightX = x3;
|
|
433
|
+
const startRightY = y3;
|
|
434
|
+
const startLeftX = x4;
|
|
435
|
+
const startLeftY = y4;
|
|
436
|
+
|
|
437
|
+
// End positions for branches
|
|
438
|
+
const endRightX = startRightX + rightDx;
|
|
439
|
+
const endRightY = startRightY + rightDy;
|
|
440
|
+
const endLeftX = startLeftX + leftDx;
|
|
441
|
+
const endLeftY = startLeftY + leftDy;
|
|
442
|
+
|
|
443
|
+
// Recursively draw the right and left branches
|
|
444
|
+
drawTree(startRightX, startRightY, endRightX, endRightY, depth - 1);
|
|
445
|
+
drawTree(startLeftX, startLeftY, endLeftX, endLeftY, depth - 1);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Ensure iterations is in a reasonable range
|
|
449
|
+
const iterations = Math.min(maxIterations, 12);
|
|
450
|
+
|
|
451
|
+
// Draw the initial segment (trunk) of the tree
|
|
452
|
+
const trunkWidth = 1.0;
|
|
453
|
+
const trunkHeight = 0.5;
|
|
454
|
+
|
|
455
|
+
// Position the trunk in the center bottom of the view
|
|
456
|
+
const startX = -trunkWidth / 2;
|
|
457
|
+
const startY = 0;
|
|
458
|
+
const endX = trunkWidth / 2;
|
|
459
|
+
const endY = 0;
|
|
460
|
+
|
|
461
|
+
// Start the tree drawing
|
|
462
|
+
drawTree(startX, startY, endX, endY, iterations);
|
|
463
|
+
|
|
464
|
+
return data;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Generates a Mandelbrot set with optimized performance
|
|
469
|
+
*
|
|
470
|
+
* @param {number} width - Image width
|
|
471
|
+
* @param {number} height - Image height
|
|
472
|
+
* @param {number} maxIterations - Maximum iterations for calculations
|
|
473
|
+
* @param {number} xMin - Minimum X coordinate in complex plane
|
|
474
|
+
* @param {number} xMax - Maximum X coordinate in complex plane
|
|
475
|
+
* @param {number} yMin - Minimum Y coordinate in complex plane
|
|
476
|
+
* @param {number} yMax - Maximum Y coordinate in complex plane
|
|
477
|
+
* @returns {Uint8Array} Array containing iteration values for each pixel
|
|
478
|
+
*/
|
|
479
|
+
static mandelbrot(
|
|
480
|
+
width,
|
|
481
|
+
height,
|
|
482
|
+
maxIterations = 100,
|
|
483
|
+
xMin = -2.5,
|
|
484
|
+
xMax = 1,
|
|
485
|
+
yMin = -1.5,
|
|
486
|
+
yMax = 1.5
|
|
487
|
+
) {
|
|
488
|
+
// Create data array with preallocated size
|
|
489
|
+
const data = new Uint8Array(width * height);
|
|
490
|
+
// Precompute scaling factors for mapping pixels to complex plane
|
|
491
|
+
const xScale = (xMax - xMin) / width;
|
|
492
|
+
const yScale = (yMax - yMin) / height;
|
|
493
|
+
// Process by rows for better memory locality
|
|
494
|
+
for (let y = 0; y < height; y++) {
|
|
495
|
+
// Calculate row base index and y-coordinate once per row
|
|
496
|
+
const rowOffset = y * width;
|
|
497
|
+
const cImag = yMin + y * yScale;
|
|
498
|
+
|
|
499
|
+
// Process each pixel in the row
|
|
500
|
+
for (let x = 0; x < width; x++) {
|
|
501
|
+
// Map to complex plane
|
|
502
|
+
const cReal = xMin + x * xScale;
|
|
503
|
+
|
|
504
|
+
// Initialize values for this pixel calculation
|
|
505
|
+
let zReal = 0;
|
|
506
|
+
let zImag = 0;
|
|
507
|
+
|
|
508
|
+
// Keep squared values to avoid redundant calculations
|
|
509
|
+
let zRealSq = 0;
|
|
510
|
+
let zImagSq = 0;
|
|
511
|
+
|
|
512
|
+
// Start iteration counter
|
|
513
|
+
let i = 0;
|
|
514
|
+
|
|
515
|
+
// Main iteration loop - optimized but equivalent to original
|
|
516
|
+
do {
|
|
517
|
+
// Calculate next z value using the current squared values
|
|
518
|
+
const zRealTemp = zRealSq - zImagSq + cReal;
|
|
519
|
+
zImag = 2 * zReal * zImag + cImag;
|
|
520
|
+
zReal = zRealTemp;
|
|
521
|
+
// Update squared values for next iteration
|
|
522
|
+
zRealSq = zReal * zReal;
|
|
523
|
+
zImagSq = zImag * zImag;
|
|
524
|
+
i++;
|
|
525
|
+
// Check escape condition
|
|
526
|
+
} while (zRealSq + zImagSq < 4 && i < maxIterations);
|
|
527
|
+
// Store the iteration value (0-255 range)
|
|
528
|
+
data[rowOffset + x] = i < maxIterations ? i % 256 : 0;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return data;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Generates a Julia set
|
|
537
|
+
*
|
|
538
|
+
* @param {number} width - Image width
|
|
539
|
+
* @param {number} height - Image height
|
|
540
|
+
* @param {number} maxIterations - Maximum iterations for calculations
|
|
541
|
+
* @param {number} cReal - Real component of C parameter
|
|
542
|
+
* @param {number} cImag - Imaginary component of C parameter
|
|
543
|
+
* @param {number} zoom - Zoom level
|
|
544
|
+
* @param {number} offsetX - X offset for panning
|
|
545
|
+
* @param {number} offsetY - Y offset for panning
|
|
546
|
+
* @returns {Uint8Array} Array containing iteration values for each pixel
|
|
547
|
+
*/
|
|
548
|
+
static julia(
|
|
549
|
+
width,
|
|
550
|
+
height,
|
|
551
|
+
maxIterations = 100,
|
|
552
|
+
cReal = -0.7,
|
|
553
|
+
cImag = 0.27,
|
|
554
|
+
zoom = 1,
|
|
555
|
+
offsetX = 0,
|
|
556
|
+
offsetY = 0
|
|
557
|
+
) {
|
|
558
|
+
const data = new Uint8Array(width * height);
|
|
559
|
+
const scale = 2 / zoom;
|
|
560
|
+
const xMin = -scale + offsetX;
|
|
561
|
+
const xMax = scale + offsetX;
|
|
562
|
+
const yMin = -scale + offsetY;
|
|
563
|
+
const yMax = scale + offsetY;
|
|
564
|
+
|
|
565
|
+
const xScale = (xMax - xMin) / width;
|
|
566
|
+
const yScale = (yMax - yMin) / height;
|
|
567
|
+
|
|
568
|
+
for (let y = 0; y < height; y++) {
|
|
569
|
+
const rowOffset = y * width;
|
|
570
|
+
const zImagInit = yMin + y * yScale;
|
|
571
|
+
|
|
572
|
+
for (let x = 0; x < width; x++) {
|
|
573
|
+
const zRealInit = xMin + x * xScale;
|
|
574
|
+
|
|
575
|
+
let zReal = zRealInit;
|
|
576
|
+
let zImag = zImagInit;
|
|
577
|
+
|
|
578
|
+
let zRealSq = 0;
|
|
579
|
+
let zImagSq = 0;
|
|
580
|
+
|
|
581
|
+
let i = 0;
|
|
582
|
+
|
|
583
|
+
do {
|
|
584
|
+
zRealSq = zReal * zReal;
|
|
585
|
+
zImagSq = zImag * zImag;
|
|
586
|
+
|
|
587
|
+
const tempZReal = zRealSq - zImagSq + cReal;
|
|
588
|
+
zImag = 2 * zReal * zImag + cImag;
|
|
589
|
+
zReal = tempZReal;
|
|
590
|
+
|
|
591
|
+
i++;
|
|
592
|
+
} while (zRealSq + zImagSq < 4 && i < maxIterations);
|
|
593
|
+
|
|
594
|
+
data[rowOffset + x] = i < maxIterations ? i % 256 : 0;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return data;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Generates a Tricorn fractal (Mandelbar set) with zoom/pan support
|
|
603
|
+
*
|
|
604
|
+
* @param {number} width - Image width
|
|
605
|
+
* @param {number} height - Image height
|
|
606
|
+
* @param {number} maxIterations - Maximum iterations
|
|
607
|
+
* @param {number} xMin - Minimum X coordinate in complex plane
|
|
608
|
+
* @param {number} xMax - Maximum X coordinate in complex plane
|
|
609
|
+
* @param {number} yMin - Minimum Y coordinate in complex plane
|
|
610
|
+
* @param {number} yMax - Maximum Y coordinate in complex plane
|
|
611
|
+
* @returns {Uint8Array} Iteration counts
|
|
612
|
+
*/
|
|
613
|
+
static tricorn(
|
|
614
|
+
width,
|
|
615
|
+
height,
|
|
616
|
+
maxIterations = 100,
|
|
617
|
+
xMin = -2.5,
|
|
618
|
+
xMax = 1.5,
|
|
619
|
+
yMin = -1.5,
|
|
620
|
+
yMax = 1.5
|
|
621
|
+
) {
|
|
622
|
+
const data = new Uint8Array(width * height);
|
|
623
|
+
const xScale = (xMax - xMin) / width;
|
|
624
|
+
const yScale = (yMax - yMin) / height;
|
|
625
|
+
|
|
626
|
+
for (let y = 0; y < height; y++) {
|
|
627
|
+
const rowOffset = y * width;
|
|
628
|
+
const cImag = yMin + y * yScale;
|
|
629
|
+
|
|
630
|
+
for (let x = 0; x < width; x++) {
|
|
631
|
+
const cReal = xMin + x * xScale;
|
|
632
|
+
|
|
633
|
+
let zReal = 0;
|
|
634
|
+
let zImag = 0;
|
|
635
|
+
let zRealSq = 0;
|
|
636
|
+
let zImagSq = 0;
|
|
637
|
+
|
|
638
|
+
let i = 0;
|
|
639
|
+
|
|
640
|
+
do {
|
|
641
|
+
// Tricorn uses the complex conjugate, so -2 instead of +2
|
|
642
|
+
const tempZReal = zRealSq - zImagSq + cReal;
|
|
643
|
+
zImag = -2 * zReal * zImag + cImag;
|
|
644
|
+
zReal = tempZReal;
|
|
645
|
+
|
|
646
|
+
zRealSq = zReal * zReal;
|
|
647
|
+
zImagSq = zImag * zImag;
|
|
648
|
+
|
|
649
|
+
i++;
|
|
650
|
+
} while (zRealSq + zImagSq < 4 && i < maxIterations);
|
|
651
|
+
|
|
652
|
+
data[rowOffset + x] = i < maxIterations ? i % 256 : 0;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return data;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Generates a Phoenix fractal with zoom/pan support
|
|
661
|
+
*
|
|
662
|
+
* @param {number} width - Image width
|
|
663
|
+
* @param {number} height - Image height
|
|
664
|
+
* @param {number} maxIterations - Maximum iterations
|
|
665
|
+
* @param {number} p - Parameter p (controls feedback)
|
|
666
|
+
* @param {number} q - Parameter q (controls feedback)
|
|
667
|
+
* @param {number} xMin - Minimum X coordinate in complex plane
|
|
668
|
+
* @param {number} xMax - Maximum X coordinate in complex plane
|
|
669
|
+
* @param {number} yMin - Minimum Y coordinate in complex plane
|
|
670
|
+
* @param {number} yMax - Maximum Y coordinate in complex plane
|
|
671
|
+
* @returns {Uint8Array} Iteration counts
|
|
672
|
+
*/
|
|
673
|
+
static phoenix(
|
|
674
|
+
width,
|
|
675
|
+
height,
|
|
676
|
+
maxIterations = 100,
|
|
677
|
+
p = 0.5,
|
|
678
|
+
q = 0.5,
|
|
679
|
+
xMin = -2,
|
|
680
|
+
xMax = 2,
|
|
681
|
+
yMin = -2,
|
|
682
|
+
yMax = 2
|
|
683
|
+
) {
|
|
684
|
+
const data = new Uint8Array(width * height);
|
|
685
|
+
const xScale = (xMax - xMin) / width;
|
|
686
|
+
const yScale = (yMax - yMin) / height;
|
|
687
|
+
|
|
688
|
+
for (let y = 0; y < height; y++) {
|
|
689
|
+
const rowOffset = y * width;
|
|
690
|
+
const cImag = yMin + y * yScale;
|
|
691
|
+
|
|
692
|
+
for (let x = 0; x < width; x++) {
|
|
693
|
+
const cReal = xMin + x * xScale;
|
|
694
|
+
|
|
695
|
+
let zReal = 0;
|
|
696
|
+
let zImag = 0;
|
|
697
|
+
let prevZReal = 0;
|
|
698
|
+
let prevZImag = 0;
|
|
699
|
+
|
|
700
|
+
let zRealSq = 0;
|
|
701
|
+
let zImagSq = 0;
|
|
702
|
+
|
|
703
|
+
let i = 0;
|
|
704
|
+
|
|
705
|
+
do {
|
|
706
|
+
// Phoenix recurrence relation with feedback
|
|
707
|
+
const tempZReal = zRealSq - zImagSq + cReal + p * prevZReal + q;
|
|
708
|
+
const tempZImag = 2 * zReal * zImag + cImag + p * prevZImag;
|
|
709
|
+
|
|
710
|
+
// Update previous values
|
|
711
|
+
prevZReal = zReal;
|
|
712
|
+
prevZImag = zImag;
|
|
713
|
+
|
|
714
|
+
// Set new values
|
|
715
|
+
zReal = tempZReal;
|
|
716
|
+
zImag = tempZImag;
|
|
717
|
+
|
|
718
|
+
// Update cached squares
|
|
719
|
+
zRealSq = zReal * zReal;
|
|
720
|
+
zImagSq = zImag * zImag;
|
|
721
|
+
|
|
722
|
+
i++;
|
|
723
|
+
} while (zRealSq + zImagSq < 4 && i < maxIterations);
|
|
724
|
+
|
|
725
|
+
data[rowOffset + x] = i < maxIterations ? i % 256 : 0;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return data;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Generates a Newton fractal with optimized performance
|
|
734
|
+
*
|
|
735
|
+
* @param {number} width - Image width
|
|
736
|
+
* @param {number} height - Image height
|
|
737
|
+
* @param {number} maxIterations - Maximum iterations for calculations
|
|
738
|
+
* @param {number} tolerance - Convergence tolerance
|
|
739
|
+
* @param {number} xMin - Minimum x-coordinate in complex plane
|
|
740
|
+
* @param {number} xMax - Maximum x-coordinate in complex plane
|
|
741
|
+
* @param {number} yMin - Minimum y-coordinate in complex plane
|
|
742
|
+
* @param {number} yMax - Maximum y-coordinate in complex plane
|
|
743
|
+
* @returns {Uint8Array} Array containing iteration and root information
|
|
744
|
+
*/
|
|
745
|
+
static newton(
|
|
746
|
+
width,
|
|
747
|
+
height,
|
|
748
|
+
maxIterations = 100,
|
|
749
|
+
tolerance = 0.000001,
|
|
750
|
+
xMin = -2,
|
|
751
|
+
xMax = 2,
|
|
752
|
+
yMin = -2,
|
|
753
|
+
yMax = 2
|
|
754
|
+
) {
|
|
755
|
+
const data = new Uint8Array(width * height);
|
|
756
|
+
const toleranceSquared = tolerance * tolerance;
|
|
757
|
+
|
|
758
|
+
// Precompute constants
|
|
759
|
+
const xRange = xMax - xMin;
|
|
760
|
+
const yRange = yMax - yMin;
|
|
761
|
+
|
|
762
|
+
// Precompute roots for z^3 - 1
|
|
763
|
+
const rootCount = 3;
|
|
764
|
+
const rootsReal = new Float64Array(rootCount);
|
|
765
|
+
const rootsImag = new Float64Array(rootCount);
|
|
766
|
+
|
|
767
|
+
for (let k = 0; k < rootCount; k++) {
|
|
768
|
+
const angle = (2 * Math.PI * k) / rootCount;
|
|
769
|
+
rootsReal[k] = Math.cos(angle);
|
|
770
|
+
rootsImag[k] = Math.sin(angle);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Precompute width factors to avoid division in inner loop
|
|
774
|
+
const xFactor = xRange / width;
|
|
775
|
+
const yFactor = yRange / height;
|
|
776
|
+
|
|
777
|
+
// Process rows
|
|
778
|
+
for (let y = 0; y < height; y++) {
|
|
779
|
+
const baseY = y * width;
|
|
780
|
+
const imag = yMin + y * yFactor;
|
|
781
|
+
|
|
782
|
+
for (let x = 0; x < width; x++) {
|
|
783
|
+
const real = xMin + x * xFactor;
|
|
784
|
+
|
|
785
|
+
// Initial z value
|
|
786
|
+
let zReal = real;
|
|
787
|
+
let zImag = imag;
|
|
788
|
+
|
|
789
|
+
let iteration = 0;
|
|
790
|
+
let rootIndex = -1;
|
|
791
|
+
|
|
792
|
+
while (iteration < maxIterations && rootIndex < 0) {
|
|
793
|
+
// Calculate z^2 (reused in both f(z) and f'(z))
|
|
794
|
+
const z2Real = zReal * zReal - zImag * zImag;
|
|
795
|
+
const z2Imag = 2 * zReal * zImag;
|
|
796
|
+
|
|
797
|
+
// f(z) = z^3 - 1
|
|
798
|
+
const fzReal = z2Real * zReal - z2Imag * zImag - 1;
|
|
799
|
+
const fzImag = z2Real * zImag + z2Imag * zReal;
|
|
800
|
+
|
|
801
|
+
// f'(z) = 3z^2
|
|
802
|
+
const dfzReal = 3 * z2Real;
|
|
803
|
+
const dfzImag = 3 * z2Imag;
|
|
804
|
+
|
|
805
|
+
// Check if derivative is too small
|
|
806
|
+
const dfzMagSquared = dfzReal * dfzReal + dfzImag * dfzImag;
|
|
807
|
+
if (dfzMagSquared < toleranceSquared) break;
|
|
808
|
+
|
|
809
|
+
// Calculate f(z)/f'(z) directly
|
|
810
|
+
const denomInv = 1 / dfzMagSquared;
|
|
811
|
+
const divReal = (fzReal * dfzReal + fzImag * dfzImag) * denomInv;
|
|
812
|
+
const divImag = (fzImag * dfzReal - fzReal * dfzImag) * denomInv;
|
|
813
|
+
|
|
814
|
+
// z - f(z)/f'(z)
|
|
815
|
+
const zNextReal = zReal - divReal;
|
|
816
|
+
const zNextImag = zImag - divImag;
|
|
817
|
+
|
|
818
|
+
// Check convergence to roots
|
|
819
|
+
for (let i = 0; i < rootCount; i++) {
|
|
820
|
+
const diffReal = zNextReal - rootsReal[i];
|
|
821
|
+
const diffImag = zNextImag - rootsImag[i];
|
|
822
|
+
const distSquared = diffReal * diffReal + diffImag * diffImag;
|
|
823
|
+
|
|
824
|
+
if (distSquared < toleranceSquared) {
|
|
825
|
+
rootIndex = i;
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// Update z for next iteration
|
|
830
|
+
zReal = zNextReal;
|
|
831
|
+
zImag = zNextImag;
|
|
832
|
+
iteration++;
|
|
833
|
+
}
|
|
834
|
+
// color based on root index
|
|
835
|
+
// Combine root index and iteration count for more varied colors
|
|
836
|
+
if (rootIndex >= 0) {
|
|
837
|
+
// For converging points, use both root and iteration information
|
|
838
|
+
// This creates bands of colors based on iteration count
|
|
839
|
+
// while keeping different roots in different color ranges
|
|
840
|
+
const iterationFactor = 1 - Math.min(iteration / maxIterations, 1);
|
|
841
|
+
const rootOffset = rootIndex * (255 / rootCount);
|
|
842
|
+
// Mix the root index and iteration data
|
|
843
|
+
// This spreads the color values more evenly through the spectrum
|
|
844
|
+
data[baseY + x] = Math.floor(
|
|
845
|
+
rootOffset + iterationFactor * (255 / rootCount)
|
|
846
|
+
);
|
|
847
|
+
} else {
|
|
848
|
+
// Points that don't converge
|
|
849
|
+
data[baseY + x] = 0;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return data;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Binary mask (1 = filled, 0 = hole) for an equilateral Sierpiński triangle.
|
|
859
|
+
* Modified to control visible triangle count with iterations parameter.
|
|
860
|
+
*
|
|
861
|
+
* @param {number} width Canvas / bitmap width (px)
|
|
862
|
+
* @param {number} height Canvas / bitmap height (px)
|
|
863
|
+
* @param {number} iterations Controls number of visible triangles (1-15)
|
|
864
|
+
* @param {number} xMin World-space left (pan / zoom)
|
|
865
|
+
* @param {number} xMax World-space right
|
|
866
|
+
* @param {number} yMin World-space bottom
|
|
867
|
+
* @param {number} yMax World-space top
|
|
868
|
+
* @returns {Uint8Array} Row-major bitmap of 0/1 values
|
|
869
|
+
*/
|
|
870
|
+
static sierpinski(
|
|
871
|
+
width,
|
|
872
|
+
height,
|
|
873
|
+
iterations = 6,
|
|
874
|
+
xMin = 0,
|
|
875
|
+
xMax = 1,
|
|
876
|
+
yMin = 0,
|
|
877
|
+
yMax = 1
|
|
878
|
+
) {
|
|
879
|
+
const data = new Uint8Array(width * height).fill(1);
|
|
880
|
+
|
|
881
|
+
/* ----------------------------------------------------------------
|
|
882
|
+
* 1. Ensure the sampling window has the correct √3/2 aspect ratio
|
|
883
|
+
* (so the triangles stay equilateral) while preserving the
|
|
884
|
+
* caller-requested centre / zoom level.
|
|
885
|
+
* ---------------------------------------------------------------- */
|
|
886
|
+
const ideal = Math.sqrt(3) / 2; // height / width for equilateral
|
|
887
|
+
const spanX = xMax - xMin;
|
|
888
|
+
const spanY = yMax - yMin;
|
|
889
|
+
const current = spanY / spanX;
|
|
890
|
+
|
|
891
|
+
if (Math.abs(current - ideal) > 1e-9) {
|
|
892
|
+
const midY = (yMin + yMax) / 2;
|
|
893
|
+
const newSpanY = spanX * ideal;
|
|
894
|
+
yMin = midY - newSpanY / 2;
|
|
895
|
+
yMax = midY + newSpanY / 2;
|
|
896
|
+
}
|
|
897
|
+
/* ----------------------------------------------------------------
|
|
898
|
+
* 2. Calculate the actual iteration depth needed for bitwise approach
|
|
899
|
+
* ---------------------------------------------------------------- */
|
|
900
|
+
|
|
901
|
+
// Cap iterations to prevent excessive subdivision
|
|
902
|
+
const effectiveIterations = Math.min(iterations, 32);
|
|
903
|
+
|
|
904
|
+
// This controls the pattern complexity using bits
|
|
905
|
+
const mask = (1 << effectiveIterations) - 1;
|
|
906
|
+
|
|
907
|
+
/* ----------------------------------------------------------------
|
|
908
|
+
* 3. Pre-compute constants for the raster loop
|
|
909
|
+
* ---------------------------------------------------------------- */
|
|
910
|
+
const invW = (xMax - xMin) / width;
|
|
911
|
+
const invH = (yMax - yMin) / height;
|
|
912
|
+
const invTri = 2 / Math.sqrt(3); // == 1 / (√3/2)
|
|
913
|
+
|
|
914
|
+
/* ----------------------------------------------------------------
|
|
915
|
+
* 4. Raster scan with controlled level of detail
|
|
916
|
+
* ---------------------------------------------------------------- */
|
|
917
|
+
for (let py = 0; py < height; ++py) {
|
|
918
|
+
const yCoord = yMin + py * invH;
|
|
919
|
+
const j = Math.floor(yCoord * invTri); // row on 60° lattice
|
|
920
|
+
const shift = j * 0.5; // x-offset for this row
|
|
921
|
+
|
|
922
|
+
for (let px = 0; px < width; ++px) {
|
|
923
|
+
const xCoord = xMin + px * invW;
|
|
924
|
+
const i = Math.floor(xCoord - shift); // column on lattice
|
|
925
|
+
|
|
926
|
+
// Modified bitwise test - this is key to controlling triangle visibility
|
|
927
|
+
// 1. Use the most significant bits for the pattern (i & j & mask)
|
|
928
|
+
// 2. Adjust which bits we use based on iterations to control triangle size
|
|
929
|
+
if ((i & j & mask) !== 0) {
|
|
930
|
+
data[py * width + px] = 0; // carve hole
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return data;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
static sierpinskiCarpet(
|
|
939
|
+
width,
|
|
940
|
+
height,
|
|
941
|
+
iterations = 5,
|
|
942
|
+
xMin = 0,
|
|
943
|
+
xMax = 1,
|
|
944
|
+
yMin = 0,
|
|
945
|
+
yMax = 1
|
|
946
|
+
) {
|
|
947
|
+
const data = new Uint8Array(width * height).fill(1);
|
|
948
|
+
|
|
949
|
+
/* 1 — Make the window square while preserving center point */
|
|
950
|
+
const spanX = xMax - xMin;
|
|
951
|
+
const spanY = yMax - yMin;
|
|
952
|
+
const size = Math.max(spanX, spanY);
|
|
953
|
+
const centerX = (xMin + xMax) / 2;
|
|
954
|
+
const centerY = (yMin + yMax) / 2;
|
|
955
|
+
|
|
956
|
+
// Recalculate boundaries to be square while keeping the center fixed
|
|
957
|
+
xMin = centerX - size / 2;
|
|
958
|
+
xMax = centerX + size / 2;
|
|
959
|
+
yMin = centerY - size / 2;
|
|
960
|
+
yMax = centerY + size / 2;
|
|
961
|
+
|
|
962
|
+
/* 2 — Calculate scaling factors for the carpet grid */
|
|
963
|
+
const pow3 = Math.pow(3, iterations);
|
|
964
|
+
|
|
965
|
+
/* 3 — Helper: test if a coordinate is in a hole */
|
|
966
|
+
const isHole = (ix, iy) => {
|
|
967
|
+
let x = ix;
|
|
968
|
+
let y = iy;
|
|
969
|
+
|
|
970
|
+
while (x > 0 || y > 0) {
|
|
971
|
+
if (x % 3 === 1 && y % 3 === 1) {
|
|
972
|
+
return true;
|
|
973
|
+
}
|
|
974
|
+
x = Math.floor(x / 3);
|
|
975
|
+
y = Math.floor(y / 3);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return false;
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
/* 4 — Process each pixel */
|
|
982
|
+
for (let py = 0; py < height; ++py) {
|
|
983
|
+
// Map from pixel to world coordinate
|
|
984
|
+
const worldY = yMin + (py / height) * (yMax - yMin);
|
|
985
|
+
|
|
986
|
+
// Map world coordinate to carpet grid space - preserve position
|
|
987
|
+
const carpetY = worldY * pow3;
|
|
988
|
+
|
|
989
|
+
// Floor to get grid index and wrap to ensure positive values
|
|
990
|
+
const iy = ((Math.floor(carpetY) % pow3) + pow3) % pow3;
|
|
991
|
+
|
|
992
|
+
for (let px = 0; px < width; ++px) {
|
|
993
|
+
// Map from pixel to world coordinate
|
|
994
|
+
const worldX = xMin + (px / width) * (xMax - xMin);
|
|
995
|
+
|
|
996
|
+
// Map world coordinate to carpet grid space - preserve position
|
|
997
|
+
const carpetX = worldX * pow3;
|
|
998
|
+
|
|
999
|
+
// Floor and wrap
|
|
1000
|
+
const ix = ((Math.floor(carpetX) % pow3) + pow3) % pow3;
|
|
1001
|
+
|
|
1002
|
+
// Apply the hole test
|
|
1003
|
+
if (isHole(ix, iy)) {
|
|
1004
|
+
data[py * width + px] = 0;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
return data;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Generates a Barnsley Fern fractal
|
|
1014
|
+
*
|
|
1015
|
+
* @param {number} width - Image width
|
|
1016
|
+
* @param {number} height - Image height
|
|
1017
|
+
* @param {number} [iterations=100000] - Number of points to generate
|
|
1018
|
+
* @returns {Uint8Array} Density map (0-255)
|
|
1019
|
+
*/
|
|
1020
|
+
static barnsleyFern(width, height, iterations = 100000) {
|
|
1021
|
+
const data = new Uint8Array(width * height).fill(0);
|
|
1022
|
+
let x = 0,
|
|
1023
|
+
y = 0;
|
|
1024
|
+
|
|
1025
|
+
// Scale and position the fern
|
|
1026
|
+
const scale = Math.min(width, height) / 10;
|
|
1027
|
+
const offsetX = width / 2;
|
|
1028
|
+
const offsetY = height;
|
|
1029
|
+
|
|
1030
|
+
for (let i = 0; i < iterations; i++) {
|
|
1031
|
+
const r = Math.random();
|
|
1032
|
+
let nx, ny;
|
|
1033
|
+
|
|
1034
|
+
if (r < 0.01) {
|
|
1035
|
+
nx = 0;
|
|
1036
|
+
ny = 0.16 * y;
|
|
1037
|
+
} else if (r < 0.86) {
|
|
1038
|
+
nx = 0.85 * x + 0.04 * y;
|
|
1039
|
+
ny = -0.04 * x + 0.85 * y + 1.6;
|
|
1040
|
+
} else if (r < 0.93) {
|
|
1041
|
+
nx = 0.2 * x - 0.26 * y;
|
|
1042
|
+
ny = 0.23 * x + 0.22 * y + 1.6;
|
|
1043
|
+
} else {
|
|
1044
|
+
nx = -0.15 * x + 0.28 * y;
|
|
1045
|
+
ny = 0.26 * x + 0.24 * y + 0.44;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
x = nx;
|
|
1049
|
+
y = ny;
|
|
1050
|
+
|
|
1051
|
+
// Map to pixel coordinates
|
|
1052
|
+
const px = Math.floor(x * scale + offsetX);
|
|
1053
|
+
const py = Math.floor(height - y * scale);
|
|
1054
|
+
|
|
1055
|
+
if (px >= 0 && px < width && py >= 0 && py < height) {
|
|
1056
|
+
const index = py * width + px;
|
|
1057
|
+
if (data[index] < 255) data[index]++;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return data;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Generates a Lyapunov fractal
|
|
1066
|
+
*
|
|
1067
|
+
* @param {number} width - Image width
|
|
1068
|
+
* @param {number} height - Image height
|
|
1069
|
+
* @param {number} maxIterations - Maximum iterations for calculations
|
|
1070
|
+
* @param {string} sequence - Sequence of A and B to use in calculation
|
|
1071
|
+
* @param {number} aMin - Minimum value for parameter A
|
|
1072
|
+
* @param {number} aMax - Maximum value for parameter A
|
|
1073
|
+
* @param {number} bMin - Minimum value for parameter B
|
|
1074
|
+
* @param {number} bMax - Maximum value for parameter B
|
|
1075
|
+
* @returns {Uint8Array} Array containing iteration values for each pixel
|
|
1076
|
+
*/
|
|
1077
|
+
static lyapunov(
|
|
1078
|
+
width,
|
|
1079
|
+
height,
|
|
1080
|
+
maxIterations = 1000,
|
|
1081
|
+
sequence = "AB",
|
|
1082
|
+
aMin = 3.4, // Adjusted to center the main graph
|
|
1083
|
+
aMax = 4.0,
|
|
1084
|
+
bMin = 3.4, // Adjusted to center the main graph
|
|
1085
|
+
bMax = 4.0
|
|
1086
|
+
) {
|
|
1087
|
+
console.time("lyapunov");
|
|
1088
|
+
// Validate sequence
|
|
1089
|
+
sequence = sequence.toUpperCase().replace(/[^AB]/g, "") || "AB";
|
|
1090
|
+
const seqLen = sequence.length;
|
|
1091
|
+
|
|
1092
|
+
const data = new Float32Array(width * height);
|
|
1093
|
+
let min = Infinity;
|
|
1094
|
+
let max = -Infinity;
|
|
1095
|
+
|
|
1096
|
+
// First pass: calculate exponents and find range
|
|
1097
|
+
for (let y = 0; y < height; y++) {
|
|
1098
|
+
const b = bMin + ((bMax - bMin) * y) / height;
|
|
1099
|
+
|
|
1100
|
+
for (let x = 0; x < width; x++) {
|
|
1101
|
+
const a = aMin + ((aMax - aMin) * x) / width;
|
|
1102
|
+
let xVal = 0.5;
|
|
1103
|
+
|
|
1104
|
+
// Warm-up iterations
|
|
1105
|
+
for (let i = 0; i < 100; i++) {
|
|
1106
|
+
const r = sequence[i % seqLen] === "A" ? a : b;
|
|
1107
|
+
xVal = r * xVal * (1 - xVal);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Calculate Lyapunov exponent with iteration limit
|
|
1111
|
+
let sum = 0;
|
|
1112
|
+
let iteration = 0;
|
|
1113
|
+
while (iteration < maxIterations) {
|
|
1114
|
+
const r = sequence[iteration % seqLen] === "A" ? a : b;
|
|
1115
|
+
xVal = r * xVal * (1 - xVal);
|
|
1116
|
+
const derivative = Math.abs(r * (1 - 2 * xVal));
|
|
1117
|
+
sum += Math.log(Math.max(derivative, 1e-10));
|
|
1118
|
+
iteration++;
|
|
1119
|
+
|
|
1120
|
+
// Optional early exit condition if needed
|
|
1121
|
+
if (Math.abs(sum / iteration) > 10) break;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const exponent = sum / iteration;
|
|
1125
|
+
data[y * width + x] = exponent;
|
|
1126
|
+
|
|
1127
|
+
// Track min/max (excluding extreme outliers)
|
|
1128
|
+
if (exponent > -10 && exponent < 10) {
|
|
1129
|
+
if (exponent < min) min = exponent;
|
|
1130
|
+
if (exponent > max) max = exponent;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Handle case where all values are the same
|
|
1136
|
+
if (min === max) {
|
|
1137
|
+
min -= 1;
|
|
1138
|
+
max += 1;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Second pass: normalize to 0-255 range
|
|
1142
|
+
const range = max - min;
|
|
1143
|
+
const output = new Uint8Array(width * height);
|
|
1144
|
+
|
|
1145
|
+
for (let i = 0; i < data.length; i++) {
|
|
1146
|
+
let value = data[i];
|
|
1147
|
+
|
|
1148
|
+
// Clamp extreme values
|
|
1149
|
+
value = Math.max(-10, Math.min(10, value));
|
|
1150
|
+
|
|
1151
|
+
// Normalize and map to 0-255
|
|
1152
|
+
let normalized = (value - min) / range;
|
|
1153
|
+
output[i] = Math.floor(normalized * 255);
|
|
1154
|
+
}
|
|
1155
|
+
console.timeEnd("lyapunov");
|
|
1156
|
+
return output;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Generates a Koch snowflake fractal
|
|
1161
|
+
*
|
|
1162
|
+
* @param {number} width - Image width
|
|
1163
|
+
* @param {number} height - Image height
|
|
1164
|
+
* @param {number} maxIterations - Maximum iterations for Koch snowflake detail
|
|
1165
|
+
* @param {number} xMin - Minimum X coordinate in viewing plane
|
|
1166
|
+
* @param {number} xMax - Maximum X coordinate in viewing plane
|
|
1167
|
+
* @param {number} yMin - Minimum Y coordinate in viewing plane
|
|
1168
|
+
* @param {number} yMax - Maximum Y coordinate in viewing plane
|
|
1169
|
+
* @returns {Uint8Array} Array containing pixel values for the Koch snowflake
|
|
1170
|
+
*/
|
|
1171
|
+
static koch(
|
|
1172
|
+
width,
|
|
1173
|
+
height,
|
|
1174
|
+
maxIterations = 4,
|
|
1175
|
+
xMin = -2,
|
|
1176
|
+
xMax = 2,
|
|
1177
|
+
yMin = -2,
|
|
1178
|
+
yMax = 2
|
|
1179
|
+
) {
|
|
1180
|
+
// Create data array
|
|
1181
|
+
const data = new Uint8Array(width * height);
|
|
1182
|
+
|
|
1183
|
+
// World to screen coordinate conversion
|
|
1184
|
+
// The key change is here - as xMin/xMax/yMin/yMax get smaller in range,
|
|
1185
|
+
// the image gets magnified (proper zoom behavior)
|
|
1186
|
+
const mapX = (x) => Math.floor(((x - xMin) * width) / (xMax - xMin));
|
|
1187
|
+
const mapY = (y) => Math.floor(((y - yMin) * height) / (yMax - yMin));
|
|
1188
|
+
|
|
1189
|
+
// Basic line drawing
|
|
1190
|
+
const drawLine = (x0, y0, x1, y1) => {
|
|
1191
|
+
const sx = mapX(x0);
|
|
1192
|
+
const sy = mapY(y0);
|
|
1193
|
+
const ex = mapX(x1);
|
|
1194
|
+
const ey = mapY(y1);
|
|
1195
|
+
|
|
1196
|
+
// Bresenham's algorithm
|
|
1197
|
+
let x = sx,
|
|
1198
|
+
y = sy;
|
|
1199
|
+
const dx = Math.abs(ex - sx),
|
|
1200
|
+
dy = Math.abs(ey - sy);
|
|
1201
|
+
const sx1 = sx < ex ? 1 : -1,
|
|
1202
|
+
sy1 = sy < ey ? 1 : -1;
|
|
1203
|
+
let err = dx - dy;
|
|
1204
|
+
|
|
1205
|
+
while (true) {
|
|
1206
|
+
if (x >= 0 && x < width && y >= 0 && y < height) {
|
|
1207
|
+
data[y * width + x] = 255;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (x === ex && y === ey) break;
|
|
1211
|
+
const e2 = 2 * err;
|
|
1212
|
+
if (e2 > -dy) {
|
|
1213
|
+
err -= dy;
|
|
1214
|
+
x += sx1;
|
|
1215
|
+
}
|
|
1216
|
+
if (e2 < dx) {
|
|
1217
|
+
err += dx;
|
|
1218
|
+
y += sy1;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
// Generate Koch curve recursively with iteration limit
|
|
1224
|
+
const kochSegment = (x1, y1, x2, y2, depth) => {
|
|
1225
|
+
if (depth <= 0) {
|
|
1226
|
+
drawLine(x1, y1, x2, y2);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Calculate positions using vector math
|
|
1231
|
+
const dx = (x2 - x1) / 3;
|
|
1232
|
+
const dy = (y2 - y1) / 3;
|
|
1233
|
+
|
|
1234
|
+
// Points along the original line (1/3 and 2/3)
|
|
1235
|
+
const x3 = x1 + dx;
|
|
1236
|
+
const y3 = y1 + dy;
|
|
1237
|
+
const x5 = x1 + 2 * dx;
|
|
1238
|
+
const y5 = y1 + 2 * dy;
|
|
1239
|
+
|
|
1240
|
+
// Calculate the peak point (rotated by 60 degrees)
|
|
1241
|
+
const angle = Math.PI / 3; // 60 degrees
|
|
1242
|
+
const x4 = x3 + dx * Math.cos(angle) - dy * Math.sin(angle);
|
|
1243
|
+
const y4 = y3 + dx * Math.sin(angle) + dy * Math.cos(angle);
|
|
1244
|
+
|
|
1245
|
+
// Recursive calls for the four segments
|
|
1246
|
+
kochSegment(x1, y1, x3, y3, depth - 1);
|
|
1247
|
+
kochSegment(x3, y3, x4, y4, depth - 1);
|
|
1248
|
+
kochSegment(x4, y4, x5, y5, depth - 1);
|
|
1249
|
+
kochSegment(x5, y5, x2, y2, depth - 1);
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// Ensure iterations is in a reasonable range
|
|
1253
|
+
const iterations = Math.min(maxIterations, 10);
|
|
1254
|
+
|
|
1255
|
+
// Define the Koch snowflake in fixed coordinates
|
|
1256
|
+
const size = 3; // Size of the base snowflake
|
|
1257
|
+
const h = (size * Math.sqrt(3)) / 2;
|
|
1258
|
+
|
|
1259
|
+
// Triangle vertices in fixed coordinates
|
|
1260
|
+
const p1 = [0, -h / 2 + 0.5]; // Top
|
|
1261
|
+
const p2 = [-size / 2, h / 2 + 0.5]; // Bottom left
|
|
1262
|
+
const p3 = [size / 2, h / 2 + 0.5]; // Bottom right
|
|
1263
|
+
|
|
1264
|
+
// Draw the three sides of the triangle with Koch segments
|
|
1265
|
+
kochSegment(p1[0], p1[1], p2[0], p2[1], iterations);
|
|
1266
|
+
kochSegment(p2[0], p2[1], p3[0], p3[1], iterations);
|
|
1267
|
+
kochSegment(p3[0], p3[1], p1[0], p1[1], iterations);
|
|
1268
|
+
|
|
1269
|
+
return data;
|
|
1270
|
+
}
|
|
1271
|
+
}
|