bookcover-3d-web 0.1.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 +159 -0
- package/README_mockup.webp +0 -0
- package/README_sample.webp +0 -0
- package/dist/geometry.d.ts +12 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +390 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +404 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +15 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +92 -0
- package/dist/math.js.map +1 -0
- package/dist/photo.d.ts +6 -0
- package/dist/photo.d.ts.map +1 -0
- package/dist/photo.js +72 -0
- package/dist/photo.js.map +1 -0
- package/dist/svg.d.ts +12 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/svg.js +40 -0
- package/dist/svg.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/webgl.d.ts +15 -0
- package/dist/webgl.d.ts.map +1 -0
- package/dist/webgl.js +119 -0
- package/dist/webgl.js.map +1 -0
- package/package.json +18 -0
- package/src/backgrounds/coffee_table.jpg +0 -0
- package/src/backgrounds/table_side.jpg +0 -0
- package/src/backgrounds/table_with_book.jpg +0 -0
- package/src/backgrounds/table_with_laptop.jpg +0 -0
- package/src/backgrounds/wood.jpg +0 -0
- package/src/backgrounds_originals/coffee_table.jpg +0 -0
- package/src/backgrounds_originals/table_side.jpg +0 -0
- package/src/backgrounds_originals/table_with_book.jpg +0 -0
- package/src/backgrounds_originals/table_with_laptop.jpg +0 -0
- package/src/backgrounds_originals/wood.jpg +0 -0
- package/src/geometry.ts +513 -0
- package/src/index.ts +507 -0
- package/src/math.ts +84 -0
- package/src/photo.ts +77 -0
- package/src/svg.ts +46 -0
- package/src/types.ts +49 -0
- package/src/webgl.ts +131 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/geometry.ts
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
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
|
+
}
|