@logomarks/logo3d 0.0.0-stage → 0.1.1
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/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +55 -2
- package/assets/hdri/kloofendal_48d_partly_cloudy_puresky_1k.hdr +0 -0
- package/assets/hdri/studio_small_09_1k.hdr +0 -0
- package/package.json +82 -4
- package/src/export/glb.d.ts +5 -0
- package/src/export/glb.js +28 -0
- package/src/export/still.d.ts +23 -0
- package/src/export/still.js +144 -0
- package/src/geometry/buffer.js +102 -0
- package/src/geometry/caps.js +53 -0
- package/src/geometry/check.js +86 -0
- package/src/geometry/index.d.ts +114 -0
- package/src/geometry/index.js +112 -0
- package/src/geometry/inset.js +339 -0
- package/src/geometry/profiles.js +126 -0
- package/src/geometry/sides.js +86 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +19 -0
- package/src/materials/factory.d.ts +11 -0
- package/src/materials/factory.js +112 -0
- package/src/materials/presets.d.ts +34 -0
- package/src/materials/presets.js +79 -0
- package/src/motion/index.d.ts +23 -0
- package/src/motion/index.js +56 -0
- package/src/scene/backdrop.d.ts +20 -0
- package/src/scene/backdrop.js +128 -0
- package/src/scene/camera.d.ts +10 -0
- package/src/scene/camera.js +51 -0
- package/src/scene/environments.d.ts +22 -0
- package/src/scene/environments.js +94 -0
- package/src/scene/studio.d.ts +12 -0
- package/src/scene/studio.js +81 -0
- package/src/scene/view.d.ts +69 -0
- package/src/scene/view.js +273 -0
- package/src/shape/index.d.ts +80 -0
- package/src/shape/index.js +32 -0
- package/src/shape/layers.js +142 -0
- package/src/shape/matrix.js +88 -0
- package/src/shape/normalise.js +92 -0
- package/src/shape/simplify.js +95 -0
- package/src/shape/svg.js +196 -0
- package/src/shape/thinnest.js +77 -0
- package/src/three/geometry.d.ts +5 -0
- package/src/three/geometry.js +25 -0
- package/src/three/index.d.ts +11 -0
- package/src/three/index.js +15 -0
- package/src/worker/build.d.ts +6 -0
- package/src/worker/build.js +37 -0
- package/src/worker/client.d.ts +29 -0
- package/src/worker/client.js +83 -0
- package/src/worker/worker.d.ts +2 -0
- package/src/worker/worker.js +23 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/logo3d/scene/backdrop
|
|
3
|
+
*
|
|
4
|
+
* What is behind the logo: nothing (a transparent PNG), one flat colour, or a seamless paper sweep (a floor curving up
|
|
5
|
+
* into a wall, as a product studio hangs one) that the logo stands on, with a soft contact shadow under it. The sweep
|
|
6
|
+
* is drawn in its own colour, unlit, a shade darker up the wall, so its colour is the colour asked for; the shadow
|
|
7
|
+
* comes from a light that casts shadows and lights nothing (the logo is lit only by what it reflects).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The backgrounds. */
|
|
11
|
+
export const BACKGROUNDS = Object.freeze(['transparent', 'solid', 'sweep']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A sweep's profile, front to top, in (z, y): a straight floor, a quarter circle, a straight wall.
|
|
15
|
+
*
|
|
16
|
+
* @param {{ back?: number, cove?: number, front?: number, rise?: number }} [o] Where the cove starts behind the
|
|
17
|
+
* origin (1.2), its radius (1.4), how far the floor runs forwards (10) and the wall up (10).
|
|
18
|
+
* @returns {[number, number][]} The profile.
|
|
19
|
+
*/
|
|
20
|
+
export function sweepProfile({ back = 1.2, cove = 1.4, front = 10, rise = 10 } = {}) {
|
|
21
|
+
/** @type {[number, number][]} */
|
|
22
|
+
const out = [];
|
|
23
|
+
for (let i = 0; i <= 6; i++) out.push([front - ((front + back) * i) / 6, 0]);
|
|
24
|
+
for (let i = 1; i <= 16; i++) {
|
|
25
|
+
const a = (i / 16) * (Math.PI / 2);
|
|
26
|
+
out.push([-back - Math.sin(a) * cove, cove - Math.cos(a) * cove]);
|
|
27
|
+
}
|
|
28
|
+
for (let i = 1; i <= 6; i++) out.push([-back - cove, cove + (rise * i) / 6]);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The sweep: its profile swept across, coloured a shade darker as it rises.
|
|
34
|
+
*
|
|
35
|
+
* @param {typeof import('three')} THREE three.js.
|
|
36
|
+
* @param {string} color The paper's colour.
|
|
37
|
+
* @param {number} [width] How wide it is (30).
|
|
38
|
+
* @returns {import('three').Mesh} The mesh, its floor at y = 0.
|
|
39
|
+
*/
|
|
40
|
+
export function sweepMesh(THREE, color, width = 30) {
|
|
41
|
+
const prof = sweepProfile();
|
|
42
|
+
const pos = [];
|
|
43
|
+
const col = [];
|
|
44
|
+
const index = [];
|
|
45
|
+
const base = new THREE.Color(color);
|
|
46
|
+
for (const [z, y] of prof) {
|
|
47
|
+
const k = 1 - 0.14 * Math.min(1, y / 4);
|
|
48
|
+
for (const x of [-width / 2, width / 2]) {
|
|
49
|
+
pos.push(x, y, z);
|
|
50
|
+
col.push(base.r * k, base.g * k, base.b * k);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0; i < prof.length - 1; i++) index.push(2 * i, 2 * i + 1, 2 * i + 3, 2 * i, 2 * i + 3, 2 * i + 2);
|
|
54
|
+
const geo = new THREE.BufferGeometry();
|
|
55
|
+
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
|
56
|
+
geo.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
|
|
57
|
+
geo.setIndex(index);
|
|
58
|
+
return new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ vertexColors: true, toneMapped: false, side: THREE.DoubleSide }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {object} Backdrop
|
|
63
|
+
* @property {import('three').Object3D} group What to add to the scene.
|
|
64
|
+
* @property {boolean} standing Whether the logo stands on a floor (the sweep).
|
|
65
|
+
* @property {(scene: import('three').Scene, renderer: import('three').WebGLRenderer) => void} apply Sets the scene's
|
|
66
|
+
* background and the renderer's shadows for it.
|
|
67
|
+
* @property {() => void} dispose Frees it.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A backdrop.
|
|
72
|
+
*
|
|
73
|
+
* @param {typeof import('three')} THREE three.js.
|
|
74
|
+
* @param {{ kind: string, color: string, size: number }} o The background, its colour, and the logo's size (its
|
|
75
|
+
* bounding sphere's radius), to fit the shadow to.
|
|
76
|
+
* @returns {Backdrop} The backdrop.
|
|
77
|
+
*/
|
|
78
|
+
export function makeBackdrop(THREE, { kind, color, size }) {
|
|
79
|
+
const group = new THREE.Group();
|
|
80
|
+
if (kind !== 'sweep') {
|
|
81
|
+
return {
|
|
82
|
+
group,
|
|
83
|
+
standing: false,
|
|
84
|
+
apply(scene, renderer) {
|
|
85
|
+
scene.background = kind === 'solid' ? new THREE.Color(color) : null;
|
|
86
|
+
renderer.shadowMap.enabled = false;
|
|
87
|
+
},
|
|
88
|
+
dispose() {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const sweep = sweepMesh(THREE, color);
|
|
92
|
+
group.add(sweep);
|
|
93
|
+
// the contact shadow: a shadow catcher on the floor, from a light high above and a little in front
|
|
94
|
+
const catcher = new THREE.Mesh(new THREE.PlaneGeometry(8 * size, 8 * size), new THREE.ShadowMaterial({ opacity: 0.32, color: 0x000000 }));
|
|
95
|
+
catcher.rotation.x = -Math.PI / 2;
|
|
96
|
+
catcher.position.y = 0.0005;
|
|
97
|
+
catcher.receiveShadow = true;
|
|
98
|
+
group.add(catcher);
|
|
99
|
+
const light = new THREE.DirectionalLight(0xffffff, 0);
|
|
100
|
+
light.position.set(0.3 * size, 6 * size, 1.4 * size);
|
|
101
|
+
light.castShadow = true;
|
|
102
|
+
light.shadow.mapSize.set(1024, 1024);
|
|
103
|
+
light.shadow.radius = 10;
|
|
104
|
+
light.shadow.blurSamples = 16;
|
|
105
|
+
light.shadow.bias = -0.0005;
|
|
106
|
+
const cam = light.shadow.camera;
|
|
107
|
+
cam.left = cam.bottom = -2 * size;
|
|
108
|
+
cam.right = cam.top = 2 * size;
|
|
109
|
+
cam.near = 0.1;
|
|
110
|
+
cam.far = 12 * size;
|
|
111
|
+
group.add(light, light.target);
|
|
112
|
+
return {
|
|
113
|
+
group,
|
|
114
|
+
standing: true,
|
|
115
|
+
apply(scene, renderer) {
|
|
116
|
+
scene.background = new THREE.Color(color);
|
|
117
|
+
renderer.shadowMap.enabled = true;
|
|
118
|
+
renderer.shadowMap.type = THREE.VSMShadowMap;
|
|
119
|
+
},
|
|
120
|
+
dispose() {
|
|
121
|
+
sweep.geometry.dispose();
|
|
122
|
+
/** @type {import('three').Material} */ (sweep.material).dispose();
|
|
123
|
+
catcher.geometry.dispose();
|
|
124
|
+
/** @type {import('three').Material} */ (catcher.material).dispose();
|
|
125
|
+
light.dispose();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A view's name. */
|
|
2
|
+
export type ViewName = 'front' | 'angle' | 'low' | 'high';
|
|
3
|
+
/** The views: degrees turned round the logo and tilted down onto it. */
|
|
4
|
+
export const VIEWS: Readonly<Record<ViewName, Readonly<{ name: string; turn: number; tilt: number }>>>;
|
|
5
|
+
/** The view shown first. */
|
|
6
|
+
export const DEFAULT_VIEW: ViewName;
|
|
7
|
+
/** How far the camera stands for a sphere to fill a share of the frame. */
|
|
8
|
+
export function fitDistance(radius: number, fov: number, aspect: number, fill?: number): number;
|
|
9
|
+
/** A camera's position for a view. */
|
|
10
|
+
export function viewPosition(name: string, o: { radius: number; aspect: number; fov: number; target: [number, number, number]; fill?: number }): [number, number, number];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/logo3d/scene/camera
|
|
3
|
+
*
|
|
4
|
+
* Where the camera stands: a few named views round the logo (straight on, a three-quarter turn, from below, from
|
|
5
|
+
* above), each placed at the distance that fits the logo's bounding sphere in the frame, whatever its aspect, so a
|
|
6
|
+
* turning logo never leaves the frame.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEG = Math.PI / 180;
|
|
10
|
+
|
|
11
|
+
/** The views: degrees turned round the logo and tilted down onto it. */
|
|
12
|
+
export const VIEWS = Object.freeze({
|
|
13
|
+
front: Object.freeze({ name: 'Front', turn: 0, tilt: 0 }),
|
|
14
|
+
angle: Object.freeze({ name: 'Three-quarter', turn: -28, tilt: 10 }),
|
|
15
|
+
low: Object.freeze({ name: 'Low', turn: -16, tilt: -12 }),
|
|
16
|
+
high: Object.freeze({ name: 'High', turn: 0, tilt: 32 }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** The view shown first. */
|
|
20
|
+
export const DEFAULT_VIEW = 'angle';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How far the camera stands for a sphere to fill a share of the frame.
|
|
24
|
+
*
|
|
25
|
+
* @param {number} radius The sphere's radius.
|
|
26
|
+
* @param {number} fov The lens's vertical field of view, degrees.
|
|
27
|
+
* @param {number} aspect The frame's width over its height.
|
|
28
|
+
* @param {number} [fill] The share of the frame's shorter side the sphere spans (0.9).
|
|
29
|
+
* @returns {number} The distance.
|
|
30
|
+
*/
|
|
31
|
+
export function fitDistance(radius, fov, aspect, fill = 0.9) {
|
|
32
|
+
const v = (fov * DEG) / 2;
|
|
33
|
+
const h = Math.atan(Math.tan(v) * aspect);
|
|
34
|
+
const half = Math.min(v, h);
|
|
35
|
+
return radius / Math.sin(Math.atan(Math.tan(half) * fill));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A camera's position for a view.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} name The view (`VIEWS`; the default for an unknown one).
|
|
42
|
+
* @param {{ radius: number, aspect: number, fov: number, target: [number, number, number], fill?: number }} o The
|
|
43
|
+
* logo's bounding radius, the frame's aspect, the lens, where to look, and how much of the frame to fill.
|
|
44
|
+
* @returns {[number, number, number]} Where the camera goes.
|
|
45
|
+
*/
|
|
46
|
+
export function viewPosition(name, { radius, aspect, fov, target, fill }) {
|
|
47
|
+
const v = VIEWS[/** @type {keyof typeof VIEWS} */ (name)] ?? VIEWS[DEFAULT_VIEW];
|
|
48
|
+
const d = fitDistance(radius, fov, aspect, fill);
|
|
49
|
+
const turn = v.turn * DEG, tilt = v.tilt * DEG;
|
|
50
|
+
return [target[0] + d * Math.cos(tilt) * Math.sin(turn), target[1] + d * Math.sin(tilt), target[2] + d * Math.cos(tilt) * Math.cos(turn)];
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/** An environment's id. */
|
|
4
|
+
export type EnvironmentId = 'studio' | 'softbox' | 'sky';
|
|
5
|
+
/** What the logo reflects. */
|
|
6
|
+
export interface Environment {
|
|
7
|
+
id: EnvironmentId;
|
|
8
|
+
name: string;
|
|
9
|
+
/** The HDRI's file in `assets/hdri/`, or null for the built studio. */
|
|
10
|
+
file: string | null;
|
|
11
|
+
intensity: number;
|
|
12
|
+
rotation: number;
|
|
13
|
+
credit: string;
|
|
14
|
+
}
|
|
15
|
+
/** The environments, in menu order. */
|
|
16
|
+
export const ENVIRONMENTS: readonly Environment[];
|
|
17
|
+
/** The environment shown first. */
|
|
18
|
+
export const DEFAULT_ENVIRONMENT: EnvironmentId;
|
|
19
|
+
/** An environment by id (the default for an unknown one). */
|
|
20
|
+
export function environmentById(id: string | undefined): Environment;
|
|
21
|
+
/** Loads environments on demand and keeps each prefiltered. `urls`: each HDRI file's URL by file name. */
|
|
22
|
+
export function environmentCache(three: typeof THREE, renderer: THREE.WebGLRenderer, urls: Record<string, string>): { get(id: string): Promise<THREE.Texture>; dispose(): void };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/logo3d/scene/environments
|
|
3
|
+
*
|
|
4
|
+
* What the logo reflects: our own studio (soft boxes in a dim room, built as a scene, no download), and two CC0 HDRIs
|
|
5
|
+
* from Poly Haven, self-hosted at 1k (`assets/hdri/`): a small photo studio and an open sky. Each is prefiltered once
|
|
6
|
+
* (PMREM) and kept, so switching back is instant.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { studioScene } from './studio.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} Environment
|
|
13
|
+
* @property {string} id
|
|
14
|
+
* @property {string} name Its name in a menu.
|
|
15
|
+
* @property {string | null} file The HDRI's file in `assets/hdri/`, or null for the built studio.
|
|
16
|
+
* @property {number} intensity How bright it is on the logo.
|
|
17
|
+
* @property {number} rotation Radians it is turned by to start, so its brightest light falls well.
|
|
18
|
+
* @property {string} credit Where it is from.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** @type {readonly Environment[]} */
|
|
22
|
+
export const ENVIRONMENTS = Object.freeze([
|
|
23
|
+
{ id: 'studio', name: 'Studio', file: null, intensity: 1.15, rotation: 0, credit: 'Built in code (scene/studio.js)' },
|
|
24
|
+
{ id: 'softbox', name: 'Softbox', file: 'studio_small_09_1k.hdr', intensity: 1, rotation: 0.6, credit: 'Studio Small 09 by Sergej Majboroda, Poly Haven (CC0)' },
|
|
25
|
+
{ id: 'sky', name: 'Open sky', file: 'kloofendal_48d_partly_cloudy_puresky_1k.hdr', intensity: 0.9, rotation: 1.2, credit: 'Kloofendal 48d Partly Cloudy (Pure Sky) by Greg Zaal and Jarod Guest, Poly Haven (CC0)' },
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/** The environment shown first. */
|
|
29
|
+
export const DEFAULT_ENVIRONMENT = 'studio';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An environment by id (the default for an unknown one).
|
|
33
|
+
*
|
|
34
|
+
* @param {string | undefined} id The id.
|
|
35
|
+
* @returns {Environment} The environment.
|
|
36
|
+
*/
|
|
37
|
+
export const environmentById = (id) => ENVIRONMENTS.find((e) => e.id === id) ?? /** @type {Environment} */ (ENVIRONMENTS[0]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Loads environments on demand and keeps each prefiltered.
|
|
41
|
+
*
|
|
42
|
+
* @param {typeof import('three')} THREE three.js.
|
|
43
|
+
* @param {import('three').WebGLRenderer} renderer The renderer.
|
|
44
|
+
* @param {Record<string, string>} urls Each HDRI file's URL (by file name), as the page serves it.
|
|
45
|
+
* @returns {{ get(id: string): Promise<import('three').Texture>, dispose(): void }} The cache.
|
|
46
|
+
*/
|
|
47
|
+
export function environmentCache(THREE, renderer, urls) {
|
|
48
|
+
const pmrem = new THREE.PMREMGenerator(renderer);
|
|
49
|
+
/** @type {Map<string, Promise<import('three').Texture>>} */
|
|
50
|
+
const done = new Map();
|
|
51
|
+
/** @type {import('three').Texture[]} */
|
|
52
|
+
const made = [];
|
|
53
|
+
/**
|
|
54
|
+
* The environment, prefiltered.
|
|
55
|
+
*
|
|
56
|
+
* @param {Environment} e The environment.
|
|
57
|
+
* @returns {Promise<import('three').Texture>} The prefiltered texture.
|
|
58
|
+
*/
|
|
59
|
+
async function prefiltered(e) {
|
|
60
|
+
if (!e.file) {
|
|
61
|
+
const studio = studioScene(THREE);
|
|
62
|
+
const env = pmrem.fromScene(studio.scene, 0.035).texture;
|
|
63
|
+
studio.dispose();
|
|
64
|
+
return env;
|
|
65
|
+
}
|
|
66
|
+
const url = urls[e.file];
|
|
67
|
+
if (!url) throw new Error(`logo3d: no URL for ${e.file}`);
|
|
68
|
+
const { HDRLoader } = await import('three/addons/loaders/HDRLoader.js');
|
|
69
|
+
const src = await new HDRLoader().loadAsync(url);
|
|
70
|
+
src.mapping = THREE.EquirectangularReflectionMapping;
|
|
71
|
+
const env = pmrem.fromEquirectangular(src).texture;
|
|
72
|
+
src.dispose();
|
|
73
|
+
return env;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
get(id) {
|
|
77
|
+
const e = environmentById(id);
|
|
78
|
+
let p = done.get(e.id);
|
|
79
|
+
if (!p) {
|
|
80
|
+
p = prefiltered(e).then((env) => {
|
|
81
|
+
made.push(env);
|
|
82
|
+
return env;
|
|
83
|
+
});
|
|
84
|
+
p.catch(() => done.delete(e.id));
|
|
85
|
+
done.set(e.id, p);
|
|
86
|
+
}
|
|
87
|
+
return p;
|
|
88
|
+
},
|
|
89
|
+
dispose() {
|
|
90
|
+
for (const t of made) t.dispose();
|
|
91
|
+
pmrem.dispose();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/** A soft box: its centre, size and brightness. */
|
|
4
|
+
export interface Softbox {
|
|
5
|
+
at: [number, number, number];
|
|
6
|
+
size: [number, number];
|
|
7
|
+
power: number;
|
|
8
|
+
}
|
|
9
|
+
/** The studio's soft boxes. */
|
|
10
|
+
export const SOFTBOXES: readonly Softbox[];
|
|
11
|
+
/** The studio as a scene, for `PMREMGenerator.fromScene`. */
|
|
12
|
+
export function studioScene(three: typeof THREE): { scene: THREE.Scene; dispose(): void };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/logo3d/scene/studio
|
|
3
|
+
*
|
|
4
|
+
* A product studio for a logo to reflect, built as a small three.js scene and prefiltered like an HDRI: a dim room
|
|
5
|
+
* with a lighter floor, a broad soft key above and in front, a tall strip light either side, a low card in front and
|
|
6
|
+
* two thin rims behind.
|
|
7
|
+
* Chrome and glass pick these up as the long, clean highlights of a product photo; there are no point lights, so
|
|
8
|
+
* there are no highlight spots.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} Softbox
|
|
13
|
+
* @property {[number, number, number]} at Its centre.
|
|
14
|
+
* @property {[number, number]} size Width and height.
|
|
15
|
+
* @property {number} power Its brightness (1 is white).
|
|
16
|
+
* @property {number} [fade] Its brightness at the bottom, as a share of the top's (1: even).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The soft boxes, round a logo at the origin facing +z: a broad key overhead in front, a tall strip either side, a
|
|
20
|
+
* low card in front for the lower edges, two thin rims behind, and a wide card behind the camera fading from light to
|
|
21
|
+
* dark, which a face turned to the camera shows as a smooth gradient. The dark gaps between them are what draw
|
|
22
|
+
* chrome's crisp lines. */
|
|
23
|
+
export const SOFTBOXES = Object.freeze(
|
|
24
|
+
/** @type {Softbox[]} */ ([
|
|
25
|
+
{ at: [0, 4.2, 3.4], size: [7, 2.6], power: 6 },
|
|
26
|
+
{ at: [-0.6, -1.1, 6.6], size: [12, 3.4], power: 1.5, fade: 0.04 },
|
|
27
|
+
{ at: [-4.6, 0.8, 1.8], size: [1.3, 6.5], power: 4.5 },
|
|
28
|
+
{ at: [4.6, 0.4, 2.2], size: [1.1, 6.5], power: 3.4 },
|
|
29
|
+
{ at: [0, -2.2, 4.6], size: [6, 1.1], power: 1.6 },
|
|
30
|
+
{ at: [-3.2, 1, -4.4], size: [0.6, 6], power: 2.6 },
|
|
31
|
+
{ at: [3.4, 1, -4.2], size: [0.6, 6], power: 2.6 },
|
|
32
|
+
]),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A unit panel whose brightness runs from 1 at the top to `fade` at the bottom (vertex colours).
|
|
37
|
+
*
|
|
38
|
+
* @param {typeof import('three')} THREE three.js.
|
|
39
|
+
* @param {number} fade The bottom's brightness.
|
|
40
|
+
* @returns {import('three').PlaneGeometry} The panel.
|
|
41
|
+
*/
|
|
42
|
+
function panelGeometry(THREE, fade) {
|
|
43
|
+
const g = new THREE.PlaneGeometry(1, 1, 1, 8);
|
|
44
|
+
const pos = g.attributes.position;
|
|
45
|
+
const col = [];
|
|
46
|
+
for (let i = 0; i < pos.count; i++) {
|
|
47
|
+
const k = fade + (1 - fade) * (pos.getY(i) + 0.5);
|
|
48
|
+
col.push(k, k, k);
|
|
49
|
+
}
|
|
50
|
+
g.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
|
|
51
|
+
return g;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The studio as a scene, for `PMREMGenerator.fromScene`.
|
|
56
|
+
*
|
|
57
|
+
* @param {typeof import('three')} THREE three.js.
|
|
58
|
+
* @returns {{ scene: import('three').Scene, dispose(): void }} The scene, and how to free it.
|
|
59
|
+
*/
|
|
60
|
+
export function studioScene(THREE) {
|
|
61
|
+
const scene = new THREE.Scene();
|
|
62
|
+
/** @type {{ dispose(): void }[]} */
|
|
63
|
+
const owned = [];
|
|
64
|
+
const keep = /** @type {<T extends { dispose(): void }>(t: T) => T} */ ((t) => (owned.push(t), t));
|
|
65
|
+
const room = new THREE.Mesh(keep(new THREE.BoxGeometry(14, 10, 14)), keep(new THREE.MeshBasicMaterial({ color: 0x232326, side: THREE.BackSide })));
|
|
66
|
+
room.position.y = 2;
|
|
67
|
+
scene.add(room);
|
|
68
|
+
// the floor a little lighter than the walls, as a studio's floor catches the key
|
|
69
|
+
const floor = new THREE.Mesh(keep(new THREE.PlaneGeometry(14, 14)), keep(new THREE.MeshBasicMaterial({ color: 0x55555a })));
|
|
70
|
+
floor.rotation.x = -Math.PI / 2;
|
|
71
|
+
floor.position.y = -2.9;
|
|
72
|
+
scene.add(floor);
|
|
73
|
+
for (const b of SOFTBOXES) {
|
|
74
|
+
const m = new THREE.Mesh(keep(panelGeometry(THREE, b.fade ?? 1)), keep(new THREE.MeshBasicMaterial({ color: new THREE.Color(1, 1, 1).multiplyScalar(b.power), vertexColors: true, side: THREE.DoubleSide })));
|
|
75
|
+
m.scale.set(b.size[0], b.size[1], 1);
|
|
76
|
+
m.position.set(...b.at);
|
|
77
|
+
m.lookAt(0, 0, 0);
|
|
78
|
+
scene.add(m);
|
|
79
|
+
}
|
|
80
|
+
return { scene, dispose: () => owned.forEach((o) => o.dispose()) };
|
|
81
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import type { LogoGeometry } from '../geometry/index.js';
|
|
3
|
+
import type { Pixels } from '../export/still.js';
|
|
4
|
+
|
|
5
|
+
/** How the logo looks. */
|
|
6
|
+
export interface Look {
|
|
7
|
+
/** The face's material (`MATERIALS`). */
|
|
8
|
+
material: string;
|
|
9
|
+
/** The sides' material, or null for the face's. */
|
|
10
|
+
side: string | null;
|
|
11
|
+
/** The logo's colour, `#rrggbb` (for materials that take one). */
|
|
12
|
+
color: string;
|
|
13
|
+
/** The environment (`ENVIRONMENTS`). */
|
|
14
|
+
environment: string;
|
|
15
|
+
/** `'transparent'`, `'solid'` or `'sweep'`. */
|
|
16
|
+
background: string;
|
|
17
|
+
backgroundColor: string;
|
|
18
|
+
/** The view (`VIEWS`). */
|
|
19
|
+
view: string;
|
|
20
|
+
/** The motion (`MOTIONS`). */
|
|
21
|
+
motion: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** How a view is made. */
|
|
25
|
+
export interface ViewOptions {
|
|
26
|
+
/** Each HDRI file's URL, by file name (`studio_small_09_1k.hdr`, …), as the page serves them. */
|
|
27
|
+
hdri?: Record<string, string>;
|
|
28
|
+
/** The canvas's pixel ratio (the device's, up to 2). */
|
|
29
|
+
pixelRatio?: number;
|
|
30
|
+
/** Whether a drag turns the logo (true). */
|
|
31
|
+
drag?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A live view. */
|
|
35
|
+
export interface LogoView {
|
|
36
|
+
readonly canvas: HTMLCanvasElement;
|
|
37
|
+
readonly renderer: THREE.WebGLRenderer;
|
|
38
|
+
readonly scene: THREE.Scene;
|
|
39
|
+
readonly camera: THREE.PerspectiveCamera;
|
|
40
|
+
readonly mesh: THREE.Mesh;
|
|
41
|
+
/** The look as it stands. */
|
|
42
|
+
readonly look: Look;
|
|
43
|
+
/** Shows a geometry; `colors`: its layers' own colours (null for the ink). */
|
|
44
|
+
setGeometry(g: LogoGeometry, colors?: (string | null)[]): void;
|
|
45
|
+
/** Changes the look; resolves once its environment is loaded. */
|
|
46
|
+
setLook(next: Partial<Look>): Promise<void>;
|
|
47
|
+
/** Undoes any drag. */
|
|
48
|
+
resetDrag(): void;
|
|
49
|
+
/** Draws a frame (at `t` seconds into the motion, or the current time). */
|
|
50
|
+
render(t?: number): void;
|
|
51
|
+
/** The motion's loop, seconds. */
|
|
52
|
+
period(): number;
|
|
53
|
+
/** A still, drawn off screen: RGBA pixels. `transparent` only applies on a transparent background. */
|
|
54
|
+
still(o: { width: number; height: number; transparent?: boolean; t?: number }): Pixels;
|
|
55
|
+
/** A still as a PNG. */
|
|
56
|
+
png(o: { width: number; height: number; transparent?: boolean; t?: number }): Promise<Blob>;
|
|
57
|
+
/** The logo as a binary glTF. */
|
|
58
|
+
glb(): Promise<Blob>;
|
|
59
|
+
/** A frame source for a video encoder: draws the frame at `t` seconds onto a canvas, at the size given. */
|
|
60
|
+
frames(o: { width: number; height: number }): (t: number, canvas: HTMLCanvasElement) => void;
|
|
61
|
+
dispose(): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The look a view starts with. */
|
|
65
|
+
export const DEFAULT_LOOK: Readonly<Look>;
|
|
66
|
+
/** Each layer's colour: one-colour logos take the look's; several keep their own, ink layers taking the look's. */
|
|
67
|
+
export function colorsFor(layerColors: (string | null)[], color: string): string[];
|
|
68
|
+
/** A live view on a canvas. */
|
|
69
|
+
export function createView(canvas: HTMLCanvasElement, o?: ViewOptions): LogoView;
|