@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.
Files changed (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. 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
+ }