@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,537 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
import { WebGLRenderer } from "../webgl/webgl-renderer.js";
|
|
4
|
+
import { SPHERE_SHADERS } from "../webgl/shaders/sphere-shaders.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sphere3D - A true 3D sphere that integrates with Camera3D
|
|
8
|
+
*
|
|
9
|
+
* Unlike the 2D Sphere class which uses isometric projection,
|
|
10
|
+
* this sphere works with Scene3D and Camera3D to provide true
|
|
11
|
+
* 3D rotation and perspective projection.
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - Integrates with Camera3D rotation state
|
|
15
|
+
* - Supports solid colors and gradients
|
|
16
|
+
* - Debug wireframe mode
|
|
17
|
+
* - Depth-sorted face rendering
|
|
18
|
+
* - Surface normal-based lighting
|
|
19
|
+
* - Optional WebGL shader rendering for advanced effects
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Basic sphere with Canvas 2D rendering
|
|
23
|
+
* const sphere = new Sphere3D(50, {
|
|
24
|
+
* color: "#FFD700",
|
|
25
|
+
* camera: this.camera,
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Sphere with WebGL shader for star effect
|
|
30
|
+
* const star = new Sphere3D(50, {
|
|
31
|
+
* camera: this.camera,
|
|
32
|
+
* useShader: true,
|
|
33
|
+
* shaderType: "star",
|
|
34
|
+
* shaderUniforms: {
|
|
35
|
+
* uStarColor: [1.0, 0.9, 0.5],
|
|
36
|
+
* uTemperature: 5778,
|
|
37
|
+
* uActivityLevel: 0.5,
|
|
38
|
+
* },
|
|
39
|
+
* });
|
|
40
|
+
*/
|
|
41
|
+
export class Sphere3D extends Shape {
|
|
42
|
+
// Shared WebGL renderer for all shader-enabled spheres
|
|
43
|
+
static _glRenderer = null;
|
|
44
|
+
static _glRendererSize = { width: 0, height: 0 };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get or create shared WebGL renderer
|
|
48
|
+
* @param {number} width - Required width
|
|
49
|
+
* @param {number} height - Required height
|
|
50
|
+
* @returns {WebGLRenderer|null}
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
static _getGLRenderer(width, height) {
|
|
54
|
+
// Create or resize renderer as needed
|
|
55
|
+
if (!Sphere3D._glRenderer) {
|
|
56
|
+
Sphere3D._glRenderer = new WebGLRenderer(width, height);
|
|
57
|
+
Sphere3D._glRendererSize = { width, height };
|
|
58
|
+
} else if (
|
|
59
|
+
Sphere3D._glRendererSize.width !== width ||
|
|
60
|
+
Sphere3D._glRendererSize.height !== height
|
|
61
|
+
) {
|
|
62
|
+
Sphere3D._glRenderer.resize(width, height);
|
|
63
|
+
Sphere3D._glRendererSize = { width, height };
|
|
64
|
+
}
|
|
65
|
+
return Sphere3D._glRenderer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a 3D sphere
|
|
70
|
+
* @param {number} radius - Sphere radius
|
|
71
|
+
* @param {object} options - Configuration options
|
|
72
|
+
* @param {string|CanvasGradient} [options.color] - Fill color or gradient
|
|
73
|
+
* @param {Camera3D} [options.camera] - Camera for rotation (optional, can be set later)
|
|
74
|
+
* @param {boolean} [options.debug=false] - Show wireframe
|
|
75
|
+
* @param {number} [options.segments=20] - Number of latitude/longitude segments
|
|
76
|
+
* @param {string} [options.stroke] - Wireframe line color
|
|
77
|
+
* @param {number} [options.lineWidth=1] - Wireframe line width
|
|
78
|
+
* @param {boolean} [options.useShader=false] - Use WebGL shader rendering
|
|
79
|
+
* @param {string} [options.shaderType='star'] - Shader type: 'star', 'blackHole', 'rockyPlanet', 'gasGiant'
|
|
80
|
+
* @param {Object} [options.shaderUniforms={}] - Custom shader uniforms
|
|
81
|
+
*/
|
|
82
|
+
constructor(radius, options = {}) {
|
|
83
|
+
super(options);
|
|
84
|
+
|
|
85
|
+
this.radius = radius;
|
|
86
|
+
this.camera = options.camera ?? null;
|
|
87
|
+
this.debug = options.debug ?? false;
|
|
88
|
+
this.segments = options.segments ?? 20;
|
|
89
|
+
|
|
90
|
+
// WebGL shader options
|
|
91
|
+
this.useShader = options.useShader ?? false;
|
|
92
|
+
this.shaderType = options.shaderType ?? "star";
|
|
93
|
+
this.shaderUniforms = options.shaderUniforms ?? {};
|
|
94
|
+
this._shaderInitialized = false;
|
|
95
|
+
|
|
96
|
+
// Generate sphere geometry (for Canvas 2D fallback)
|
|
97
|
+
this._generateGeometry();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Set or update the camera reference
|
|
102
|
+
* @param {Camera3D} camera - Camera instance
|
|
103
|
+
*/
|
|
104
|
+
setCamera(camera) {
|
|
105
|
+
this.camera = camera;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Update shader uniforms dynamically
|
|
111
|
+
* @param {Object} uniforms - Uniform name -> value pairs
|
|
112
|
+
*/
|
|
113
|
+
setShaderUniforms(uniforms) {
|
|
114
|
+
Object.assign(this.shaderUniforms, uniforms);
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get fragment shader source for the current shader type
|
|
120
|
+
* @returns {string}
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
_getFragmentShader() {
|
|
124
|
+
switch (this.shaderType) {
|
|
125
|
+
case "star":
|
|
126
|
+
return SPHERE_SHADERS.star;
|
|
127
|
+
case "blackHole":
|
|
128
|
+
return SPHERE_SHADERS.blackHole;
|
|
129
|
+
case "rockyPlanet":
|
|
130
|
+
return SPHERE_SHADERS.rockyPlanet;
|
|
131
|
+
case "gasGiant":
|
|
132
|
+
return SPHERE_SHADERS.gasGiant;
|
|
133
|
+
default:
|
|
134
|
+
return SPHERE_SHADERS.star;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Initialize or update the WebGL shader
|
|
140
|
+
* @param {number} renderWidth - Render width
|
|
141
|
+
* @param {number} renderHeight - Render height
|
|
142
|
+
* @private
|
|
143
|
+
*/
|
|
144
|
+
_initShader(renderWidth, renderHeight) {
|
|
145
|
+
const gl = Sphere3D._getGLRenderer(renderWidth, renderHeight);
|
|
146
|
+
if (!gl || !gl.isAvailable()) {
|
|
147
|
+
this.useShader = false; // Fallback to Canvas 2D
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Initialize shader program
|
|
152
|
+
const programName = `sphere_${this.shaderType}`;
|
|
153
|
+
gl.useProgram(programName, SPHERE_SHADERS.vertex, this._getFragmentShader());
|
|
154
|
+
this._shaderInitialized = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Render using WebGL shader
|
|
159
|
+
* @param {CanvasRenderingContext2D} ctx - 2D context to composite onto
|
|
160
|
+
* @param {number} screenX - Screen X position
|
|
161
|
+
* @param {number} screenY - Screen Y position
|
|
162
|
+
* @param {number} screenRadius - Radius on screen
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
_renderWithShader(ctx, screenX, screenY, screenRadius) {
|
|
166
|
+
// Calculate render size (larger for glow effects and to prevent clipping)
|
|
167
|
+
// Account for tidal stretch - stretched stars extend beyond normal radius
|
|
168
|
+
const tidalStretch = this.shaderUniforms?.uTidalStretch ?? 0;
|
|
169
|
+
const stretchMultiplier = 1 + tidalStretch; // e.g., 1.8 stretch = 2.8x multiplier
|
|
170
|
+
const padding = screenRadius * stretchMultiplier; // Dynamic padding based on stretch
|
|
171
|
+
const renderSize = Math.ceil((screenRadius + padding) * 2);
|
|
172
|
+
|
|
173
|
+
const gl = Sphere3D._getGLRenderer(renderSize, renderSize);
|
|
174
|
+
if (!gl || !gl.isAvailable()) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Initialize shader if needed
|
|
179
|
+
if (!this._shaderInitialized) {
|
|
180
|
+
this._initShader(renderSize, renderSize);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Use the program
|
|
184
|
+
const programName = `sphere_${this.shaderType}`;
|
|
185
|
+
gl.useProgram(programName, SPHERE_SHADERS.vertex, this._getFragmentShader());
|
|
186
|
+
|
|
187
|
+
// Clear with transparency
|
|
188
|
+
gl.clear(0, 0, 0, 0);
|
|
189
|
+
|
|
190
|
+
// Calculate the base radius for the shader
|
|
191
|
+
// The camera's visible half-width at z=0 is about 1.25 units (based on FOV)
|
|
192
|
+
// We want the unstretched star to appear at screenRadius size
|
|
193
|
+
// baseRadius = visible_half_width * screenRadius / (renderSize / 2)
|
|
194
|
+
const visibleHalfWidth = 1.25;
|
|
195
|
+
const uBaseRadius = visibleHalfWidth * screenRadius / (renderSize / 2);
|
|
196
|
+
|
|
197
|
+
// Set common uniforms
|
|
198
|
+
gl.setUniforms({
|
|
199
|
+
uTime: performance.now() / 1000,
|
|
200
|
+
uResolution: [renderSize, renderSize],
|
|
201
|
+
uBaseRadius: uBaseRadius,
|
|
202
|
+
uCameraRotation: [
|
|
203
|
+
this.camera?.rotationX ?? 0,
|
|
204
|
+
this.camera?.rotationY ?? 0,
|
|
205
|
+
this.camera?.rotationZ ?? 0,
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Set shader-specific uniforms
|
|
210
|
+
gl.setUniforms(this.shaderUniforms);
|
|
211
|
+
|
|
212
|
+
// Handle color uniforms (convert hex to RGB)
|
|
213
|
+
for (const [name, value] of Object.entries(this.shaderUniforms)) {
|
|
214
|
+
if (typeof value === "string" && value.startsWith("#")) {
|
|
215
|
+
gl.setColorUniform(name, value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Render
|
|
220
|
+
gl.render();
|
|
221
|
+
|
|
222
|
+
// Composite onto 2D canvas
|
|
223
|
+
// No clip needed - shader renders with proper alpha transparency
|
|
224
|
+
// This allows stretched/elliptical shapes and their glows to render fully
|
|
225
|
+
const drawX = screenX - renderSize / 2;
|
|
226
|
+
const drawY = screenY - renderSize / 2;
|
|
227
|
+
|
|
228
|
+
gl.compositeOnto(ctx, drawX, drawY, renderSize, renderSize);
|
|
229
|
+
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Generate sphere vertices and faces using UV parameterization
|
|
235
|
+
* @private
|
|
236
|
+
*/
|
|
237
|
+
_generateGeometry() {
|
|
238
|
+
this.vertices = [];
|
|
239
|
+
this.faces = [];
|
|
240
|
+
|
|
241
|
+
const latSegments = this.segments;
|
|
242
|
+
const lonSegments = this.segments * 2;
|
|
243
|
+
|
|
244
|
+
// Generate vertices
|
|
245
|
+
for (let lat = 0; lat <= latSegments; lat++) {
|
|
246
|
+
const theta = (lat * Math.PI) / latSegments; // 0 to PI
|
|
247
|
+
const sinTheta = Math.sin(theta);
|
|
248
|
+
const cosTheta = Math.cos(theta);
|
|
249
|
+
|
|
250
|
+
for (let lon = 0; lon <= lonSegments; lon++) {
|
|
251
|
+
const phi = (lon * 2 * Math.PI) / lonSegments; // 0 to 2PI
|
|
252
|
+
const sinPhi = Math.sin(phi);
|
|
253
|
+
const cosPhi = Math.cos(phi);
|
|
254
|
+
|
|
255
|
+
// Spherical to Cartesian coordinates
|
|
256
|
+
const x = this.radius * sinTheta * cosPhi;
|
|
257
|
+
const y = this.radius * cosTheta;
|
|
258
|
+
const z = this.radius * sinTheta * sinPhi;
|
|
259
|
+
|
|
260
|
+
// Store vertex with its normal (normalized position vector)
|
|
261
|
+
this.vertices.push({
|
|
262
|
+
x,
|
|
263
|
+
y,
|
|
264
|
+
z,
|
|
265
|
+
nx: sinTheta * cosPhi,
|
|
266
|
+
ny: cosTheta,
|
|
267
|
+
nz: sinTheta * sinPhi,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Generate faces (quads split into triangles)
|
|
273
|
+
for (let lat = 0; lat < latSegments; lat++) {
|
|
274
|
+
for (let lon = 0; lon < lonSegments; lon++) {
|
|
275
|
+
const first = lat * (lonSegments + 1) + lon;
|
|
276
|
+
const second = first + lonSegments + 1;
|
|
277
|
+
|
|
278
|
+
// Two triangles per quad
|
|
279
|
+
this.faces.push([first, second, first + 1]);
|
|
280
|
+
this.faces.push([second, second + 1, first + 1]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Calculate lighting intensity based on surface normal
|
|
287
|
+
* @param {number} nx - Normal x component
|
|
288
|
+
* @param {number} ny - Normal y component
|
|
289
|
+
* @param {number} nz - Normal z component
|
|
290
|
+
* @returns {number} Intensity 0-1
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
_calculateLighting(nx, ny, nz) {
|
|
294
|
+
// Simple directional light from top-right-front
|
|
295
|
+
const lightX = 0.5;
|
|
296
|
+
const lightY = 0.7;
|
|
297
|
+
const lightZ = 0.5;
|
|
298
|
+
const lightLen = Math.sqrt(
|
|
299
|
+
lightX * lightX + lightY * lightY + lightZ * lightZ
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Normalized light direction
|
|
303
|
+
const lx = lightX / lightLen;
|
|
304
|
+
const ly = lightY / lightLen;
|
|
305
|
+
const lz = lightZ / lightLen;
|
|
306
|
+
|
|
307
|
+
// Dot product for diffuse lighting
|
|
308
|
+
let intensity = nx * lx + ny * ly + nz * lz;
|
|
309
|
+
|
|
310
|
+
// Clamp and add ambient light
|
|
311
|
+
intensity = Math.max(0, intensity) * 0.7 + 0.3;
|
|
312
|
+
|
|
313
|
+
return intensity;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Apply lighting to a color
|
|
318
|
+
* @param {string} color - Base color (hex format)
|
|
319
|
+
* @param {number} intensity - Light intensity 0-1
|
|
320
|
+
* @returns {string} RGB color string
|
|
321
|
+
* @private
|
|
322
|
+
*/
|
|
323
|
+
_applyLighting(color, intensity) {
|
|
324
|
+
// If it's a gradient or non-hex color, return as-is
|
|
325
|
+
if (!color || typeof color !== "string" || !color.startsWith("#")) {
|
|
326
|
+
return color;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Parse hex color
|
|
330
|
+
const hex = color.replace("#", "");
|
|
331
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
332
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
333
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
334
|
+
|
|
335
|
+
// Apply intensity
|
|
336
|
+
const lr = Math.round(r * intensity);
|
|
337
|
+
const lg = Math.round(g * intensity);
|
|
338
|
+
const lb = Math.round(b * intensity);
|
|
339
|
+
|
|
340
|
+
return `rgb(${lr}, ${lg}, ${lb})`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Main render method
|
|
345
|
+
*/
|
|
346
|
+
draw() {
|
|
347
|
+
super.draw();
|
|
348
|
+
|
|
349
|
+
if (!this.camera) {
|
|
350
|
+
// Fallback: draw a simple circle if no camera
|
|
351
|
+
if (this.color) {
|
|
352
|
+
Painter.shapes.fillCircle(0, 0, this.radius, this.color);
|
|
353
|
+
}
|
|
354
|
+
if (this.debug && this.stroke) {
|
|
355
|
+
Painter.shapes.strokeCircle(0, 0, this.radius, this.stroke, this.lineWidth);
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// WebGL shader rendering path
|
|
361
|
+
if (this.useShader && !this.debug) {
|
|
362
|
+
// Project sphere center to get screen position and scale
|
|
363
|
+
const projected = this.camera.project(
|
|
364
|
+
this.x || 0,
|
|
365
|
+
this.y || 0,
|
|
366
|
+
this.z || 0
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Calculate screen radius based on perspective
|
|
370
|
+
const scale = this.camera.perspective / (this.camera.perspective + projected.z);
|
|
371
|
+
const screenRadius = this.radius * scale;
|
|
372
|
+
|
|
373
|
+
// Get the current canvas transform to find the scene center
|
|
374
|
+
// The Scene3D translates to (game.width/2, game.height/2)
|
|
375
|
+
// We need absolute screen coordinates for WebGL compositing
|
|
376
|
+
const ctx = Painter.ctx;
|
|
377
|
+
const transform = ctx.getTransform();
|
|
378
|
+
const sceneX = transform.e; // Translation X (scene center)
|
|
379
|
+
const sceneY = transform.f; // Translation Y (scene center)
|
|
380
|
+
|
|
381
|
+
// Render with shader at absolute screen position
|
|
382
|
+
// Reset transform temporarily for compositing
|
|
383
|
+
ctx.save();
|
|
384
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
385
|
+
const success = this._renderWithShader(
|
|
386
|
+
ctx,
|
|
387
|
+
sceneX + projected.x,
|
|
388
|
+
sceneY + projected.y,
|
|
389
|
+
screenRadius
|
|
390
|
+
);
|
|
391
|
+
ctx.restore();
|
|
392
|
+
|
|
393
|
+
if (success) {
|
|
394
|
+
return; // Successfully rendered with shader
|
|
395
|
+
}
|
|
396
|
+
// Fall through to Canvas 2D if shader failed
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Project all vertices and normals through the camera
|
|
400
|
+
// Add position offset so sphere appears at correct world position
|
|
401
|
+
const projectedVertices = this.vertices.map((v) => {
|
|
402
|
+
const projected = this.camera.project(
|
|
403
|
+
v.x + (this.x || 0),
|
|
404
|
+
v.y + (this.y || 0),
|
|
405
|
+
v.z + (this.z || 0)
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Rotate normals using the same rotation sequence as Camera3D.project
|
|
409
|
+
// (Z, then Y, then X)
|
|
410
|
+
let nx = v.nx;
|
|
411
|
+
let ny = v.ny;
|
|
412
|
+
let nz = v.nz;
|
|
413
|
+
|
|
414
|
+
// Rotate around Z axis (roll)
|
|
415
|
+
if (this.camera.rotationZ !== 0) {
|
|
416
|
+
const cosZ = Math.cos(this.camera.rotationZ);
|
|
417
|
+
const sinZ = Math.sin(this.camera.rotationZ);
|
|
418
|
+
const nx0 = nx;
|
|
419
|
+
const ny0 = ny;
|
|
420
|
+
nx = nx0 * cosZ - ny0 * sinZ;
|
|
421
|
+
ny = nx0 * sinZ + ny0 * cosZ;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Rotate around Y axis (horizontal spin)
|
|
425
|
+
const cosY = Math.cos(this.camera.rotationY);
|
|
426
|
+
const sinY = Math.sin(this.camera.rotationY);
|
|
427
|
+
const nx1 = nx * cosY - nz * sinY;
|
|
428
|
+
const nz1 = nx * sinY + nz * cosY;
|
|
429
|
+
|
|
430
|
+
// Rotate around X axis (vertical tilt)
|
|
431
|
+
const cosX = Math.cos(this.camera.rotationX);
|
|
432
|
+
const sinX = Math.sin(this.camera.rotationX);
|
|
433
|
+
const ny1 = ny * cosX - nz1 * sinX;
|
|
434
|
+
const nz2 = ny * sinX + nz1 * cosX;
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
...projected,
|
|
438
|
+
nx: nx1,
|
|
439
|
+
ny: ny1,
|
|
440
|
+
nz: nz2,
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (this.debug) {
|
|
445
|
+
this.trace("Sphere3D.draw: projected vertices", projectedVertices.length);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Build face list with depth and lighting
|
|
449
|
+
const renderFaces = [];
|
|
450
|
+
|
|
451
|
+
for (const face of this.faces) {
|
|
452
|
+
const v0 = projectedVertices[face[0]];
|
|
453
|
+
const v1 = projectedVertices[face[1]];
|
|
454
|
+
const v2 = projectedVertices[face[2]];
|
|
455
|
+
|
|
456
|
+
// Skip if any vertex is behind camera
|
|
457
|
+
if (
|
|
458
|
+
v0.z < -this.camera.perspective + 10 ||
|
|
459
|
+
v1.z < -this.camera.perspective + 10 ||
|
|
460
|
+
v2.z < -this.camera.perspective + 10
|
|
461
|
+
) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Calculate average depth for sorting
|
|
466
|
+
const avgZ = (v0.z + v1.z + v2.z) / 3;
|
|
467
|
+
|
|
468
|
+
// Calculate average normal for lighting and backface culling
|
|
469
|
+
const avgNx = (v0.nx + v1.nx + v2.nx) / 3;
|
|
470
|
+
const avgNy = (v0.ny + v1.ny + v2.ny) / 3;
|
|
471
|
+
const avgNz = (v0.nz + v1.nz + v2.nz) / 3;
|
|
472
|
+
|
|
473
|
+
// Backface culling: skip faces pointing away from camera
|
|
474
|
+
// In camera space after rotations, Z points into the screen (away from user).
|
|
475
|
+
// A face is visible if its view-space normal points towards the user (negative Z).
|
|
476
|
+
// Wait, Camera3D.project uses z2 = y * sinX + z1 * cosX; and scale = perspective / (perspective + z2)
|
|
477
|
+
// If z2 is positive, it's further away.
|
|
478
|
+
// So if normal.z is positive, it's pointing away from the user.
|
|
479
|
+
if (avgNz > 0.1) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const intensity = this._calculateLighting(avgNx, avgNy, avgNz);
|
|
484
|
+
|
|
485
|
+
renderFaces.push({
|
|
486
|
+
vertices: [v0, v1, v2],
|
|
487
|
+
avgZ,
|
|
488
|
+
intensity,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Sort back to front
|
|
493
|
+
renderFaces.sort((a, b) => b.avgZ - a.avgZ);
|
|
494
|
+
|
|
495
|
+
// Render faces
|
|
496
|
+
for (const face of renderFaces) {
|
|
497
|
+
const points = face.vertices.map((v) => ({ x: v.x, y: v.y }));
|
|
498
|
+
|
|
499
|
+
if (this.debug) {
|
|
500
|
+
// Wireframe mode
|
|
501
|
+
Painter.ctx.beginPath();
|
|
502
|
+
Painter.ctx.moveTo(points[0].x, points[0].y);
|
|
503
|
+
Painter.ctx.lineTo(points[1].x, points[1].y);
|
|
504
|
+
Painter.ctx.lineTo(points[2].x, points[2].y);
|
|
505
|
+
Painter.ctx.closePath();
|
|
506
|
+
|
|
507
|
+
if (this.stroke) {
|
|
508
|
+
Painter.ctx.strokeStyle = this.stroke;
|
|
509
|
+
Painter.ctx.lineWidth = this.lineWidth ?? 1;
|
|
510
|
+
Painter.ctx.stroke();
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
// Filled mode with lighting
|
|
514
|
+
if (this.color) {
|
|
515
|
+
const faceColor = this._applyLighting(this.color, face.intensity);
|
|
516
|
+
|
|
517
|
+
Painter.ctx.beginPath();
|
|
518
|
+
Painter.ctx.moveTo(points[0].x, points[0].y);
|
|
519
|
+
Painter.ctx.lineTo(points[1].x, points[1].y);
|
|
520
|
+
Painter.ctx.lineTo(points[2].x, points[2].y);
|
|
521
|
+
Painter.ctx.closePath();
|
|
522
|
+
|
|
523
|
+
Painter.ctx.fillStyle = faceColor;
|
|
524
|
+
Painter.ctx.fill();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Calculate bounding box
|
|
532
|
+
*/
|
|
533
|
+
calculateBounds() {
|
|
534
|
+
const size = this.radius * 2;
|
|
535
|
+
return { x: this.x, y: this.y, width: size, height: size };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Rectangle } from "./rect.js";
|
|
2
|
+
/**
|
|
3
|
+
* Square - A shortcut for creating rectangles with equal width/height.
|
|
4
|
+
*/
|
|
5
|
+
export class Square extends Rectangle {
|
|
6
|
+
/**
|
|
7
|
+
* @param {number} size - Side length of the square
|
|
8
|
+
* @param {Object} [options] - Shape rendering options
|
|
9
|
+
*/
|
|
10
|
+
constructor(size, options = {}) {
|
|
11
|
+
super(options);
|
|
12
|
+
this.width = size;
|
|
13
|
+
this.height = size;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
|
|
4
|
+
export class Star extends Shape {
|
|
5
|
+
constructor(radius = 40, spikes = 5, inset = 0.5, options = {}) {
|
|
6
|
+
super(options);
|
|
7
|
+
this.radius = radius;
|
|
8
|
+
this.spikes = spikes;
|
|
9
|
+
this.inset = inset;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
draw() {
|
|
13
|
+
super.draw();
|
|
14
|
+
const step = Math.PI / this.spikes;
|
|
15
|
+
const rotationOffset = -Math.PI / 2;
|
|
16
|
+
// Render
|
|
17
|
+
Painter.lines.beginPath();
|
|
18
|
+
// Draw the star shape
|
|
19
|
+
for (let i = 0; i < this.spikes * 2; i++) {
|
|
20
|
+
const isOuter = i % 2 === 0;
|
|
21
|
+
const r = isOuter ? this.radius : this.radius * this.inset;
|
|
22
|
+
const angle = i * step + rotationOffset;
|
|
23
|
+
const x = Math.cos(angle) * r;
|
|
24
|
+
const y = Math.sin(angle) * r;
|
|
25
|
+
if (i === 0) {
|
|
26
|
+
Painter.lines.moveTo(x, y);
|
|
27
|
+
} else {
|
|
28
|
+
Painter.lines.lineTo(x, y);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Close the path
|
|
32
|
+
Painter.lines.closePath();
|
|
33
|
+
// Fill
|
|
34
|
+
if (this.color) {
|
|
35
|
+
Painter.colors.fill(this.color);
|
|
36
|
+
}
|
|
37
|
+
// Stroke
|
|
38
|
+
if (this.stroke) {
|
|
39
|
+
Painter.colors.stroke(this.stroke, this.lineWidth);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* draw() {
|
|
44
|
+
// First apply transforms as usual
|
|
45
|
+
super.draw();
|
|
46
|
+
|
|
47
|
+
// Get the current transform matrix to preserve positioning
|
|
48
|
+
const transform = Painter.ctx.getTransform();
|
|
49
|
+
|
|
50
|
+
// Create a temporary off-screen canvas sized to fit the star
|
|
51
|
+
const tempCanvas = document.createElement('canvas');
|
|
52
|
+
const padding = Math.ceil(this.radius * 0.1) + 2; // Small padding
|
|
53
|
+
tempCanvas.width = this.radius * 2 + padding * 2;
|
|
54
|
+
tempCanvas.height = this.radius * 2 + padding * 2;
|
|
55
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
56
|
+
|
|
57
|
+
// Center the drawing in the temp canvas
|
|
58
|
+
tempCtx.translate(tempCanvas.width/2, tempCanvas.height/2);
|
|
59
|
+
|
|
60
|
+
// Draw the star with clean state
|
|
61
|
+
tempCtx.beginPath();
|
|
62
|
+
const step = Math.PI / this.spikes;
|
|
63
|
+
const rotationOffset = -Math.PI / 2;
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < this.spikes * 2; i++) {
|
|
66
|
+
const isOuter = i % 2 === 0;
|
|
67
|
+
const r = isOuter ? this.radius : this.radius * this.inset;
|
|
68
|
+
const angle = i * step + rotationOffset;
|
|
69
|
+
const x = Math.cos(angle) * r;
|
|
70
|
+
const y = Math.sin(angle) * r;
|
|
71
|
+
|
|
72
|
+
if (i === 0) {
|
|
73
|
+
tempCtx.moveTo(x, y);
|
|
74
|
+
} else {
|
|
75
|
+
tempCtx.lineTo(x, y);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
tempCtx.closePath();
|
|
80
|
+
|
|
81
|
+
// Fill with explicit color
|
|
82
|
+
tempCtx.fillStyle = this.color || 'white';
|
|
83
|
+
tempCtx.fill();
|
|
84
|
+
|
|
85
|
+
// Stroke if needed
|
|
86
|
+
if (this.stroke) {
|
|
87
|
+
tempCtx.strokeStyle = this.stroke;
|
|
88
|
+
tempCtx.lineWidth = this.lineWidth;
|
|
89
|
+
tempCtx.stroke();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Now draw this to the main canvas at the correct position
|
|
93
|
+
Painter.ctx.drawImage(
|
|
94
|
+
tempCanvas,
|
|
95
|
+
-tempCanvas.width/2,
|
|
96
|
+
-tempCanvas.height/2
|
|
97
|
+
);
|
|
98
|
+
} */
|
|
99
|
+
}
|