3d-spinner 0.9.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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/animation.d.ts +28 -0
- package/dist/animation.js +1 -0
- package/dist/animations/object-motion.d.ts +96 -0
- package/dist/animations/object-motion.js +339 -0
- package/dist/animations/spin.d.ts +44 -0
- package/dist/animations/spin.js +108 -0
- package/dist/engines/little-3d-engine/core/camera.d.ts +26 -0
- package/dist/engines/little-3d-engine/core/camera.js +32 -0
- package/dist/engines/little-3d-engine/core/geometry.d.ts +27 -0
- package/dist/engines/little-3d-engine/core/geometry.js +87 -0
- package/dist/engines/little-3d-engine/core/light.d.ts +31 -0
- package/dist/engines/little-3d-engine/core/light.js +35 -0
- package/dist/engines/little-3d-engine/core/math.d.ts +50 -0
- package/dist/engines/little-3d-engine/core/math.js +109 -0
- package/dist/engines/little-3d-engine/core/mesh.d.ts +22 -0
- package/dist/engines/little-3d-engine/core/mesh.js +8 -0
- package/dist/engines/little-3d-engine/little-3d-engine.d.ts +75 -0
- package/dist/engines/little-3d-engine/little-3d-engine.js +167 -0
- package/dist/engines/little-3d-engine/loaders/obj.d.ts +24 -0
- package/dist/engines/little-3d-engine/loaders/obj.js +48 -0
- package/dist/engines/little-3d-engine/renderer.d.ts +43 -0
- package/dist/engines/little-3d-engine/renderer.js +16 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d.d.ts +12 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d.js +73 -0
- package/dist/engines/little-3d-engine/renderers/webgl.d.ts +15 -0
- package/dist/engines/little-3d-engine/renderers/webgl.js +146 -0
- package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +24 -0
- package/dist/engines/little-3d-engine/renderers/webgpu.js +222 -0
- package/dist/engines/little-3d-engine/shapes/cube-sphere.d.ts +11 -0
- package/dist/engines/little-3d-engine/shapes/cube-sphere.js +50 -0
- package/dist/engines/little-3d-engine/shapes/cube.d.ts +9 -0
- package/dist/engines/little-3d-engine/shapes/cube.js +30 -0
- package/dist/engines/little-3d-engine/shapes/icosphere.d.ts +11 -0
- package/dist/engines/little-3d-engine/shapes/icosphere.js +51 -0
- package/dist/engines/little-3d-engine/shapes/octa-sphere.d.ts +10 -0
- package/dist/engines/little-3d-engine/shapes/octa-sphere.js +31 -0
- package/dist/engines/little-3d-engine/shapes/octahedron.d.ts +8 -0
- package/dist/engines/little-3d-engine/shapes/octahedron.js +38 -0
- package/dist/engines/little-3d-engine/shapes/pyramid.d.ts +9 -0
- package/dist/engines/little-3d-engine/shapes/pyramid.js +26 -0
- package/dist/engines/little-3d-engine/shapes/tetrahedron.d.ts +8 -0
- package/dist/engines/little-3d-engine/shapes/tetrahedron.js +23 -0
- package/dist/engines/little-3d-engine/shapes/uv-sphere.d.ts +10 -0
- package/dist/engines/little-3d-engine/shapes/uv-sphere.js +50 -0
- package/dist/engines/little-tween-engine/core/tweens.d.ts +39 -0
- package/dist/engines/little-tween-engine/core/tweens.js +231 -0
- package/dist/engines/little-tween-engine/little-tween-engine.d.ts +21 -0
- package/dist/engines/little-tween-engine/little-tween-engine.js +16 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +110 -0
- package/dist/motion/circle.d.ts +17 -0
- package/dist/motion/circle.js +20 -0
- package/dist/motion/controller.d.ts +13 -0
- package/dist/motion/controller.js +1 -0
- package/dist/motion/figure-eight.d.ts +13 -0
- package/dist/motion/figure-eight.js +23 -0
- package/dist/motion/motion.d.ts +5 -0
- package/dist/motion/motion.js +4 -0
- package/dist/motion/square.d.ts +17 -0
- package/dist/motion/square.js +39 -0
- package/dist/motion/transitions.d.ts +44 -0
- package/dist/motion/transitions.js +60 -0
- package/dist/motion/wander.d.ts +22 -0
- package/dist/motion/wander.js +50 -0
- package/dist/progress-animation.d.ts +55 -0
- package/dist/progress-animation.js +128 -0
- package/package.json +74 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AnimationFrame, SpinnerAnimation } from "../animation.js";
|
|
2
|
+
import { type Backend, type Mesh } from "../engines/little-3d-engine/little-3d-engine.js";
|
|
3
|
+
import { type ProgressAnimationOptions } from "../progress-animation.js";
|
|
4
|
+
export interface SpinAnimationOptions {
|
|
5
|
+
/** Shape to spin: a mesh, or a factory that returns one. Default: a cube. */
|
|
6
|
+
shape?: Mesh | (() => Mesh);
|
|
7
|
+
/** Face color(s): one color for every face, or an array applied per face. */
|
|
8
|
+
color?: string | string[];
|
|
9
|
+
/** Rotation speed around the X axis, in radians per millisecond. Default `0.0007`. */
|
|
10
|
+
spinX?: number;
|
|
11
|
+
/** Rotation speed around the Y axis, in radians per millisecond. Default `0.0011`. */
|
|
12
|
+
spinY?: number;
|
|
13
|
+
/** Rendering backend. Default `"canvas2d"`. */
|
|
14
|
+
backend?: Backend;
|
|
15
|
+
/**
|
|
16
|
+
* Enable the start/end pop and progress-driven scale, with an optional overlay
|
|
17
|
+
* label. Omit to spin at constant size with no progress reaction.
|
|
18
|
+
*/
|
|
19
|
+
progressAnimation?: ProgressAnimationOptions;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A spinning, flat-lit 3D shape (a cube by default). With `progressAnimation`
|
|
23
|
+
* set it pops in/out and its scale tracks progress, with an optional label;
|
|
24
|
+
* otherwise it just spins at a constant size.
|
|
25
|
+
*/
|
|
26
|
+
export declare class SpinAnimation implements SpinnerAnimation {
|
|
27
|
+
private engine?;
|
|
28
|
+
private handle?;
|
|
29
|
+
private label?;
|
|
30
|
+
private readonly mesh;
|
|
31
|
+
private readonly spinX;
|
|
32
|
+
private readonly spinY;
|
|
33
|
+
private readonly backend?;
|
|
34
|
+
private readonly progress?;
|
|
35
|
+
private exited;
|
|
36
|
+
constructor(options?: SpinAnimationOptions);
|
|
37
|
+
mount(target: HTMLElement): void;
|
|
38
|
+
enter(now: number): void;
|
|
39
|
+
exit(now: number): void;
|
|
40
|
+
isFinished(): boolean;
|
|
41
|
+
render(now: number, frame: AnimationFrame): void;
|
|
42
|
+
destroy(): void;
|
|
43
|
+
private applyLabel;
|
|
44
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Little3dEngine, cube, } from "../engines/little-3d-engine/little-3d-engine.js";
|
|
2
|
+
import { ProgressAnimation, } from "../progress-animation.js";
|
|
3
|
+
const LABEL_STYLE = [
|
|
4
|
+
"position:absolute",
|
|
5
|
+
"inset:0",
|
|
6
|
+
"display:flex",
|
|
7
|
+
"align-items:center",
|
|
8
|
+
"justify-content:center",
|
|
9
|
+
"pointer-events:none",
|
|
10
|
+
"font:600 1.1rem/1.2 system-ui,sans-serif",
|
|
11
|
+
"letter-spacing:0.06em",
|
|
12
|
+
"text-transform:lowercase",
|
|
13
|
+
"color:rgba(255,255,255,0.65)",
|
|
14
|
+
"z-index:1",
|
|
15
|
+
].join(";");
|
|
16
|
+
function resolveMesh(shape) {
|
|
17
|
+
if (!shape)
|
|
18
|
+
return cube();
|
|
19
|
+
return typeof shape === "function" ? shape() : shape;
|
|
20
|
+
}
|
|
21
|
+
function applyColor(mesh, color) {
|
|
22
|
+
if (color === undefined || (Array.isArray(color) && color.length === 0))
|
|
23
|
+
return mesh;
|
|
24
|
+
const pick = Array.isArray(color) ? (i) => color[i % color.length] : () => color;
|
|
25
|
+
return { vertices: mesh.vertices, faces: mesh.faces.map((f, i) => ({ ...f, color: pick(i) })) };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A spinning, flat-lit 3D shape (a cube by default). With `progressAnimation`
|
|
29
|
+
* set it pops in/out and its scale tracks progress, with an optional label;
|
|
30
|
+
* otherwise it just spins at a constant size.
|
|
31
|
+
*/
|
|
32
|
+
export class SpinAnimation {
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.exited = false;
|
|
35
|
+
this.mesh = applyColor(resolveMesh(options.shape), options.color);
|
|
36
|
+
this.spinX = options.spinX ?? 0.0007;
|
|
37
|
+
this.spinY = options.spinY ?? 0.0011;
|
|
38
|
+
this.backend = options.backend;
|
|
39
|
+
this.progress = options.progressAnimation
|
|
40
|
+
? new ProgressAnimation(options.progressAnimation)
|
|
41
|
+
: undefined;
|
|
42
|
+
}
|
|
43
|
+
mount(target) {
|
|
44
|
+
target.style.position = "relative";
|
|
45
|
+
const engine = new Little3dEngine({
|
|
46
|
+
backend: this.backend,
|
|
47
|
+
camera: { position: { x: 0, y: 0, z: 2.8 } },
|
|
48
|
+
});
|
|
49
|
+
this.handle = engine.add(this.mesh);
|
|
50
|
+
this.engine = engine;
|
|
51
|
+
engine.mount(target).catch((error) => {
|
|
52
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
53
|
+
});
|
|
54
|
+
if (this.progress) {
|
|
55
|
+
const label = document.createElement("div");
|
|
56
|
+
label.style.cssText = LABEL_STYLE;
|
|
57
|
+
label.setAttribute("aria-hidden", "true");
|
|
58
|
+
label.hidden = true;
|
|
59
|
+
target.appendChild(label);
|
|
60
|
+
this.label = label;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
enter(now) {
|
|
64
|
+
this.progress?.enter(now);
|
|
65
|
+
}
|
|
66
|
+
exit(now) {
|
|
67
|
+
this.exited = true;
|
|
68
|
+
this.progress?.exit(now);
|
|
69
|
+
}
|
|
70
|
+
isFinished() {
|
|
71
|
+
return this.progress ? this.progress.isFinished() : this.exited;
|
|
72
|
+
}
|
|
73
|
+
render(now, frame) {
|
|
74
|
+
if (!this.engine || !this.handle)
|
|
75
|
+
return;
|
|
76
|
+
const rotation = this.handle.transform.rotation;
|
|
77
|
+
rotation.x = now * this.spinX;
|
|
78
|
+
rotation.y = now * this.spinY;
|
|
79
|
+
if (this.progress) {
|
|
80
|
+
const visual = this.progress.update(now, frame.progress, frame.targetProgress);
|
|
81
|
+
this.handle.transform.scale = visual.hidden ? 0 : visual.scale;
|
|
82
|
+
this.applyLabel(visual);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.handle.transform.scale = 1;
|
|
86
|
+
}
|
|
87
|
+
this.engine.render();
|
|
88
|
+
}
|
|
89
|
+
destroy() {
|
|
90
|
+
this.label?.remove();
|
|
91
|
+
this.label = undefined;
|
|
92
|
+
this.engine?.destroy();
|
|
93
|
+
this.engine = undefined;
|
|
94
|
+
this.handle = undefined;
|
|
95
|
+
}
|
|
96
|
+
applyLabel(visual) {
|
|
97
|
+
if (!this.label)
|
|
98
|
+
return;
|
|
99
|
+
if (visual.hidden || visual.text == null) {
|
|
100
|
+
this.label.hidden = true;
|
|
101
|
+
this.label.textContent = "";
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.label.hidden = false;
|
|
105
|
+
this.label.textContent = visual.text;
|
|
106
|
+
this.label.style.opacity = String(visual.textOpacity);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Mat4, type Vec3 } from "./math.js";
|
|
2
|
+
/** Camera configuration. */
|
|
3
|
+
export interface CameraOptions {
|
|
4
|
+
/** Eye position. Default `{ x: 0, y: 0, z: 4 }` (looking toward -Z). */
|
|
5
|
+
position: Vec3;
|
|
6
|
+
/** Vertical field of view in radians. Default `~0.96` (55 degrees). */
|
|
7
|
+
fov: number;
|
|
8
|
+
/** Near clip distance. Default `0.1`. */
|
|
9
|
+
near: number;
|
|
10
|
+
/** Far clip distance. Default `100`. */
|
|
11
|
+
far: number;
|
|
12
|
+
}
|
|
13
|
+
/** A perspective camera looking down the -Z axis from its `position`. */
|
|
14
|
+
export declare class Camera {
|
|
15
|
+
readonly options: CameraOptions;
|
|
16
|
+
constructor(options?: Partial<CameraOptions>);
|
|
17
|
+
/** Transform a world-space point into view (camera) space. */
|
|
18
|
+
toView(p: Vec3): Vec3;
|
|
19
|
+
/** Combined view-projection matrix for the given viewport aspect ratio. */
|
|
20
|
+
viewProjection(aspect: number): Mat4;
|
|
21
|
+
/** Convert a normalized device coordinate (-1..1) to a pixel position. */
|
|
22
|
+
toScreen(ndc: Vec3, width: number, height: number): {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { multiply, perspective, transformAffine, translation, } from "./math.js";
|
|
2
|
+
const DEFAULTS = {
|
|
3
|
+
position: { x: 0, y: 0, z: 4 },
|
|
4
|
+
fov: (55 * Math.PI) / 180,
|
|
5
|
+
near: 0.1,
|
|
6
|
+
far: 100,
|
|
7
|
+
};
|
|
8
|
+
/** A perspective camera looking down the -Z axis from its `position`. */
|
|
9
|
+
export class Camera {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = { ...DEFAULTS, ...options };
|
|
12
|
+
}
|
|
13
|
+
/** Transform a world-space point into view (camera) space. */
|
|
14
|
+
toView(p) {
|
|
15
|
+
const { position } = this.options;
|
|
16
|
+
return transformAffine(translation(-position.x, -position.y, -position.z), p);
|
|
17
|
+
}
|
|
18
|
+
/** Combined view-projection matrix for the given viewport aspect ratio. */
|
|
19
|
+
viewProjection(aspect) {
|
|
20
|
+
const { position, fov, near, far } = this.options;
|
|
21
|
+
const view = translation(-position.x, -position.y, -position.z);
|
|
22
|
+
const projection = perspective(fov, aspect, near, far);
|
|
23
|
+
return multiply(projection, view);
|
|
24
|
+
}
|
|
25
|
+
/** Convert a normalized device coordinate (-1..1) to a pixel position. */
|
|
26
|
+
toScreen(ndc, width, height) {
|
|
27
|
+
return {
|
|
28
|
+
x: (ndc.x * 0.5 + 0.5) * width,
|
|
29
|
+
y: (1 - (ndc.y * 0.5 + 0.5)) * height,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Vec3 } from "./math.js";
|
|
2
|
+
import type { Mesh } from "./mesh.js";
|
|
3
|
+
/** Parse a CSS hex color (`#rgb` or `#rrggbb`) into 0..255 channels. */
|
|
4
|
+
export declare function parseColor(color: string): [number, number, number];
|
|
5
|
+
/** Flat triangle soup ready for GPU upload: 3 floats per vertex per array. */
|
|
6
|
+
export interface TriangleData {
|
|
7
|
+
positions: Float32Array;
|
|
8
|
+
normals: Float32Array;
|
|
9
|
+
colors: Float32Array;
|
|
10
|
+
/** Number of vertices (positions.length / 3). */
|
|
11
|
+
count: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Triangulate a mesh into a non-indexed soup where every vertex carries its
|
|
15
|
+
* face's normal and color. This is what GPU backends upload to render flat
|
|
16
|
+
* shading without per-primitive state.
|
|
17
|
+
*/
|
|
18
|
+
export declare function expandToTriangles(mesh: Mesh): TriangleData;
|
|
19
|
+
/**
|
|
20
|
+
* Build a sphere by subdividing a triangular seed polyhedron and projecting
|
|
21
|
+
* every vertex onto the sphere. `detail` 1 keeps the seed; each extra level
|
|
22
|
+
* splits every triangle into four. Used by the icosphere and octa-sphere.
|
|
23
|
+
*
|
|
24
|
+
* @param seedVertices Seed polyhedron vertices (any radius; they are normalized).
|
|
25
|
+
* @param seedFaces Seed triangles, wound CCW as seen from outside.
|
|
26
|
+
*/
|
|
27
|
+
export declare function sphereFromTriangles(seedVertices: Vec3[], seedFaces: number[][], size: number, detail: number, colors: string[]): Mesh;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { cross, normalize, scale, subtract } from "./math.js";
|
|
2
|
+
/** Parse a CSS hex color (`#rgb` or `#rrggbb`) into 0..255 channels. */
|
|
3
|
+
export function parseColor(color) {
|
|
4
|
+
const hex = color.trim().replace("#", "");
|
|
5
|
+
const full = hex.length === 3
|
|
6
|
+
? hex
|
|
7
|
+
.split("")
|
|
8
|
+
.map((c) => c + c)
|
|
9
|
+
.join("")
|
|
10
|
+
: hex;
|
|
11
|
+
const n = parseInt(full, 16);
|
|
12
|
+
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Triangulate a mesh into a non-indexed soup where every vertex carries its
|
|
16
|
+
* face's normal and color. This is what GPU backends upload to render flat
|
|
17
|
+
* shading without per-primitive state.
|
|
18
|
+
*/
|
|
19
|
+
export function expandToTriangles(mesh) {
|
|
20
|
+
let triangles = 0;
|
|
21
|
+
for (const face of mesh.faces)
|
|
22
|
+
triangles += Math.max(0, face.indices.length - 2);
|
|
23
|
+
const positions = new Float32Array(triangles * 9);
|
|
24
|
+
const normals = new Float32Array(triangles * 9);
|
|
25
|
+
const colors = new Float32Array(triangles * 9);
|
|
26
|
+
let o = 0;
|
|
27
|
+
for (const face of mesh.faces) {
|
|
28
|
+
const v0 = mesh.vertices[face.indices[0]];
|
|
29
|
+
const v1 = mesh.vertices[face.indices[1]];
|
|
30
|
+
const v2 = mesh.vertices[face.indices[2]];
|
|
31
|
+
const normal = normalize(cross(subtract(v1, v0), subtract(v2, v0)));
|
|
32
|
+
const [r, g, b] = parseColor(face.color);
|
|
33
|
+
const cr = r / 255;
|
|
34
|
+
const cg = g / 255;
|
|
35
|
+
const cb = b / 255;
|
|
36
|
+
for (let k = 1; k < face.indices.length - 1; k++) {
|
|
37
|
+
const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
|
|
38
|
+
for (const index of tri) {
|
|
39
|
+
const v = mesh.vertices[index];
|
|
40
|
+
positions[o] = v.x;
|
|
41
|
+
positions[o + 1] = v.y;
|
|
42
|
+
positions[o + 2] = v.z;
|
|
43
|
+
normals[o] = normal.x;
|
|
44
|
+
normals[o + 1] = normal.y;
|
|
45
|
+
normals[o + 2] = normal.z;
|
|
46
|
+
colors[o] = cr;
|
|
47
|
+
colors[o + 1] = cg;
|
|
48
|
+
colors[o + 2] = cb;
|
|
49
|
+
o += 3;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { positions, normals, colors, count: positions.length / 3 };
|
|
54
|
+
}
|
|
55
|
+
function midpoint(a, b) {
|
|
56
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a sphere by subdividing a triangular seed polyhedron and projecting
|
|
60
|
+
* every vertex onto the sphere. `detail` 1 keeps the seed; each extra level
|
|
61
|
+
* splits every triangle into four. Used by the icosphere and octa-sphere.
|
|
62
|
+
*
|
|
63
|
+
* @param seedVertices Seed polyhedron vertices (any radius; they are normalized).
|
|
64
|
+
* @param seedFaces Seed triangles, wound CCW as seen from outside.
|
|
65
|
+
*/
|
|
66
|
+
export function sphereFromTriangles(seedVertices, seedFaces, size, detail, colors) {
|
|
67
|
+
const radius = size / 2;
|
|
68
|
+
let triangles = seedFaces.map((f) => f.map((i) => normalize(seedVertices[i])));
|
|
69
|
+
const levels = Math.max(0, Math.floor(detail) - 1);
|
|
70
|
+
for (let level = 0; level < levels; level++) {
|
|
71
|
+
const next = [];
|
|
72
|
+
for (const [a, b, c] of triangles) {
|
|
73
|
+
const ab = normalize(midpoint(a, b));
|
|
74
|
+
const bc = normalize(midpoint(b, c));
|
|
75
|
+
const ca = normalize(midpoint(c, a));
|
|
76
|
+
next.push([a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]);
|
|
77
|
+
}
|
|
78
|
+
triangles = next;
|
|
79
|
+
}
|
|
80
|
+
const vertices = [];
|
|
81
|
+
const faces = triangles.map((tri, i) => {
|
|
82
|
+
const base = vertices.length;
|
|
83
|
+
vertices.push(scale(tri[0], radius), scale(tri[1], radius), scale(tri[2], radius));
|
|
84
|
+
return { indices: [base, base + 1, base + 2], color: colors[i % colors.length] };
|
|
85
|
+
});
|
|
86
|
+
return { vertices, faces };
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Vec3 } from "./math.js";
|
|
2
|
+
/** Directional light configuration. */
|
|
3
|
+
export interface LightOptions {
|
|
4
|
+
/** Direction the light travels. Default points down-forward into the scene. */
|
|
5
|
+
direction: Vec3;
|
|
6
|
+
/** Direct light strength `0..1`. Default `0.85`. */
|
|
7
|
+
intensity: number;
|
|
8
|
+
/** Base illumination on unlit faces `0..1`. Default `0.25`. */
|
|
9
|
+
ambient: number;
|
|
10
|
+
}
|
|
11
|
+
/** Resolved light values passed to a renderer each frame. */
|
|
12
|
+
export interface LightParams {
|
|
13
|
+
/** Unit vector pointing toward the light. */
|
|
14
|
+
toLight: Vec3;
|
|
15
|
+
intensity: number;
|
|
16
|
+
ambient: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Flat-shade a face: brighten its base `color` by how directly its `normal`
|
|
20
|
+
* faces the light, floored at the ambient level. Returns an `rgb(...)` string.
|
|
21
|
+
*/
|
|
22
|
+
export declare function shadeColor(normal: Vec3, color: string, light: LightParams): string;
|
|
23
|
+
/** A single directional light with an ambient term, used for flat shading. */
|
|
24
|
+
export declare class Light {
|
|
25
|
+
readonly options: LightOptions;
|
|
26
|
+
/** Resolved values for renderers. */
|
|
27
|
+
readonly params: LightParams;
|
|
28
|
+
constructor(options?: Partial<LightOptions>);
|
|
29
|
+
/** Convenience wrapper around {@link shadeColor} using this light. */
|
|
30
|
+
shade(normal: Vec3, color: string): string;
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { dot, normalize, scale } from "./math.js";
|
|
2
|
+
import { parseColor } from "./geometry.js";
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
direction: { x: -0.4, y: -0.7, z: -0.6 },
|
|
5
|
+
intensity: 0.85,
|
|
6
|
+
ambient: 0.25,
|
|
7
|
+
};
|
|
8
|
+
function clamp01(value) {
|
|
9
|
+
return Math.min(1, Math.max(0, value));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Flat-shade a face: brighten its base `color` by how directly its `normal`
|
|
13
|
+
* faces the light, floored at the ambient level. Returns an `rgb(...)` string.
|
|
14
|
+
*/
|
|
15
|
+
export function shadeColor(normal, color, light) {
|
|
16
|
+
const lambert = Math.max(0, dot(normal, light.toLight));
|
|
17
|
+
const brightness = clamp01(light.ambient + light.intensity * lambert);
|
|
18
|
+
const [r, g, b] = parseColor(color);
|
|
19
|
+
return `rgb(${Math.round(r * brightness)}, ${Math.round(g * brightness)}, ${Math.round(b * brightness)})`;
|
|
20
|
+
}
|
|
21
|
+
/** A single directional light with an ambient term, used for flat shading. */
|
|
22
|
+
export class Light {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.options = { ...DEFAULTS, ...options };
|
|
25
|
+
this.params = {
|
|
26
|
+
toLight: normalize(scale(this.options.direction, -1)),
|
|
27
|
+
intensity: this.options.intensity,
|
|
28
|
+
ambient: this.options.ambient,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Convenience wrapper around {@link shadeColor} using this light. */
|
|
32
|
+
shade(normal, color) {
|
|
33
|
+
return shadeColor(normal, color, this.params);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** A point or direction in 3D space. */
|
|
2
|
+
export interface Vec3 {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
z: number;
|
|
6
|
+
}
|
|
7
|
+
/** Create a {@link Vec3}. */
|
|
8
|
+
export declare function vec3(x: number, y: number, z: number): Vec3;
|
|
9
|
+
/** Subtract `b` from `a` (`a - b`). */
|
|
10
|
+
export declare function subtract(a: Vec3, b: Vec3): Vec3;
|
|
11
|
+
/** Cross product `a x b` (a vector perpendicular to both). */
|
|
12
|
+
export declare function cross(a: Vec3, b: Vec3): Vec3;
|
|
13
|
+
/** Dot product `a . b`. */
|
|
14
|
+
export declare function dot(a: Vec3, b: Vec3): number;
|
|
15
|
+
/** Scale a vector by a scalar. */
|
|
16
|
+
export declare function scale(v: Vec3, s: number): Vec3;
|
|
17
|
+
/** Return a unit-length copy of `v` (zero vector is returned unchanged). */
|
|
18
|
+
export declare function normalize(v: Vec3): Vec3;
|
|
19
|
+
/**
|
|
20
|
+
* A 4x4 matrix in column-major order (16 numbers), suitable for chaining
|
|
21
|
+
* model, view, and projection transforms.
|
|
22
|
+
*/
|
|
23
|
+
export type Mat4 = number[];
|
|
24
|
+
/** The 4x4 identity matrix. */
|
|
25
|
+
export declare function identity(): Mat4;
|
|
26
|
+
/** Multiply two matrices (`a * b`); applies `b` first, then `a`. */
|
|
27
|
+
export declare function multiply(a: Mat4, b: Mat4): Mat4;
|
|
28
|
+
/** Translation matrix. */
|
|
29
|
+
export declare function translation(x: number, y: number, z: number): Mat4;
|
|
30
|
+
/** Uniform scale matrix. */
|
|
31
|
+
export declare function scaleMatrix(s: number): Mat4;
|
|
32
|
+
/** Rotation matrix about the X axis (radians). */
|
|
33
|
+
export declare function rotationX(rad: number): Mat4;
|
|
34
|
+
/** Rotation matrix about the Y axis (radians). */
|
|
35
|
+
export declare function rotationY(rad: number): Mat4;
|
|
36
|
+
/** Rotation matrix about the Z axis (radians). */
|
|
37
|
+
export declare function rotationZ(rad: number): Mat4;
|
|
38
|
+
/**
|
|
39
|
+
* Perspective projection matrix.
|
|
40
|
+
*
|
|
41
|
+
* @param fovY Vertical field of view in radians.
|
|
42
|
+
* @param aspect Viewport width divided by height.
|
|
43
|
+
* @param near Near clip distance.
|
|
44
|
+
* @param far Far clip distance.
|
|
45
|
+
*/
|
|
46
|
+
export declare function perspective(fovY: number, aspect: number, near: number, far: number): Mat4;
|
|
47
|
+
/** Transform a point by a matrix, applying the perspective divide. */
|
|
48
|
+
export declare function transformPoint(m: Mat4, p: Vec3): Vec3;
|
|
49
|
+
/** Transform a point by a matrix without the perspective divide. */
|
|
50
|
+
export declare function transformAffine(m: Mat4, p: Vec3): Vec3;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Create a {@link Vec3}. */
|
|
2
|
+
export function vec3(x, y, z) {
|
|
3
|
+
return { x, y, z };
|
|
4
|
+
}
|
|
5
|
+
/** Subtract `b` from `a` (`a - b`). */
|
|
6
|
+
export function subtract(a, b) {
|
|
7
|
+
return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
|
|
8
|
+
}
|
|
9
|
+
/** Cross product `a x b` (a vector perpendicular to both). */
|
|
10
|
+
export function cross(a, b) {
|
|
11
|
+
return {
|
|
12
|
+
x: a.y * b.z - a.z * b.y,
|
|
13
|
+
y: a.z * b.x - a.x * b.z,
|
|
14
|
+
z: a.x * b.y - a.y * b.x,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Dot product `a . b`. */
|
|
18
|
+
export function dot(a, b) {
|
|
19
|
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
20
|
+
}
|
|
21
|
+
/** Scale a vector by a scalar. */
|
|
22
|
+
export function scale(v, s) {
|
|
23
|
+
return { x: v.x * s, y: v.y * s, z: v.z * s };
|
|
24
|
+
}
|
|
25
|
+
/** Return a unit-length copy of `v` (zero vector is returned unchanged). */
|
|
26
|
+
export function normalize(v) {
|
|
27
|
+
const length = Math.hypot(v.x, v.y, v.z);
|
|
28
|
+
if (length === 0)
|
|
29
|
+
return { x: 0, y: 0, z: 0 };
|
|
30
|
+
return { x: v.x / length, y: v.y / length, z: v.z / length };
|
|
31
|
+
}
|
|
32
|
+
/** The 4x4 identity matrix. */
|
|
33
|
+
export function identity() {
|
|
34
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
35
|
+
}
|
|
36
|
+
/** Multiply two matrices (`a * b`); applies `b` first, then `a`. */
|
|
37
|
+
export function multiply(a, b) {
|
|
38
|
+
const out = new Array(16);
|
|
39
|
+
for (let col = 0; col < 4; col++) {
|
|
40
|
+
for (let row = 0; row < 4; row++) {
|
|
41
|
+
let sum = 0;
|
|
42
|
+
for (let k = 0; k < 4; k++) {
|
|
43
|
+
sum += a[k * 4 + row] * b[col * 4 + k];
|
|
44
|
+
}
|
|
45
|
+
out[col * 4 + row] = sum;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** Translation matrix. */
|
|
51
|
+
export function translation(x, y, z) {
|
|
52
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
|
|
53
|
+
}
|
|
54
|
+
/** Uniform scale matrix. */
|
|
55
|
+
export function scaleMatrix(s) {
|
|
56
|
+
return [s, 0, 0, 0, 0, s, 0, 0, 0, 0, s, 0, 0, 0, 0, 1];
|
|
57
|
+
}
|
|
58
|
+
/** Rotation matrix about the X axis (radians). */
|
|
59
|
+
export function rotationX(rad) {
|
|
60
|
+
const c = Math.cos(rad);
|
|
61
|
+
const s = Math.sin(rad);
|
|
62
|
+
return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1];
|
|
63
|
+
}
|
|
64
|
+
/** Rotation matrix about the Y axis (radians). */
|
|
65
|
+
export function rotationY(rad) {
|
|
66
|
+
const c = Math.cos(rad);
|
|
67
|
+
const s = Math.sin(rad);
|
|
68
|
+
return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1];
|
|
69
|
+
}
|
|
70
|
+
/** Rotation matrix about the Z axis (radians). */
|
|
71
|
+
export function rotationZ(rad) {
|
|
72
|
+
const c = Math.cos(rad);
|
|
73
|
+
const s = Math.sin(rad);
|
|
74
|
+
return [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Perspective projection matrix.
|
|
78
|
+
*
|
|
79
|
+
* @param fovY Vertical field of view in radians.
|
|
80
|
+
* @param aspect Viewport width divided by height.
|
|
81
|
+
* @param near Near clip distance.
|
|
82
|
+
* @param far Far clip distance.
|
|
83
|
+
*/
|
|
84
|
+
export function perspective(fovY, aspect, near, far) {
|
|
85
|
+
const f = 1 / Math.tan(fovY / 2);
|
|
86
|
+
const nf = 1 / (near - far);
|
|
87
|
+
return [
|
|
88
|
+
f / aspect, 0, 0, 0,
|
|
89
|
+
0, f, 0, 0,
|
|
90
|
+
0, 0, (far + near) * nf, -1,
|
|
91
|
+
0, 0, 2 * far * near * nf, 0,
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
/** Transform a point by a matrix, applying the perspective divide. */
|
|
95
|
+
export function transformPoint(m, p) {
|
|
96
|
+
const x = m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12];
|
|
97
|
+
const y = m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13];
|
|
98
|
+
const z = m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14];
|
|
99
|
+
const w = m[3] * p.x + m[7] * p.y + m[11] * p.z + m[15] || 1;
|
|
100
|
+
return { x: x / w, y: y / w, z: z / w };
|
|
101
|
+
}
|
|
102
|
+
/** Transform a point by a matrix without the perspective divide. */
|
|
103
|
+
export function transformAffine(m, p) {
|
|
104
|
+
return {
|
|
105
|
+
x: m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12],
|
|
106
|
+
y: m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13],
|
|
107
|
+
z: m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Vec3 } from "./math.js";
|
|
2
|
+
/** A single flat polygon: indices into the mesh `vertices` plus a base color. */
|
|
3
|
+
export interface Face {
|
|
4
|
+
/** Vertex indices, wound counter-clockwise when viewed from outside. */
|
|
5
|
+
indices: number[];
|
|
6
|
+
/** Base CSS color, for example `"#3b82f6"`. Shading is applied on top of it. */
|
|
7
|
+
color: string;
|
|
8
|
+
}
|
|
9
|
+
/** Geometry: a list of vertices and the colored faces that connect them. */
|
|
10
|
+
export interface Mesh {
|
|
11
|
+
vertices: Vec3[];
|
|
12
|
+
faces: Face[];
|
|
13
|
+
}
|
|
14
|
+
/** Position and orientation (Euler radians) applied to a mesh when rendered. */
|
|
15
|
+
export interface Transform {
|
|
16
|
+
position: Vec3;
|
|
17
|
+
rotation: Vec3;
|
|
18
|
+
/** Uniform scale multiplier. Default `1`. */
|
|
19
|
+
scale: number;
|
|
20
|
+
}
|
|
21
|
+
/** Create a {@link Transform} with sensible defaults (origin, no rotation). */
|
|
22
|
+
export declare function transform(init?: Partial<Transform>): Transform;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Create a {@link Transform} with sensible defaults (origin, no rotation). */
|
|
2
|
+
export function transform(init) {
|
|
3
|
+
return {
|
|
4
|
+
position: init?.position ?? { x: 0, y: 0, z: 0 },
|
|
5
|
+
rotation: init?.rotation ?? { x: 0, y: 0, z: 0 },
|
|
6
|
+
scale: init?.scale ?? 1,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type CameraOptions } from "./core/camera.js";
|
|
2
|
+
import { type LightOptions } from "./core/light.js";
|
|
3
|
+
import { type Mesh, type Transform } from "./core/mesh.js";
|
|
4
|
+
import { type Backend } from "./renderer.js";
|
|
5
|
+
/** Options for {@link Little3dEngine}. */
|
|
6
|
+
export interface Little3dEngineOptions {
|
|
7
|
+
/** Rendering backend. Loaded on demand. Default `"canvas2d"`. */
|
|
8
|
+
backend?: Backend;
|
|
9
|
+
camera?: Partial<CameraOptions>;
|
|
10
|
+
light?: Partial<LightOptions>;
|
|
11
|
+
/** Solid background color; omit for a transparent canvas (overlay use). */
|
|
12
|
+
background?: string;
|
|
13
|
+
}
|
|
14
|
+
/** A live mesh in the scene. Mutate `transform` to move or rotate it. */
|
|
15
|
+
export interface MeshHandle {
|
|
16
|
+
readonly mesh: Mesh;
|
|
17
|
+
readonly transform: Transform;
|
|
18
|
+
/** Remove this mesh from the scene. */
|
|
19
|
+
remove(): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A minimal software/hardware 3D engine. It projects colored meshes with flat
|
|
23
|
+
* directional lighting through a swappable {@link Backend} renderer. Mount it
|
|
24
|
+
* into any element to render in a component, or into a transparent positioned
|
|
25
|
+
* element to overlay a page.
|
|
26
|
+
*/
|
|
27
|
+
export declare class Little3dEngine {
|
|
28
|
+
private readonly camera;
|
|
29
|
+
private readonly light;
|
|
30
|
+
private readonly backend;
|
|
31
|
+
private readonly background?;
|
|
32
|
+
private readonly scene;
|
|
33
|
+
private canvas?;
|
|
34
|
+
private observer?;
|
|
35
|
+
private renderer?;
|
|
36
|
+
private cssWidth;
|
|
37
|
+
private cssHeight;
|
|
38
|
+
private ready;
|
|
39
|
+
private generation;
|
|
40
|
+
private rafId;
|
|
41
|
+
private running;
|
|
42
|
+
constructor(options?: Little3dEngineOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Create the canvas inside `target`, load the selected backend, and start
|
|
45
|
+
* tracking size. Resolves once the renderer is ready; rejects if the backend
|
|
46
|
+
* is unavailable. Drawing is a no-op until it resolves.
|
|
47
|
+
*/
|
|
48
|
+
mount(target: HTMLElement): Promise<void>;
|
|
49
|
+
/** Add a mesh to the scene and return a handle for animating it. */
|
|
50
|
+
add(mesh: Mesh, init?: Partial<Transform>): MeshHandle;
|
|
51
|
+
private resize;
|
|
52
|
+
/** Draw a single frame from the current scene state. */
|
|
53
|
+
render(): void;
|
|
54
|
+
/** Start an internal animation loop that calls {@link render} each frame. */
|
|
55
|
+
start(): void;
|
|
56
|
+
/** Stop the internal animation loop started by {@link start}. */
|
|
57
|
+
stop(): void;
|
|
58
|
+
/** Stop animating, release the renderer, and remove the canvas. */
|
|
59
|
+
destroy(): void;
|
|
60
|
+
}
|
|
61
|
+
export { Camera, type CameraOptions } from "./core/camera.js";
|
|
62
|
+
export { Light, type LightOptions, type LightParams } from "./core/light.js";
|
|
63
|
+
export { cube } from "./shapes/cube.js";
|
|
64
|
+
export { tetrahedron } from "./shapes/tetrahedron.js";
|
|
65
|
+
export { octahedron } from "./shapes/octahedron.js";
|
|
66
|
+
export { pyramid } from "./shapes/pyramid.js";
|
|
67
|
+
export { uvSphere } from "./shapes/uv-sphere.js";
|
|
68
|
+
export { icosphere } from "./shapes/icosphere.js";
|
|
69
|
+
export { octaSphere } from "./shapes/octa-sphere.js";
|
|
70
|
+
export { cubeSphere } from "./shapes/cube-sphere.js";
|
|
71
|
+
export { expandToTriangles } from "./core/geometry.js";
|
|
72
|
+
export type { Mesh, Face, Transform } from "./core/mesh.js";
|
|
73
|
+
export { transform } from "./core/mesh.js";
|
|
74
|
+
export type { Backend, Renderer, RenderFrame, RenderItem, RendererOptions } from "./renderer.js";
|
|
75
|
+
export { type Vec3, vec3, subtract, cross, dot, scale, normalize, } from "./core/math.js";
|