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 +8 -5
- package/dist/photo.d.ts.map +1 -1
- package/dist/photo.js +3 -2
- package/dist/photo.js.map +1 -1
- package/package.json +4 -1
- package/README_mockup.webp +0 -0
- package/README_sample.webp +0 -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 +0 -513
- package/src/index.ts +0 -507
- package/src/math.ts +0 -84
- package/src/photo.ts +0 -77
- package/src/svg.ts +0 -46
- package/src/types.ts +0 -49
- package/src/webgl.ts +0 -131
- package/tsconfig.json +0 -19
- package/tsconfig.tsbuildinfo +0 -1
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
|

|
|
115
115
|
|
|
116
|
-
The library
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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}.
|
|
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
|
|
package/dist/photo.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
9
|
-
//
|
|
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,
|
|
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.
|
|
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"
|
package/README_mockup.webp
DELETED
|
Binary file
|
package/README_sample.webp
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/backgrounds/wood.jpg
DELETED
|
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
|
-
}
|