@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,226 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Prism - A 3D-looking isometric triangular prism with rotation support.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - Individual face colors
|
|
9
|
+
* - Full bounding box
|
|
10
|
+
* - Face visibility control (top, bottom, left, right, front, back)
|
|
11
|
+
* - Rotation around X, Y, and Z axes
|
|
12
|
+
*
|
|
13
|
+
* Note: This is a 2.5D visual illusion — not actual 3D rendering.
|
|
14
|
+
*/
|
|
15
|
+
export class Prism extends Shape {
|
|
16
|
+
/**
|
|
17
|
+
* Create a triangular prism
|
|
18
|
+
* @param {number} x - X position (center of the prism)
|
|
19
|
+
* @param {number} y - Y position (center of the prism)
|
|
20
|
+
* @param {number} width - Width of the prism
|
|
21
|
+
* @param {number} height - Height of the triangular face
|
|
22
|
+
* @param {number} depth - Depth/length of the prism
|
|
23
|
+
* @param {object} options - Customization options
|
|
24
|
+
* @param {string} [options.faceTopColor] - Color of the top face
|
|
25
|
+
* @param {string} [options.faceBottomColor] - Color of the bottom face
|
|
26
|
+
* @param {string} [options.faceLeftColor] - Color of the left face
|
|
27
|
+
* @param {string} [options.faceRightColor] - Color of the right face
|
|
28
|
+
* @param {string} [options.faceFrontColor] - Color of the front face
|
|
29
|
+
* @param {string} [options.faceBackColor] - Color of the back face
|
|
30
|
+
* @param {Array<string>} [options.visibleFaces] - Array of face keys to render
|
|
31
|
+
* @param {string} [options.stroke] - Optional stroke around each face
|
|
32
|
+
* @param {number} [options.lineWidth] - Stroke width
|
|
33
|
+
* @param {number} [options.rotationX] - Rotation around X axis in radians
|
|
34
|
+
* @param {number} [options.rotationY] - Rotation around Y axis in radians
|
|
35
|
+
* @param {number} [options.rotationZ] - Rotation around Z axis in radians
|
|
36
|
+
*/
|
|
37
|
+
constructor(depth = 100, options = {}) {
|
|
38
|
+
super(options);
|
|
39
|
+
this.depth = depth;
|
|
40
|
+
|
|
41
|
+
this.faceTopColor = options.faceTopColor || "#eee";
|
|
42
|
+
this.faceBottomColor = options.faceBottomColor || "#ccc";
|
|
43
|
+
this.faceLeftColor = options.faceLeftColor || "#aaa";
|
|
44
|
+
this.faceRightColor = options.faceRightColor || "#888";
|
|
45
|
+
this.faceFrontColor = options.faceFrontColor || "#666";
|
|
46
|
+
this.faceBackColor = options.faceBackColor || "#444";
|
|
47
|
+
|
|
48
|
+
this.stroke = options.stroke || null;
|
|
49
|
+
this.lineWidth = options.lineWidth || 1;
|
|
50
|
+
|
|
51
|
+
// Rotation angles (in radians)
|
|
52
|
+
this.rotationX = options.rotationX || 0;
|
|
53
|
+
this.rotationY = options.rotationY || 0;
|
|
54
|
+
this.rotationZ = options.rotationZ || 0;
|
|
55
|
+
|
|
56
|
+
/** @type {Array<'top'|'bottom'|'left'|'right'|'front'|'back'>} */
|
|
57
|
+
this.visibleFaces = options.visibleFaces || [
|
|
58
|
+
"top",
|
|
59
|
+
"left",
|
|
60
|
+
"right",
|
|
61
|
+
"front",
|
|
62
|
+
"back",
|
|
63
|
+
"bottom",
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set rotation angles
|
|
69
|
+
* @param {number} x - Rotation around X axis in radians
|
|
70
|
+
* @param {number} y - Rotation around Y axis in radians
|
|
71
|
+
* @param {number} z - Rotation around Z axis in radians
|
|
72
|
+
*/
|
|
73
|
+
setRotation(x, y, z) {
|
|
74
|
+
this.rotationX = x;
|
|
75
|
+
this.rotationY = y;
|
|
76
|
+
this.rotationZ = z;
|
|
77
|
+
return this; // Enable method chaining
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Rotate the prism incrementally
|
|
82
|
+
* @param {number} x - Increment for X rotation in radians
|
|
83
|
+
* @param {number} y - Increment for Y rotation in radians
|
|
84
|
+
* @param {number} z - Increment for Z rotation in radians
|
|
85
|
+
*/
|
|
86
|
+
rotate(x, y, z) {
|
|
87
|
+
this.rotationX += x;
|
|
88
|
+
this.rotationY += y;
|
|
89
|
+
this.rotationZ += z;
|
|
90
|
+
return this; // Enable method chaining
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Internal draw logic
|
|
95
|
+
*/
|
|
96
|
+
draw() {
|
|
97
|
+
super.draw();
|
|
98
|
+
const w = this.width / 2; // Half width for positioning
|
|
99
|
+
const h = this.height / 2; // Half height for positioning
|
|
100
|
+
const d = this.depth / 2; // Half depth for positioning
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Apply 3D rotation to a point
|
|
104
|
+
* @param {number} x
|
|
105
|
+
* @param {number} y
|
|
106
|
+
* @param {number} z
|
|
107
|
+
* @returns {{x: number, y: number, z: number}}
|
|
108
|
+
*/
|
|
109
|
+
const rotate3D = (x, y, z) => {
|
|
110
|
+
// Apply X-axis rotation
|
|
111
|
+
let y1 = y;
|
|
112
|
+
let z1 = z;
|
|
113
|
+
y = y1 * Math.cos(this.rotationX) - z1 * Math.sin(this.rotationX);
|
|
114
|
+
z = y1 * Math.sin(this.rotationX) + z1 * Math.cos(this.rotationX);
|
|
115
|
+
|
|
116
|
+
// Apply Y-axis rotation
|
|
117
|
+
let x1 = x;
|
|
118
|
+
z1 = z;
|
|
119
|
+
x = x1 * Math.cos(this.rotationY) + z1 * Math.sin(this.rotationY);
|
|
120
|
+
z = -x1 * Math.sin(this.rotationY) + z1 * Math.cos(this.rotationY);
|
|
121
|
+
|
|
122
|
+
// Apply Z-axis rotation
|
|
123
|
+
x1 = x;
|
|
124
|
+
y1 = y;
|
|
125
|
+
x = x1 * Math.cos(this.rotationZ) - y1 * Math.sin(this.rotationZ);
|
|
126
|
+
y = x1 * Math.sin(this.rotationZ) + y1 * Math.cos(this.rotationZ);
|
|
127
|
+
|
|
128
|
+
return { x, y, z };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Isometric projection of 3D point
|
|
133
|
+
* @param {number} x
|
|
134
|
+
* @param {number} y
|
|
135
|
+
* @param {number} z
|
|
136
|
+
* @returns {{x: number, y: number}}
|
|
137
|
+
*/
|
|
138
|
+
const iso = (x, y, z) => {
|
|
139
|
+
// Apply rotations first
|
|
140
|
+
const rotated = rotate3D(x, y, z);
|
|
141
|
+
|
|
142
|
+
// Then apply isometric projection
|
|
143
|
+
const isoX = (rotated.x - rotated.y) * Math.cos(Math.PI / 6);
|
|
144
|
+
const isoY = (rotated.x + rotated.y) * Math.sin(Math.PI / 6) - rotated.z;
|
|
145
|
+
return { x: isoX, y: isoY, z: rotated.z }; // Include z for depth sorting
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Define vertices for the triangular prism
|
|
149
|
+
const p = {
|
|
150
|
+
// Front triangular face
|
|
151
|
+
p0: iso(-w, -d, -h), // Front bottom left
|
|
152
|
+
p1: iso(w, -d, -h), // Front bottom right
|
|
153
|
+
p2: iso(0, -d, h), // Front top center
|
|
154
|
+
|
|
155
|
+
// Back triangular face
|
|
156
|
+
p3: iso(-w, d, -h), // Back bottom left
|
|
157
|
+
p4: iso(w, d, -h), // Back bottom right
|
|
158
|
+
p5: iso(0, d, h), // Back top center
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Faces mapped to corner points
|
|
162
|
+
const faces = {
|
|
163
|
+
// Triangular front and back faces
|
|
164
|
+
front: { points: [p.p0, p.p1, p.p2], color: this.faceFrontColor },
|
|
165
|
+
back: { points: [p.p3, p.p4, p.p5], color: this.faceBackColor },
|
|
166
|
+
|
|
167
|
+
// Rectangular side faces
|
|
168
|
+
bottom: { points: [p.p0, p.p1, p.p4, p.p3], color: this.faceBottomColor },
|
|
169
|
+
right: { points: [p.p1, p.p2, p.p5, p.p4], color: this.faceRightColor },
|
|
170
|
+
left: { points: [p.p0, p.p2, p.p5, p.p3], color: this.faceLeftColor },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Calculate visibility based on depth sorting
|
|
174
|
+
const visibleFacesWithDepth = this.visibleFaces
|
|
175
|
+
.filter((key) => faces[key])
|
|
176
|
+
.map((key) => {
|
|
177
|
+
const face = faces[key];
|
|
178
|
+
|
|
179
|
+
// Calculate face center for z-ordering
|
|
180
|
+
const centerX =
|
|
181
|
+
face.points.reduce((sum, pt) => sum + pt.x, 0) / face.points.length;
|
|
182
|
+
const centerY =
|
|
183
|
+
face.points.reduce((sum, pt) => sum + pt.y, 0) / face.points.length;
|
|
184
|
+
const centerZ =
|
|
185
|
+
face.points.reduce((sum, pt) => sum + (pt.z || 0), 0) /
|
|
186
|
+
face.points.length;
|
|
187
|
+
|
|
188
|
+
// Calculate approximate depth
|
|
189
|
+
// Higher value = farther back
|
|
190
|
+
const depth = centerX * centerX + centerY * centerY + centerZ * centerZ;
|
|
191
|
+
|
|
192
|
+
return { key, face, depth };
|
|
193
|
+
})
|
|
194
|
+
.sort((a, b) => b.depth - a.depth); // Sort by depth (back to front)
|
|
195
|
+
|
|
196
|
+
// Draw faces in depth order
|
|
197
|
+
visibleFacesWithDepth.forEach(({ key, face }) => {
|
|
198
|
+
if (face?.color) {
|
|
199
|
+
Painter.shapes.polygon(
|
|
200
|
+
face.points,
|
|
201
|
+
face.color,
|
|
202
|
+
this.stroke,
|
|
203
|
+
this.lineWidth
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Compute bounding box for interactivity and layout
|
|
211
|
+
* @returns {{x: number, y: number, width: number, height: number}}
|
|
212
|
+
*/
|
|
213
|
+
getBounds() {
|
|
214
|
+
// Calculate actual bounds based on isometric projection
|
|
215
|
+
const projectionFactor = 1.5; // Approximation for isometric projection
|
|
216
|
+
const maxDimension = Math.max(this.width, this.height, this.depth);
|
|
217
|
+
const adjustedSize = maxDimension * projectionFactor;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
x: this.x - adjustedSize / 2,
|
|
221
|
+
y: this.y - adjustedSize / 2,
|
|
222
|
+
width: adjustedSize,
|
|
223
|
+
height: adjustedSize,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rectangle - A drawable centered rectangle using the canvas.
|
|
6
|
+
*
|
|
7
|
+
* Draws a rectangle from its center (not top-left) using Painter.
|
|
8
|
+
*/
|
|
9
|
+
export class Rectangle extends Shape {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
super(options);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Renders the rectangle using Painter.
|
|
16
|
+
*/
|
|
17
|
+
draw() {
|
|
18
|
+
super.draw(); // Apply constraints from Shape
|
|
19
|
+
this.drawRect();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
drawRect() {
|
|
23
|
+
// When in a group context, use relative coordinates
|
|
24
|
+
const x = -this.width / 2;
|
|
25
|
+
const y = -this.height / 2;
|
|
26
|
+
|
|
27
|
+
if (this.color) {
|
|
28
|
+
Painter.shapes.rect(x, y, this.width, this.height, this.color);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (this.stroke) {
|
|
32
|
+
Painter.shapes.outlineRect(x, y, this.width, this.height, this.stroke, this.lineWidth);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { Geometry2d } from "./geometry.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
import { Traceable } from "./traceable.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renderable
|
|
7
|
+
* ----------
|
|
8
|
+
*
|
|
9
|
+
* A render-capable spatial object in the gcanvas engine.
|
|
10
|
+
*
|
|
11
|
+
* This class introduces the core rendering lifecycle — it knows when to draw,
|
|
12
|
+
* how to draw, and how to **not draw** when conditions say so (invisible, opacity = 0).
|
|
13
|
+
*
|
|
14
|
+
* ### Architectural Role
|
|
15
|
+
*
|
|
16
|
+
* - Adds **rendering lifecycle control** (`render()`)
|
|
17
|
+
* - Supports **canvas state management** (save/restore)
|
|
18
|
+
* - Exposes **visual props** like opacity, visibility, and debug
|
|
19
|
+
* - Optional **shadow styling** support
|
|
20
|
+
*
|
|
21
|
+
* `Renderable` should be extended by anything that draws visuals:
|
|
22
|
+
* - Shapes (e.g., `Rectangle`, `Text`)
|
|
23
|
+
* - Sprites
|
|
24
|
+
* - UI elements
|
|
25
|
+
*
|
|
26
|
+
* @abstract
|
|
27
|
+
* @extends Geometry2d
|
|
28
|
+
*/
|
|
29
|
+
export class Renderable extends Traceable {
|
|
30
|
+
/**
|
|
31
|
+
* @param {Object} [options={}]
|
|
32
|
+
* @param {boolean} [options.visible=true] - Whether this object should be drawn
|
|
33
|
+
* @param {number} [options.opacity=1] - Alpha transparency (0–1)
|
|
34
|
+
* @param {boolean} [options.active=true] - Whether this object receives updates
|
|
35
|
+
* @param {string} [options.blendMode="source-over"] - Optional blend mode
|
|
36
|
+
* @param {string} [options.shadowColor] - Optional shadow color
|
|
37
|
+
* @param {number} [options.shadowBlur=0] - Shadow blur radius
|
|
38
|
+
* @param {number} [options.shadowOffsetX=0] - Shadow X offset
|
|
39
|
+
* @param {number} [options.shadowOffsetY=0] - Shadow Y offset
|
|
40
|
+
* @param {number} [options.zIndex=0] - Z-index for stacking order
|
|
41
|
+
*/
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
super(options);
|
|
44
|
+
this._visible = options.visible !== false;
|
|
45
|
+
this._opacity = typeof options.opacity === "number" ? options.opacity : 1;
|
|
46
|
+
this._active = options.active !== false;
|
|
47
|
+
this.zIndex = options.zIndex ?? 0;
|
|
48
|
+
|
|
49
|
+
this._shadowColor = options.shadowColor ?? undefined;
|
|
50
|
+
this._shadowBlur = options.shadowBlur ?? 0;
|
|
51
|
+
this._shadowOffsetX = options.shadowOffsetX ?? 0;
|
|
52
|
+
this._shadowOffsetY = options.shadowOffsetY ?? 0;
|
|
53
|
+
|
|
54
|
+
// Render caching - when enabled, renders to an offscreen canvas once
|
|
55
|
+
// and blits the cached bitmap on subsequent frames for better performance.
|
|
56
|
+
this._cacheRendering = options.cacheRendering ?? false;
|
|
57
|
+
this._cacheCanvas = null;
|
|
58
|
+
this._cacheDirty = true;
|
|
59
|
+
this._cachePadding = options.cachePadding ?? 2; // Extra padding for anti-aliasing/strokes
|
|
60
|
+
|
|
61
|
+
this._tick = 0;
|
|
62
|
+
this.logger.log("Renderable", this.x, this.y, this.width, this.height);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Main render method.
|
|
67
|
+
* Handles visibility, translation, and calls draw() in the transformed context.
|
|
68
|
+
* If cacheRendering is enabled, renders to an offscreen canvas once and blits it.
|
|
69
|
+
*/
|
|
70
|
+
render() {
|
|
71
|
+
if (!this._visible || this._opacity <= 0) return;
|
|
72
|
+
|
|
73
|
+
Painter.save();
|
|
74
|
+
Painter.effects.setBlendMode(this._blendMode);
|
|
75
|
+
|
|
76
|
+
if (this.crisp) {
|
|
77
|
+
Painter.translateTo(Math.round(this.x), Math.round(this.y));
|
|
78
|
+
} else {
|
|
79
|
+
Painter.translateTo(this.x, this.y);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.applyShadow(Painter.ctx);
|
|
83
|
+
|
|
84
|
+
// If caching IS NOT enabled or we're the base class, render normally
|
|
85
|
+
if (!this._cacheRendering || this.constructor.name === "Renderable") {
|
|
86
|
+
Painter.opacity.pushOpacity(this._opacity);
|
|
87
|
+
this.draw();
|
|
88
|
+
Painter.opacity.popOpacity();
|
|
89
|
+
} else {
|
|
90
|
+
// Caching logic - cache the IDENTITY shape (untransformed)
|
|
91
|
+
const rawWidth = typeof this.width === "number" ? this.width : 0;
|
|
92
|
+
const rawHeight = typeof this.height === "number" ? this.height : 0;
|
|
93
|
+
const padding = this._cachePadding * 2;
|
|
94
|
+
const cacheWidth = Math.ceil(rawWidth + padding) || 1;
|
|
95
|
+
const cacheHeight = Math.ceil(rawHeight + padding) || 1;
|
|
96
|
+
|
|
97
|
+
// Create or resize cache canvas if needed
|
|
98
|
+
if (
|
|
99
|
+
!this._cacheCanvas ||
|
|
100
|
+
this._cacheCanvas.width !== cacheWidth ||
|
|
101
|
+
this._cacheCanvas.height !== cacheHeight
|
|
102
|
+
) {
|
|
103
|
+
this._cacheCanvas = document.createElement("canvas");
|
|
104
|
+
this._cacheCanvas.width = cacheWidth;
|
|
105
|
+
this._cacheCanvas.height = cacheHeight;
|
|
106
|
+
this._cacheDirty = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Re-render to cache if dirty
|
|
110
|
+
if (this._cacheDirty) {
|
|
111
|
+
this._renderToCache(cacheWidth, cacheHeight);
|
|
112
|
+
this._cacheDirty = false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Blit cached canvas with current opacity AND transforms
|
|
116
|
+
// This allows efficient rotation/scaling of the cached bitmap
|
|
117
|
+
Painter.opacity.pushOpacity(this._opacity);
|
|
118
|
+
|
|
119
|
+
// Extract transform properties if they exist
|
|
120
|
+
const rotation = this.rotation ?? 0;
|
|
121
|
+
const scaleX = this.scaleX ?? 1;
|
|
122
|
+
const scaleY = this.scaleY ?? 1;
|
|
123
|
+
|
|
124
|
+
Painter.img.draw(this._cacheCanvas, 0, 0, {
|
|
125
|
+
width: cacheWidth,
|
|
126
|
+
height: cacheHeight,
|
|
127
|
+
rotation: rotation,
|
|
128
|
+
scaleX: scaleX,
|
|
129
|
+
scaleY: scaleY,
|
|
130
|
+
anchor: "center",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
Painter.opacity.popOpacity();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Painter.restore();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Internal method to render the object to the offscreen cache canvas.
|
|
141
|
+
* @param {number} width - Cache width
|
|
142
|
+
* @param {number} height - Cache height
|
|
143
|
+
* @protected
|
|
144
|
+
*/
|
|
145
|
+
_renderToCache(width, height) {
|
|
146
|
+
const cacheCtx = this._cacheCanvas.getContext("2d");
|
|
147
|
+
cacheCtx.clearRect(0, 0, width, height);
|
|
148
|
+
|
|
149
|
+
// Swap to cache context
|
|
150
|
+
const mainCtx = Painter.ctx;
|
|
151
|
+
Painter.ctx = cacheCtx;
|
|
152
|
+
|
|
153
|
+
// Signal subclasses to skip transforms if they are transform-aware
|
|
154
|
+
this._isCaching = true;
|
|
155
|
+
|
|
156
|
+
cacheCtx.save();
|
|
157
|
+
// Translate to center of cache so (0,0) draws correctly
|
|
158
|
+
cacheCtx.translate(width / 2, height / 2);
|
|
159
|
+
|
|
160
|
+
// Call draw() to render to the cache at full opacity and identity transform
|
|
161
|
+
this.draw();
|
|
162
|
+
|
|
163
|
+
cacheCtx.restore();
|
|
164
|
+
|
|
165
|
+
this._isCaching = false;
|
|
166
|
+
|
|
167
|
+
// Restore main context
|
|
168
|
+
Painter.ctx = mainCtx;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Mark the render cache as needing refresh.
|
|
173
|
+
*/
|
|
174
|
+
invalidateCache() {
|
|
175
|
+
this._cacheDirty = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
draw() {
|
|
179
|
+
this.drawDebug();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Called once per frame if the object is active.
|
|
184
|
+
* @param {number} dt - Time delta since last frame (seconds)
|
|
185
|
+
*/
|
|
186
|
+
update(dt) {
|
|
187
|
+
this.trace("Renderable.update");
|
|
188
|
+
this._tick += dt;
|
|
189
|
+
super.update(dt);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Apply shadow styles to the current canvas context.
|
|
194
|
+
* Only called if `shadowColor` is defined.
|
|
195
|
+
*
|
|
196
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
197
|
+
*/
|
|
198
|
+
applyShadow(ctx) {
|
|
199
|
+
if (!this._shadowColor) return;
|
|
200
|
+
ctx.shadowColor = this._shadowColor;
|
|
201
|
+
ctx.shadowBlur = this._shadowBlur;
|
|
202
|
+
ctx.shadowOffsetX = this._shadowOffsetX;
|
|
203
|
+
ctx.shadowOffsetY = this._shadowOffsetY;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Gets whether the object is visible (drawn during render).
|
|
208
|
+
* @type {boolean}
|
|
209
|
+
*/
|
|
210
|
+
get visible() {
|
|
211
|
+
return this._visible;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
set visible(v) {
|
|
215
|
+
this._visible = Boolean(v);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
get width() {
|
|
219
|
+
return super.width;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
set width(v) {
|
|
223
|
+
super.width = v;
|
|
224
|
+
this.invalidateCache();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
get height() {
|
|
228
|
+
return super.height;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
set height(v) {
|
|
232
|
+
super.height = v;
|
|
233
|
+
this.invalidateCache();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Gets whether the object is active (updated during game loop).
|
|
238
|
+
* @type {boolean}
|
|
239
|
+
*/
|
|
240
|
+
get active() {
|
|
241
|
+
return this._active;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
set active(v) {
|
|
245
|
+
this._active = Boolean(v);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Gets the object's opacity (0–1).
|
|
250
|
+
* @type {number}
|
|
251
|
+
*/
|
|
252
|
+
get opacity() {
|
|
253
|
+
return this._opacity;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
set opacity(v) {
|
|
257
|
+
this._opacity = Math.min(1, Math.max(0, typeof v === "number" ? v : 1));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Gets the current shadow color (if any).
|
|
262
|
+
* @type {string|undefined}
|
|
263
|
+
*/
|
|
264
|
+
get shadowColor() {
|
|
265
|
+
return this._shadowColor;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
set shadowColor(v) {
|
|
269
|
+
this._shadowColor = v;
|
|
270
|
+
this.invalidateCache();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Gets the blur radius for the drop shadow.
|
|
275
|
+
* @type {number}
|
|
276
|
+
*/
|
|
277
|
+
get shadowBlur() {
|
|
278
|
+
return this._shadowBlur;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
set shadowBlur(v) {
|
|
282
|
+
this._shadowBlur = v;
|
|
283
|
+
this.invalidateCache();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Gets the horizontal offset of the drop shadow.
|
|
288
|
+
* @type {number}
|
|
289
|
+
*/
|
|
290
|
+
get shadowOffsetX() {
|
|
291
|
+
return this._shadowOffsetX;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
set shadowOffsetX(v) {
|
|
295
|
+
this._shadowOffsetX = v;
|
|
296
|
+
this.invalidateCache();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Gets the vertical offset of the drop shadow.
|
|
301
|
+
* @type {number}
|
|
302
|
+
*/
|
|
303
|
+
get shadowOffsetY() {
|
|
304
|
+
return this._shadowOffsetY;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
set shadowOffsetY(v) {
|
|
308
|
+
this._shadowOffsetY = v;
|
|
309
|
+
this.invalidateCache();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Total elapsed time this object has been alive (updated).
|
|
314
|
+
* @type {number}
|
|
315
|
+
* @readonly
|
|
316
|
+
*/
|
|
317
|
+
get tick() {
|
|
318
|
+
return this._tick;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Whether render caching is enabled for this object.
|
|
323
|
+
* @type {boolean}
|
|
324
|
+
*/
|
|
325
|
+
get cacheRendering() {
|
|
326
|
+
return this._cacheRendering;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
set cacheRendering(v) {
|
|
330
|
+
this._cacheRendering = Boolean(v);
|
|
331
|
+
if (v) this.invalidateCache();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
|
|
4
|
+
export class Ring extends Shape {
|
|
5
|
+
constructor(outerRadius, innerRadius, options = {}) {
|
|
6
|
+
super(options);
|
|
7
|
+
this.outerRadius = outerRadius;
|
|
8
|
+
this.innerRadius = innerRadius;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
draw() {
|
|
12
|
+
super.draw();
|
|
13
|
+
Painter.lines.beginPath();
|
|
14
|
+
Painter.shapes.arc(0, 0, this.outerRadius, 0, Math.PI * 2);
|
|
15
|
+
Painter.shapes.arc(0, 0, this.innerRadius, 0, Math.PI * 2, true);
|
|
16
|
+
Painter.lines.closePath();
|
|
17
|
+
|
|
18
|
+
if (this.color) {
|
|
19
|
+
Painter.colors.fill(this.color);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (this.stroke) {
|
|
23
|
+
Painter.colors.stroke(this.stroke, this.lineWidth);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* RoundedRectangle - A drawable centered rectangle with rounded corners.
|
|
6
|
+
*
|
|
7
|
+
* Draws a rounded rectangle from its center using Painter.
|
|
8
|
+
*/
|
|
9
|
+
export class RoundedRectangle extends Shape {
|
|
10
|
+
/**
|
|
11
|
+
* @param {number} x - Center X
|
|
12
|
+
* @param {number} y - Center Y
|
|
13
|
+
* @param {number} width - Rectangle width
|
|
14
|
+
* @param {number} height - Rectangle height
|
|
15
|
+
* @param {number|number[]} radii - Corner radius or array of radii for each corner
|
|
16
|
+
* @param {Object} [options] - Shape rendering options
|
|
17
|
+
*/
|
|
18
|
+
constructor(radii = 0, options = {}) {
|
|
19
|
+
super(options);
|
|
20
|
+
// Handle radius either as a single value or array of 4 values
|
|
21
|
+
if (typeof radii === "number") {
|
|
22
|
+
this.radii = [radii, radii, radii, radii]; // [topLeft, topRight, bottomRight, bottomLeft]
|
|
23
|
+
} else if (Array.isArray(radii)) {
|
|
24
|
+
// Ensure we have exactly 4 values
|
|
25
|
+
this.radii =
|
|
26
|
+
radii.length === 4
|
|
27
|
+
? radii
|
|
28
|
+
: [
|
|
29
|
+
radii[0] || 0,
|
|
30
|
+
radii[1] || radii[0] || 0,
|
|
31
|
+
radii[2] || radii[0] || 0,
|
|
32
|
+
radii[3] || radii[1] || radii[0] || 0,
|
|
33
|
+
];
|
|
34
|
+
} else {
|
|
35
|
+
this.radii = [0, 0, 0, 0];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Renders the rounded rectangle using Painter's roundRect method.
|
|
41
|
+
*/
|
|
42
|
+
draw() {
|
|
43
|
+
super.draw();
|
|
44
|
+
const x = -this.width / 2;
|
|
45
|
+
const y = -this.height / 2;
|
|
46
|
+
// Use the Painter's roundRect utility methods
|
|
47
|
+
if (this.color && this.stroke) {
|
|
48
|
+
// If both fill and stroke are needed
|
|
49
|
+
Painter.shapes.roundRect(
|
|
50
|
+
x,
|
|
51
|
+
y,
|
|
52
|
+
this.width,
|
|
53
|
+
this.height,
|
|
54
|
+
this.radii,
|
|
55
|
+
this.color,
|
|
56
|
+
this.stroke,
|
|
57
|
+
this.lineWidth
|
|
58
|
+
);
|
|
59
|
+
} else if (this.color) {
|
|
60
|
+
// If only fill is needed
|
|
61
|
+
Painter.shapes.fillRoundRect(
|
|
62
|
+
x,
|
|
63
|
+
y,
|
|
64
|
+
this.width,
|
|
65
|
+
this.height,
|
|
66
|
+
this.radii,
|
|
67
|
+
this.color
|
|
68
|
+
);
|
|
69
|
+
} else if (this.stroke) {
|
|
70
|
+
// If only stroke is needed
|
|
71
|
+
Painter.shapes.strokeRoundRect(
|
|
72
|
+
x,
|
|
73
|
+
y,
|
|
74
|
+
this.width,
|
|
75
|
+
this.height,
|
|
76
|
+
this.radii,
|
|
77
|
+
this.stroke,
|
|
78
|
+
this.lineWidth
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the bounding box for the rounded rectangle
|
|
85
|
+
* @returns {{x: number, y: number, width: number, height: number}}
|
|
86
|
+
*/
|
|
87
|
+
getBounds() {
|
|
88
|
+
return {
|
|
89
|
+
x: this.x,
|
|
90
|
+
y: this.y,
|
|
91
|
+
width: this.width,
|
|
92
|
+
height: this.height,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|