@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +55 -2
  4. package/assets/hdri/kloofendal_48d_partly_cloudy_puresky_1k.hdr +0 -0
  5. package/assets/hdri/studio_small_09_1k.hdr +0 -0
  6. package/package.json +82 -4
  7. package/src/export/glb.d.ts +5 -0
  8. package/src/export/glb.js +28 -0
  9. package/src/export/still.d.ts +23 -0
  10. package/src/export/still.js +144 -0
  11. package/src/geometry/buffer.js +102 -0
  12. package/src/geometry/caps.js +53 -0
  13. package/src/geometry/check.js +86 -0
  14. package/src/geometry/index.d.ts +114 -0
  15. package/src/geometry/index.js +112 -0
  16. package/src/geometry/inset.js +339 -0
  17. package/src/geometry/profiles.js +126 -0
  18. package/src/geometry/sides.js +86 -0
  19. package/src/index.d.ts +17 -0
  20. package/src/index.js +19 -0
  21. package/src/materials/factory.d.ts +11 -0
  22. package/src/materials/factory.js +112 -0
  23. package/src/materials/presets.d.ts +34 -0
  24. package/src/materials/presets.js +79 -0
  25. package/src/motion/index.d.ts +23 -0
  26. package/src/motion/index.js +56 -0
  27. package/src/scene/backdrop.d.ts +20 -0
  28. package/src/scene/backdrop.js +128 -0
  29. package/src/scene/camera.d.ts +10 -0
  30. package/src/scene/camera.js +51 -0
  31. package/src/scene/environments.d.ts +22 -0
  32. package/src/scene/environments.js +94 -0
  33. package/src/scene/studio.d.ts +12 -0
  34. package/src/scene/studio.js +81 -0
  35. package/src/scene/view.d.ts +69 -0
  36. package/src/scene/view.js +273 -0
  37. package/src/shape/index.d.ts +80 -0
  38. package/src/shape/index.js +32 -0
  39. package/src/shape/layers.js +142 -0
  40. package/src/shape/matrix.js +88 -0
  41. package/src/shape/normalise.js +92 -0
  42. package/src/shape/simplify.js +95 -0
  43. package/src/shape/svg.js +196 -0
  44. package/src/shape/thinnest.js +77 -0
  45. package/src/three/geometry.d.ts +5 -0
  46. package/src/three/geometry.js +25 -0
  47. package/src/three/index.d.ts +11 -0
  48. package/src/three/index.js +15 -0
  49. package/src/worker/build.d.ts +6 -0
  50. package/src/worker/build.js +37 -0
  51. package/src/worker/client.d.ts +29 -0
  52. package/src/worker/client.js +83 -0
  53. package/src/worker/worker.d.ts +2 -0
  54. package/src/worker/worker.js +23 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @module @logomarks/logo3d/geometry/profiles
3
+ *
4
+ * The edge profiles: the path a logo's side takes from its back face (z = 0) to its front face (z = depth), as
5
+ * points `(inset, z)`, inset measured inwards from the outline. Flat is a straight wall; chamfer a 45° cut on each
6
+ * edge; round a quarter circle on each edge; pillow one curve over the whole thickness, so thin strokes come out as
7
+ * tubes and wide parts as cushions.
8
+ */
9
+
10
+ /** The profiles. */
11
+ export const PROFILES = Object.freeze(['flat', 'chamfer', 'round', 'pillow']);
12
+
13
+ /**
14
+ * @typedef {object} ProfilePoint
15
+ * @property {number} inset How far in from the outline.
16
+ * @property {number} z How far up from the back.
17
+ */
18
+
19
+ /**
20
+ * @typedef {object} Profile
21
+ * @property {ProfilePoint[]} points From the back face's edge to the front face's.
22
+ * @property {boolean} smooth Whether the side is shaded smooth across the points (curves) or faceted (cuts).
23
+ */
24
+
25
+ /**
26
+ * A quarter of an ellipse between two profile points: from (inset0, z0) curving to (inset1, z1).
27
+ *
28
+ * @param {number} b The bevel's width (inset).
29
+ * @param {number} t Its height (z).
30
+ * @param {number} z0 Where it starts up the side.
31
+ * @param {number} n Segments.
32
+ * @param {boolean} front The front edge (curving in towards the face) or the back (curving out from it).
33
+ * @returns {ProfilePoint[]} The points, the first included.
34
+ */
35
+ function quarter(b, t, z0, n, front) {
36
+ /** @type {ProfilePoint[]} */
37
+ const out = [];
38
+ for (let k = 0; k <= n; k++) {
39
+ const a = (k / n) * (Math.PI / 2);
40
+ // the front edge: from the wall (inset 0) up and in to the face (inset b); the back mirrors it
41
+ out.push(front ? { inset: b * (1 - Math.cos(a)), z: z0 + t * Math.sin(a) } : { inset: b * (1 - Math.sin(a)), z: z0 + t * (1 - Math.cos(a)) });
42
+ }
43
+ return out;
44
+ }
45
+
46
+ /**
47
+ * A profile's points.
48
+ *
49
+ * @param {string} kind `'flat'`, `'chamfer'`, `'round'` or `'pillow'`.
50
+ * @param {{ depth: number, bevel: number, segments?: number }} o The depth, the bevel's width (already capped), and
51
+ * the segments in each curve (round 5, pillow 8).
52
+ * @returns {Profile} The profile.
53
+ */
54
+ export function profile(kind, { depth, bevel, segments }) {
55
+ const b = Math.max(0, bevel);
56
+ if (kind === 'flat' || b <= 0 || depth <= 0) return { points: [{ inset: 0, z: 0 }, { inset: 0, z: depth }], smooth: false };
57
+ if (kind === 'pillow') {
58
+ const n = segments ?? 8;
59
+ return { points: [...quarter(b, depth / 2, 0, n, false), ...quarter(b, depth / 2, depth / 2, n, true).slice(1)], smooth: true };
60
+ }
61
+ const t = Math.min(b, depth * 0.45);
62
+ if (kind === 'chamfer')
63
+ return {
64
+ points: [
65
+ { inset: t, z: 0 },
66
+ { inset: 0, z: t },
67
+ { inset: 0, z: depth - t },
68
+ { inset: t, z: depth },
69
+ ],
70
+ smooth: false,
71
+ };
72
+ const n = segments ?? 5;
73
+ return { points: [...quarter(t, t, 0, n, false), ...quarter(t, t, depth - t, n, true)], smooth: true };
74
+ }
75
+
76
+ /**
77
+ * The bevel's width for a look: a share of the most a logo takes, which is under half its thinnest stroke and, but
78
+ * for the pillow, under half the depth.
79
+ *
80
+ * @param {string} kind The profile.
81
+ * @param {{ depth: number, bevel: number, halfStroke: number }} o The depth, the bevel as a share (0–1) of its most,
82
+ * and half the logo's thinnest stroke.
83
+ * @returns {number} The width.
84
+ */
85
+ export function bevelWidth(kind, { depth, bevel, halfStroke }) {
86
+ if (kind === 'flat') return 0;
87
+ const cap = 0.85 * halfStroke;
88
+ const most = kind === 'pillow' ? cap : Math.min(cap, depth * 0.45);
89
+ return Math.max(0, Math.min(1, bevel)) * most;
90
+ }
91
+
92
+ /**
93
+ * Each profile point's normal in the (out, up) plane: per segment for a faceted profile, averaged at the joins for a
94
+ * smooth one.
95
+ *
96
+ * @param {Profile} p The profile.
97
+ * @returns {{ rows: { k: number, n: [number, number] }[], bands: [number, number][] }} The rows (a profile point
98
+ * and the normal it is shaded with; a faceted join is two rows) and, per segment, its first and last row.
99
+ */
100
+ export function profileRows(p) {
101
+ const pts = p.points;
102
+ /** @type {[number, number][]} */
103
+ const seg = [];
104
+ for (let k = 0; k < pts.length - 1; k++) {
105
+ const di = pts[k + 1].inset - pts[k].inset, dz = pts[k + 1].z - pts[k].z;
106
+ const l = Math.hypot(di, dz) || 1;
107
+ seg.push([dz / l, di / l]);
108
+ }
109
+ /** @type {{ k: number, n: [number, number] }[]} */
110
+ const rows = [];
111
+ /** @type {[number, number][]} */
112
+ const bands = [];
113
+ if (p.smooth) {
114
+ pts.forEach((_, k) => {
115
+ const a = seg[Math.max(0, k - 1)], b = seg[Math.min(seg.length - 1, k)];
116
+ const x = a[0] + b[0], y = a[1] + b[1], l = Math.hypot(x, y) || 1;
117
+ rows.push({ k, n: [x / l, y / l] });
118
+ });
119
+ seg.forEach((_, k) => bands.push([k, k + 1]));
120
+ } else
121
+ seg.forEach((n, k) => {
122
+ rows.push({ k, n }, { k: k + 1, n });
123
+ bands.push([2 * k, 2 * k + 1]);
124
+ });
125
+ return { rows, bands };
126
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @module @logomarks/logo3d/geometry/sides
3
+ *
4
+ * The side of one ring: a band of quads for each segment of the profile, between the ring's inset levels. Normals
5
+ * follow the outline (smooth along curves, split at corners sharper than 35°) and the profile (smooth along a round
6
+ * edge, split at a chamfer's folds). U runs along the outline (its length, in logo units), V up the profile.
7
+ */
8
+
9
+ import { edgeNormals } from './inset.js';
10
+
11
+ /** Corners turning more than this are hard: the side is split there. */
12
+ const HARD = Math.cos((35 * Math.PI) / 180);
13
+
14
+ /**
15
+ * @typedef {object} Column
16
+ * @property {number} i The ring point.
17
+ * @property {[number, number]} n Its outward normal in the plane.
18
+ * @property {number} u Its distance along the outline.
19
+ */
20
+
21
+ /**
22
+ * The side's columns: one per point along curves, two at hard corners (each edge's own normal), the first point
23
+ * repeated at the end so the texture has a seam rather than a jump.
24
+ *
25
+ * @param {readonly (readonly number[])[]} r The ring (open, the solid on its left).
26
+ * @returns {{ cols: Column[], edges: [number, number][] }} The columns, and each edge's first and last column.
27
+ */
28
+ export function columns(r) {
29
+ const n = r.length;
30
+ const N = edgeNormals(r).map(([x, y]) => /** @type {[number, number]} */ ([-x, -y]));
31
+ /** @type {Column[]} */
32
+ const cols = [];
33
+ /** @type {number[]} */
34
+ const ins = [], outs = [];
35
+ let u = 0;
36
+ for (let i = 0; i <= n; i++) {
37
+ const p = i % n;
38
+ if (i) u += Math.hypot(r[p][0] - r[i - 1][0], r[p][1] - r[i - 1][1]);
39
+ const a = N[(p + n - 1) % n], b = N[p];
40
+ if (a[0] * b[0] + a[1] * b[1] < HARD) {
41
+ if (i > 0) ins.push(cols.push({ i: p, n: a, u }) - 1);
42
+ if (i < n) outs.push(cols.push({ i: p, n: b, u }) - 1);
43
+ } else {
44
+ const x = a[0] + b[0], y = a[1] + b[1], l = Math.hypot(x, y) || 1;
45
+ const c = cols.push({ i: p, n: [x / l, y / l], u }) - 1;
46
+ if (i > 0) ins.push(c);
47
+ if (i < n) outs.push(c);
48
+ }
49
+ }
50
+ /** @type {[number, number][]} */
51
+ const edges = [];
52
+ for (let e = 0; e < n; e++) edges.push([outs[e], ins[e]]);
53
+ return { cols, edges };
54
+ }
55
+
56
+ /**
57
+ * Adds a ring's side.
58
+ *
59
+ * @param {import('./buffer.js').MeshBuffer} mesh The mesh.
60
+ * @param {readonly (readonly number[])[]} ring The ring.
61
+ * @param {[number, number][][]} levels The ring inset to each profile point (same points, in order).
62
+ * @param {import('./profiles.js').Profile} profile The profile.
63
+ * @param {{ rows: { k: number, n: [number, number] }[], bands: [number, number][] }} shading The profile's rows.
64
+ * @param {number} z0 The back face's height.
65
+ */
66
+ export function addSide(mesh, ring, levels, profile, shading, z0) {
67
+ const { cols, edges } = columns(ring);
68
+ const pts = profile.points;
69
+ /** @type {number[]} */
70
+ const vAt = [0];
71
+ for (let k = 1; k < pts.length; k++) vAt.push(vAt[k - 1] + Math.hypot(pts[k].inset - pts[k - 1].inset, pts[k].z - pts[k - 1].z));
72
+ const base = mesh.position.length / 3;
73
+ for (const row of shading.rows) {
74
+ const level = levels[row.k], z = z0 + pts[row.k].z;
75
+ for (const c of cols) {
76
+ const p = level[c.i];
77
+ mesh.vertex(p[0], p[1], z, c.n[0] * row.n[0], c.n[1] * row.n[0], row.n[1], c.u, vAt[row.k]);
78
+ }
79
+ }
80
+ const at = (/** @type {number} */ c, /** @type {number} */ r) => base + r * cols.length + c;
81
+ for (const [c0, c1] of edges)
82
+ for (const [r0, r1] of shading.bands) {
83
+ mesh.triangle(at(c0, r0), at(c1, r0), at(c1, r1));
84
+ mesh.triangle(at(c0, r0), at(c1, r1), at(c0, r1));
85
+ }
86
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export * from './shape/index.js';
2
+ export * from './geometry/index.js';
3
+ export { MOTIONS, PERIODS, REST, poseAt } from './motion/index.js';
4
+ export type { Motion, Pose } from './motion/index.js';
5
+ export { MATERIALS, DEFAULT_MATERIAL, FLAT_SIDES, materialById, shade, baseColor } from './materials/presets.js';
6
+ export type { MaterialId, MaterialPreset } from './materials/presets.js';
7
+ export { ENVIRONMENTS, DEFAULT_ENVIRONMENT, environmentById } from './scene/environments.js';
8
+ export type { Environment, EnvironmentId } from './scene/environments.js';
9
+ export { SOFTBOXES } from './scene/studio.js';
10
+ export type { Softbox } from './scene/studio.js';
11
+ export { BACKGROUNDS, sweepProfile } from './scene/backdrop.js';
12
+ export type { Background } from './scene/backdrop.js';
13
+ export { VIEWS, DEFAULT_VIEW, fitDistance, viewPosition } from './scene/camera.js';
14
+ export type { ViewName } from './scene/camera.js';
15
+ export { createBuilder } from './worker/client.js';
16
+ export type { BuildRequest, BuildResult, Builder, WorkerLike } from './worker/client.js';
17
+ export { makeBuild } from './worker/build.js';
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module @logomarks/logo3d
3
+ *
4
+ * 3D logos from SVG, the parts that need no WebGL: reading a logo's SVG into colour layers (`readLogo`), extruding it
5
+ * with an edge profile capped by its thinnest stroke (`buildGeometry`), the motions as pure functions of time
6
+ * (`poseAt`), the material, environment, background and view tables, and the worker client (`createBuilder`). The
7
+ * three.js view, materials and exports are `@logomarks/logo3d/three`; the worker is `@logomarks/logo3d/worker`.
8
+ */
9
+
10
+ export * from './shape/index.js';
11
+ export * from './geometry/index.js';
12
+ export { MOTIONS, PERIODS, REST, poseAt } from './motion/index.js';
13
+ export { MATERIALS, DEFAULT_MATERIAL, FLAT_SIDES, materialById, shade, baseColor } from './materials/presets.js';
14
+ export { ENVIRONMENTS, DEFAULT_ENVIRONMENT, environmentById } from './scene/environments.js';
15
+ export { SOFTBOXES } from './scene/studio.js';
16
+ export { BACKGROUNDS, sweepProfile } from './scene/backdrop.js';
17
+ export { VIEWS, DEFAULT_VIEW, fitDistance, viewPosition } from './scene/camera.js';
18
+ export { createBuilder } from './worker/client.js';
19
+ export { makeBuild } from './worker/build.js';
@@ -0,0 +1,11 @@
1
+ import type * as THREE from 'three';
2
+ import type { MaterialPreset } from './presets.js';
3
+
4
+ /** The flat colour's sides: unlit, darker by which way they turn. */
5
+ export function flatSides(three: typeof THREE, color: string): THREE.ShaderMaterial;
6
+ /** One preset as a material for a part. */
7
+ export function presetMaterial(three: typeof THREE, p: MaterialPreset, o: { color: string; depth: number; part: 'face' | 'side' | 'back' }): THREE.Material;
8
+ /** Every material a logo's mesh needs, in its groups' order (each layer's face, side, back). */
9
+ export function logoMaterials(three: typeof THREE, o: { face: string; side?: string; colors: string[]; depth: number }): THREE.Material[];
10
+ /** A material as glTF can carry it. */
11
+ export function exportable(three: typeof THREE, m: THREE.Material): THREE.Material;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @module @logomarks/logo3d/materials/factory
3
+ *
4
+ * three.js materials from the presets: a physical material per preset (metals, glass through transmission, brushed
5
+ * steel through anisotropy), and the flat colour as an unlit face with sides shaded darker by which way they turn.
6
+ * Materials come in the geometry's group order (each layer's face, side and back), so one mesh draws every part.
7
+ * For glTF, `exportable` swaps the flat sides' shader for an unlit colour (KHR_materials_unlit); the physical
8
+ * materials go out as they are (KHR_materials_clearcoat, _transmission, _volume, _ior, _anisotropy).
9
+ */
10
+
11
+ import { FLAT_SIDES, baseColor, materialById } from './presets.js';
12
+
13
+ /**
14
+ * The flat colour's sides: unlit, the colour darkened by which way the side turns from a light up and to the left.
15
+ *
16
+ * @param {typeof import('three')} THREE three.js.
17
+ * @param {string} color The colour.
18
+ * @returns {import('three').ShaderMaterial} The material.
19
+ */
20
+ export function flatSides(THREE, color) {
21
+ return new THREE.ShaderMaterial({
22
+ name: 'flat-sides',
23
+ uniforms: {
24
+ uColor: { value: new THREE.Color(color) },
25
+ uToward: { value: new THREE.Vector3(...FLAT_SIDES.toward).normalize() },
26
+ uDark: { value: FLAT_SIDES.dark },
27
+ uLight: { value: FLAT_SIDES.light },
28
+ },
29
+ vertexShader: `varying vec3 vWorldN;
30
+ void main() {
31
+ vWorldN = normalize(mat3(modelMatrix) * normal);
32
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
33
+ }`,
34
+ fragmentShader: `uniform vec3 uColor; uniform vec3 uToward; uniform float uDark; uniform float uLight; varying vec3 vWorldN;
35
+ void main() {
36
+ float l = dot(normalize(vWorldN), uToward) * 0.5 + 0.5;
37
+ gl_FragColor = vec4(uColor * mix(uDark, uLight, l), 1.0);
38
+ #include <colorspace_fragment>
39
+ }`,
40
+ });
41
+ }
42
+
43
+ /**
44
+ * One preset as a material.
45
+ *
46
+ * @param {typeof import('three')} THREE three.js.
47
+ * @param {import('./presets.js').MaterialPreset} p The preset.
48
+ * @param {{ color: string, depth: number, part: 'face' | 'side' | 'back' }} o The logo's colour, its depth (glass's
49
+ * thickness) and the part it's for.
50
+ * @returns {import('three').Material} The material.
51
+ */
52
+ export function presetMaterial(THREE, p, { color, depth, part }) {
53
+ if (p.kind === 'flat') {
54
+ if (part === 'side') return flatSides(THREE, color);
55
+ return new THREE.MeshBasicMaterial({ name: 'flat', color, toneMapped: false });
56
+ }
57
+ const m = new THREE.MeshPhysicalMaterial({
58
+ name: p.id,
59
+ color: baseColor(p, color),
60
+ metalness: p.metalness,
61
+ roughness: p.roughness,
62
+ clearcoat: p.clearcoat ?? 0,
63
+ clearcoatRoughness: p.clearcoatRoughness ?? 0,
64
+ envMapIntensity: p.envIntensity ?? 1,
65
+ });
66
+ if (p.anisotropy) m.anisotropy = p.anisotropy;
67
+ if (p.transmission) {
68
+ m.transmission = p.transmission;
69
+ m.ior = p.ior ?? 1.5;
70
+ m.thickness = depth;
71
+ m.specularIntensity = 1;
72
+ // a soft tint through the glass: half way from white to the colour, so no colour of light is stopped dead
73
+ const tinted = p.takes === 'tint' && color.toLowerCase() !== '#ffffff';
74
+ m.attenuationColor = new THREE.Color('#ffffff').lerp(new THREE.Color(color), tinted ? 0.5 : 0);
75
+ m.attenuationDistance = tinted ? depth * 2 : Infinity;
76
+ }
77
+ return m;
78
+ }
79
+
80
+ /**
81
+ * Every material a logo's mesh needs, in its groups' order: for each layer, the face, side and back.
82
+ *
83
+ * @param {typeof import('three')} THREE three.js.
84
+ * @param {{ face: string, side?: string, colors: string[], depth: number }} o The face's preset, the sides' (the
85
+ * face's when unset), each layer's colour, and the depth.
86
+ * @returns {import('three').Material[]} The materials.
87
+ */
88
+ export function logoMaterials(THREE, { face, side, colors, depth }) {
89
+ const f = materialById(face);
90
+ const s = side ? materialById(side) : f;
91
+ return colors.flatMap((color) => {
92
+ const front = presetMaterial(THREE, f, { color, depth, part: 'face' });
93
+ const sides = presetMaterial(THREE, s, { color, depth, part: 'side' });
94
+ return [front, sides, front];
95
+ });
96
+ }
97
+
98
+ /**
99
+ * A material as glTF can carry it: the flat sides' shader becomes an unlit colour (their middle shade); anything else
100
+ * is returned as it is.
101
+ *
102
+ * @param {typeof import('three')} THREE three.js.
103
+ * @param {import('three').Material} m The material.
104
+ * @returns {import('three').Material} One the exporter writes.
105
+ */
106
+ export function exportable(THREE, m) {
107
+ if (m instanceof THREE.ShaderMaterial) {
108
+ const c = /** @type {import('three').Color} */ (m.uniforms.uColor.value).clone().multiplyScalar((FLAT_SIDES.dark + FLAT_SIDES.light) / 2);
109
+ return new THREE.MeshBasicMaterial({ name: 'flat-sides', color: c });
110
+ }
111
+ return m;
112
+ }
@@ -0,0 +1,34 @@
1
+ /** A material preset's id. */
2
+ export type MaterialId = 'chrome' | 'black-chrome' | 'gold' | 'brushed-steel' | 'glass' | 'frosted-glass' | 'clay' | 'matte' | 'gloss' | 'flat';
3
+
4
+ /** A material, as data. */
5
+ export interface MaterialPreset {
6
+ id: MaterialId;
7
+ name: string;
8
+ /** A lit physical material, or the unlit flat colour. */
9
+ kind: 'physical' | 'flat';
10
+ /** What it takes from the logo's colour: its base colour, a tint through glass, or nothing. */
11
+ takes: 'color' | 'tint' | false;
12
+ color: string;
13
+ metalness: number;
14
+ roughness: number;
15
+ clearcoat?: number;
16
+ clearcoatRoughness?: number;
17
+ transmission?: number;
18
+ ior?: number;
19
+ anisotropy?: number;
20
+ envIntensity?: number;
21
+ }
22
+
23
+ /** The materials, in menu order. */
24
+ export const MATERIALS: readonly MaterialPreset[];
25
+ /** The material shown first. */
26
+ export const DEFAULT_MATERIAL: MaterialId;
27
+ /** How dark the flat colour's sides are. */
28
+ export const FLAT_SIDES: Readonly<{ dark: number; light: number; toward: readonly [number, number, number] }>;
29
+ /** A preset by id (the default for an unknown one). */
30
+ export function materialById(id: string | undefined): MaterialPreset;
31
+ /** A colour scaled towards black. */
32
+ export function shade(hex: string, k: number): string;
33
+ /** The colour a material is drawn in for a layer. */
34
+ export function baseColor(preset: MaterialPreset, color: string): string;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @module @logomarks/logo3d/materials/presets
3
+ *
4
+ * The materials, as data: minimal and modern finishes lit by what they reflect (a studio or a sky), never by
5
+ * highlight spots, and no novelty paints. Metals keep their own colour; clay, matte, gloss and the flat colour take
6
+ * the logo's; glass takes it as a tint through its depth. The house "flat colour" draws the face exactly the colour
7
+ * and the sides in darker shades of it, unlit.
8
+ */
9
+
10
+ /**
11
+ * @typedef {object} MaterialPreset
12
+ * @property {string} id
13
+ * @property {string} name Its name in a menu.
14
+ * @property {'physical' | 'flat'} kind A lit physical material, or the unlit flat colour.
15
+ * @property {'color' | 'tint' | false} takes What it takes from the logo's colour: its base colour, a tint through
16
+ * glass, or nothing (a metal's own colour).
17
+ * @property {string} color Its own colour (the base for metals; white for glass).
18
+ * @property {number} metalness
19
+ * @property {number} roughness
20
+ * @property {number} [clearcoat]
21
+ * @property {number} [clearcoatRoughness]
22
+ * @property {number} [transmission] Glass: how much light passes through.
23
+ * @property {number} [ior]
24
+ * @property {number} [anisotropy] Brushed metal: how far its highlights stretch along the grain.
25
+ * @property {number} [envIntensity] How strongly it reflects the environment (1).
26
+ */
27
+
28
+ /** @type {readonly MaterialPreset[]} */
29
+ export const MATERIALS = Object.freeze([
30
+ { id: 'chrome', name: 'Chrome', kind: 'physical', takes: false, color: '#f4f5f7', metalness: 1, roughness: 0.04 },
31
+ { id: 'black-chrome', name: 'Black chrome', kind: 'physical', takes: false, color: '#2b2c30', metalness: 1, roughness: 0.08 },
32
+ { id: 'gold', name: 'Gold', kind: 'physical', takes: false, color: '#f2c66d', metalness: 1, roughness: 0.12 },
33
+ { id: 'brushed-steel', name: 'Brushed steel', kind: 'physical', takes: false, color: '#cfd2d6', metalness: 1, roughness: 0.3, anisotropy: 0.85 },
34
+ { id: 'glass', name: 'Clear glass', kind: 'physical', takes: 'tint', color: '#ffffff', metalness: 0, roughness: 0.02, transmission: 1, ior: 1.5 },
35
+ { id: 'frosted-glass', name: 'Frosted glass', kind: 'physical', takes: 'tint', color: '#ffffff', metalness: 0, roughness: 0.42, transmission: 1, ior: 1.5 },
36
+ { id: 'clay', name: 'Clay', kind: 'physical', takes: 'color', color: '#ece7df', metalness: 0, roughness: 0.92, envIntensity: 1.1 },
37
+ { id: 'matte', name: 'Matte', kind: 'physical', takes: 'color', color: '#ff4f00', metalness: 0, roughness: 0.6 },
38
+ { id: 'gloss', name: 'Gloss', kind: 'physical', takes: 'color', color: '#ff4f00', metalness: 0, roughness: 0.22, clearcoat: 1, clearcoatRoughness: 0.03 },
39
+ { id: 'flat', name: 'Flat colour', kind: 'flat', takes: 'color', color: '#c8ff00', metalness: 0, roughness: 1 },
40
+ ]);
41
+
42
+ /** The material shown first. */
43
+ export const DEFAULT_MATERIAL = 'chrome';
44
+
45
+ /**
46
+ * A preset by id (the default for an unknown one).
47
+ *
48
+ * @param {string | undefined} id The id.
49
+ * @returns {MaterialPreset} The preset.
50
+ */
51
+ export const materialById = (id) => MATERIALS.find((m) => m.id === id) ?? /** @type {MaterialPreset} */ (MATERIALS.find((m) => m.id === DEFAULT_MATERIAL));
52
+
53
+ /**
54
+ * How dark the flat colour's sides are: a share of the colour, by how far a side turns from a light up and to the
55
+ * left (0.38 facing away, 0.78 facing it).
56
+ */
57
+ export const FLAT_SIDES = Object.freeze({ dark: 0.38, light: 0.78, toward: Object.freeze([-0.45, 0.75, 0.5]) });
58
+
59
+ /**
60
+ * A colour scaled towards black, as `#rrggbb` (in sRGB, as the flat colour draws it).
61
+ *
62
+ * @param {string} hex `#rrggbb`.
63
+ * @param {number} k The share kept, 0–1.
64
+ * @returns {string} The darker colour.
65
+ */
66
+ export function shade(hex, k) {
67
+ const n = parseInt(hex.replace('#', '').slice(0, 6).padEnd(6, '0'), 16);
68
+ const ch = (/** @type {number} */ v) => Math.round(Math.max(0, Math.min(255, v * k))).toString(16).padStart(2, '0');
69
+ return `#${ch((n >> 16) & 255)}${ch((n >> 8) & 255)}${ch(n & 255)}`;
70
+ }
71
+
72
+ /**
73
+ * The colour a material is drawn in for a layer.
74
+ *
75
+ * @param {MaterialPreset} preset The material.
76
+ * @param {string} color The logo's colour (or the layer's own).
77
+ * @returns {string} The base colour.
78
+ */
79
+ export const baseColor = (preset, color) => (preset.takes === 'color' ? color : preset.color);
@@ -0,0 +1,23 @@
1
+ /** A motion's name. */
2
+ export type Motion = 'none' | 'turntable' | 'swing' | 'float' | 'light-sweep';
3
+
4
+ /** Where the logo and the light are at one moment. */
5
+ export interface Pose {
6
+ /** Radians about the vertical axis. */
7
+ turn: number;
8
+ /** Radians about the horizontal axis (positive leans the top away). */
9
+ tilt: number;
10
+ /** Up, as a share of the logo's longest side. */
11
+ lift: number;
12
+ /** Radians the environment has turned about the vertical axis. */
13
+ light: number;
14
+ }
15
+
16
+ /** The motions, in menu order. */
17
+ export const MOTIONS: readonly Motion[];
18
+ /** Each motion's loop, seconds. */
19
+ export const PERIODS: Readonly<Record<Motion, number>>;
20
+ /** The still pose. */
21
+ export const REST: Readonly<Pose>;
22
+ /** The pose at `t` seconds into the motion's loop; periodic, so `poseAt(m, period)` equals `poseAt(m, 0)`. */
23
+ export function poseAt(motion: Motion | string, t: number, o?: { period?: number; amount?: number }): Pose;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @module @logomarks/logo3d/motion
3
+ *
4
+ * The motions, as pure functions of time: each gives the logo's turn (about the vertical axis), tilt (about the
5
+ * horizontal), lift, and the environment's turn, at `t` seconds into a loop of `period` seconds. Every one is
6
+ * periodic: the pose at `t = period` is the pose at `t = 0`, so a recorded loop joins without a jump.
7
+ */
8
+
9
+ const TAU = Math.PI * 2;
10
+
11
+ /** The motions, in menu order. */
12
+ export const MOTIONS = Object.freeze(['none', 'turntable', 'swing', 'float', 'light-sweep']);
13
+
14
+ /** Each motion's loop, seconds. */
15
+ export const PERIODS = Object.freeze({ none: 6, turntable: 8, swing: 6, float: 6, 'light-sweep': 6 });
16
+
17
+ /**
18
+ * @typedef {object} Pose
19
+ * @property {number} turn Radians about the vertical axis.
20
+ * @property {number} tilt Radians about the horizontal axis (positive leans the top away).
21
+ * @property {number} lift Up, as a share of the logo's longest side.
22
+ * @property {number} light Radians the environment has turned about the vertical axis.
23
+ */
24
+
25
+ /** The still pose. */
26
+ export const REST = Object.freeze({ turn: 0, tilt: 0, lift: 0, light: 0 });
27
+
28
+ /**
29
+ * The pose at `t` seconds.
30
+ *
31
+ * @param {string} motion One of `MOTIONS` (an unknown one is still).
32
+ * @param {number} t Seconds.
33
+ * @param {{ period?: number, amount?: number }} [o] `period`: the loop (the motion's own); `amount`: how far it
34
+ * moves, 0–1 (1).
35
+ * @returns {Pose} The pose.
36
+ */
37
+ export function poseAt(motion, t, o = {}) {
38
+ const period = o.period ?? PERIODS[/** @type {keyof typeof PERIODS} */ (motion)] ?? 6;
39
+ const amount = o.amount ?? 1;
40
+ const u = (((t / period) % 1) + 1) % 1;
41
+ const a = TAU * u;
42
+ switch (motion) {
43
+ case 'turntable':
44
+ // a whole turn, so the loop ends where it began
45
+ return { ...REST, turn: a };
46
+ case 'swing':
47
+ return { ...REST, turn: 0.6 * amount * Math.sin(a), tilt: 0.05 * amount * Math.sin(2 * a) };
48
+ case 'float':
49
+ return { turn: 0.18 * amount * Math.sin(a), tilt: 0.08 * amount * Math.cos(a), lift: 0.025 * amount * Math.sin(a), light: 0 };
50
+ case 'light-sweep':
51
+ // the studio turns once round the logo: reflections run across it
52
+ return { ...REST, light: a };
53
+ default:
54
+ return { ...REST };
55
+ }
56
+ }
@@ -0,0 +1,20 @@
1
+ import type * as THREE from 'three';
2
+
3
+ /** A background's name. */
4
+ export type Background = 'transparent' | 'solid' | 'sweep';
5
+ /** The backgrounds. */
6
+ export const BACKGROUNDS: readonly Background[];
7
+ /** A sweep's profile, front to top, as (z, y). */
8
+ export function sweepProfile(o?: { back?: number; cove?: number; front?: number; rise?: number }): [number, number][];
9
+ /** The sweep mesh, its floor at y = 0, unlit in its colour. */
10
+ export function sweepMesh(three: typeof THREE, color: string, width?: number): THREE.Mesh;
11
+ /** What is behind the logo. */
12
+ export interface Backdrop {
13
+ group: THREE.Object3D;
14
+ /** Whether the logo stands on a floor. */
15
+ standing: boolean;
16
+ apply(scene: THREE.Scene, renderer: THREE.WebGLRenderer): void;
17
+ dispose(): void;
18
+ }
19
+ /** A backdrop for a background, its colour, and the logo's bounding radius. */
20
+ export function makeBackdrop(three: typeof THREE, o: { kind: string; color: string; size: number }): Backdrop;