@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,1339 @@
1
+ import { Noise } from "./noise.js";
2
+ import { generatePenroseTilingPixels } from "./penrose.js";
3
+
4
+ export class Patterns {
5
+ static void(width, height, options = {}) {
6
+ const {
7
+ background = [255, 255, 255, 255], // white
8
+ foreground = [0, 0, 200, 255], // blue
9
+ } = options;
10
+
11
+ // Create data array with background color
12
+ const data = new Uint8ClampedArray(width * height * 4);
13
+ for (let i = 0; i < data.length; i += 4) {
14
+ data[i] = background[0];
15
+ data[i + 1] = background[1];
16
+ data[i + 2] = background[2];
17
+ data[i + 3] = background[3];
18
+ }
19
+
20
+ return data;
21
+ }
22
+
23
+ /**
24
+ * Generates an RGBA grid pattern with transparency support.
25
+ * @param {number} width
26
+ * @param {number} height
27
+ * @param {{
28
+ * spacing?: number,
29
+ * background?: [r, g, b, a],
30
+ * foreground?: [r, g, b, a]
31
+ * }} options
32
+ * @returns {Uint8ClampedArray}
33
+ */
34
+ static solidGrid(width, height, options = {}) {
35
+ const {
36
+ spacing = 8,
37
+ background = [0, 0, 0, 0], // transparent
38
+ foreground = [128, 128, 128, 255], // solid gray
39
+ } = options;
40
+
41
+ const data = new Uint8ClampedArray(width * height * 4);
42
+
43
+ for (let y = 0; y < height; y++) {
44
+ const yLine = y % spacing === 0;
45
+
46
+ for (let x = 0; x < width; x++) {
47
+ const xLine = x % spacing === 0;
48
+ const isLine = xLine || yLine;
49
+ const offset = (y * width + x) * 4;
50
+
51
+ const color = isLine ? foreground : background;
52
+ data[offset] = color[0]; // R
53
+ data[offset + 1] = color[1]; // G
54
+ data[offset + 2] = color[2]; // B
55
+ data[offset + 3] = color[3]; // A
56
+ }
57
+ }
58
+
59
+ return data;
60
+ }
61
+
62
+ /**
63
+ * Checkerboard pattern
64
+ */
65
+ static checkerboard(width, height, options = {}) {
66
+ const {
67
+ cellSize = 8,
68
+ color1 = [0, 0, 0, 255],
69
+ color2 = [255, 255, 255, 255],
70
+ } = options;
71
+
72
+ const data = new Uint8ClampedArray(width * height * 4);
73
+
74
+ for (let y = 0; y < height; y++) {
75
+ const yCell = Math.floor(y / cellSize);
76
+ for (let x = 0; x < width; x++) {
77
+ const xCell = Math.floor(x / cellSize);
78
+ const useColor1 = (xCell + yCell) % 2 === 0;
79
+ const color = useColor1 ? color1 : color2;
80
+ const offset = (y * width + x) * 4;
81
+ data.set(color, offset);
82
+ }
83
+ }
84
+
85
+ return data;
86
+ }
87
+
88
+ /**
89
+ * Diagonal stripe pattern
90
+ */
91
+ static stripes(width, height, options = {}) {
92
+ const {
93
+ spacing = 4,
94
+ thickness = 1,
95
+ background = [0, 0, 0, 0],
96
+ foreground = [255, 255, 0, 255],
97
+ } = options;
98
+
99
+ const data = new Uint8ClampedArray(width * height * 4);
100
+
101
+ for (let y = 0; y < height; y++) {
102
+ for (let x = 0; x < width; x++) {
103
+ const diag = (x + y) % spacing;
104
+ const isStripe = diag < thickness;
105
+ const offset = (y * width + x) * 4;
106
+ data.set(isStripe ? foreground : background, offset);
107
+ }
108
+ }
109
+
110
+ return data;
111
+ }
112
+
113
+ static honeycomb(width, height, options = {}) {
114
+ const {
115
+ radius = 10, // Radius of the hexagon
116
+ lineWidth = 1, // Border thickness
117
+ foreground = [255, 255, 255, 255],
118
+ background = [0, 0, 0, 255],
119
+ } = options;
120
+
121
+ const data = new Uint8ClampedArray(width * height * 4);
122
+
123
+ // Fill with background color first
124
+ for (let i = 0; i < data.length; i += 4) {
125
+ data[i] = background[0]; // R
126
+ data[i + 1] = background[1]; // G
127
+ data[i + 2] = background[2]; // B
128
+ data[i + 3] = background[3]; // A
129
+ }
130
+
131
+ // Center of the canvas
132
+ const centerX = Math.floor(width / 2);
133
+ const centerY = Math.floor(height / 2);
134
+
135
+ // Function to check if a point is inside a hexagon
136
+ const isInsideHexagon = (px, py, cx, cy, r) => {
137
+ // For flat-topped hexagon
138
+ const dx = Math.abs(px - cx);
139
+ const dy = Math.abs(py - cy);
140
+
141
+ // Height of the hexagon from center to top
142
+ const hexHeight = (r * Math.sqrt(3)) / 2;
143
+
144
+ // Outside vertical bounds
145
+ if (dy > hexHeight) return false;
146
+
147
+ // Outside horizontal bounds
148
+ if (dx > r) return false;
149
+
150
+ // Check diagonal edges
151
+ return r * hexHeight * 2 >= r * dy * 2 + hexHeight * dx;
152
+ };
153
+
154
+ // Calculate inner hexagon radius for border
155
+ const innerRadius = radius - lineWidth;
156
+
157
+ // Define the bounds for checking pixels
158
+ const hexHeight = radius * Math.sqrt(3);
159
+ const minX = Math.max(0, Math.floor(centerX - radius - 1));
160
+ const maxX = Math.min(width - 1, Math.ceil(centerX + radius + 1));
161
+ const minY = Math.max(0, Math.floor(centerY - hexHeight / 2 - 1));
162
+ const maxY = Math.min(height - 1, Math.ceil(centerY + hexHeight / 2 + 1));
163
+
164
+ // Check each pixel in the bounding box
165
+ for (let y = minY; y <= maxY; y++) {
166
+ for (let x = minX; x <= maxX; x++) {
167
+ // Check if the point is inside the outer hexagon but outside the inner hexagon
168
+ const isOuterHex = isInsideHexagon(x, y, centerX, centerY, radius);
169
+ const isInnerHex =
170
+ innerRadius > 0
171
+ ? isInsideHexagon(x, y, centerX, centerY, innerRadius)
172
+ : false;
173
+
174
+ if (isOuterHex && !isInnerHex) {
175
+ const offset = (y * width + x) * 4;
176
+ data[offset] = foreground[0]; // R
177
+ data[offset + 1] = foreground[1]; // G
178
+ data[offset + 2] = foreground[2]; // B
179
+ data[offset + 3] = foreground[3]; // A
180
+ }
181
+ }
182
+ }
183
+
184
+ return data;
185
+ }
186
+
187
+ static harlequin(width, height, options = {}) {
188
+ const {
189
+ size = 20, // Size of each diamond (distance from center to point)
190
+ spacing = 0, // Gap between diamonds
191
+ background = [255, 255, 255, 255], // White
192
+ foreground = [0, 0, 0, 255], // Black
193
+ } = options;
194
+
195
+ const data = new Uint8ClampedArray(width * height * 4);
196
+
197
+ // Fill with background color first
198
+ for (let i = 0; i < data.length; i += 4) {
199
+ data[i] = background[0]; // R
200
+ data[i + 1] = background[1]; // G
201
+ data[i + 2] = background[2]; // B
202
+ data[i + 3] = background[3]; // A
203
+ }
204
+
205
+ // Calculate diamond dimensions and spacing
206
+ const diamondWidth = size * 2;
207
+ const diamondHeight = size * 2;
208
+
209
+ // Calculate grid spacing with added spacing parameter
210
+ const gridWidth = diamondWidth + spacing;
211
+ const gridHeight = diamondHeight + spacing;
212
+
213
+ // Function to check if a point is inside a diamond
214
+ const isInsideDiamond = (px, py, cx, cy) => {
215
+ // Calculate distance from point to center, scaled by diamond dimensions
216
+ const dx = Math.abs(px - cx) / (diamondWidth / 2);
217
+ const dy = Math.abs(py - cy) / (diamondHeight / 2);
218
+
219
+ // Inside diamond if the sum of normalized distances is <= 1
220
+ return dx + dy <= 1;
221
+ };
222
+
223
+ // Draw diamonds in a grid pattern
224
+ for (let row = -1; row < height / gridHeight + 1; row++) {
225
+ for (let col = -1; col < width / gridWidth + 1; col++) {
226
+ // Calculate center of this diamond
227
+ const centerX = col * gridWidth + gridWidth / 2;
228
+ const centerY = row * gridHeight + gridHeight / 2;
229
+
230
+ // Only draw every other diamond (checkerboard pattern)
231
+ const shouldDraw = (row + col) % 2 === 0;
232
+ if (!shouldDraw) continue;
233
+
234
+ // Define the bounds for checking pixels
235
+ const minX = Math.max(0, Math.floor(centerX - diamondWidth / 2));
236
+ const maxX = Math.min(width - 1, Math.ceil(centerX + diamondWidth / 2));
237
+ const minY = Math.max(0, Math.floor(centerY - diamondHeight / 2));
238
+ const maxY = Math.min(
239
+ height - 1,
240
+ Math.ceil(centerY + diamondHeight / 2)
241
+ );
242
+
243
+ // Check each pixel in the bounding box
244
+ for (let y = minY; y <= maxY; y++) {
245
+ for (let x = minX; x <= maxX; x++) {
246
+ if (isInsideDiamond(x, y, centerX, centerY)) {
247
+ const offset = (y * width + x) * 4;
248
+
249
+ // Apply foreground color
250
+ data[offset] = foreground[0]; // R
251
+ data[offset + 1] = foreground[1]; // G
252
+ data[offset + 2] = foreground[2]; // B
253
+ data[offset + 3] = foreground[3]; // A
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ return data;
261
+ }
262
+
263
+ static circles(width, height, options = {}) {
264
+ const {
265
+ radius = 10, // Radius of each circle
266
+ lineWidth = 2, // Width of the circle border
267
+ spacing = 5, // Space between circles
268
+ background = [0, 0, 0, 255], // Black background
269
+ foreground = [255, 255, 255, 255], // White foreground for circles
270
+ } = options;
271
+
272
+ const data = new Uint8ClampedArray(width * height * 4);
273
+
274
+ // Fill with background color first
275
+ for (let i = 0; i < data.length; i += 4) {
276
+ data[i] = background[0]; // R
277
+ data[i + 1] = background[1]; // G
278
+ data[i + 2] = background[2]; // B
279
+ data[i + 3] = background[3]; // A
280
+ }
281
+
282
+ // Calculate the distance between circle centers
283
+ const gridSize = radius * 2 + spacing;
284
+
285
+ // Function to check if a point is inside a circle
286
+ const isInsideCircle = (px, py, cx, cy, r) => {
287
+ const dx = px - cx;
288
+ const dy = py - cy;
289
+ const distanceSquared = dx * dx + dy * dy;
290
+ return distanceSquared <= r * r;
291
+ };
292
+
293
+ // Draw circles in a grid pattern
294
+ for (let row = 0; row < Math.ceil(height / gridSize) + 1; row++) {
295
+ for (let col = 0; col < Math.ceil(width / gridSize) + 1; col++) {
296
+ // Calculate center of this circle
297
+ const centerX = col * gridSize + radius;
298
+ const centerY = row * gridSize + radius;
299
+
300
+ // Skip if circle center is outside the canvas (with some margin)
301
+ if (
302
+ centerX < -radius ||
303
+ centerX > width + radius ||
304
+ centerY < -radius ||
305
+ centerY > height + radius
306
+ ) {
307
+ continue;
308
+ }
309
+
310
+ // Define the bounds for checking pixels
311
+ const minX = Math.max(0, Math.floor(centerX - radius));
312
+ const maxX = Math.min(width - 1, Math.ceil(centerX + radius));
313
+ const minY = Math.max(0, Math.floor(centerY - radius));
314
+ const maxY = Math.min(height - 1, Math.ceil(centerY + radius));
315
+
316
+ // Inner radius for hollow circles
317
+ const innerRadius = radius - lineWidth;
318
+
319
+ // Check each pixel in the bounding box
320
+ for (let y = minY; y <= maxY; y++) {
321
+ for (let x = minX; x <= maxX; x++) {
322
+ // Check if point is inside outer circle but outside inner circle
323
+ const isOuterCircle = isInsideCircle(
324
+ x,
325
+ y,
326
+ centerX,
327
+ centerY,
328
+ radius
329
+ );
330
+ const isInnerCircle = isInsideCircle(
331
+ x,
332
+ y,
333
+ centerX,
334
+ centerY,
335
+ innerRadius
336
+ );
337
+
338
+ if (isOuterCircle && !isInnerCircle) {
339
+ const offset = (y * width + x) * 4;
340
+
341
+ // Apply foreground color for the circle outline
342
+ data[offset] = foreground[0]; // R
343
+ data[offset + 1] = foreground[1]; // G
344
+ data[offset + 2] = foreground[2]; // B
345
+ data[offset + 3] = foreground[3]; // A
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ return data;
353
+ }
354
+
355
+ static diamonds(width, height, options = {}) {
356
+ const {
357
+ size = 16, // Size of the pattern cell
358
+ squareSize = 6, // Size of the inner square
359
+ background = [255, 255, 255, 255], // White background
360
+ foreground = [0, 0, 0, 255], // Black foreground for diamonds
361
+ innerColor = [255, 255, 255, 255], // White color for inner squares
362
+ } = options;
363
+
364
+ const data = new Uint8ClampedArray(width * height * 4);
365
+
366
+ // Fill with background color first
367
+ for (let i = 0; i < data.length; i += 4) {
368
+ data[i] = background[0]; // R
369
+ data[i + 1] = background[1]; // G
370
+ data[i + 2] = background[2]; // B
371
+ data[i + 3] = background[3]; // A
372
+ }
373
+
374
+ // Calculate the grid size
375
+ const gridSize = size;
376
+
377
+ // Function to check if a point is inside a diamond
378
+ const isInsideDiamond = (px, py, cx, cy, s) => {
379
+ // Calculate distance from point to center, using manhattan distance for diamond shape
380
+ const dx = Math.abs(px - cx);
381
+ const dy = Math.abs(py - cy);
382
+
383
+ // Inside diamond if the sum of normalized distances is <= half the size
384
+ return dx + dy <= s / 2;
385
+ };
386
+
387
+ // Function to check if a point is inside a square
388
+ const isInsideSquare = (px, py, cx, cy, s) => {
389
+ return Math.abs(px - cx) <= s / 2 && Math.abs(py - cy) <= s / 2;
390
+ };
391
+
392
+ // Draw the pattern
393
+ for (let row = -1; row < height / gridSize + 1; row++) {
394
+ for (let col = -1; col < width / gridSize + 1; col++) {
395
+ // Center of this cell
396
+ const centerX = col * gridSize + gridSize / 2;
397
+ const centerY = row * gridSize + gridSize / 2;
398
+
399
+ // Skip if cell center is far outside the canvas
400
+ if (
401
+ centerX < -gridSize ||
402
+ centerX > width + gridSize ||
403
+ centerY < -gridSize ||
404
+ centerY > height + gridSize
405
+ ) {
406
+ continue;
407
+ }
408
+
409
+ // Define the bounds for checking pixels
410
+ const minX = Math.max(0, Math.floor(centerX - gridSize / 2));
411
+ const maxX = Math.min(width - 1, Math.ceil(centerX + gridSize / 2));
412
+ const minY = Math.max(0, Math.floor(centerY - gridSize / 2));
413
+ const maxY = Math.min(height - 1, Math.ceil(centerY + gridSize / 2));
414
+
415
+ // Check each pixel in the bounding box
416
+ for (let y = minY; y <= maxY; y++) {
417
+ for (let x = minX; x <= maxX; x++) {
418
+ const isDiamond = isInsideDiamond(x, y, centerX, centerY, gridSize);
419
+ const isSquare = isInsideSquare(x, y, centerX, centerY, squareSize);
420
+
421
+ if (isDiamond) {
422
+ const offset = (y * width + x) * 4;
423
+
424
+ if (isSquare) {
425
+ // Inner square color
426
+ data[offset] = innerColor[0]; // R
427
+ data[offset + 1] = innerColor[1]; // G
428
+ data[offset + 2] = innerColor[2]; // B
429
+ data[offset + 3] = innerColor[3]; // A
430
+ } else {
431
+ // Diamond color
432
+ data[offset] = foreground[0]; // R
433
+ data[offset + 1] = foreground[1]; // G
434
+ data[offset + 2] = foreground[2]; // B
435
+ data[offset + 3] = foreground[3]; // A
436
+ }
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ return data;
444
+ }
445
+
446
+ static cubes(width, height, options = {}) {
447
+ const {
448
+ size = 10, // Size of each square
449
+ spacing = 2, // Space between squares
450
+ background = [0, 0, 0, 255], // Black background
451
+ foreground = [255, 100, 0, 255], // Orange foreground for squares
452
+ } = options;
453
+
454
+ const data = new Uint8ClampedArray(width * height * 4);
455
+
456
+ // Fill with background color first
457
+ for (let i = 0; i < data.length; i += 4) {
458
+ data[i] = background[0]; // R
459
+ data[i + 1] = background[1]; // G
460
+ data[i + 2] = background[2]; // B
461
+ data[i + 3] = background[3]; // A
462
+ }
463
+
464
+ // Calculate grid dimensions
465
+ const gridSize = size + spacing;
466
+
467
+ // Draw squares in a grid pattern
468
+ for (let row = 0; row < Math.ceil(height / gridSize) + 1; row++) {
469
+ for (let col = 0; col < Math.ceil(width / gridSize) + 1; col++) {
470
+ // Base position for this square
471
+ const x = col * gridSize;
472
+ const y = row * gridSize;
473
+
474
+ // Skip if square is entirely outside the canvas
475
+ if (x >= width || y >= height) {
476
+ continue;
477
+ }
478
+
479
+ // Draw a simple square
480
+ for (let py = y; py < Math.min(y + size, height); py++) {
481
+ for (let px = x; px < Math.min(x + size, width); px++) {
482
+ const offset = (py * width + px) * 4;
483
+
484
+ // Apply foreground color for the square
485
+ data[offset] = foreground[0]; // R
486
+ data[offset + 1] = foreground[1]; // G
487
+ data[offset + 2] = foreground[2]; // B
488
+ data[offset + 3] = foreground[3]; // A
489
+ }
490
+ }
491
+ }
492
+ }
493
+
494
+ return data;
495
+ }
496
+
497
+ static cross(width, height, options = {}) {
498
+ const {
499
+ size = 8, // Size of each cross (total width/height)
500
+ thickness = 2, // Thickness of the cross lines
501
+ spacing = 16, // Space between crosses (center to center)
502
+ background = [255, 255, 255, 255], // White background
503
+ foreground = [80, 80, 80, 255], // Dark gray foreground for crosses
504
+ } = options;
505
+
506
+ const data = new Uint8ClampedArray(width * height * 4);
507
+
508
+ // Fill with background color first
509
+ for (let i = 0; i < data.length; i += 4) {
510
+ data[i] = background[0]; // R
511
+ data[i + 1] = background[1]; // G
512
+ data[i + 2] = background[2]; // B
513
+ data[i + 3] = background[3]; // A
514
+ }
515
+
516
+ // Draw crosses in a grid pattern
517
+ for (let row = 0; row < Math.ceil(height / spacing) + 1; row++) {
518
+ for (let col = 0; col < Math.ceil(width / spacing) + 1; col++) {
519
+ // Center of this cross
520
+ const centerX = col * spacing;
521
+ const centerY = row * spacing;
522
+
523
+ // Skip if cross center is far outside the canvas
524
+ if (
525
+ centerX < -size ||
526
+ centerX > width + size ||
527
+ centerY < -size ||
528
+ centerY > height + size
529
+ ) {
530
+ continue;
531
+ }
532
+
533
+ // Draw the horizontal line of the cross
534
+ const hStartX = centerX - size / 2;
535
+ const hEndX = centerX + size / 2;
536
+ const hStartY = centerY - thickness / 2;
537
+ const hEndY = centerY + thickness / 2;
538
+
539
+ for (
540
+ let y = Math.max(0, Math.floor(hStartY));
541
+ y < Math.min(height, Math.ceil(hEndY));
542
+ y++
543
+ ) {
544
+ for (
545
+ let x = Math.max(0, Math.floor(hStartX));
546
+ x < Math.min(width, Math.ceil(hEndX));
547
+ x++
548
+ ) {
549
+ const offset = (y * width + x) * 4;
550
+ data[offset] = foreground[0]; // R
551
+ data[offset + 1] = foreground[1]; // G
552
+ data[offset + 2] = foreground[2]; // B
553
+ data[offset + 3] = foreground[3]; // A
554
+ }
555
+ }
556
+
557
+ // Draw the vertical line of the cross
558
+ const vStartX = centerX - thickness / 2;
559
+ const vEndX = centerX + thickness / 2;
560
+ const vStartY = centerY - size / 2;
561
+ const vEndY = centerY + size / 2;
562
+
563
+ for (
564
+ let y = Math.max(0, Math.floor(vStartY));
565
+ y < Math.min(height, Math.ceil(vEndY));
566
+ y++
567
+ ) {
568
+ for (
569
+ let x = Math.max(0, Math.floor(vStartX));
570
+ x < Math.min(width, Math.ceil(vEndX));
571
+ x++
572
+ ) {
573
+ const offset = (y * width + x) * 4;
574
+ data[offset] = foreground[0]; // R
575
+ data[offset + 1] = foreground[1]; // G
576
+ data[offset + 2] = foreground[2]; // B
577
+ data[offset + 3] = foreground[3]; // A
578
+ }
579
+ }
580
+ }
581
+ }
582
+
583
+ return data;
584
+ }
585
+
586
+ static mesh(width, height, options = {}) {
587
+ const {
588
+ spacing = 20, // Distance between parallel lines
589
+ lineWidth = 2, // Thickness of the lines
590
+ background = [255, 255, 255, 0], // Transparent background
591
+ foreground = [0, 0, 0, 255], // Black lines
592
+ } = options;
593
+
594
+ const data = new Uint8ClampedArray(width * height * 4);
595
+
596
+ // Fill with background color first
597
+ for (let i = 0; i < data.length; i += 4) {
598
+ data[i] = background[0]; // R
599
+ data[i + 1] = background[1]; // G
600
+ data[i + 2] = background[2]; // B
601
+ data[i + 3] = background[3]; // A
602
+ }
603
+
604
+ // Create the isometric grid pattern
605
+ for (let y = 0; y < height; y++) {
606
+ for (let x = 0; x < width; x++) {
607
+ // Calculate if we're on a line for the first set of diagonals (/)
608
+ const d1 = (x + y) % spacing;
609
+ const isLine1 = d1 < lineWidth || d1 > spacing - lineWidth;
610
+
611
+ // Calculate if we're on a line for the second set of diagonals (\)
612
+ const d2 = (x - y + height) % spacing;
613
+ const isLine2 = d2 < lineWidth || d2 > spacing - lineWidth;
614
+
615
+ // If it's on a line, set foreground color
616
+ if (isLine1 || isLine2) {
617
+ const offset = (y * width + x) * 4;
618
+ data[offset] = foreground[0]; // R
619
+ data[offset + 1] = foreground[1]; // G
620
+ data[offset + 2] = foreground[2]; // B
621
+ data[offset + 3] = foreground[3]; // A
622
+ }
623
+ }
624
+ }
625
+
626
+ return data;
627
+ }
628
+
629
+ static isometric(width, height, options = {}) {
630
+ const {
631
+ cellSize = 20, // Controls the size of the diamonds
632
+ lineWidth = 1, // Thickness of the lines
633
+ background = [0, 0, 0, 0], // Transparent background
634
+ foreground = [0, 255, 0, 255], // Green lines
635
+ } = options;
636
+
637
+ const data = new Uint8ClampedArray(width * height * 4);
638
+
639
+ // Fill with background color
640
+ for (let i = 0; i < data.length; i += 4) {
641
+ data[i] = background[0];
642
+ data[i + 1] = background[1];
643
+ data[i + 2] = background[2];
644
+ data[i + 3] = background[3];
645
+ }
646
+
647
+ // Isometric dimensions (2:1 ratio)
648
+ const tileWidth = cellSize;
649
+ const tileHeight = cellSize / 2;
650
+
651
+ for (let y = 0; y < height; y++) {
652
+ for (let x = 0; x < width; x++) {
653
+ // Position within the repeating tile
654
+ const relX = x % tileWidth;
655
+ const relY = y % tileHeight;
656
+
657
+ // Distance from left and right edges of the diamond
658
+ const leftEdge = relY - relX / 2;
659
+ const rightEdge = relY + relX / 2 - tileHeight;
660
+
661
+ // Check if pixel is near any of the diamond edges
662
+ const nearLeftEdge = Math.abs(leftEdge) < lineWidth / 2;
663
+ const nearRightEdge = Math.abs(rightEdge) < lineWidth / 2;
664
+
665
+ // Draw the pixel if it's on an edge
666
+ if (nearLeftEdge || nearRightEdge) {
667
+ const offset = (y * width + x) * 4;
668
+ data[offset] = foreground[0];
669
+ data[offset + 1] = foreground[1];
670
+ data[offset + 2] = foreground[2];
671
+ data[offset + 3] = foreground[3];
672
+ }
673
+ }
674
+ }
675
+
676
+ return data;
677
+ }
678
+
679
+ static weave(width, height, options = {}) {
680
+ const {
681
+ tileSize = 40,
682
+ lineWidth = 2,
683
+ background = [255, 255, 255, 255], // white
684
+ foreground = [0, 0, 0, 255], // black
685
+ } = options;
686
+
687
+ const data = new Uint8ClampedArray(width * height * 4);
688
+
689
+ // Fill with background color first
690
+ for (let i = 0; i < data.length; i += 4) {
691
+ data[i] = background[0]; // R
692
+ data[i + 1] = background[1]; // G
693
+ data[i + 2] = background[2]; // B
694
+ data[i + 3] = background[3]; // A
695
+ }
696
+
697
+ // Create the pattern by calculating pixel positions directly
698
+ for (let y = 0; y < height; y++) {
699
+ for (let x = 0; x < width; x++) {
700
+ // Find position in the repeating pattern
701
+ const tileX = x % tileSize;
702
+ const tileY = y % tileSize;
703
+
704
+ // Calculate the three axes for our isometric pattern
705
+ // Each axis determines if we're on a line in that direction
706
+ const axisH =
707
+ Math.abs(((tileY + tileSize / 2) % tileSize) - tileSize / 2) <
708
+ lineWidth / 2;
709
+ const axis60 =
710
+ Math.abs(
711
+ ((tileX + tileY * 2 + tileSize * 1.5) % tileSize) - tileSize / 2
712
+ ) <
713
+ lineWidth / 2;
714
+ const axis120 =
715
+ Math.abs(
716
+ ((tileX - tileY * 2 + tileSize * 1.5) % tileSize) - tileSize / 2
717
+ ) <
718
+ lineWidth / 2;
719
+
720
+ // Determine if this pixel should be part of a line
721
+ const isLine = axisH || axis60 || axis120;
722
+
723
+ // If it's on a line, set foreground color
724
+ if (isLine) {
725
+ const offset = (y * width + x) * 4;
726
+ data[offset] = foreground[0]; // R
727
+ data[offset + 1] = foreground[1]; // G
728
+ data[offset + 2] = foreground[2]; // B
729
+ data[offset + 3] = foreground[3]; // A
730
+ }
731
+ }
732
+ }
733
+
734
+ return data;
735
+ }
736
+
737
+ /**
738
+ * Generates a Perlin noise pattern using the Noise class
739
+ * @param {number} width - Width of the pattern
740
+ * @param {number} height - Height of the pattern
741
+ * @param {{
742
+ * background?: [r, g, b, a],
743
+ * foreground?: [r, g, b, a],
744
+ * scale?: number,
745
+ * octaves?: number,
746
+ * persistence?: number,
747
+ * lacunarity?: number,
748
+ * seed?: number
749
+ * }} options - Configuration options
750
+ * @returns {Uint8ClampedArray} - RGBA pixel data
751
+ */
752
+ static perlinNoise(width, height, options = {}) {
753
+ const {
754
+ background = [0, 0, 0, 0],
755
+ foreground = [255, 255, 255, 255],
756
+ scale = 0.1,
757
+ octaves = 4,
758
+ persistence = 0.5,
759
+ lacunarity = 2.0,
760
+ seed = Math.random() * 65536,
761
+ } = options;
762
+
763
+ const data = new Uint8ClampedArray(width * height * 4);
764
+
765
+ // Set the seed for the noise generator
766
+ Noise.seed(seed);
767
+
768
+ for (let y = 0; y < height; y++) {
769
+ for (let x = 0; x < width; x++) {
770
+ let amplitude = 1;
771
+ let frequency = 1;
772
+ let noiseHeight = 0;
773
+ let maxValue = 0;
774
+
775
+ // Apply multiple octaves of noise
776
+ for (let i = 0; i < octaves; i++) {
777
+ const sampleX = x * scale * frequency;
778
+ const sampleY = y * scale * frequency;
779
+
780
+ // Get noise value in range [-1, 1]
781
+ const noiseValue = Noise.perlin2(sampleX, sampleY);
782
+
783
+ // Add weighted noise to total
784
+ noiseHeight += noiseValue * amplitude;
785
+
786
+ // Keep track of maximum possible values
787
+ maxValue += amplitude;
788
+
789
+ // Prepare for next octave
790
+ amplitude *= persistence;
791
+ frequency *= lacunarity;
792
+ }
793
+
794
+ // Normalize noise value to [0, 1] range
795
+ noiseHeight /= maxValue;
796
+
797
+ // Convert to [0, 1] range from [-1, 1]
798
+ const normalizedValue = (noiseHeight + 1) * 0.5;
799
+
800
+ // Get color based on noise value by interpolating between background and foreground
801
+ const color = [
802
+ Math.floor(
803
+ background[0] + normalizedValue * (foreground[0] - background[0])
804
+ ),
805
+ Math.floor(
806
+ background[1] + normalizedValue * (foreground[1] - background[1])
807
+ ),
808
+ Math.floor(
809
+ background[2] + normalizedValue * (foreground[2] - background[2])
810
+ ),
811
+ Math.floor(
812
+ background[3] + normalizedValue * (foreground[3] - background[3])
813
+ ),
814
+ ];
815
+
816
+ // Set pixel color
817
+ const offset = (y * width + x) * 4;
818
+ data.set(color, offset);
819
+ }
820
+ }
821
+
822
+ return data;
823
+ }
824
+
825
+ /**
826
+ * Creates a circular gradient pattern
827
+ * @param {number} width - Width of the pattern
828
+ * @param {number} height - Height of the pattern
829
+ * @param {{
830
+ * innerColor?: [r, g, b, a],
831
+ * outerColor?: [r, g, b, a],
832
+ * centerX?: number,
833
+ * centerY?: number,
834
+ * radius?: number,
835
+ * fadeExponent?: number
836
+ * }} options - Configuration options
837
+ * @returns {Uint8ClampedArray} - RGBA pixel data
838
+ */
839
+ static circularGradient(width, height, options = {}) {
840
+ const {
841
+ innerColor = [255, 255, 255, 255],
842
+ outerColor = [0, 0, 0, 255],
843
+ centerX = width / 2,
844
+ centerY = height / 2,
845
+ radius = Math.min(width, height) / 2,
846
+ fadeExponent = 1, // Controls how quickly the gradient fades (1 = linear)
847
+ } = options;
848
+
849
+ const data = new Uint8ClampedArray(width * height * 4);
850
+
851
+ for (let y = 0; y < height; y++) {
852
+ for (let x = 0; x < width; x++) {
853
+ const offset = (y * width + x) * 4;
854
+
855
+ // Calculate distance from center
856
+ const dx = x - centerX;
857
+ const dy = y - centerY;
858
+ const distance = Math.sqrt(dx * dx + dy * dy);
859
+
860
+ // Calculate gradient factor (0 to 1)
861
+ let factor = Math.min(distance / radius, 1.0);
862
+
863
+ // Apply fade exponent for non-linear gradients
864
+ factor = Math.pow(factor, fadeExponent);
865
+
866
+ // Interpolate between inner and outer colors
867
+ const color = [
868
+ Math.floor(innerColor[0] + factor * (outerColor[0] - innerColor[0])),
869
+ Math.floor(innerColor[1] + factor * (outerColor[1] - innerColor[1])),
870
+ Math.floor(innerColor[2] + factor * (outerColor[2] - innerColor[2])),
871
+ Math.floor(innerColor[3] + factor * (outerColor[3] - innerColor[3])),
872
+ ];
873
+
874
+ data.set(color, offset);
875
+ }
876
+ }
877
+
878
+ return data;
879
+ }
880
+
881
+ /**
882
+ * Creates a noise-based displacement pattern (distorted grid)
883
+ * @param {number} width - Width of the pattern
884
+ * @param {number} height - Height of the pattern
885
+ * @param {{
886
+ * gridSpacing?: number,
887
+ * gridColor?: [r, g, b, a],
888
+ * background?: [r, g, b, a],
889
+ * displacementScale?: number,
890
+ * noiseScale?: number,
891
+ * gridThickness?: number,
892
+ * seed?: number
893
+ * }} options - Configuration options
894
+ * @returns {Uint8ClampedArray} - RGBA pixel data
895
+ */
896
+ static noiseDisplacement(width, height, options = {}) {
897
+ const {
898
+ gridSpacing = 16,
899
+ gridColor = [255, 255, 255, 255],
900
+ background = [0, 0, 0, 0],
901
+ displacementScale = 8,
902
+ noiseScale = 0.05,
903
+ gridThickness = 1,
904
+ seed = Math.random() * 65536,
905
+ } = options;
906
+
907
+ const data = new Uint8ClampedArray(width * height * 4);
908
+
909
+ // Set the seed for the noise generator
910
+ Noise.seed(seed);
911
+
912
+ // Fill background first
913
+ for (let i = 0; i < data.length; i += 4) {
914
+ data.set(background, i);
915
+ }
916
+
917
+ // Apply distorted grid
918
+ for (let y = 0; y < height; y++) {
919
+ for (let x = 0; x < width; x++) {
920
+ // Get noise values for displacement
921
+ const noiseX = Noise.perlin2(x * noiseScale, y * noiseScale);
922
+ const noiseY = Noise.perlin2(
923
+ (x + 31.416) * noiseScale,
924
+ (y + 27.182) * noiseScale
925
+ );
926
+
927
+ // Apply displacement
928
+ const displacedX = x + noiseX * displacementScale;
929
+ const displacedY = y + noiseY * displacementScale;
930
+
931
+ // Check if this point is on a grid line
932
+ const isGridX =
933
+ displacedX % gridSpacing < gridThickness ||
934
+ displacedX % gridSpacing > gridSpacing - gridThickness;
935
+ const isGridY =
936
+ displacedY % gridSpacing < gridThickness ||
937
+ displacedY % gridSpacing > gridSpacing - gridThickness;
938
+
939
+ // If on a grid line, draw it
940
+ if (isGridX || isGridY) {
941
+ const offset = (y * width + x) * 4;
942
+ data.set(gridColor, offset);
943
+ }
944
+ }
945
+ }
946
+
947
+ return data;
948
+ }
949
+
950
+ /**
951
+ * Creates a dot pattern with either regular spacing or noise-based distribution
952
+ * @param {number} width - Width of the pattern
953
+ * @param {number} height - Height of the pattern
954
+ * @param {{
955
+ * dotSize?: number,
956
+ * spacing?: number,
957
+ * dotColor?: [r, g, b, a],
958
+ * background?: [r, g, b, a],
959
+ * useNoise?: boolean,
960
+ * noiseScale?: number,
961
+ * noiseDensity?: number,
962
+ * seed?: number
963
+ * }} options - Configuration options
964
+ * @returns {Uint8ClampedArray} - RGBA pixel data
965
+ */
966
+ static dotPattern(width, height, options = {}) {
967
+ const {
968
+ dotSize = 3,
969
+ spacing = 12,
970
+ dotColor = [0, 0, 0, 255],
971
+ background = [255, 255, 255, 255],
972
+ useNoise = false,
973
+ noiseScale = 0.1,
974
+ noiseDensity = 0.4, // Threshold for placing dots when using noise (0-1)
975
+ seed = Math.random() * 65536,
976
+ } = options;
977
+
978
+ const data = new Uint8ClampedArray(width * height * 4);
979
+
980
+ // Set the seed for the noise generator if using noise
981
+ if (useNoise) {
982
+ Noise.seed(seed);
983
+ }
984
+
985
+ // Fill with background color
986
+ for (let i = 0; i < data.length; i += 4) {
987
+ data.set(background, i);
988
+ }
989
+
990
+ if (useNoise) {
991
+ // Generate noise-based dot pattern
992
+ for (let y = 0; y < height; y++) {
993
+ for (let x = 0; x < width; x++) {
994
+ // Get noise value in range [-1, 1]
995
+ const noiseValue = Noise.perlin2(x * noiseScale, y * noiseScale);
996
+
997
+ // Convert to [0, 1] range
998
+ const normNoise = (noiseValue + 1) * 0.5;
999
+
1000
+ // Place dot if noise value exceeds threshold
1001
+ if (normNoise > noiseDensity) {
1002
+ // Draw dot
1003
+ for (let dy = -dotSize; dy <= dotSize; dy++) {
1004
+ for (let dx = -dotSize; dx <= dotSize; dx++) {
1005
+ const px = x + dx;
1006
+ const py = y + dy;
1007
+
1008
+ // Check if within bounds and within circle
1009
+ if (px >= 0 && px < width && py >= 0 && py < height) {
1010
+ const dist = dx * dx + dy * dy;
1011
+ if (dist <= dotSize * dotSize) {
1012
+ const offset = (py * width + px) * 4;
1013
+ data.set(dotColor, offset);
1014
+ }
1015
+ }
1016
+ }
1017
+ }
1018
+ }
1019
+ }
1020
+ }
1021
+ } else {
1022
+ // Generate regular grid dot pattern
1023
+ for (let y = Math.floor(spacing / 2); y < height; y += spacing) {
1024
+ for (let x = Math.floor(spacing / 2); x < width; x += spacing) {
1025
+ // Draw dot
1026
+ for (let dy = -dotSize; dy <= dotSize; dy++) {
1027
+ for (let dx = -dotSize; dx <= dotSize; dx++) {
1028
+ const px = x + dx;
1029
+ const py = y + dy;
1030
+
1031
+ // Check if within bounds and within circle
1032
+ if (px >= 0 && px < width && py >= 0 && py < height) {
1033
+ const dist = dx * dx + dy * dy;
1034
+ if (dist <= dotSize * dotSize) {
1035
+ const offset = (py * width + px) * 4;
1036
+ data.set(dotColor, offset);
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+
1045
+ return data;
1046
+ }
1047
+
1048
+ /**
1049
+ * Generates a Voronoi cell pattern that tiles seamlessly
1050
+ * @param {number} width - Width of the pattern
1051
+ * @param {number} height - Height of the pattern
1052
+ * @param {{
1053
+ * cellCount?: number,
1054
+ * cellColors?: [r, g, b, a][],
1055
+ * edgeColor?: [r, g, b, a],
1056
+ * edgeThickness?: number,
1057
+ * seed?: number,
1058
+ * jitter?: number,
1059
+ * baseColor?: [r, g, b, a],
1060
+ * colorVariation?: number
1061
+ * }} options - Configuration options
1062
+ * @returns {Uint8ClampedArray} - RGBA pixel data
1063
+ */
1064
+ static voronoi(width, height, options = {}) {
1065
+ const {
1066
+ cellCount = 20,
1067
+ cellColors = null, // Will generate random colors if null
1068
+ edgeColor = [0, 0, 0, 255],
1069
+ edgeThickness = 1.5,
1070
+ seed = Math.random() * 1000,
1071
+ jitter = 0.5, // How much to randomize cell positions (0-1)
1072
+ baseColor = null, // Base color for theming, if null will use random colors
1073
+ colorVariation = 0.3, // How much variation to add to the base color (0-1)
1074
+ } = options;
1075
+
1076
+ const data = new Uint8ClampedArray(width * height * 4);
1077
+
1078
+ // Use Perlin noise for better randomness
1079
+ Noise.seed(seed);
1080
+
1081
+ // Generate cell centers in a more structured grid for better Voronoi appearance
1082
+ const cellPoints = [];
1083
+ const colors = [];
1084
+
1085
+ // Use a simple seeded random number generator
1086
+ const random = () => {
1087
+ let x = Math.sin(seed * 0.167 + cellPoints.length * 0.423) * 10000;
1088
+ return x - Math.floor(x);
1089
+ };
1090
+
1091
+ // Calculate grid dimensions based on cell count
1092
+ const gridSize = Math.sqrt(cellCount);
1093
+ const cellWidth = width / gridSize;
1094
+ const cellHeight = height / gridSize;
1095
+
1096
+ // Helper function to generate a color based on the base color
1097
+ const generateColorFromBase = (index) => {
1098
+ if (baseColor) {
1099
+ // Extract RGBA from base color
1100
+ const [r, g, b, a] = baseColor;
1101
+
1102
+ // Convert RGB to HSL for better color variation
1103
+ // This lets us vary hue and saturation while keeping colors in the same family
1104
+ const max = Math.max(r, g, b) / 255;
1105
+ const min = Math.min(r, g, b) / 255;
1106
+ const l = (max + min) / 2;
1107
+
1108
+ let h, s;
1109
+
1110
+ if (max === min) {
1111
+ h = s = 0; // achromatic
1112
+ } else {
1113
+ const d = max - min;
1114
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1115
+
1116
+ if (max === r / 255) {
1117
+ h = (g / 255 - b / 255) / d + (g / 255 < b / 255 ? 6 : 0);
1118
+ } else if (max === g / 255) {
1119
+ h = (b / 255 - r / 255) / d + 2;
1120
+ } else {
1121
+ h = (r / 255 - g / 255) / d + 4;
1122
+ }
1123
+ h /= 6;
1124
+ }
1125
+
1126
+ // Add variation to HSL values
1127
+ // Perlin noise creates smoother variations
1128
+ const hueVariation =
1129
+ Noise.perlin2(index * 0.15, 0) * colorVariation * 0.3;
1130
+ const satVariation = Noise.perlin2(0, index * 0.15) * colorVariation;
1131
+ const lightVariation =
1132
+ Noise.perlin2(index * 0.15, index * 0.15) * colorVariation * 0.5;
1133
+
1134
+ // Apply variations
1135
+ h = (h + hueVariation) % 1.0; // Keep hue in [0,1] range
1136
+ s = Math.min(1, Math.max(0, s * (1 + satVariation))); // Keep saturation in [0,1]
1137
+ const newL = Math.min(0.9, Math.max(0.1, l * (1 + lightVariation))); // Keep lightness in reasonable range
1138
+
1139
+ // Convert back to RGB
1140
+ let r1, g1, b1;
1141
+
1142
+ if (s === 0) {
1143
+ r1 = g1 = b1 = newL; // achromatic
1144
+ } else {
1145
+ const hue2rgb = (p, q, t) => {
1146
+ if (t < 0) t += 1;
1147
+ if (t > 1) t -= 1;
1148
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
1149
+ if (t < 1 / 2) return q;
1150
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1151
+ return p;
1152
+ };
1153
+
1154
+ const q = newL < 0.5 ? newL * (1 + s) : newL + s - newL * s;
1155
+ const p = 2 * newL - q;
1156
+
1157
+ r1 = hue2rgb(p, q, h + 1 / 3);
1158
+ g1 = hue2rgb(p, q, h);
1159
+ b1 = hue2rgb(p, q, h - 1 / 3);
1160
+ }
1161
+
1162
+ // Add slight random noise for texture
1163
+ const noiseAmount = 0.05;
1164
+ const noise = () => (random() * 2 - 1) * noiseAmount;
1165
+
1166
+ // Return final color
1167
+ return [
1168
+ Math.min(255, Math.max(0, Math.floor(r1 * 255 * (1 + noise())))),
1169
+ Math.min(255, Math.max(0, Math.floor(g1 * 255 * (1 + noise())))),
1170
+ Math.min(255, Math.max(0, Math.floor(b1 * 255 * (1 + noise())))),
1171
+ a,
1172
+ ];
1173
+ } else {
1174
+ // Generate colors with better distribution using golden ratio
1175
+ const hue = (index * 0.618033988749895) % 1; // Golden ratio distribution
1176
+
1177
+ // Convert HSV to RGB for better color distribution
1178
+ let r, g, b;
1179
+ const h = hue * 6;
1180
+ const i = Math.floor(h);
1181
+ const f = h - i;
1182
+ const p = 0.5;
1183
+ const q = 0.5 * (1 - f);
1184
+ const t = 0.5 * (1 - (1 - f));
1185
+
1186
+ switch (i % 6) {
1187
+ case 0:
1188
+ r = 0.5;
1189
+ g = t;
1190
+ b = p;
1191
+ break;
1192
+ case 1:
1193
+ r = q;
1194
+ g = 0.5;
1195
+ b = p;
1196
+ break;
1197
+ case 2:
1198
+ r = p;
1199
+ g = 0.5;
1200
+ b = t;
1201
+ break;
1202
+ case 3:
1203
+ r = p;
1204
+ g = q;
1205
+ b = 0.5;
1206
+ break;
1207
+ case 4:
1208
+ r = t;
1209
+ g = p;
1210
+ b = 0.5;
1211
+ break;
1212
+ case 5:
1213
+ r = 0.5;
1214
+ g = p;
1215
+ b = q;
1216
+ break;
1217
+ }
1218
+
1219
+ return [
1220
+ Math.floor(r * 255 + 50 + random() * 100),
1221
+ Math.floor(g * 255 + 50 + random() * 100),
1222
+ Math.floor(b * 255 + 50 + random() * 100),
1223
+ 255,
1224
+ ];
1225
+ }
1226
+ };
1227
+
1228
+ // Generate cell points in a grid with jitter
1229
+ for (let gridY = 0; gridY < gridSize; gridY++) {
1230
+ for (let gridX = 0; gridX < gridSize; gridX++) {
1231
+ if (cellPoints.length >= cellCount) break;
1232
+
1233
+ // Base position in grid
1234
+ const baseX = gridX * cellWidth + cellWidth / 2;
1235
+ const baseY = gridY * cellHeight + cellHeight / 2;
1236
+
1237
+ // Add jitter
1238
+ const jitterX = (random() * 2 - 1) * jitter * cellWidth;
1239
+ const jitterY = (random() * 2 - 1) * jitter * cellHeight;
1240
+
1241
+ cellPoints.push({
1242
+ x: Math.floor(baseX + jitterX),
1243
+ y: Math.floor(baseY + jitterY),
1244
+ });
1245
+
1246
+ // Generate color for this cell
1247
+ if (cellColors && cellPoints.length - 1 < cellColors.length) {
1248
+ colors.push(cellColors[cellPoints.length - 1]);
1249
+ } else {
1250
+ colors.push(generateColorFromBase(cellPoints.length - 1));
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ // Tiled distance calculation for seamless wrapping
1256
+ const tiledDistance = (x1, y1, x2, y2) => {
1257
+ // Calculate direct distance
1258
+ let dx = Math.abs(x1 - x2);
1259
+ let dy = Math.abs(y1 - y2);
1260
+
1261
+ // Consider wrapping around for tiling
1262
+ dx = Math.min(dx, width - dx);
1263
+ dy = Math.min(dy, height - dy);
1264
+
1265
+ // Use a mix of Euclidean and Manhattan distance for interesting patterns
1266
+ const euclidean = Math.sqrt(dx * dx + dy * dy);
1267
+ const manhattan = dx + dy;
1268
+
1269
+ return euclidean * 0.8 + manhattan * 0.2;
1270
+ };
1271
+
1272
+ // Generate the pattern
1273
+ for (let y = 0; y < height; y++) {
1274
+ for (let x = 0; x < width; x++) {
1275
+ const offset = (y * width + x) * 4;
1276
+
1277
+ // Find two closest cell centers
1278
+ let closestDist = Infinity;
1279
+ let secondClosestDist = Infinity;
1280
+ let closestIndex = 0;
1281
+
1282
+ for (let i = 0; i < cellPoints.length; i++) {
1283
+ const dist = tiledDistance(x, y, cellPoints[i].x, cellPoints[i].y);
1284
+
1285
+ if (dist < closestDist) {
1286
+ secondClosestDist = closestDist;
1287
+ closestDist = dist;
1288
+ closestIndex = i;
1289
+ } else if (dist < secondClosestDist) {
1290
+ secondClosestDist = dist;
1291
+ }
1292
+ }
1293
+
1294
+ // Multiple wrappings for better tiling at edges
1295
+ // Check distances to virtual cells across the boundaries
1296
+ for (let i = 0; i < cellPoints.length; i++) {
1297
+ // Check cell wrapping in all 8 directions
1298
+ for (let wrapX = -1; wrapX <= 1; wrapX++) {
1299
+ for (let wrapY = -1; wrapY <= 1; wrapY++) {
1300
+ if (wrapX === 0 && wrapY === 0) continue; // Skip the original cell
1301
+
1302
+ const wrappedX = cellPoints[i].x + wrapX * width;
1303
+ const wrappedY = cellPoints[i].y + wrapY * height;
1304
+
1305
+ const dist = Math.sqrt(
1306
+ Math.pow(x - wrappedX, 2) + Math.pow(y - wrappedY, 2)
1307
+ );
1308
+
1309
+ if (dist < closestDist) {
1310
+ secondClosestDist = closestDist;
1311
+ closestDist = dist;
1312
+ closestIndex = i;
1313
+ } else if (dist < secondClosestDist) {
1314
+ secondClosestDist = dist;
1315
+ }
1316
+ }
1317
+ }
1318
+ }
1319
+
1320
+ // More precise edge detection
1321
+ const edgeDist = secondClosestDist - closestDist;
1322
+ const isEdge = edgeDist < edgeThickness;
1323
+
1324
+ // Set pixel color
1325
+ if (isEdge) {
1326
+ data.set(edgeColor, offset);
1327
+ } else {
1328
+ data.set(colors[closestIndex], offset);
1329
+ }
1330
+ }
1331
+ }
1332
+
1333
+ return data;
1334
+ }
1335
+
1336
+ static penrose(width, height, options = {}) {
1337
+ return generatePenroseTilingPixels(width, height, options);
1338
+ }
1339
+ }