@guinetik/gcanvas 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- package/vitest.config.js +13 -0
|
@@ -0,0 +1,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
|
+
}
|