3d-spinner 0.9.2 → 0.9.4
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/README.md +66 -4
- package/dist/animation-label.d.ts +8 -0
- package/dist/animation-label.js +47 -0
- package/dist/animation.d.ts +2 -0
- package/dist/animations/charged-orb.d.ts +55 -0
- package/dist/animations/charged-orb.js +229 -0
- package/dist/animations/ghost-train.d.ts +72 -0
- package/dist/animations/ghost-train.js +247 -0
- package/dist/animations/grid-assembly.d.ts +70 -0
- package/dist/animations/grid-assembly.js +282 -0
- package/dist/animations/object-motion.d.ts +19 -7
- package/dist/animations/object-motion.js +38 -32
- package/dist/animations/particles.d.ts +110 -0
- package/dist/animations/particles.js +209 -0
- package/dist/animations/rocket-launch.d.ts +58 -0
- package/dist/animations/rocket-launch.js +375 -0
- package/dist/cjs/animations/charged-orb.cjs +1334 -0
- package/dist/cjs/animations/grid-assembly.cjs +1380 -0
- package/dist/cjs/animations/object-motion.cjs +124 -43
- package/dist/cjs/animations/particles.cjs +1940 -0
- package/dist/cjs/animations/spin.cjs +4 -3
- package/dist/cjs/composite-animation.cjs +58 -0
- package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +143 -22
- package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +30 -1
- package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +345 -0
- package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +528 -0
- package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +712 -0
- package/dist/cjs/prefabs/prefabs.cjs +3916 -0
- package/dist/composite-animation.d.ts +18 -0
- package/dist/composite-animation.js +39 -0
- package/dist/engines/little-3d-engine/little-3d-engine.d.ts +17 -12
- package/dist/engines/little-3d-engine/little-3d-engine.js +13 -8
- package/dist/engines/little-3d-engine/loaders/obj.d.ts +12 -4
- package/dist/engines/little-3d-engine/loaders/obj.js +44 -5
- package/dist/engines/little-3d-engine/renderer.d.ts +7 -1
- package/dist/engines/little-3d-engine/renderer.js +2 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d-textured.d.ts +20 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d-textured.js +135 -0
- package/dist/engines/little-3d-engine/renderers/textured-helpers.d.ts +9 -0
- package/dist/engines/little-3d-engine/renderers/textured-helpers.js +35 -0
- package/dist/engines/little-3d-engine/renderers/webgl-textured.d.ts +43 -0
- package/dist/engines/little-3d-engine/renderers/webgl-textured.js +211 -0
- package/dist/engines/little-3d-engine/renderers/webgpu-textured.d.ts +45 -0
- package/dist/engines/little-3d-engine/renderers/webgpu-textured.js +300 -0
- package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +11 -6
- package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +3 -0
- package/dist/engines/little-3d-engine/shapes/complex/plane.js +33 -0
- package/dist/engines/little-3d-engine/shapes/{cube.d.ts → primitives/cube.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{octahedron.d.ts → primitives/octahedron.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{pyramid.d.ts → primitives/pyramid.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +9 -0
- package/dist/engines/little-3d-engine/shapes/primitives/quad.js +18 -0
- package/dist/engines/little-3d-engine/shapes/{cube-sphere.d.ts → primitives/spheres/cube-sphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{icosphere.d.ts → primitives/spheres/icosphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{icosphere.js → primitives/spheres/icosphere.js} +1 -1
- package/dist/engines/little-3d-engine/shapes/{octa-sphere.d.ts → primitives/spheres/octa-sphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{octa-sphere.js → primitives/spheres/octa-sphere.js} +1 -1
- package/dist/engines/little-3d-engine/shapes/{uv-sphere.d.ts → primitives/spheres/uv-sphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{tetrahedron.d.ts → primitives/tetrahedron.d.ts} +1 -1
- package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.d.ts +1 -0
- package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.js +8 -0
- package/dist/engines/little-3d-engine/textures/dynamic/shine.d.ts +2 -0
- package/dist/engines/little-3d-engine/textures/dynamic/shine.js +14 -0
- package/dist/engines/little-3d-engine/textures/dynamic/star.d.ts +14 -0
- package/dist/engines/little-3d-engine/textures/dynamic/star.js +31 -0
- package/dist/engines/little-3d-engine/textures/dynamic/streak.d.ts +5 -0
- package/dist/engines/little-3d-engine/textures/dynamic/streak.js +20 -0
- package/dist/index.d.ts +2 -1
- package/dist/prefabs/charged-orb.d.ts +17 -0
- package/dist/prefabs/charged-orb.js +37 -0
- package/dist/prefabs/crystal-comet.d.ts +4 -0
- package/dist/prefabs/crystal-comet.js +41 -0
- package/dist/prefabs/ghost-train.d.ts +10 -0
- package/dist/prefabs/ghost-train.js +37 -0
- package/dist/prefabs/grid-assembly.d.ts +13 -0
- package/dist/prefabs/grid-assembly.js +15 -0
- package/dist/prefabs/monochrome-streak.d.ts +4 -0
- package/dist/prefabs/monochrome-streak.js +25 -0
- package/dist/prefabs/plane-star-trail.d.ts +4 -0
- package/dist/prefabs/plane-star-trail.js +40 -0
- package/dist/prefabs/prefabs.d.ts +10 -0
- package/dist/prefabs/prefabs.js +9 -0
- package/dist/prefabs/pulsing-starfield.d.ts +4 -0
- package/dist/prefabs/pulsing-starfield.js +27 -0
- package/dist/prefabs/rocket-launch.d.ts +10 -0
- package/dist/prefabs/rocket-launch.js +16 -0
- package/dist/prefabs/spinner.d.ts +7 -0
- package/dist/prefabs/spinner.js +17 -0
- package/dist/prefabs/star-swarm.d.ts +4 -0
- package/dist/prefabs/star-swarm.js +27 -0
- package/dist/prefabs/types.d.ts +46 -0
- package/dist/prefabs/types.js +1 -0
- package/dist/umd/spinner.global.js +2407 -85
- package/dist/umd/spinner.global.min.js +60 -5
- package/package.json +43 -2
- /package/dist/engines/little-3d-engine/shapes/{cube.js → primitives/cube.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{octahedron.js → primitives/octahedron.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{pyramid.js → primitives/pyramid.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{cube-sphere.js → primitives/spheres/cube-sphere.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{uv-sphere.js → primitives/spheres/uv-sphere.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{tetrahedron.js → primitives/tetrahedron.js} +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AnimationFrame, SpinnerAnimation } from "./animation.js";
|
|
2
|
+
/** One animation layer in a {@link CompositeAnimation}. Later layers render above earlier ones. */
|
|
3
|
+
export interface CompositeAnimationLayer {
|
|
4
|
+
readonly animation: SpinnerAnimation;
|
|
5
|
+
readonly zIndex?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Run multiple animations through one spinner lifecycle in stacked layers. */
|
|
8
|
+
export declare class CompositeAnimation implements SpinnerAnimation {
|
|
9
|
+
private readonly layers;
|
|
10
|
+
private readonly elements;
|
|
11
|
+
constructor(layers: ReadonlyArray<SpinnerAnimation | CompositeAnimationLayer>);
|
|
12
|
+
mount(target: HTMLElement): void;
|
|
13
|
+
enter(now: number): void;
|
|
14
|
+
exit(now: number): void;
|
|
15
|
+
render(now: number, frame: AnimationFrame): void;
|
|
16
|
+
isFinished(): boolean;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Run multiple animations through one spinner lifecycle in stacked layers. */
|
|
2
|
+
export class CompositeAnimation {
|
|
3
|
+
constructor(layers) {
|
|
4
|
+
this.elements = [];
|
|
5
|
+
this.layers = layers.map((layer) => "animation" in layer ? layer : { animation: layer });
|
|
6
|
+
}
|
|
7
|
+
mount(target) {
|
|
8
|
+
target.style.position = "relative";
|
|
9
|
+
for (const [index, layer] of this.layers.entries()) {
|
|
10
|
+
const element = document.createElement("div");
|
|
11
|
+
element.style.cssText = `position:absolute;inset:0;z-index:${layer.zIndex ?? index}`;
|
|
12
|
+
target.appendChild(element);
|
|
13
|
+
this.elements.push(element);
|
|
14
|
+
layer.animation.mount(element);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
enter(now) {
|
|
18
|
+
for (const layer of this.layers)
|
|
19
|
+
layer.animation.enter(now);
|
|
20
|
+
}
|
|
21
|
+
exit(now) {
|
|
22
|
+
for (const layer of this.layers)
|
|
23
|
+
layer.animation.exit(now);
|
|
24
|
+
}
|
|
25
|
+
render(now, frame) {
|
|
26
|
+
for (const layer of this.layers)
|
|
27
|
+
layer.animation.render(now, frame);
|
|
28
|
+
}
|
|
29
|
+
isFinished() {
|
|
30
|
+
return this.layers.every((layer) => layer.animation.isFinished());
|
|
31
|
+
}
|
|
32
|
+
destroy() {
|
|
33
|
+
for (const layer of this.layers)
|
|
34
|
+
layer.animation.destroy();
|
|
35
|
+
for (const element of this.elements)
|
|
36
|
+
element.remove();
|
|
37
|
+
this.elements.length = 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type CameraOptions } from "./core/camera.js";
|
|
2
2
|
import { type LightOptions } from "./core/light.js";
|
|
3
3
|
import { type Mesh, type Transform, type Transparency } from "./core/mesh.js";
|
|
4
|
-
import { type Backend } from "./renderer.js";
|
|
4
|
+
import { type Backend, type RendererFactory } from "./renderer.js";
|
|
5
5
|
/** Options for {@link Little3dEngine}. */
|
|
6
6
|
export interface Little3dEngineOptions {
|
|
7
|
-
/** Rendering backend. Loaded on demand. Default `"canvas2d"`. */
|
|
8
|
-
backend?: Backend;
|
|
7
|
+
/** Rendering backend, or a factory building a custom renderer. Loaded on demand. Default `"canvas2d"`. */
|
|
8
|
+
backend?: Backend | RendererFactory;
|
|
9
9
|
camera?: Partial<CameraOptions>;
|
|
10
10
|
light?: Partial<LightOptions>;
|
|
11
11
|
/** Solid background color; omit for a transparent canvas (overlay use). */
|
|
@@ -66,17 +66,22 @@ export declare class Little3dEngine {
|
|
|
66
66
|
}
|
|
67
67
|
export { Camera, type CameraOptions } from "./core/camera.js";
|
|
68
68
|
export { Light, type LightOptions, type LightParams } from "./core/light.js";
|
|
69
|
-
export { cube } from "./shapes/cube.js";
|
|
70
|
-
export {
|
|
71
|
-
export {
|
|
72
|
-
export {
|
|
73
|
-
export {
|
|
74
|
-
export {
|
|
75
|
-
export {
|
|
76
|
-
export {
|
|
69
|
+
export { cube } from "./shapes/primitives/cube.js";
|
|
70
|
+
export { quad } from "./shapes/primitives/quad.js";
|
|
71
|
+
export { tetrahedron } from "./shapes/primitives/tetrahedron.js";
|
|
72
|
+
export { octahedron } from "./shapes/primitives/octahedron.js";
|
|
73
|
+
export { pyramid } from "./shapes/primitives/pyramid.js";
|
|
74
|
+
export { uvSphere } from "./shapes/primitives/spheres/uv-sphere.js";
|
|
75
|
+
export { icosphere } from "./shapes/primitives/spheres/icosphere.js";
|
|
76
|
+
export { octaSphere } from "./shapes/primitives/spheres/octa-sphere.js";
|
|
77
|
+
export { cubeSphere } from "./shapes/primitives/spheres/cube-sphere.js";
|
|
78
|
+
export { planeMesh } from "./shapes/complex/plane.js";
|
|
79
|
+
export { starTexture } from "./textures/dynamic/star.js";
|
|
80
|
+
export { shineTexture } from "./textures/dynamic/shine.js";
|
|
81
|
+
export { streakTexture } from "./textures/dynamic/streak.js";
|
|
77
82
|
export { expandToTriangles } from "./core/geometry.js";
|
|
78
83
|
export type { Mesh, Face, Transform, Transparency, OneSidedTransparency, TwoSidedTransparency, } from "./core/mesh.js";
|
|
79
84
|
export { transform } from "./core/mesh.js";
|
|
80
|
-
export type { Backend, Renderer, RenderFrame, RenderItem, RendererOptions } from "./renderer.js";
|
|
85
|
+
export type { Backend, Renderer, RendererFactory, RenderFrame, RenderItem, RendererOptions, } from "./renderer.js";
|
|
81
86
|
export { orderRenderItems } from "./renderer.js";
|
|
82
87
|
export { type Vec3, vec3, subtract, cross, dot, scale, normalize, } from "./core/math.js";
|
|
@@ -157,14 +157,19 @@ export class Little3dEngine {
|
|
|
157
157
|
}
|
|
158
158
|
export { Camera } from "./core/camera.js";
|
|
159
159
|
export { Light } from "./core/light.js";
|
|
160
|
-
export { cube } from "./shapes/cube.js";
|
|
161
|
-
export {
|
|
162
|
-
export {
|
|
163
|
-
export {
|
|
164
|
-
export {
|
|
165
|
-
export {
|
|
166
|
-
export {
|
|
167
|
-
export {
|
|
160
|
+
export { cube } from "./shapes/primitives/cube.js";
|
|
161
|
+
export { quad } from "./shapes/primitives/quad.js";
|
|
162
|
+
export { tetrahedron } from "./shapes/primitives/tetrahedron.js";
|
|
163
|
+
export { octahedron } from "./shapes/primitives/octahedron.js";
|
|
164
|
+
export { pyramid } from "./shapes/primitives/pyramid.js";
|
|
165
|
+
export { uvSphere } from "./shapes/primitives/spheres/uv-sphere.js";
|
|
166
|
+
export { icosphere } from "./shapes/primitives/spheres/icosphere.js";
|
|
167
|
+
export { octaSphere } from "./shapes/primitives/spheres/octa-sphere.js";
|
|
168
|
+
export { cubeSphere } from "./shapes/primitives/spheres/cube-sphere.js";
|
|
169
|
+
export { planeMesh } from "./shapes/complex/plane.js";
|
|
170
|
+
export { starTexture } from "./textures/dynamic/star.js";
|
|
171
|
+
export { shineTexture } from "./textures/dynamic/shine.js";
|
|
172
|
+
export { streakTexture } from "./textures/dynamic/streak.js";
|
|
168
173
|
export { expandToTriangles } from "./core/geometry.js";
|
|
169
174
|
export { transform } from "./core/mesh.js";
|
|
170
175
|
export { orderRenderItems } from "./renderer.js";
|
|
@@ -7,6 +7,13 @@ export interface ObjOptions {
|
|
|
7
7
|
* a uniform color.
|
|
8
8
|
*/
|
|
9
9
|
colors?: string[];
|
|
10
|
+
/** Contents of an `.mtl` file referenced by the OBJ. */
|
|
11
|
+
mtl?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Use MTL diffuse (`Kd`) values for faces with matching `usemtl`
|
|
14
|
+
* statements. Default `false`.
|
|
15
|
+
*/
|
|
16
|
+
useMtlColors?: boolean;
|
|
10
17
|
}
|
|
11
18
|
/**
|
|
12
19
|
* Parse Wavefront OBJ text into a {@link Mesh}.
|
|
@@ -14,11 +21,12 @@ export interface ObjOptions {
|
|
|
14
21
|
* Reads `v` vertex positions and `f` faces (triangles, quads, or n-gons, in
|
|
15
22
|
* `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
|
|
16
23
|
* Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
|
|
17
|
-
* computes a flat normal per face
|
|
18
|
-
*
|
|
19
|
-
* winding as seen from
|
|
24
|
+
* computes a flat normal per face. Material names can select diffuse colors
|
|
25
|
+
* from supplied MTL text; groups and other statements are ignored. Face
|
|
26
|
+
* winding is preserved as-is; the engine expects CCW winding as seen from
|
|
27
|
+
* outside.
|
|
20
28
|
*
|
|
21
29
|
* @param text Contents of an `.obj` file.
|
|
22
|
-
* @param options
|
|
30
|
+
* @param options Face palette and optional MTL diffuse colors.
|
|
23
31
|
*/
|
|
24
32
|
export declare function parseObj(text: string, options?: ObjOptions): Mesh;
|
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
const DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
2
|
+
function channelToHex(value) {
|
|
3
|
+
const channel = Number.parseFloat(value);
|
|
4
|
+
if (!Number.isFinite(channel))
|
|
5
|
+
return undefined;
|
|
6
|
+
return Math.round(Math.min(1, Math.max(0, channel)) * 255)
|
|
7
|
+
.toString(16)
|
|
8
|
+
.padStart(2, "0");
|
|
9
|
+
}
|
|
10
|
+
function parseMtlColors(text) {
|
|
11
|
+
const colors = new Map();
|
|
12
|
+
let material;
|
|
13
|
+
for (const line of text.split("\n")) {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
16
|
+
continue;
|
|
17
|
+
const parts = trimmed.split(/\s+/);
|
|
18
|
+
if (parts[0] === "newmtl") {
|
|
19
|
+
material = parts.slice(1).join(" ");
|
|
20
|
+
}
|
|
21
|
+
else if (parts[0] === "Kd" && material) {
|
|
22
|
+
const channels = parts.slice(1, 4).map(channelToHex);
|
|
23
|
+
if (channels.length === 3 && channels.every((channel) => channel !== undefined)) {
|
|
24
|
+
colors.set(material, `#${channels.join("")}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return colors;
|
|
29
|
+
}
|
|
2
30
|
function resolveIndex(token, vertexCount) {
|
|
3
31
|
const n = parseInt(token, 10);
|
|
4
32
|
return n < 0 ? vertexCount + n : n - 1;
|
|
@@ -9,17 +37,22 @@ function resolveIndex(token, vertexCount) {
|
|
|
9
37
|
* Reads `v` vertex positions and `f` faces (triangles, quads, or n-gons, in
|
|
10
38
|
* `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
|
|
11
39
|
* Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
|
|
12
|
-
* computes a flat normal per face
|
|
13
|
-
*
|
|
14
|
-
* winding as seen from
|
|
40
|
+
* computes a flat normal per face. Material names can select diffuse colors
|
|
41
|
+
* from supplied MTL text; groups and other statements are ignored. Face
|
|
42
|
+
* winding is preserved as-is; the engine expects CCW winding as seen from
|
|
43
|
+
* outside.
|
|
15
44
|
*
|
|
16
45
|
* @param text Contents of an `.obj` file.
|
|
17
|
-
* @param options
|
|
46
|
+
* @param options Face palette and optional MTL diffuse colors.
|
|
18
47
|
*/
|
|
19
48
|
export function parseObj(text, options = {}) {
|
|
20
49
|
const colors = options.colors ?? DEFAULT_COLORS;
|
|
50
|
+
const materialColors = options.useMtlColors && options.mtl
|
|
51
|
+
? parseMtlColors(options.mtl)
|
|
52
|
+
: undefined;
|
|
21
53
|
const vertices = [];
|
|
22
54
|
const faces = [];
|
|
55
|
+
let material;
|
|
23
56
|
for (const line of text.split("\n")) {
|
|
24
57
|
const trimmed = line.trim();
|
|
25
58
|
if (trimmed === "" || trimmed.startsWith("#"))
|
|
@@ -33,6 +66,9 @@ export function parseObj(text, options = {}) {
|
|
|
33
66
|
z: parseFloat(parts[3]),
|
|
34
67
|
});
|
|
35
68
|
}
|
|
69
|
+
else if (keyword === "usemtl") {
|
|
70
|
+
material = parts.slice(1).join(" ");
|
|
71
|
+
}
|
|
36
72
|
else if (keyword === "f") {
|
|
37
73
|
const indices = [];
|
|
38
74
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -40,7 +76,10 @@ export function parseObj(text, options = {}) {
|
|
|
40
76
|
indices.push(resolveIndex(vertexToken, vertices.length));
|
|
41
77
|
}
|
|
42
78
|
if (indices.length >= 3) {
|
|
43
|
-
|
|
79
|
+
const mtlColor = material ? materialColors?.get(material) : undefined;
|
|
80
|
+
const color = mtlColor
|
|
81
|
+
?? (materialColors ? (colors[0] ?? "#888888") : colors[faces.length % colors.length]);
|
|
82
|
+
faces.push({ indices, color });
|
|
44
83
|
}
|
|
45
84
|
}
|
|
46
85
|
}
|
|
@@ -38,6 +38,12 @@ export interface RendererOptions {
|
|
|
38
38
|
/** Solid background; omit for a transparent canvas (overlay use). */
|
|
39
39
|
background?: string;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Builds a custom {@link Renderer}; an alternative to naming a built-in
|
|
43
|
+
* backend. Load the renderer module inside the factory with a dynamic
|
|
44
|
+
* `import()` to keep it out of the bundle until it is used.
|
|
45
|
+
*/
|
|
46
|
+
export type RendererFactory = (options: RendererOptions) => Renderer | Promise<Renderer>;
|
|
41
47
|
/**
|
|
42
48
|
* A pluggable drawing backend. The engine owns the canvas and sizing; a
|
|
43
49
|
* renderer only initializes its context, reacts to resizes, and draws frames.
|
|
@@ -54,4 +60,4 @@ export interface Renderer {
|
|
|
54
60
|
* module and is pulled in with a dynamic `import()`, so the bytes for the
|
|
55
61
|
* backends you do not use are never downloaded or compiled.
|
|
56
62
|
*/
|
|
57
|
-
export declare function createRenderer(backend: Backend, options?: RendererOptions): Promise<Renderer>;
|
|
63
|
+
export declare function createRenderer(backend: Backend | RendererFactory, options?: RendererOptions): Promise<Renderer>;
|
|
@@ -40,6 +40,8 @@ export function orderRenderItems(items, eye) {
|
|
|
40
40
|
* backends you do not use are never downloaded or compiled.
|
|
41
41
|
*/
|
|
42
42
|
export async function createRenderer(backend, options = {}) {
|
|
43
|
+
if (typeof backend === "function")
|
|
44
|
+
return backend(options);
|
|
43
45
|
switch (backend) {
|
|
44
46
|
case "webgl":
|
|
45
47
|
return new (await import("./renderers/webgl.js")).WebGLRenderer(options);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Mesh } from "../core/mesh.js";
|
|
2
|
+
import { type Renderer, type RenderFrame, type RendererOptions } from "../renderer.js";
|
|
3
|
+
import type { TextureSource } from "./textured-helpers.js";
|
|
4
|
+
export type { TextureSource } from "./textured-helpers.js";
|
|
5
|
+
/** Canvas 2D texture renderer optimized for planar billboard meshes. */
|
|
6
|
+
export declare class Canvas2DTexturedRenderer implements Renderer {
|
|
7
|
+
private readonly inner;
|
|
8
|
+
private readonly sources;
|
|
9
|
+
private readonly loaded;
|
|
10
|
+
private ctx?;
|
|
11
|
+
private dpr;
|
|
12
|
+
constructor(options?: RendererOptions);
|
|
13
|
+
/** Texture every instance of `mesh` with `source`. Call any time, also before init. */
|
|
14
|
+
setTexture(mesh: Mesh, source: TextureSource): void;
|
|
15
|
+
init(canvas: HTMLCanvasElement): void;
|
|
16
|
+
resize(cssWidth: number, cssHeight: number, dpr: number): void;
|
|
17
|
+
private drawable;
|
|
18
|
+
render(frame: RenderFrame): void;
|
|
19
|
+
destroy(): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { transformAffine, transformPoint } from "../core/math.js";
|
|
2
|
+
import { DEFAULT_ONE_SIDED_OPACITY, opacity } from "../renderer.js";
|
|
3
|
+
import { Canvas2DRenderer } from "./canvas2d.js";
|
|
4
|
+
function imageSize(source) {
|
|
5
|
+
if (source instanceof HTMLImageElement) {
|
|
6
|
+
return source.complete && source.naturalWidth > 0
|
|
7
|
+
? { width: source.naturalWidth, height: source.naturalHeight }
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
if (source instanceof HTMLVideoElement) {
|
|
11
|
+
return source.readyState >= 2
|
|
12
|
+
? { width: source.videoWidth, height: source.videoHeight }
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
if (source instanceof SVGImageElement) {
|
|
16
|
+
const width = source.width.baseVal.value;
|
|
17
|
+
const height = source.height.baseVal.value;
|
|
18
|
+
return width > 0 && height > 0 ? { width, height } : undefined;
|
|
19
|
+
}
|
|
20
|
+
if (typeof VideoFrame !== "undefined" && source instanceof VideoFrame) {
|
|
21
|
+
return { width: source.displayWidth, height: source.displayHeight };
|
|
22
|
+
}
|
|
23
|
+
const sized = source;
|
|
24
|
+
return sized.width > 0 && sized.height > 0 ? { width: sized.width, height: sized.height } : undefined;
|
|
25
|
+
}
|
|
26
|
+
function drawMappedTriangle(ctx, image, source, target) {
|
|
27
|
+
const [s0, s1, s2] = source;
|
|
28
|
+
const [d0, d1, d2] = target;
|
|
29
|
+
const determinant = s0.x * (s1.y - s2.y) + s1.x * (s2.y - s0.y) + s2.x * (s0.y - s1.y);
|
|
30
|
+
if (Math.abs(determinant) < 1e-8)
|
|
31
|
+
return;
|
|
32
|
+
const a = (d0.x * (s1.y - s2.y) + d1.x * (s2.y - s0.y) + d2.x * (s0.y - s1.y)) / determinant;
|
|
33
|
+
const c = (d0.x * (s2.x - s1.x) + d1.x * (s0.x - s2.x) + d2.x * (s1.x - s0.x)) / determinant;
|
|
34
|
+
const e = (d0.x * (s1.x * s2.y - s2.x * s1.y) + d1.x * (s2.x * s0.y - s0.x * s2.y) + d2.x * (s0.x * s1.y - s1.x * s0.y)) / determinant;
|
|
35
|
+
const b = (d0.y * (s1.y - s2.y) + d1.y * (s2.y - s0.y) + d2.y * (s0.y - s1.y)) / determinant;
|
|
36
|
+
const d = (d0.y * (s2.x - s1.x) + d1.y * (s0.x - s2.x) + d2.y * (s1.x - s0.x)) / determinant;
|
|
37
|
+
const f = (d0.y * (s1.x * s2.y - s2.x * s1.y) + d1.y * (s2.x * s0.y - s0.x * s2.y) + d2.y * (s0.x * s1.y - s1.x * s0.y)) / determinant;
|
|
38
|
+
ctx.save();
|
|
39
|
+
ctx.beginPath();
|
|
40
|
+
ctx.moveTo(d0.x, d0.y);
|
|
41
|
+
ctx.lineTo(d1.x, d1.y);
|
|
42
|
+
ctx.lineTo(d2.x, d2.y);
|
|
43
|
+
ctx.closePath();
|
|
44
|
+
ctx.clip();
|
|
45
|
+
ctx.transform(a, b, c, d, e, f);
|
|
46
|
+
ctx.drawImage(image, 0, 0);
|
|
47
|
+
ctx.restore();
|
|
48
|
+
}
|
|
49
|
+
/** Canvas 2D texture renderer optimized for planar billboard meshes. */
|
|
50
|
+
export class Canvas2DTexturedRenderer {
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.sources = new Map();
|
|
53
|
+
this.loaded = new Map();
|
|
54
|
+
this.dpr = 1;
|
|
55
|
+
this.inner = new Canvas2DRenderer(options);
|
|
56
|
+
}
|
|
57
|
+
/** Texture every instance of `mesh` with `source`. Call any time, also before init. */
|
|
58
|
+
setTexture(mesh, source) {
|
|
59
|
+
this.sources.set(mesh, source);
|
|
60
|
+
if (typeof source === "string" && !this.loaded.has(source)) {
|
|
61
|
+
const image = new Image();
|
|
62
|
+
image.src = source;
|
|
63
|
+
this.loaded.set(source, image);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
init(canvas) {
|
|
67
|
+
this.inner.init(canvas);
|
|
68
|
+
this.ctx = canvas.getContext("2d") ?? undefined;
|
|
69
|
+
}
|
|
70
|
+
resize(cssWidth, cssHeight, dpr) {
|
|
71
|
+
this.dpr = dpr;
|
|
72
|
+
this.inner.resize(cssWidth, cssHeight, dpr);
|
|
73
|
+
}
|
|
74
|
+
drawable(mesh) {
|
|
75
|
+
const source = this.sources.get(mesh);
|
|
76
|
+
return typeof source === "string" ? this.loaded.get(source) : source;
|
|
77
|
+
}
|
|
78
|
+
render(frame) {
|
|
79
|
+
const plain = frame.items.filter((item) => {
|
|
80
|
+
if (!this.sources.has(item.mesh))
|
|
81
|
+
return true;
|
|
82
|
+
const source = this.drawable(item.mesh);
|
|
83
|
+
return !source || !imageSize(source);
|
|
84
|
+
});
|
|
85
|
+
this.inner.render({ ...frame, items: plain });
|
|
86
|
+
const ctx = this.ctx;
|
|
87
|
+
if (!ctx)
|
|
88
|
+
return;
|
|
89
|
+
ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
90
|
+
const tinted = new Map();
|
|
91
|
+
for (const item of frame.items) {
|
|
92
|
+
const source = this.drawable(item.mesh);
|
|
93
|
+
if (!source)
|
|
94
|
+
continue;
|
|
95
|
+
const size = imageSize(source);
|
|
96
|
+
if (!size)
|
|
97
|
+
continue;
|
|
98
|
+
let image = tinted.get(item.mesh);
|
|
99
|
+
if (!image) {
|
|
100
|
+
image = document.createElement("canvas");
|
|
101
|
+
image.width = size.width;
|
|
102
|
+
image.height = size.height;
|
|
103
|
+
const tint = image.getContext("2d");
|
|
104
|
+
if (!tint)
|
|
105
|
+
continue;
|
|
106
|
+
tint.drawImage(source, 0, 0, size.width, size.height);
|
|
107
|
+
tint.globalCompositeOperation = "source-in";
|
|
108
|
+
tint.fillStyle = item.mesh.faces[0]?.color ?? "#ffffff";
|
|
109
|
+
tint.fillRect(0, 0, size.width, size.height);
|
|
110
|
+
tinted.set(item.mesh, image);
|
|
111
|
+
}
|
|
112
|
+
const world = item.mesh.vertices.map((vertex) => transformAffine(item.model, vertex));
|
|
113
|
+
const projected = world.map((vertex) => {
|
|
114
|
+
const ndc = transformPoint(frame.viewProjection, vertex);
|
|
115
|
+
return { x: (ndc.x * 0.5 + 0.5) * frame.width, y: (1 - (ndc.y * 0.5 + 0.5)) * frame.height };
|
|
116
|
+
});
|
|
117
|
+
const face = item.mesh.faces[0];
|
|
118
|
+
if (!face || face.indices.length !== 4)
|
|
119
|
+
continue;
|
|
120
|
+
const [a, b, c, d] = face.indices.map((index) => projected[index]);
|
|
121
|
+
ctx.globalAlpha = item.transparency?.mode === "one-sided"
|
|
122
|
+
? opacity(item.transparency.opacity, DEFAULT_ONE_SIDED_OPACITY)
|
|
123
|
+
: 1;
|
|
124
|
+
drawMappedTriangle(ctx, image, [{ x: 0, y: size.height }, { x: size.width, y: size.height }, { x: size.width, y: 0 }], [a, b, c]);
|
|
125
|
+
drawMappedTriangle(ctx, image, [{ x: 0, y: size.height }, { x: size.width, y: 0 }, { x: 0, y: 0 }], [a, c, d]);
|
|
126
|
+
}
|
|
127
|
+
ctx.globalAlpha = 1;
|
|
128
|
+
}
|
|
129
|
+
destroy() {
|
|
130
|
+
this.inner.destroy();
|
|
131
|
+
this.ctx = undefined;
|
|
132
|
+
this.sources.clear();
|
|
133
|
+
this.loaded.clear();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Mesh } from "../core/mesh.js";
|
|
2
|
+
/** An image source accepted for a mesh texture: a URL or a drawable element. */
|
|
3
|
+
export type TextureSource = string | TexImageSource;
|
|
4
|
+
/**
|
|
5
|
+
* UVs as a planar projection of the mesh's XY bounds (u right, v up), emitted
|
|
6
|
+
* in the same face-fan order as `expandToTriangles` so the arrays stay
|
|
7
|
+
* aligned. Exact for billboard quads; flat meshes map incidentally.
|
|
8
|
+
*/
|
|
9
|
+
export declare function planarUVs(mesh: Mesh): Float32Array;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UVs as a planar projection of the mesh's XY bounds (u right, v up), emitted
|
|
3
|
+
* in the same face-fan order as `expandToTriangles` so the arrays stay
|
|
4
|
+
* aligned. Exact for billboard quads; flat meshes map incidentally.
|
|
5
|
+
*/
|
|
6
|
+
export function planarUVs(mesh) {
|
|
7
|
+
let minX = Infinity;
|
|
8
|
+
let minY = Infinity;
|
|
9
|
+
let maxX = -Infinity;
|
|
10
|
+
let maxY = -Infinity;
|
|
11
|
+
for (const v of mesh.vertices) {
|
|
12
|
+
minX = Math.min(minX, v.x);
|
|
13
|
+
minY = Math.min(minY, v.y);
|
|
14
|
+
maxX = Math.max(maxX, v.x);
|
|
15
|
+
maxY = Math.max(maxY, v.y);
|
|
16
|
+
}
|
|
17
|
+
const width = maxX - minX || 1;
|
|
18
|
+
const height = maxY - minY || 1;
|
|
19
|
+
let triangles = 0;
|
|
20
|
+
for (const face of mesh.faces)
|
|
21
|
+
triangles += Math.max(0, face.indices.length - 2);
|
|
22
|
+
const uvs = new Float32Array(triangles * 6);
|
|
23
|
+
let o = 0;
|
|
24
|
+
for (const face of mesh.faces) {
|
|
25
|
+
for (let k = 1; k < face.indices.length - 1; k++) {
|
|
26
|
+
for (const index of [face.indices[0], face.indices[k], face.indices[k + 1]]) {
|
|
27
|
+
const v = mesh.vertices[index];
|
|
28
|
+
uvs[o] = (v.x - minX) / width;
|
|
29
|
+
uvs[o + 1] = 1 - (v.y - minY) / height;
|
|
30
|
+
o += 2;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return uvs;
|
|
35
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Mesh } from "../core/mesh.js";
|
|
2
|
+
import { type Renderer, type RenderFrame, type RendererOptions } from "../renderer.js";
|
|
3
|
+
import { type TextureSource } from "./textured-helpers.js";
|
|
4
|
+
export type { TextureSource } from "./textured-helpers.js";
|
|
5
|
+
/**
|
|
6
|
+
* WebGL renderer with image textures on registered meshes; unregistered
|
|
7
|
+
* meshes render exactly as in the plain WebGL backend. Made for billboard
|
|
8
|
+
* quads: UVs are a planar projection of the mesh's XY extent, textured faces
|
|
9
|
+
* render unlit with the texture tinted by the face color, and textured
|
|
10
|
+
* instances draw after the other transparent instances. Until an image URL
|
|
11
|
+
* finishes loading its mesh renders with a plain white texture.
|
|
12
|
+
*
|
|
13
|
+
* Pass it to the engine (or an animation) as a `backend` factory so the
|
|
14
|
+
* module is only fetched when used:
|
|
15
|
+
*
|
|
16
|
+
* ```js
|
|
17
|
+
* backend: async (options) => {
|
|
18
|
+
* const { WebGLTexturedRenderer } =
|
|
19
|
+
* await import("3d-spinner/engines/little-3d-engine/renderers/webgl-textured");
|
|
20
|
+
* const renderer = new WebGLTexturedRenderer(options);
|
|
21
|
+
* renderer.setTexture(mesh, "/sprite.png");
|
|
22
|
+
* return renderer;
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class WebGLTexturedRenderer implements Renderer {
|
|
27
|
+
private readonly inner;
|
|
28
|
+
private gl?;
|
|
29
|
+
private program?;
|
|
30
|
+
private locations?;
|
|
31
|
+
private readonly sources;
|
|
32
|
+
private readonly textures;
|
|
33
|
+
private readonly buffers;
|
|
34
|
+
constructor(options?: RendererOptions);
|
|
35
|
+
/** Texture every instance of `mesh` with `source`. Call any time, also before init. */
|
|
36
|
+
setTexture(mesh: Mesh, source: TextureSource): void;
|
|
37
|
+
init(canvas: HTMLCanvasElement): void;
|
|
38
|
+
resize(): void;
|
|
39
|
+
private textureFor;
|
|
40
|
+
private buffersFor;
|
|
41
|
+
render(frame: RenderFrame): void;
|
|
42
|
+
destroy(): void;
|
|
43
|
+
}
|