@guinetik/gcanvas 1.0.0 → 1.0.2
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/demos/coordinates.html +698 -0
- package/demos/cube3d.html +23 -0
- package/demos/demos.css +17 -3
- package/demos/dino.html +42 -0
- package/demos/fluid-simple.html +22 -0
- package/demos/fluid.html +37 -0
- package/demos/gameobjects.html +626 -0
- package/demos/index.html +19 -7
- package/demos/js/blob.js +18 -5
- package/demos/js/coordinates.js +840 -0
- package/demos/js/cube3d.js +789 -0
- package/demos/js/dino.js +1420 -0
- package/demos/js/fluid-simple.js +253 -0
- package/demos/js/fluid.js +527 -0
- package/demos/js/gameobjects.js +176 -0
- package/demos/js/plane3d.js +256 -0
- package/demos/js/platformer.js +1579 -0
- package/demos/js/sphere3d.js +229 -0
- package/demos/js/sprite.js +473 -0
- package/demos/js/tde/accretiondisk.js +65 -12
- package/demos/js/tde/blackholescene.js +2 -2
- package/demos/js/tde/config.js +2 -2
- package/demos/js/tde/index.js +152 -27
- package/demos/js/tde/lensedstarfield.js +32 -25
- package/demos/js/tde/tdestar.js +78 -98
- package/demos/js/tde/tidalstream.js +24 -8
- package/demos/plane3d.html +24 -0
- package/demos/platformer.html +43 -0
- package/demos/sphere3d.html +24 -0
- package/demos/sprite.html +18 -0
- package/docs/README.md +230 -222
- package/docs/api/FluidSystem.md +173 -0
- package/docs/concepts/architecture-overview.md +204 -204
- package/docs/concepts/coordinate-system.md +384 -0
- package/docs/concepts/rendering-pipeline.md +279 -279
- package/docs/concepts/shapes-vs-gameobjects.md +187 -0
- package/docs/concepts/two-layer-architecture.md +229 -229
- package/docs/fluid-dynamics.md +99 -0
- package/docs/getting-started/first-game.md +354 -354
- package/docs/getting-started/installation.md +175 -157
- package/docs/modules/collision/README.md +2 -2
- package/docs/modules/fluent/README.md +6 -6
- package/docs/modules/game/README.md +303 -303
- package/docs/modules/isometric-camera.md +2 -2
- package/docs/modules/isometric.md +1 -1
- package/docs/modules/painter/README.md +328 -328
- package/docs/modules/particle/README.md +3 -3
- package/docs/modules/shapes/README.md +221 -221
- package/docs/modules/shapes/base/euclidian.md +123 -123
- package/docs/modules/shapes/base/shape.md +262 -262
- package/docs/modules/shapes/base/transformable.md +243 -243
- package/docs/modules/state/README.md +2 -2
- package/docs/modules/util/README.md +1 -1
- package/docs/modules/util/camera3d.md +3 -3
- package/docs/modules/util/scene3d.md +1 -1
- package/package.json +3 -1
- package/readme.md +19 -5
- package/src/collision/collision.js +75 -0
- package/src/game/game.js +11 -5
- package/src/game/index.js +2 -1
- package/src/game/objects/index.js +3 -0
- package/src/game/objects/platformer-scene.js +411 -0
- package/src/game/objects/scene.js +14 -0
- package/src/game/objects/sprite.js +529 -0
- package/src/game/pipeline.js +20 -16
- package/src/game/systems/FluidSystem.js +835 -0
- package/src/game/systems/index.js +11 -0
- package/src/game/ui/button.js +39 -18
- package/src/game/ui/cursor.js +14 -0
- package/src/game/ui/fps.js +12 -4
- package/src/game/ui/index.js +2 -0
- package/src/game/ui/stepper.js +549 -0
- package/src/game/ui/theme.js +123 -0
- package/src/game/ui/togglebutton.js +9 -3
- package/src/game/ui/tooltip.js +11 -4
- package/src/io/input.js +75 -45
- package/src/io/mouse.js +44 -19
- package/src/io/touch.js +35 -12
- package/src/math/fluid.js +507 -0
- package/src/math/index.js +2 -0
- package/src/mixins/anchor.js +17 -7
- package/src/motion/tweenetik.js +16 -0
- package/src/shapes/cube3d.js +599 -0
- package/src/shapes/index.js +3 -0
- package/src/shapes/plane3d.js +687 -0
- package/src/shapes/sphere3d.js +75 -6
- package/src/util/camera2d.js +315 -0
- package/src/util/camera3d.js +218 -12
- package/src/util/index.js +1 -0
- package/src/webgl/shaders/plane-shaders.js +332 -0
- package/src/webgl/shaders/sphere-shaders.js +4 -2
- package/types/fluent.d.ts +361 -0
- package/types/game.d.ts +303 -0
- package/types/index.d.ts +144 -5
- package/types/math.d.ts +361 -0
- package/types/motion.d.ts +271 -0
- package/types/particle.d.ts +373 -0
- package/types/shapes.d.ts +107 -9
- package/types/util.d.ts +353 -0
- package/types/webgl.d.ts +109 -0
- package/disk_example.png +0 -0
- package/tde.png +0 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
import { WebGLRenderer } from "../webgl/webgl-renderer.js";
|
|
4
|
+
import { PLANE_SHADERS } from "../webgl/shaders/plane-shaders.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Plane3D - A true 3D plane that integrates with Camera3D
|
|
8
|
+
*
|
|
9
|
+
* A flat rectangular surface in 3D space with proper perspective projection,
|
|
10
|
+
* lighting, and optional WebGL shader effects.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Integrates with Camera3D for rotation and perspective
|
|
14
|
+
* - Self-rotation around any axis
|
|
15
|
+
* - Solid color with lighting
|
|
16
|
+
* - Image texture support
|
|
17
|
+
* - WebGL shader support (gradient, grid, checkerboard)
|
|
18
|
+
* - Double-sided rendering option
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Basic plane with Canvas 2D rendering
|
|
22
|
+
* const plane = new Plane3D(100, 80, {
|
|
23
|
+
* color: "#4A90D9",
|
|
24
|
+
* camera: this.camera,
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Plane with WebGL gradient shader
|
|
29
|
+
* const gradientPlane = new Plane3D(100, 80, {
|
|
30
|
+
* camera: this.camera,
|
|
31
|
+
* useShader: true,
|
|
32
|
+
* shaderType: "gradient",
|
|
33
|
+
* shaderUniforms: {
|
|
34
|
+
* uColor1: [1.0, 0.2, 0.4],
|
|
35
|
+
* uColor2: [0.2, 0.4, 1.0],
|
|
36
|
+
* uAngle: Math.PI / 4,
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
*/
|
|
40
|
+
export class Plane3D extends Shape {
|
|
41
|
+
// Shared WebGL renderer for all shader-enabled planes
|
|
42
|
+
static _glRenderer = null;
|
|
43
|
+
static _glRendererSize = { width: 0, height: 0 };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get or create shared WebGL renderer
|
|
47
|
+
* @param {number} width - Required width
|
|
48
|
+
* @param {number} height - Required height
|
|
49
|
+
* @returns {WebGLRenderer|null}
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
static _getGLRenderer(width, height) {
|
|
53
|
+
if (!Plane3D._glRenderer) {
|
|
54
|
+
Plane3D._glRenderer = new WebGLRenderer(width, height);
|
|
55
|
+
Plane3D._glRendererSize = { width, height };
|
|
56
|
+
} else if (
|
|
57
|
+
Plane3D._glRendererSize.width !== width ||
|
|
58
|
+
Plane3D._glRendererSize.height !== height
|
|
59
|
+
) {
|
|
60
|
+
Plane3D._glRenderer.resize(width, height);
|
|
61
|
+
Plane3D._glRendererSize = { width, height };
|
|
62
|
+
}
|
|
63
|
+
return Plane3D._glRenderer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a 3D plane
|
|
68
|
+
* @param {number} width - Plane width
|
|
69
|
+
* @param {number} height - Plane height
|
|
70
|
+
* @param {object} options - Configuration options
|
|
71
|
+
* @param {number} [options.x=0] - X position in 3D space
|
|
72
|
+
* @param {number} [options.y=0] - Y position in 3D space
|
|
73
|
+
* @param {number} [options.z=0] - Z position in 3D space
|
|
74
|
+
* @param {string} [options.color="#888"] - Fill color (hex format)
|
|
75
|
+
* @param {Camera3D} [options.camera] - Camera for projection
|
|
76
|
+
* @param {boolean} [options.debug=false] - Show wireframe
|
|
77
|
+
* @param {string} [options.stroke] - Wireframe line color
|
|
78
|
+
* @param {number} [options.lineWidth=1] - Wireframe line width
|
|
79
|
+
* @param {boolean} [options.doubleSided=false] - Render both sides
|
|
80
|
+
* @param {HTMLImageElement} [options.texture] - Image texture
|
|
81
|
+
* @param {number} [options.selfRotationX=0] - Self-rotation around X axis (radians)
|
|
82
|
+
* @param {number} [options.selfRotationY=0] - Self-rotation around Y axis (radians)
|
|
83
|
+
* @param {number} [options.selfRotationZ=0] - Self-rotation around Z axis (radians)
|
|
84
|
+
* @param {boolean} [options.useShader=false] - Use WebGL shader rendering
|
|
85
|
+
* @param {string} [options.shaderType='gradient'] - Shader type: 'gradient', 'grid', 'checkerboard'
|
|
86
|
+
* @param {Object} [options.shaderUniforms={}] - Custom shader uniforms
|
|
87
|
+
*/
|
|
88
|
+
constructor(width, height, options = {}) {
|
|
89
|
+
super(options);
|
|
90
|
+
|
|
91
|
+
this.planeWidth = width;
|
|
92
|
+
this.planeHeight = height;
|
|
93
|
+
|
|
94
|
+
// 3D position
|
|
95
|
+
this.x = options.x ?? 0;
|
|
96
|
+
this.y = options.y ?? 0;
|
|
97
|
+
this.z = options.z ?? 0;
|
|
98
|
+
|
|
99
|
+
// Camera reference
|
|
100
|
+
this.camera = options.camera ?? null;
|
|
101
|
+
|
|
102
|
+
// Rendering options
|
|
103
|
+
this.debug = options.debug ?? false;
|
|
104
|
+
this.doubleSided = options.doubleSided ?? true; // Default to double-sided for standalone planes
|
|
105
|
+
this.texture = options.texture ?? null;
|
|
106
|
+
|
|
107
|
+
// Self-rotation (radians)
|
|
108
|
+
this.selfRotationX = options.selfRotationX ?? 0;
|
|
109
|
+
this.selfRotationY = options.selfRotationY ?? 0;
|
|
110
|
+
this.selfRotationZ = options.selfRotationZ ?? 0;
|
|
111
|
+
|
|
112
|
+
// WebGL shader options
|
|
113
|
+
this.useShader = options.useShader ?? false;
|
|
114
|
+
this.shaderType = options.shaderType ?? "gradient";
|
|
115
|
+
this.shaderUniforms = options.shaderUniforms ?? {};
|
|
116
|
+
this._shaderInitialized = false;
|
|
117
|
+
|
|
118
|
+
// Generate plane geometry
|
|
119
|
+
this._generateGeometry();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Set or update the camera reference
|
|
124
|
+
* @param {Camera3D} camera - Camera instance
|
|
125
|
+
* @returns {Plane3D} this for chaining
|
|
126
|
+
*/
|
|
127
|
+
setCamera(camera) {
|
|
128
|
+
this.camera = camera;
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Set texture image
|
|
134
|
+
* @param {HTMLImageElement} image - Image element
|
|
135
|
+
* @returns {Plane3D} this for chaining
|
|
136
|
+
*/
|
|
137
|
+
setTexture(image) {
|
|
138
|
+
this.texture = image;
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Update shader uniforms dynamically
|
|
144
|
+
* @param {Object} uniforms - Uniform name -> value pairs
|
|
145
|
+
* @returns {Plane3D} this for chaining
|
|
146
|
+
*/
|
|
147
|
+
setShaderUniforms(uniforms) {
|
|
148
|
+
Object.assign(this.shaderUniforms, uniforms);
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Generate plane geometry (4 vertices, 2 triangular faces)
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
_generateGeometry() {
|
|
157
|
+
const hw = this.planeWidth / 2;
|
|
158
|
+
const hh = this.planeHeight / 2;
|
|
159
|
+
|
|
160
|
+
// 4 vertices with normals and UVs
|
|
161
|
+
// Plane at z=0, facing -Z (towards camera by default)
|
|
162
|
+
this.vertices = [
|
|
163
|
+
{ x: -hw, y: -hh, z: 0, nx: 0, ny: 0, nz: -1, u: 0, v: 0 }, // top-left
|
|
164
|
+
{ x: hw, y: -hh, z: 0, nx: 0, ny: 0, nz: -1, u: 1, v: 0 }, // top-right
|
|
165
|
+
{ x: hw, y: hh, z: 0, nx: 0, ny: 0, nz: -1, u: 1, v: 1 }, // bottom-right
|
|
166
|
+
{ x: -hw, y: hh, z: 0, nx: 0, ny: 0, nz: -1, u: 0, v: 1 }, // bottom-left
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// 2 triangular faces (CCW winding)
|
|
170
|
+
this.faces = [
|
|
171
|
+
[0, 1, 2], // upper-right triangle
|
|
172
|
+
[0, 2, 3], // lower-left triangle
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get fragment shader source for the current shader type
|
|
178
|
+
* @returns {string}
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
_getFragmentShader() {
|
|
182
|
+
switch (this.shaderType) {
|
|
183
|
+
case "gradient":
|
|
184
|
+
return PLANE_SHADERS.gradient;
|
|
185
|
+
case "grid":
|
|
186
|
+
return PLANE_SHADERS.grid;
|
|
187
|
+
case "checkerboard":
|
|
188
|
+
return PLANE_SHADERS.checkerboard;
|
|
189
|
+
case "noise":
|
|
190
|
+
return PLANE_SHADERS.noise;
|
|
191
|
+
default:
|
|
192
|
+
return PLANE_SHADERS.gradient;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Initialize or update the WebGL shader
|
|
198
|
+
* @param {number} renderWidth - Render width
|
|
199
|
+
* @param {number} renderHeight - Render height
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
_initShader(renderWidth, renderHeight) {
|
|
203
|
+
const gl = Plane3D._getGLRenderer(renderWidth, renderHeight);
|
|
204
|
+
if (!gl || !gl.isAvailable()) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const programName = `plane_${this.shaderType}`;
|
|
209
|
+
const vertexShader = PLANE_SHADERS.vertex;
|
|
210
|
+
const fragmentShader = this._getFragmentShader();
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
gl.useProgram(programName, vertexShader, fragmentShader);
|
|
214
|
+
this._shaderInitialized = true;
|
|
215
|
+
return true;
|
|
216
|
+
} catch (e) {
|
|
217
|
+
console.warn("Plane3D shader init failed:", e);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Render plane using WebGL shader
|
|
224
|
+
* @param {CanvasRenderingContext2D} ctx - 2D context for compositing
|
|
225
|
+
* @param {number} screenX - Screen X position
|
|
226
|
+
* @param {number} screenY - Screen Y position
|
|
227
|
+
* @param {number} screenWidth - Screen width after projection
|
|
228
|
+
* @param {number} screenHeight - Screen height after projection
|
|
229
|
+
* @returns {boolean} True if shader rendering succeeded
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
_renderWithShader(ctx, screenX, screenY, screenWidth, screenHeight) {
|
|
233
|
+
// Ensure minimum size for rendering
|
|
234
|
+
const renderSize = Math.max(
|
|
235
|
+
Math.ceil(Math.max(screenWidth, screenHeight)),
|
|
236
|
+
16
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (!this._shaderInitialized) {
|
|
240
|
+
if (!this._initShader(renderSize, renderSize)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const gl = Plane3D._getGLRenderer(renderSize, renderSize);
|
|
246
|
+
if (!gl || !gl.isAvailable()) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Ensure correct program is active
|
|
251
|
+
const programName = `plane_${this.shaderType}`;
|
|
252
|
+
gl.useProgram(programName, PLANE_SHADERS.vertex, this._getFragmentShader());
|
|
253
|
+
|
|
254
|
+
// Set common uniforms
|
|
255
|
+
const time = performance.now() / 1000;
|
|
256
|
+
gl.setUniforms({
|
|
257
|
+
uTime: time,
|
|
258
|
+
uResolution: [renderSize, renderSize],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Set shader-specific uniforms
|
|
262
|
+
gl.setUniforms(this.shaderUniforms);
|
|
263
|
+
|
|
264
|
+
// Render
|
|
265
|
+
gl.render();
|
|
266
|
+
|
|
267
|
+
// Composite onto 2D canvas at the projected position
|
|
268
|
+
const drawX = screenX - screenWidth / 2;
|
|
269
|
+
const drawY = screenY - screenHeight / 2;
|
|
270
|
+
gl.compositeOnto(ctx, drawX, drawY, screenWidth, screenHeight);
|
|
271
|
+
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Apply self-rotation to a point (vertex or normal)
|
|
277
|
+
* @param {number} x - X component
|
|
278
|
+
* @param {number} y - Y component
|
|
279
|
+
* @param {number} z - Z component
|
|
280
|
+
* @returns {{x: number, y: number, z: number}} Rotated point
|
|
281
|
+
* @private
|
|
282
|
+
*/
|
|
283
|
+
_applySelfRotation(x, y, z) {
|
|
284
|
+
// Rotate around Y axis first (most common for spinning objects)
|
|
285
|
+
if (this.selfRotationY !== 0) {
|
|
286
|
+
const cosY = Math.cos(this.selfRotationY);
|
|
287
|
+
const sinY = Math.sin(this.selfRotationY);
|
|
288
|
+
const x1 = x * cosY - z * sinY;
|
|
289
|
+
const z1 = x * sinY + z * cosY;
|
|
290
|
+
x = x1;
|
|
291
|
+
z = z1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Rotate around X axis
|
|
295
|
+
if (this.selfRotationX !== 0) {
|
|
296
|
+
const cosX = Math.cos(this.selfRotationX);
|
|
297
|
+
const sinX = Math.sin(this.selfRotationX);
|
|
298
|
+
const y1 = y * cosX - z * sinX;
|
|
299
|
+
const z1 = y * sinX + z * cosX;
|
|
300
|
+
y = y1;
|
|
301
|
+
z = z1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Rotate around Z axis
|
|
305
|
+
if (this.selfRotationZ !== 0) {
|
|
306
|
+
const cosZ = Math.cos(this.selfRotationZ);
|
|
307
|
+
const sinZ = Math.sin(this.selfRotationZ);
|
|
308
|
+
const x1 = x * cosZ - y * sinZ;
|
|
309
|
+
const y1 = x * sinZ + y * cosZ;
|
|
310
|
+
x = x1;
|
|
311
|
+
y = y1;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { x, y, z };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Calculate lighting intensity based on surface normal
|
|
319
|
+
* @param {number} nx - Normal x component
|
|
320
|
+
* @param {number} ny - Normal y component
|
|
321
|
+
* @param {number} nz - Normal z component
|
|
322
|
+
* @returns {number} Intensity 0-1
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
_calculateLighting(nx, ny, nz) {
|
|
326
|
+
// Simple directional light from top-right-front
|
|
327
|
+
const lightX = 0.5;
|
|
328
|
+
const lightY = 0.7;
|
|
329
|
+
const lightZ = 0.5;
|
|
330
|
+
const lightLen = Math.sqrt(
|
|
331
|
+
lightX * lightX + lightY * lightY + lightZ * lightZ
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Normalized light direction
|
|
335
|
+
const lx = lightX / lightLen;
|
|
336
|
+
const ly = lightY / lightLen;
|
|
337
|
+
const lz = lightZ / lightLen;
|
|
338
|
+
|
|
339
|
+
// Dot product for diffuse lighting
|
|
340
|
+
let intensity = nx * lx + ny * ly + nz * lz;
|
|
341
|
+
|
|
342
|
+
// Clamp and add ambient light
|
|
343
|
+
intensity = Math.max(0, intensity) * 0.7 + 0.3;
|
|
344
|
+
|
|
345
|
+
return intensity;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Apply lighting to a color
|
|
350
|
+
* @param {string} color - Base color (hex format)
|
|
351
|
+
* @param {number} intensity - Light intensity 0-1
|
|
352
|
+
* @returns {string} RGB color string
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
_applyLighting(color, intensity) {
|
|
356
|
+
if (!color || typeof color !== "string" || !color.startsWith("#")) {
|
|
357
|
+
return color;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Parse hex color
|
|
361
|
+
const hex = color.replace("#", "");
|
|
362
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
363
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
364
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
365
|
+
|
|
366
|
+
// Apply intensity
|
|
367
|
+
const lr = Math.round(r * intensity);
|
|
368
|
+
const lg = Math.round(g * intensity);
|
|
369
|
+
const lb = Math.round(b * intensity);
|
|
370
|
+
|
|
371
|
+
return `rgb(${lr}, ${lg}, ${lb})`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Main render method
|
|
376
|
+
*/
|
|
377
|
+
draw() {
|
|
378
|
+
super.draw();
|
|
379
|
+
|
|
380
|
+
if (!this.camera) {
|
|
381
|
+
// Fallback: draw a simple rectangle if no camera
|
|
382
|
+
if (this.color) {
|
|
383
|
+
Painter.shapes.fillRect(
|
|
384
|
+
-this.planeWidth / 2,
|
|
385
|
+
-this.planeHeight / 2,
|
|
386
|
+
this.planeWidth,
|
|
387
|
+
this.planeHeight,
|
|
388
|
+
this.color
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Check if any self-rotation is applied
|
|
395
|
+
const hasSelfRotation =
|
|
396
|
+
this.selfRotationX !== 0 ||
|
|
397
|
+
this.selfRotationY !== 0 ||
|
|
398
|
+
this.selfRotationZ !== 0;
|
|
399
|
+
|
|
400
|
+
// Project all vertices
|
|
401
|
+
const projectedVertices = this.vertices.map((v) => {
|
|
402
|
+
let vx = v.x;
|
|
403
|
+
let vy = v.y;
|
|
404
|
+
let vz = v.z;
|
|
405
|
+
let nx = v.nx;
|
|
406
|
+
let ny = v.ny;
|
|
407
|
+
let nz = v.nz;
|
|
408
|
+
|
|
409
|
+
// Apply self-rotation
|
|
410
|
+
if (hasSelfRotation) {
|
|
411
|
+
const rotatedPos = this._applySelfRotation(vx, vy, vz);
|
|
412
|
+
vx = rotatedPos.x;
|
|
413
|
+
vy = rotatedPos.y;
|
|
414
|
+
vz = rotatedPos.z;
|
|
415
|
+
|
|
416
|
+
const rotatedNormal = this._applySelfRotation(nx, ny, nz);
|
|
417
|
+
nx = rotatedNormal.x;
|
|
418
|
+
ny = rotatedNormal.y;
|
|
419
|
+
nz = rotatedNormal.z;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Project through camera
|
|
423
|
+
const projected = this.camera.project(
|
|
424
|
+
vx + this.x,
|
|
425
|
+
vy + this.y,
|
|
426
|
+
vz + this.z
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Rotate normals through camera rotation
|
|
430
|
+
// Z axis rotation
|
|
431
|
+
if (this.camera.rotationZ !== 0) {
|
|
432
|
+
const cosZ = Math.cos(this.camera.rotationZ);
|
|
433
|
+
const sinZ = Math.sin(this.camera.rotationZ);
|
|
434
|
+
const nx0 = nx;
|
|
435
|
+
const ny0 = ny;
|
|
436
|
+
nx = nx0 * cosZ - ny0 * sinZ;
|
|
437
|
+
ny = nx0 * sinZ + ny0 * cosZ;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Y axis rotation
|
|
441
|
+
const cosY = Math.cos(this.camera.rotationY);
|
|
442
|
+
const sinY = Math.sin(this.camera.rotationY);
|
|
443
|
+
const nx1 = nx * cosY - nz * sinY;
|
|
444
|
+
const nz1 = nx * sinY + nz * cosY;
|
|
445
|
+
|
|
446
|
+
// X axis rotation
|
|
447
|
+
const cosX = Math.cos(this.camera.rotationX);
|
|
448
|
+
const sinX = Math.sin(this.camera.rotationX);
|
|
449
|
+
const ny1 = ny * cosX - nz1 * sinX;
|
|
450
|
+
const nz2 = ny * sinX + nz1 * cosX;
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
...projected,
|
|
454
|
+
nx: nx1,
|
|
455
|
+
ny: ny1,
|
|
456
|
+
nz: nz2,
|
|
457
|
+
u: v.u,
|
|
458
|
+
v: v.v,
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Calculate average normal for backface culling
|
|
463
|
+
const avgNz =
|
|
464
|
+
(projectedVertices[0].nz +
|
|
465
|
+
projectedVertices[1].nz +
|
|
466
|
+
projectedVertices[2].nz +
|
|
467
|
+
projectedVertices[3].nz) /
|
|
468
|
+
4;
|
|
469
|
+
|
|
470
|
+
// Backface culling (unless double-sided)
|
|
471
|
+
if (!this.doubleSided && avgNz > 0.1) {
|
|
472
|
+
return; // Face is pointing away from camera
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Calculate average depth for z-sorting (used by parent containers)
|
|
476
|
+
const avgZ =
|
|
477
|
+
(projectedVertices[0].z +
|
|
478
|
+
projectedVertices[1].z +
|
|
479
|
+
projectedVertices[2].z +
|
|
480
|
+
projectedVertices[3].z) /
|
|
481
|
+
4;
|
|
482
|
+
|
|
483
|
+
// Check if any vertex is behind camera
|
|
484
|
+
const behindCamera = projectedVertices.some(
|
|
485
|
+
(v) => v.z < -this.camera.perspective + 10
|
|
486
|
+
);
|
|
487
|
+
if (behindCamera) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// For WebGL shader rendering, calculate screen bounds
|
|
492
|
+
if (this.useShader && !this.debug) {
|
|
493
|
+
const xs = projectedVertices.map((v) => v.x);
|
|
494
|
+
const ys = projectedVertices.map((v) => v.y);
|
|
495
|
+
const minX = Math.min(...xs);
|
|
496
|
+
const maxX = Math.max(...xs);
|
|
497
|
+
const minY = Math.min(...ys);
|
|
498
|
+
const maxY = Math.max(...ys);
|
|
499
|
+
const screenWidth = maxX - minX;
|
|
500
|
+
const screenHeight = maxY - minY;
|
|
501
|
+
const centerX = (minX + maxX) / 2;
|
|
502
|
+
const centerY = (minY + maxY) / 2;
|
|
503
|
+
|
|
504
|
+
// Get canvas transform for absolute positioning
|
|
505
|
+
const ctx = Painter.ctx;
|
|
506
|
+
const transform = ctx.getTransform();
|
|
507
|
+
const sceneX = transform.e;
|
|
508
|
+
const sceneY = transform.f;
|
|
509
|
+
|
|
510
|
+
ctx.save();
|
|
511
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
512
|
+
const success = this._renderWithShader(
|
|
513
|
+
ctx,
|
|
514
|
+
sceneX + centerX,
|
|
515
|
+
sceneY + centerY,
|
|
516
|
+
screenWidth,
|
|
517
|
+
screenHeight
|
|
518
|
+
);
|
|
519
|
+
ctx.restore();
|
|
520
|
+
|
|
521
|
+
if (success) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
// Fall through to Canvas 2D if shader failed
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Canvas 2D rendering path
|
|
528
|
+
const ctx = Painter.ctx;
|
|
529
|
+
|
|
530
|
+
// Calculate lighting based on normal direction
|
|
531
|
+
let avgNx =
|
|
532
|
+
(projectedVertices[0].nx +
|
|
533
|
+
projectedVertices[1].nx +
|
|
534
|
+
projectedVertices[2].nx +
|
|
535
|
+
projectedVertices[3].nx) /
|
|
536
|
+
4;
|
|
537
|
+
let avgNy =
|
|
538
|
+
(projectedVertices[0].ny +
|
|
539
|
+
projectedVertices[1].ny +
|
|
540
|
+
projectedVertices[2].ny +
|
|
541
|
+
projectedVertices[3].ny) /
|
|
542
|
+
4;
|
|
543
|
+
|
|
544
|
+
// Flip normal for lighting when viewing the back side
|
|
545
|
+
let lightNx = avgNx;
|
|
546
|
+
let lightNy = avgNy;
|
|
547
|
+
let lightNz = avgNz;
|
|
548
|
+
if (this.doubleSided && avgNz > 0) {
|
|
549
|
+
lightNx = -avgNx;
|
|
550
|
+
lightNy = -avgNy;
|
|
551
|
+
lightNz = -avgNz;
|
|
552
|
+
}
|
|
553
|
+
const intensity = this._calculateLighting(lightNx, lightNy, lightNz);
|
|
554
|
+
|
|
555
|
+
// Render as two triangles
|
|
556
|
+
for (const face of this.faces) {
|
|
557
|
+
const v0 = projectedVertices[face[0]];
|
|
558
|
+
const v1 = projectedVertices[face[1]];
|
|
559
|
+
const v2 = projectedVertices[face[2]];
|
|
560
|
+
|
|
561
|
+
if (this.debug) {
|
|
562
|
+
// Wireframe mode
|
|
563
|
+
ctx.beginPath();
|
|
564
|
+
ctx.moveTo(v0.x, v0.y);
|
|
565
|
+
ctx.lineTo(v1.x, v1.y);
|
|
566
|
+
ctx.lineTo(v2.x, v2.y);
|
|
567
|
+
ctx.closePath();
|
|
568
|
+
|
|
569
|
+
if (this.stroke) {
|
|
570
|
+
ctx.strokeStyle = this.stroke;
|
|
571
|
+
ctx.lineWidth = this.lineWidth ?? 1;
|
|
572
|
+
ctx.stroke();
|
|
573
|
+
}
|
|
574
|
+
} else if (this.texture) {
|
|
575
|
+
// Texture mapping (simplified - for accurate perspective would need more complex math)
|
|
576
|
+
this._renderTexturedTriangle(ctx, v0, v1, v2);
|
|
577
|
+
} else if (this.color) {
|
|
578
|
+
// Solid color with lighting
|
|
579
|
+
const faceColor = this._applyLighting(this.color, intensity);
|
|
580
|
+
|
|
581
|
+
ctx.beginPath();
|
|
582
|
+
ctx.moveTo(v0.x, v0.y);
|
|
583
|
+
ctx.lineTo(v1.x, v1.y);
|
|
584
|
+
ctx.lineTo(v2.x, v2.y);
|
|
585
|
+
ctx.closePath();
|
|
586
|
+
ctx.fillStyle = faceColor;
|
|
587
|
+
ctx.fill();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Render a textured triangle (simplified affine mapping)
|
|
594
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas context
|
|
595
|
+
* @param {Object} v0 - Vertex 0 with x, y, u, v
|
|
596
|
+
* @param {Object} v1 - Vertex 1 with x, y, u, v
|
|
597
|
+
* @param {Object} v2 - Vertex 2 with x, y, u, v
|
|
598
|
+
* @private
|
|
599
|
+
*/
|
|
600
|
+
_renderTexturedTriangle(ctx, v0, v1, v2) {
|
|
601
|
+
if (!this.texture || !this.texture.complete) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const img = this.texture;
|
|
606
|
+
const imgW = img.width;
|
|
607
|
+
const imgH = img.height;
|
|
608
|
+
|
|
609
|
+
// Use affine texture mapping (works well for roughly planar surfaces)
|
|
610
|
+
// Transform from UV space to screen space
|
|
611
|
+
|
|
612
|
+
// Source triangle in texture coordinates
|
|
613
|
+
const su0 = v0.u * imgW;
|
|
614
|
+
const sv0 = v0.v * imgH;
|
|
615
|
+
const su1 = v1.u * imgW;
|
|
616
|
+
const sv1 = v1.v * imgH;
|
|
617
|
+
const su2 = v2.u * imgW;
|
|
618
|
+
const sv2 = v2.v * imgH;
|
|
619
|
+
|
|
620
|
+
// Destination triangle in screen coordinates
|
|
621
|
+
const dx0 = v0.x;
|
|
622
|
+
const dy0 = v0.y;
|
|
623
|
+
const dx1 = v1.x;
|
|
624
|
+
const dy1 = v1.y;
|
|
625
|
+
const dx2 = v2.x;
|
|
626
|
+
const dy2 = v2.y;
|
|
627
|
+
|
|
628
|
+
// Calculate affine transformation matrix
|
|
629
|
+
// This maps from texture space to screen space
|
|
630
|
+
const det =
|
|
631
|
+
(su1 - su0) * (sv2 - sv0) - (su2 - su0) * (sv1 - sv0);
|
|
632
|
+
|
|
633
|
+
if (Math.abs(det) < 0.0001) {
|
|
634
|
+
return; // Degenerate triangle
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const a =
|
|
638
|
+
((dx1 - dx0) * (sv2 - sv0) - (dx2 - dx0) * (sv1 - sv0)) / det;
|
|
639
|
+
const b =
|
|
640
|
+
((dx2 - dx0) * (su1 - su0) - (dx1 - dx0) * (su2 - su0)) / det;
|
|
641
|
+
const c = dx0 - a * su0 - b * sv0;
|
|
642
|
+
const d =
|
|
643
|
+
((dy1 - dy0) * (sv2 - sv0) - (dy2 - dy0) * (sv1 - sv0)) / det;
|
|
644
|
+
const e =
|
|
645
|
+
((dy2 - dy0) * (su1 - su0) - (dy1 - dy0) * (su2 - su0)) / det;
|
|
646
|
+
const f = dy0 - d * su0 - e * sv0;
|
|
647
|
+
|
|
648
|
+
ctx.save();
|
|
649
|
+
|
|
650
|
+
// Clip to triangle
|
|
651
|
+
ctx.beginPath();
|
|
652
|
+
ctx.moveTo(dx0, dy0);
|
|
653
|
+
ctx.lineTo(dx1, dy1);
|
|
654
|
+
ctx.lineTo(dx2, dy2);
|
|
655
|
+
ctx.closePath();
|
|
656
|
+
ctx.clip();
|
|
657
|
+
|
|
658
|
+
// Apply transformation and draw image
|
|
659
|
+
ctx.setTransform(a, d, b, e, c, f);
|
|
660
|
+
ctx.drawImage(img, 0, 0);
|
|
661
|
+
|
|
662
|
+
ctx.restore();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Get the center point in world coordinates
|
|
667
|
+
* @returns {{x: number, y: number, z: number}}
|
|
668
|
+
*/
|
|
669
|
+
getCenter() {
|
|
670
|
+
return { x: this.x, y: this.y, z: this.z };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Get bounding box for the plane
|
|
675
|
+
* @returns {{x: number, y: number, width: number, height: number}}
|
|
676
|
+
*/
|
|
677
|
+
getBounds() {
|
|
678
|
+
// Simple bounds based on plane dimensions
|
|
679
|
+
// Note: This doesn't account for rotation
|
|
680
|
+
return {
|
|
681
|
+
x: this.x - this.planeWidth / 2,
|
|
682
|
+
y: this.y - this.planeHeight / 2,
|
|
683
|
+
width: this.planeWidth,
|
|
684
|
+
height: this.planeHeight,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
}
|