bookcover-3d-web 0.3.0 → 0.5.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/README.md CHANGED
@@ -113,10 +113,13 @@ background image, with a shadow derived from the light direction. It returns an
113
113
 
114
114
  ![Example photo](./README_mockup.webp)
115
115
 
116
- The library ships a set of built-in background photos via `BACKGROUNDS`. Each entry has
117
- an `id` (to identify which image to load) and pre-tuned camera/lighting options that
118
- make the book look natural in that scene. Pass the background entry directly as the
119
- `options` argument — it extends `PhotoCompositeOptions`.
116
+ The library describes a set of background photos via `BACKGROUNDS` (metadata only — the
117
+ JPGs themselves are not bundled in the npm package; they live in the repo's
118
+ [assets tree](https://github.com/gracious-tech/bookcover/tree/main/assets/3d/backgrounds)
119
+ for you to host alongside your other static assets). Each entry has an `id` (to identify
120
+ which image to load) and pre-tuned camera/lighting options that make the book look
121
+ natural in that scene. Pass the background entry directly as the `options` argument — it
122
+ extends `PhotoCompositeOptions`.
120
123
 
121
124
  ```ts
122
125
  import {Book3DRenderer, BACKGROUNDS} from 'bookcover-3d-web'
@@ -126,7 +129,7 @@ import type {Background} from 'bookcover-3d-web'
126
129
  const bg: Background = BACKGROUNDS.find(b => b.id === 'coffee_table')!
127
130
 
128
131
  // Load the image however you serve your assets
129
- const img = await fetch(`/assets/backgrounds/${bg.id}.webp`)
132
+ const img = await fetch(`/assets/3d/backgrounds/${bg.id}.jpg`)
130
133
  const blob = await img.blob()
131
134
  const bitmap = await createImageBitmap(blob)
132
135
 
@@ -1 +1 @@
1
- {"version":3,"file":"photo.d.ts","sourceRoot":"","sources":["../src/photo.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,YAAY,CAAA;AAI1C,eAAO,MAAM,aAAa,MAAM,CAAA;AAChC,eAAO,MAAM,eAAe,KAAK,CAAA;AAGjC,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAIpC,eAAO,MAAM,WAAW,EAAE,UAAU,EA6DnC,CAAA"}
1
+ {"version":3,"file":"photo.d.ts","sourceRoot":"","sources":["../src/photo.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,YAAY,CAAA;AAI1C,eAAO,MAAM,aAAa,MAAM,CAAA;AAChC,eAAO,MAAM,eAAe,KAAK,CAAA;AAGjC,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAKpC,eAAO,MAAM,WAAW,EAAE,UAAU,EA6DnC,CAAA"}
package/dist/photo.js CHANGED
@@ -5,8 +5,9 @@ export const PHOTO_AZIMUTH = -15;
5
5
  export const PHOTO_ELEVATION = 35;
6
6
  // Book render width as a fraction of the background image width
7
7
  export const PHOTO_BOOK_SCALE = 0.55;
8
- // Available background photos (images live in src/backgrounds/).
9
- // Each entry can override azimuth, elevation, and book_scale for a natural fit.
8
+ // Available background photos — metadata only; the JPGs themselves live in the repo's shared
9
+ // assets tree (assets/3d/backgrounds/<id>.jpg) and consumers fetch them from wherever they
10
+ // serve that tree. Each entry can override azimuth, elevation, and book_scale for a natural fit.
10
11
  export const BACKGROUNDS = [
11
12
  {
12
13
  id: 'table_with_book',
package/dist/photo.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"photo.js","sourceRoot":"","sources":["../src/photo.ts"],"names":[],"mappings":"AACA,6EAA6E;AAI7E,qFAAqF;AACrF,gEAAgE;AAChE,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAAE,CAAA;AAChC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAEjC,gEAAgE;AAChE,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAA;AAEpC,iEAAiE;AACjE,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAiB;IACrC;QACI,EAAE,EAAE,iBAAiB;QACrB,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,CAAC,EAAE;QACd,UAAU,EAAE,CAAC;QACb,QAAQ,EAAE,CAAC,GAAG;QACd,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,CAAC,EAAE;QACT,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,CAAC;KACb;IACD;QACI,EAAE,EAAE,MAAM;QACV,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,CAAC;QACX,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,CAAC;QACP,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,CAAC;KACb;IACD;QACI,EAAE,EAAE,cAAc;QAClB,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,CAAC,EAAE;QACd,UAAU,EAAE,GAAG;QACf,QAAQ,EAAE,CAAC;QACX,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,CAAC,EAAE;QACT,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,IAAI;KAChB;IACD;QACI,EAAE,EAAE,mBAAmB;QACvB,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,CAAC,EAAE;QACd,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,EAAE;QACR,QAAQ,EAAE,CAAC,EAAE;QACb,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,GAAG;KACf;IACD;QACI,EAAE,EAAE,YAAY;QAChB,OAAO,EAAE,CAAC,EAAE;QACZ,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,CAAC,KAAK;QAChB,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,CAAC;QACP,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,CAAC;KACb;CACJ,CAAA"}
1
+ {"version":3,"file":"photo.js","sourceRoot":"","sources":["../src/photo.ts"],"names":[],"mappings":"AACA,6EAA6E;AAI7E,qFAAqF;AACrF,gEAAgE;AAChE,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAAE,CAAA;AAChC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAEjC,gEAAgE;AAChE,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAA;AAEpC,6FAA6F;AAC7F,2FAA2F;AAC3F,iGAAiG;AACjG,MAAM,CAAC,MAAM,WAAW,GAAiB;IACrC;QACI,EAAE,EAAE,iBAAiB;QACrB,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,CAAC,EAAE;QACd,UAAU,EAAE,CAAC;QACb,QAAQ,EAAE,CAAC,GAAG;QACd,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,CAAC,EAAE;QACT,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,CAAC;KACb;IACD;QACI,EAAE,EAAE,MAAM;QACV,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,CAAC;QACX,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,CAAC;QACP,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,CAAC;KACb;IACD;QACI,EAAE,EAAE,cAAc;QAClB,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,CAAC,EAAE;QACd,UAAU,EAAE,GAAG;QACf,QAAQ,EAAE,CAAC;QACX,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,CAAC,EAAE;QACT,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,IAAI;KAChB;IACD;QACI,EAAE,EAAE,mBAAmB;QACvB,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,CAAC,EAAE;QACd,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,EAAE;QACR,QAAQ,EAAE,CAAC,EAAE;QACb,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,GAAG;KACf;IACD;QACI,EAAE,EAAE,YAAY;QAChB,OAAO,EAAE,CAAC,EAAE;QACZ,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,CAAC,KAAK;QAChB,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,CAAC;QACP,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,CAAC;KACb;CACJ,CAAA"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  {
3
3
  "name": "bookcover-3d-web",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "license": "MIT-0",
6
6
  "repository": {"type": "git", "url": "https://github.com/gracious-tech/bookcover.git", "directory": "3d"},
7
7
  "homepage": "https://github.com/gracious-tech/bookcover/tree/main/3d#readme",
@@ -9,6 +9,9 @@
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
11
11
  "types": "dist/index.d.ts",
12
+ "files": [
13
+ "dist/"
14
+ ],
12
15
  "scripts": {
13
16
  "build": "tsc",
14
17
  "dev": "tsc --watch"
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/geometry.ts DELETED
@@ -1,513 +0,0 @@
1
-
2
- // Book box geometry — builds the vertex/index data for a given cover size
3
-
4
- import type {CoverType} from './types.js'
5
-
6
- // Hardback board overhang (how much cover extends beyond pages on top/bottom/fore-edge)
7
- // and board thickness (depth each board consumes from total spine width), in normalised units (h=1.0)
8
- const BOARD_EXTEND = 0.02
9
- const BOARD_THICKNESS = 0.007
10
-
11
- // Cream colour for page-edge faces (fore-edge, top, bottom)
12
- const PAGE_COLOR:[number,number,number] = [0.94, 0.91, 0.86]
13
-
14
- // Hole colour: same warm cream as page edges but darkened
15
- const HOLE_COLOR:[number,number,number] = [0.38, 0.36, 0.33]
16
-
17
- // Hole spacing (centre-to-centre) and size in normalised units (h=1.0 = cover height)
18
- const HOLE_PITCH = 0.032 // tighter spacing, roughly twice as many holes
19
- const HOLE_W = 0.010 // rectangle half-width (along Z / spine depth axis)
20
- const HOLE_H = 0.008 // rectangle half-height (along Y / book height axis)
21
- const HOLE_CORNER = 0.003 // corner radius for rounded rect
22
- const HOLE_MARGIN = 0.025 // inset from spine edge to hole centre on cover faces
23
- const HOLE_EDGE_GAP = 0.018 // margin from top/bottom cover edge to first/last hole centre
24
- const HOLE_SEGS = 4 // segments per rounded corner (quarter-circle)
25
- const HOLE_EPSILON = 0.001 // z nudge to prevent z-fighting with underlying face
26
-
27
- /** A single face ready for GPU upload: interleaved [xyz, uv, normal] × 4 verts + 6 indices */
28
- export interface FaceData {
29
- vertices:number[] // 4 × 8 floats
30
- indices:number[] // 6 ints (two triangles, zero-based within this face)
31
- texture:WebGLTexture | null
32
- color:[number,number,number]
33
- }
34
-
35
- /** Build a filled rounded-rectangle on a cover face for coil/wire hole rendering.
36
- * The shape sits in the XY plane at a fixed z; nx/ny/nz is the face normal.
37
- * Corners are approximated with HOLE_SEGS arc segments each. */
38
- function make_hole_rect(
39
- cx:number, cy:number, cz:number,
40
- nx:number, ny:number, nz:number,
41
- ):FaceData {
42
- const verts:number[] = []
43
- const indices:number[] = []
44
-
45
- // Inner rect extents (full half-extents minus corner radius)
46
- const iw = HOLE_W - HOLE_CORNER
47
- const ih = HOLE_H - HOLE_CORNER
48
-
49
- // Corner centres (dx, dy) and starting angle for each of the 4 corners
50
- const corners:[number, number, number][] = [
51
- [ iw, ih, 0], // top-right, 0° → 90°
52
- [-iw, ih, Math.PI / 2], // top-left, 90° → 180°
53
- [-iw, -ih, Math.PI], // bottom-left, 180° → 270°
54
- [ iw, -ih, Math.PI * 1.5], // bottom-right,270° → 360°
55
- ]
56
-
57
- // Center vertex (index 0)
58
- verts.push(cx, cy, cz, 0.5, 0.5, nx, ny, nz)
59
-
60
- // Perimeter vertices — HOLE_SEGS+1 points per corner arc
61
- for (const [ox, oy, start_a] of corners) {
62
- for (let s = 0; s <= HOLE_SEGS; s++) {
63
- const a = start_a + (s / HOLE_SEGS) * (Math.PI / 2)
64
- verts.push(
65
- cx + ox + Math.cos(a) * HOLE_CORNER,
66
- cy + oy + Math.sin(a) * HOLE_CORNER,
67
- cz, 0.5, 0.5, nx, ny, nz,
68
- )
69
- }
70
- }
71
-
72
- // Fan triangles from center to each consecutive pair of perimeter verts
73
- const perimeter = corners.length * (HOLE_SEGS + 1)
74
- for (let i = 0; i < perimeter; i++)
75
- indices.push(0, 1 + i, 1 + (i + 1) % perimeter)
76
-
77
- return {vertices: verts, indices, texture: null, color: HOLE_COLOR}
78
- }
79
-
80
- /** Build painted-on binding holes on front and back cover faces for coil/wire bindings */
81
- function build_holes(hw:number, hh:number, hd:number):FaceData[] {
82
- const faces:FaceData[] = []
83
-
84
- // Fill from near the top edge to near the bottom edge
85
- const usable = hh * 2 - HOLE_EDGE_GAP * 2
86
- const hole_count = Math.max(1, Math.round(usable / HOLE_PITCH) + 1)
87
- const span = (hole_count - 1) * HOLE_PITCH
88
- const y0 = span / 2
89
-
90
- for (let i = 0; i < hole_count; i++) {
91
- const cy = y0 - i * HOLE_PITCH
92
- const cx = -hw + HOLE_MARGIN
93
-
94
- // Front cover (+z normal), nudged forward to avoid z-fighting
95
- faces.push(make_hole_rect(cx, cy, hd + HOLE_EPSILON, 0, 0, 1))
96
- // Back cover (-z normal), nudged backward
97
- faces.push(make_hole_rect(cx, cy, -(hd + HOLE_EPSILON), 0, 0, -1))
98
- }
99
-
100
- return faces
101
- }
102
-
103
- /** Build a quad face from 4 [x,y,z,u,v] vertices + a flat normal */
104
- function make_face(
105
- verts:[[number,number,number,number,number],[number,number,number,number,number],
106
- [number,number,number,number,number],[number,number,number,number,number]],
107
- nx:number, ny:number, nz:number,
108
- tex:WebGLTexture | null,
109
- color:[number,number,number] = [1,1,1],
110
- ):FaceData {
111
- const vertices:number[] = []
112
- for (const [x,y,z,u,v] of verts)
113
- vertices.push(x, y, z, u, v, nx, ny, nz)
114
- return {vertices, indices: [0,1,2, 0,2,3], texture: tex, color}
115
- }
116
-
117
- // Saddle-stitch curve: how far from the spine edge the covers bend, and subdivision count
118
- const STITCH_CURVE_W = 0.011 // ~2.5mm normalised (h=1.0 = cover height ~229mm)
119
- const STITCH_CURVE_SEGS = 5 // strip columns across the curved region
120
-
121
- /** Build the top or bottom edge face for saddle-stitch: flat across most of the width,
122
- * curving to a point only in the STITCH_CURVE_W region near the spine, matching the covers. */
123
- function make_stitch_edge(
124
- hw:number, y:number, vhd:number,
125
- ny:number, page_tex:WebGLTexture,
126
- ):FaceData {
127
- const verts:number[] = []
128
- const idx:number[] = []
129
- const cw = Math.min(STITCH_CURVE_W, hw)
130
- const flip = ny < 0
131
-
132
- // Columns from fore-edge to spine: [x, z_front, z_back]
133
- const cols:[number, number, number][] = [
134
- [hw, vhd, -vhd], // fore-edge
135
- [-hw + cw, vhd, -vhd], // start of curve
136
- ]
137
- // Curve region — mirrors make_stitch_cover: t=0 at spine (z=0), t=1 at curve end (z=±vhd)
138
- for (let i = STITCH_CURVE_SEGS - 1; i >= 0; i--) {
139
- const t = i / STITCH_CURVE_SEGS
140
- const z = vhd * (1 - Math.cos(t * Math.PI / 2))
141
- cols.push([-hw + t * cw, z, -z])
142
- }
143
-
144
- for (let c = 0; c < cols.length - 1; c++) {
145
- const [x0, zf0, zb0] = cols[c]
146
- const [x1, zf1, zb1] = cols[c + 1]
147
- const u0 = (x0 + hw) / (2 * hw)
148
- const u1 = (x1 + hw) / (2 * hw)
149
- const base = (verts.length / 8)
150
-
151
- // Top (ny>0): spine-front, fore-front, fore-back, spine-back
152
- // Bottom (ny<0): spine-back, fore-back, fore-front, spine-front
153
- if (!flip) {
154
- verts.push(x1, y, zf1, u1, 0, 0, ny, 0)
155
- verts.push(x0, y, zf0, u0, 0, 0, ny, 0)
156
- verts.push(x0, y, zb0, u0, 1, 0, ny, 0)
157
- verts.push(x1, y, zb1, u1, 1, 0, ny, 0)
158
- } else {
159
- verts.push(x1, y, zb1, u1, 1, 0, ny, 0)
160
- verts.push(x0, y, zb0, u0, 1, 0, ny, 0)
161
- verts.push(x0, y, zf0, u0, 0, 0, ny, 0)
162
- verts.push(x1, y, zf1, u1, 0, 0, ny, 0)
163
- }
164
- idx.push(base, base+1, base+2, base, base+2, base+3)
165
- }
166
-
167
- return {vertices: verts, indices: idx, texture: page_tex, color: [1, 1, 1]}
168
- }
169
-
170
- /** Build one curved cover panel for saddle-stitch binding.
171
- * sign = +1 for front (normal points +z), -1 for back (-z).
172
- * The spine edge (x = -hw) bends inward to z = 0 over STITCH_CURVE_W. */
173
- function make_stitch_cover(
174
- hw:number, hh:number, hd:number,
175
- sign:number, tex:WebGLTexture,
176
- ):FaceData {
177
- const verts:number[] = []
178
- const idx:number[] = []
179
- const cw = Math.min(STITCH_CURVE_W, hw)
180
-
181
- // Columns from spine edge to fore-edge, each with (x, z, u)
182
- const cols:{x:number, z:number, u:number}[] = []
183
-
184
- // Curved region: cosine ease from z=0 at spine to z=±hd at curve end
185
- for (let i = 0; i <= STITCH_CURVE_SEGS; i++) {
186
- const t = i / STITCH_CURVE_SEGS
187
- cols.push({
188
- x: -hw + t * cw,
189
- z: sign * hd * (1 - Math.cos(t * Math.PI / 2)),
190
- u: (-hw + t * cw + hw) / (2 * hw),
191
- })
192
- }
193
- // Flat region: single column at fore-edge
194
- cols.push({x: hw, z: sign * hd, u: 1.0})
195
-
196
- // Back face UVs are mirrored horizontally (u=0 at fore-edge, u=1 at spine)
197
- if (sign < 0) {
198
- for (const c of cols) c.u = 1 - c.u
199
- }
200
-
201
- // Vertex strip: top and bottom vert per column
202
- for (const {x, z, u} of cols) {
203
- verts.push(x, hh, z, u, 0, 0, 0, sign)
204
- verts.push(x, -hh, z, u, 1, 0, 0, sign)
205
- }
206
-
207
- // Quads between adjacent column pairs
208
- for (let c = 0; c < cols.length - 1; c++) {
209
- const tl = c * 2, bl = c * 2 + 1
210
- const tr = (c + 1) * 2, br = (c + 1) * 2 + 1
211
- idx.push(tl, tr, br, tl, br, bl)
212
- }
213
-
214
- return {vertices: verts, indices: idx, texture: tex, color: [1, 1, 1]}
215
- }
216
-
217
- /** Build faces for a saddle-stitch binding: covers curve inward to meet at the spine fold */
218
- function build_stitch(
219
- hw:number, hh:number, hd:number,
220
- front_tex:WebGLTexture, back_tex:WebGLTexture,
221
- _spine_tex:WebGLTexture | null, page_tex:WebGLTexture,
222
- ):FaceData[] {
223
- const vhd = hd
224
- return [
225
- make_stitch_cover(hw, hh, vhd, 1, front_tex),
226
- make_stitch_cover(hw, hh, vhd, -1, back_tex),
227
-
228
- // Spine fold edge — covers meet at z=0 here so this is a zero-width seam;
229
- // render a thin sliver of page color as a visual hint of the fold
230
- make_face([
231
- [-hw, hh, -0.001, 0, 0],
232
- [-hw, hh, 0.001, 1, 0],
233
- [-hw, -hh, 0.001, 1, 1],
234
- [-hw, -hh, -0.001, 0, 1],
235
- ], -1, 0, 0, null, PAGE_COLOR),
236
-
237
- // Fore-edge (x = +hw)
238
- make_face([
239
- [ hw, hh, vhd, 0, 0],
240
- [ hw, hh, -vhd, 0, 1],
241
- [ hw, -hh, -vhd, 1, 1],
242
- [ hw, -hh, vhd, 1, 0],
243
- ], 1, 0, 0, page_tex),
244
-
245
- // Top and bottom — curved strip matching the cover profile
246
- make_stitch_edge(hw, hh, vhd, 1, page_tex),
247
- make_stitch_edge(hw, -hh, vhd, -1, page_tex),
248
- ]
249
- }
250
-
251
- /** Build faces for a paperback (simple box, no overhang or board thickness) */
252
- function build_paperback(
253
- hw:number, hh:number, hd:number,
254
- front_tex:WebGLTexture, back_tex:WebGLTexture,
255
- spine_tex:WebGLTexture | null, page_tex:WebGLTexture,
256
- ):FaceData[] {
257
- return [
258
- // Front (z = +hd)
259
- make_face([
260
- [-hw, hh, hd, 0, 0],
261
- [ hw, hh, hd, 1, 0],
262
- [ hw, -hh, hd, 1, 1],
263
- [-hw, -hh, hd, 0, 1],
264
- ], 0, 0, 1, front_tex),
265
-
266
- // Back (z = -hd)
267
- make_face([
268
- [ hw, hh,-hd, 0, 0],
269
- [-hw, hh,-hd, 1, 0],
270
- [-hw, -hh,-hd, 1, 1],
271
- [ hw, -hh,-hd, 0, 1],
272
- ], 0, 0,-1, back_tex),
273
-
274
- // Spine (x = -hw)
275
- make_face([
276
- [-hw, hh,-hd, 0, 0],
277
- [-hw, hh, hd, 1, 0],
278
- [-hw, -hh, hd, 1, 1],
279
- [-hw, -hh,-hd, 0, 1],
280
- ],-1, 0, 0, spine_tex, PAGE_COLOR),
281
-
282
- // Fore-edge (x = +hw)
283
- make_face([
284
- [ hw, hh, hd, 0, 0],
285
- [ hw, hh,-hd, 0, 1],
286
- [ hw, -hh,-hd, 1, 1],
287
- [ hw, -hh, hd, 1, 0],
288
- ], 1, 0, 0, page_tex),
289
-
290
- // Top (y = +hh)
291
- make_face([
292
- [-hw, hh, hd, 0, 0],
293
- [ hw, hh, hd, 1, 0],
294
- [ hw, hh,-hd, 1, 1],
295
- [-hw, hh,-hd, 0, 1],
296
- ], 0, 1, 0, page_tex),
297
-
298
- // Bottom (y = -hh)
299
- make_face([
300
- [-hw, -hh,-hd, 0, 0],
301
- [ hw, -hh,-hd, 1, 0],
302
- [ hw, -hh, hd, 1, 1],
303
- [-hw, -hh, hd, 0, 1],
304
- ], 0,-1, 0, page_tex),
305
- ]
306
- }
307
-
308
- /** Build faces for a hardcover book with accurate interior trim vs cover trim.
309
- * Cover panels use full SVG dimensions; page block is inset by board overhang/thickness.
310
- * Front and back boards are separate (not connected at the fore-edge).
311
- * Board edge colour is sampled from the cover textures (material wraps around). */
312
- function build_hardcover(
313
- hw:number, hh:number, hd:number,
314
- front_tex:WebGLTexture, back_tex:WebGLTexture,
315
- spine_tex:WebGLTexture | null, page_tex:WebGLTexture,
316
- ):FaceData[] {
317
-
318
- // Page block extents — all four boards consume BOARD_THICKNESS, interior shifts/moves intact
319
- const py = hh - BOARD_EXTEND // page half-height (top/bottom inset)
320
- const pz = hd - BOARD_THICKNESS // page half-depth (front/back boards consume thickness)
321
- const sx = -hw + BOARD_THICKNESS // page block left edge (spine board pushes it right)
322
- const fx = hw - BOARD_EXTEND // page block right edge (fore-edge inset)
323
-
324
- // UV sample points for board edges — sample near corners of the cover textures
325
- // where solid background colour is most likely.
326
- // Front cover: u=0 is spine, u=1 is fore-edge, v=0 is top, v=1 is bottom
327
- const f_uv:[number,number] = [0.98, 0.98]
328
- // Back cover: u=0 is fore-edge, u=1 is spine
329
- const b_uv:[number,number] = [0.02, 0.98]
330
- // Spine board surfaces — sample spine texture center, fall back to front cover spine edge
331
- const cloth_tex = spine_tex ?? front_tex
332
- const s_uv:[number,number] = spine_tex ? [0.5, 0.5] : [0.02, 0.5]
333
-
334
- const faces:FaceData[] = []
335
-
336
- // -- Cover panels (full size from SVG) --
337
-
338
- // Front cover (z = +hd)
339
- faces.push(make_face([
340
- [-hw, hh, hd, 0, 0],
341
- [ hw, hh, hd, 1, 0],
342
- [ hw, -hh, hd, 1, 1],
343
- [-hw, -hh, hd, 0, 1],
344
- ], 0, 0, 1, front_tex))
345
-
346
- // Back cover (z = -hd)
347
- faces.push(make_face([
348
- [ hw, hh,-hd, 0, 0],
349
- [-hw, hh,-hd, 1, 0],
350
- [-hw, -hh,-hd, 1, 1],
351
- [ hw, -hh,-hd, 0, 1],
352
- ], 0, 0,-1, back_tex))
353
-
354
- // Spine (x = -hw) — full cover height and depth
355
- faces.push(make_face([
356
- [-hw, hh,-hd, 0, 0],
357
- [-hw, hh, hd, 1, 0],
358
- [-hw, -hh, hd, 1, 1],
359
- [-hw, -hh,-hd, 0, 1],
360
- ],-1, 0, 0, spine_tex, PAGE_COLOR))
361
-
362
- // -- Page block (inset from cover) --
363
-
364
- // Page fore-edge (x = +fx) — page block right edge
365
- faces.push(make_face([
366
- [ fx, py, pz, 0, 0],
367
- [ fx, py,-pz, 0, 1],
368
- [ fx, -py,-pz, 1, 1],
369
- [ fx, -py, pz, 1, 0],
370
- ], 1, 0, 0, page_tex))
371
-
372
- // Page top (y = +py) — spine board inner edge to page fore-edge
373
- faces.push(make_face([
374
- [ sx, py, pz, 0, 0],
375
- [ fx, py, pz, 1, 0],
376
- [ fx, py,-pz, 1, 1],
377
- [ sx, py,-pz, 0, 1],
378
- ], 0, 1, 0, page_tex))
379
-
380
- // Page bottom (y = -py) — spine board inner edge to page fore-edge
381
- faces.push(make_face([
382
- [ sx, -py,-pz, 0, 0],
383
- [ fx, -py,-pz, 1, 0],
384
- [ fx, -py, pz, 1, 1],
385
- [ sx, -py, pz, 0, 1],
386
- ], 0,-1, 0, page_tex))
387
-
388
- // -- Fore-edge board strips (at x = +hw, facing +x) --
389
- // Front and back boards are independent — gap between them is open (no connection)
390
-
391
- // Front board edge (full height, z from +pz to +hd)
392
- faces.push(make_face([
393
- [ hw, hh, hd, f_uv[0], f_uv[1]],
394
- [ hw, hh, pz, f_uv[0], f_uv[1]],
395
- [ hw, -hh, pz, f_uv[0], f_uv[1]],
396
- [ hw, -hh, hd, f_uv[0], f_uv[1]],
397
- ], 1, 0, 0, front_tex))
398
-
399
- // Back board edge (full height, z from -hd to -pz)
400
- faces.push(make_face([
401
- [ hw, hh,-pz, b_uv[0], b_uv[1]],
402
- [ hw, hh,-hd, b_uv[0], b_uv[1]],
403
- [ hw, -hh,-hd, b_uv[0], b_uv[1]],
404
- [ hw, -hh,-pz, b_uv[0], b_uv[1]],
405
- ], 1, 0, 0, back_tex))
406
-
407
- // -- Top board strips (at y = +hh, facing +y) --
408
-
409
- // Front board top (full width, z from +pz to +hd)
410
- faces.push(make_face([
411
- [-hw, hh, hd, f_uv[0], f_uv[1]],
412
- [ hw, hh, hd, f_uv[0], f_uv[1]],
413
- [ hw, hh, pz, f_uv[0], f_uv[1]],
414
- [-hw, hh, pz, f_uv[0], f_uv[1]],
415
- ], 0, 1, 0, front_tex))
416
-
417
- // Back board top (full width, z from -hd to -pz)
418
- faces.push(make_face([
419
- [-hw, hh,-pz, b_uv[0], b_uv[1]],
420
- [ hw, hh,-pz, b_uv[0], b_uv[1]],
421
- [ hw, hh,-hd, b_uv[0], b_uv[1]],
422
- [-hw, hh,-hd, b_uv[0], b_uv[1]],
423
- ], 0, 1, 0, back_tex))
424
-
425
-
426
- // -- Bottom board strips (at y = -hh, facing -y) --
427
-
428
- // Front board bottom (full width, z from +pz to +hd)
429
- faces.push(make_face([
430
- [-hw, -hh, pz, f_uv[0], f_uv[1]],
431
- [ hw, -hh, pz, f_uv[0], f_uv[1]],
432
- [ hw, -hh, hd, f_uv[0], f_uv[1]],
433
- [-hw, -hh, hd, f_uv[0], f_uv[1]],
434
- ], 0,-1, 0, front_tex))
435
-
436
- // Back board bottom (full width, z from -hd to -pz)
437
- faces.push(make_face([
438
- [-hw, -hh,-hd, b_uv[0], b_uv[1]],
439
- [ hw, -hh,-hd, b_uv[0], b_uv[1]],
440
- [ hw, -hh,-pz, b_uv[0], b_uv[1]],
441
- [-hw, -hh,-pz, b_uv[0], b_uv[1]],
442
- ], 0,-1, 0, back_tex))
443
-
444
- // -- Spine board surfaces --
445
- // The spine board has thickness BOARD_THICKNESS (sx - (-hw)).
446
- // Its outer face is the spine panel already rendered above.
447
- // These faces close the top, bottom, and inner ledges of the spine board.
448
-
449
- // Spine board top (y = +hh, x from -hw to sx, z from -pz to +pz)
450
- faces.push(make_face([
451
- [-hw, hh, pz, s_uv[0], s_uv[1]],
452
- [ sx, hh, pz, s_uv[0], s_uv[1]],
453
- [ sx, hh,-pz, s_uv[0], s_uv[1]],
454
- [-hw, hh,-pz, s_uv[0], s_uv[1]],
455
- ], 0, 1, 0, cloth_tex))
456
-
457
- // Spine board bottom (y = -hh, x from -hw to sx, z from -pz to +pz)
458
- faces.push(make_face([
459
- [-hw, -hh,-pz, s_uv[0], s_uv[1]],
460
- [ sx, -hh,-pz, s_uv[0], s_uv[1]],
461
- [ sx, -hh, pz, s_uv[0], s_uv[1]],
462
- [-hw, -hh, pz, s_uv[0], s_uv[1]],
463
- ], 0,-1, 0, cloth_tex))
464
-
465
- // Spine board inner ledge top (y = +py, x from -hw to sx) — closes gap at page level
466
- faces.push(make_face([
467
- [-hw, py, pz, s_uv[0], s_uv[1]],
468
- [ sx, py, pz, s_uv[0], s_uv[1]],
469
- [ sx, py,-pz, s_uv[0], s_uv[1]],
470
- [-hw, py,-pz, s_uv[0], s_uv[1]],
471
- ], 0, 1, 0, cloth_tex))
472
-
473
- // Spine board inner ledge bottom (y = -py, x from -hw to sx)
474
- faces.push(make_face([
475
- [-hw, -py,-pz, s_uv[0], s_uv[1]],
476
- [ sx, -py,-pz, s_uv[0], s_uv[1]],
477
- [ sx, -py, pz, s_uv[0], s_uv[1]],
478
- [-hw, -py, pz, s_uv[0], s_uv[1]],
479
- ], 0,-1, 0, cloth_tex))
480
-
481
- return faces
482
- }
483
-
484
- /** Build all faces of the book using normalised dimensions (h = 1).
485
- * w = cover_width / cover_height, d = spine_width / cover_height. */
486
- export function build_faces(
487
- w:number,
488
- h:number,
489
- d:number,
490
- front_tex:WebGLTexture,
491
- back_tex:WebGLTexture,
492
- spine_tex:WebGLTexture | null,
493
- cover_type:CoverType,
494
- page_tex:WebGLTexture,
495
- ):FaceData[] {
496
-
497
- // Half-extents
498
- const hw = w / 2, hh = h / 2, hd = d / 2
499
-
500
- if (cover_type === 'hardcover' || cover_type === 'hardcover_jacket')
501
- return build_hardcover(hw, hh, hd, front_tex, back_tex, spine_tex, page_tex)
502
-
503
- if (cover_type === 'paperback_stitch')
504
- return build_stitch(hw, hh, hd, front_tex, back_tex, spine_tex, page_tex)
505
-
506
- const faces = build_paperback(hw, hh, hd, front_tex, back_tex, spine_tex, page_tex)
507
-
508
- // Coil and wire bindings get painted-on holes near the spine edge
509
- if (cover_type === 'paperback_coil' || cover_type === 'paperback_wire')
510
- faces.push(...build_holes(hw, hh, hd))
511
-
512
- return faces
513
- }