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.
Files changed (51) hide show
  1. package/README.md +159 -0
  2. package/README_mockup.webp +0 -0
  3. package/README_sample.webp +0 -0
  4. package/dist/geometry.d.ts +12 -0
  5. package/dist/geometry.d.ts.map +1 -0
  6. package/dist/geometry.js +390 -0
  7. package/dist/geometry.js.map +1 -0
  8. package/dist/index.d.ts +44 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +404 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/math.d.ts +15 -0
  13. package/dist/math.d.ts.map +1 -0
  14. package/dist/math.js +92 -0
  15. package/dist/math.js.map +1 -0
  16. package/dist/photo.d.ts +6 -0
  17. package/dist/photo.d.ts.map +1 -0
  18. package/dist/photo.js +72 -0
  19. package/dist/photo.js.map +1 -0
  20. package/dist/svg.d.ts +12 -0
  21. package/dist/svg.d.ts.map +1 -0
  22. package/dist/svg.js +40 -0
  23. package/dist/svg.js.map +1 -0
  24. package/dist/types.d.ts +33 -0
  25. package/dist/types.d.ts.map +1 -0
  26. package/dist/types.js +3 -0
  27. package/dist/types.js.map +1 -0
  28. package/dist/webgl.d.ts +15 -0
  29. package/dist/webgl.d.ts.map +1 -0
  30. package/dist/webgl.js +119 -0
  31. package/dist/webgl.js.map +1 -0
  32. package/package.json +18 -0
  33. package/src/backgrounds/coffee_table.jpg +0 -0
  34. package/src/backgrounds/table_side.jpg +0 -0
  35. package/src/backgrounds/table_with_book.jpg +0 -0
  36. package/src/backgrounds/table_with_laptop.jpg +0 -0
  37. package/src/backgrounds/wood.jpg +0 -0
  38. package/src/backgrounds_originals/coffee_table.jpg +0 -0
  39. package/src/backgrounds_originals/table_side.jpg +0 -0
  40. package/src/backgrounds_originals/table_with_book.jpg +0 -0
  41. package/src/backgrounds_originals/table_with_laptop.jpg +0 -0
  42. package/src/backgrounds_originals/wood.jpg +0 -0
  43. package/src/geometry.ts +513 -0
  44. package/src/index.ts +507 -0
  45. package/src/math.ts +84 -0
  46. package/src/photo.ts +77 -0
  47. package/src/svg.ts +46 -0
  48. package/src/types.ts +49 -0
  49. package/src/webgl.ts +131 -0
  50. package/tsconfig.json +19 -0
  51. package/tsconfig.tsbuildinfo +1 -0
package/src/svg.ts ADDED
@@ -0,0 +1,46 @@
1
+
2
+ // SVG parsing and rasterisation utilities
3
+
4
+ /** Parse dimensions from the root SVG element's width/height attributes.
5
+ * Values are in pt (Typst's SVG renderer uses pt as its coordinate system).
6
+ * Accepts both "Xpt" (node backend) and bare "X" (web backend) forms. */
7
+ export function parse_svg_size(svg:string):{width:number, height:number} {
8
+ const w = svg.match(/width="([\d.]+)(?:pt)?"/)
9
+ const h = svg.match(/height="([\d.]+)(?:pt)?"/)
10
+ if (!w || !h)
11
+ throw new Error('[cover-3d] Could not parse SVG dimensions')
12
+ return {width: parseFloat(w[1]), height: parseFloat(h[1])}
13
+ }
14
+
15
+ /** Render an SVG string to an ImageBitmap via an HTMLImageElement.
16
+ * createImageBitmap(svgBlob) is unsupported in many browsers; the img→canvas
17
+ * route works universally. Must be called on the main thread. */
18
+ export async function svg_to_bitmap(svg:string, w_pt:number, h_pt:number):Promise<ImageBitmap> {
19
+ const w = Math.max(1, Math.round(w_pt))
20
+ const h = Math.max(1, Math.round(h_pt))
21
+
22
+ // Strip out foreignObject elements (not rendered by image conversion)
23
+ let cleaned_svg = svg.replace(/<foreignObject[^>]*>[\s\S]*?<\/foreignObject>/g, '')
24
+
25
+ // Set explicit pixel dimensions so very narrow SVGs (e.g. thin spines) render at a usable size
26
+ cleaned_svg = cleaned_svg
27
+ .replace(/(<svg[^>]*?)width="[^"]*"/, `$1width="${w}"`)
28
+ .replace(/(<svg[^>]*?)height="[^"]*"/, `$1height="${h}"`)
29
+
30
+ const blob = new Blob([cleaned_svg], {type: 'image/svg+xml'})
31
+ const url = URL.createObjectURL(blob)
32
+ try {
33
+ // Load SVG as an image element — handles CSS vars, units, etc.
34
+ const img = new Image()
35
+ await new Promise<void>((resolve, reject) => {
36
+ img.onload = () => resolve()
37
+ img.onerror = () => reject(new Error('[cover-3d] SVG failed to load as image'))
38
+ img.src = url
39
+ })
40
+
41
+ return createImageBitmap(img)
42
+ }
43
+ finally {
44
+ URL.revokeObjectURL(url)
45
+ }
46
+ }
package/src/types.ts ADDED
@@ -0,0 +1,49 @@
1
+
2
+ // Exported public types for the cover-3d module
3
+
4
+ /** SVG strings for each face of the book cover */
5
+ export interface BookFaces {
6
+ front:string
7
+ back:string
8
+ spine?:string
9
+ }
10
+
11
+ export type CoverType = 'paperback' | 'paperback_coil' | 'paperback_wire' | 'paperback_stitch' | 'hardcover' | 'hardcover_jacket'
12
+
13
+ export interface GenerateOptions {
14
+ cover_type?:CoverType
15
+ // Horizontal camera angle in degrees (0 = straight-on, negative = see spine)
16
+ azimuth?:number
17
+ // Vertical camera angle in degrees (positive = looking down)
18
+ elevation?:number
19
+ // Clockwise roll in degrees around the view axis (positive = clockwise)
20
+ roll?:number
21
+ width?:number
22
+ height?:number
23
+ }
24
+
25
+ /** Options for compositing the book render onto a background photo */
26
+ export interface PhotoCompositeOptions {
27
+ // Camera angles — defaults give a flat-on-table perspective
28
+ azimuth?:number
29
+ elevation?:number
30
+ zoom?:number
31
+ // Clockwise roll in degrees around the view axis (positive = clockwise)
32
+ roll?:number
33
+ // Book width as a fraction of background image width
34
+ book_scale?:number
35
+ // Position offset as a fraction of background dimensions (0 = centred)
36
+ offset_x?:number
37
+ offset_y?:number
38
+ // Light source direction: where the light comes FROM, in degrees.
39
+ // light_az: horizontal (0=front, 90=right, -90=left); light_el: vertical (positive=above)
40
+ light_az?:number
41
+ light_el?:number
42
+ // Ambient light level 0–1 (higher = brighter, less contrast between faces)
43
+ ambient?:number
44
+ }
45
+
46
+ /** A named background photo — extends PhotoCompositeOptions so it can be passed directly */
47
+ export interface Background extends PhotoCompositeOptions {
48
+ id:string
49
+ }
package/src/webgl.ts ADDED
@@ -0,0 +1,131 @@
1
+
2
+ // WebGL shader sources and low-level helpers
3
+
4
+ const AMBIENT = 0.7
5
+
6
+ // Default light direction as azimuth/elevation degrees (see light_dir_vec3 in index.ts)
7
+ export const DEFAULT_LIGHT_AZ = 15
8
+ export const DEFAULT_LIGHT_EL = 25
9
+ export const DEFAULT_AMBIENT = AMBIENT
10
+
11
+ export const VERT_SRC = `
12
+ attribute vec3 a_pos;
13
+ attribute vec2 a_uv;
14
+ attribute vec3 a_normal;
15
+ uniform mat4 u_mvp;
16
+ uniform mat4 u_mv;
17
+ uniform mat3 u_norm_mat;
18
+ uniform vec3 u_light_dir;
19
+ uniform float u_ambient;
20
+ uniform float u_exposure;
21
+ varying vec2 v_uv;
22
+ varying float v_light;
23
+ varying float v_view_z;
24
+ void main() {
25
+ gl_Position = u_mvp * vec4(a_pos, 1.0);
26
+ v_uv = a_uv;
27
+ float diffuse = max(dot(normalize(u_norm_mat * a_normal), u_light_dir), 0.0);
28
+ v_light = u_exposure * (u_ambient + (1.0 - u_ambient) * diffuse);
29
+ // View-space Z: used in fragment shader to create depth-based gradient
30
+ v_view_z = (u_mv * vec4(a_pos, 1.0)).z;
31
+ }
32
+ `
33
+
34
+ export const FRAG_SRC = `
35
+ precision mediump float;
36
+ varying vec2 v_uv;
37
+ varying float v_light;
38
+ varying float v_view_z;
39
+ uniform sampler2D u_tex;
40
+ uniform bool u_use_tex;
41
+ uniform vec3 u_color;
42
+ uniform float u_cam_z;
43
+ void main() {
44
+ vec3 base = u_use_tex ? texture2D(u_tex, v_uv).rgb : u_color;
45
+ // View-angle gradient: a smooth ramp from bright (near side) to dark (far side)
46
+ // across the whole face. rel_z > 0 = closer to camera, rel_z < 0 = farther away.
47
+ // Negative shadow (near side) slightly brightens; positive shadow (far side) darkens.
48
+ // Faces directly facing the camera have near-zero rel_z variance → no visible gradient.
49
+ float rel_z = v_view_z - u_cam_z;
50
+ float shadow = clamp(-rel_z * 0.6, -0.08, 0.15);
51
+ gl_FragColor = vec4(base * v_light * (1.0 - shadow), 1.0);
52
+ }
53
+ `
54
+
55
+ /** Compile a single shader stage from GLSL source */
56
+ export function compile_shader(gl:WebGLRenderingContext, type:number, src:string):WebGLShader {
57
+ const shader = gl.createShader(type)!
58
+ gl.shaderSource(shader, src)
59
+ gl.compileShader(shader)
60
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
61
+ throw new Error(`[cover-3d] Shader compile error: ${gl.getShaderInfoLog(shader)}`)
62
+ return shader
63
+ }
64
+
65
+ /** Link a vertex + fragment shader into a program */
66
+ export function build_program(gl:WebGLRenderingContext, vert:string, frag:string):WebGLProgram {
67
+ const prog = gl.createProgram()!
68
+ gl.attachShader(prog, compile_shader(gl, gl.VERTEX_SHADER, vert))
69
+ gl.attachShader(prog, compile_shader(gl, gl.FRAGMENT_SHADER, frag))
70
+ gl.linkProgram(prog)
71
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS))
72
+ throw new Error(`[cover-3d] Program link error: ${gl.getProgramInfoLog(prog)}`)
73
+ return prog
74
+ }
75
+
76
+ /** Create a procedural page-edge texture scaled to the book's spine width.
77
+ * spine_d is the normalised depth (spine_width / cover_height). */
78
+ export function create_page_edge_texture(gl:WebGLRenderingContext, spine_d:number):WebGLTexture {
79
+ // Derive stripe count proportional to spine thickness (600 lines per unit is visually calibrated)
80
+ const n_stripes = Math.max(6, Math.round(spine_d * 600))
81
+
82
+ // Pack into exactly 256px (power-of-2 required for mipmap generation).
83
+ // Each stripe: body + 2px separator; body scales with available space.
84
+ const px_per_stripe = Math.max(3, Math.floor(256 / n_stripes))
85
+ const actual_n = Math.floor(256 / px_per_stripe)
86
+ const body_h = px_per_stripe - 2
87
+
88
+ // Pre-fill with cream so the unpainted tail of the texture blends seamlessly
89
+ const canvas = new OffscreenCanvas(4, 256)
90
+ const ctx = canvas.getContext('2d')!
91
+ ctx.fillStyle = '#EDE5D4'
92
+ ctx.fillRect(0, 0, 4, 256)
93
+
94
+ // Draw separator lines; page body is already the pre-filled cream
95
+ for (let i = 0; i < actual_n; i++) {
96
+ ctx.fillStyle = '#847C6E'
97
+ ctx.fillRect(0, i * px_per_stripe + body_h, 4, 2)
98
+ }
99
+
100
+ // Mipmaps + LINEAR_MIPMAP_LINEAR: lines degrade smoothly at oblique angles rather
101
+ // than aliasing/flickering as the camera rotates. Requires power-of-2 dimensions (4×256).
102
+ const bitmap = canvas.transferToImageBitmap()
103
+ const tex = gl.createTexture()!
104
+ gl.bindTexture(gl.TEXTURE_2D, tex)
105
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
106
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap)
107
+ gl.generateMipmap(gl.TEXTURE_2D)
108
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
109
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
110
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
111
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
112
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false)
113
+ bitmap.close()
114
+ return tex
115
+ }
116
+
117
+ /** Upload an ImageBitmap to a WebGL texture (no mipmaps for NPOT support) */
118
+ export function upload_texture(gl:WebGLRenderingContext, bitmap:ImageBitmap):WebGLTexture {
119
+ const tex = gl.createTexture()!
120
+ gl.bindTexture(gl.TEXTURE_2D, tex)
121
+ // Flip Y so UV (0,0) maps to the top-left of the source image
122
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
123
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap)
124
+ // Skip mipmap generation for NPOT textures (SVG dimensions may not be powers of 2)
125
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
126
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
127
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
128
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
129
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false)
130
+ return tex
131
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+
2
+ {
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "lib": ["ES2022", "DOM"],
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "strict": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "skipLibCheck": true
16
+ },
17
+ "include": ["src"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/geometry.ts","./src/index.ts","./src/math.ts","./src/svg.ts","./src/types.ts","./src/webgl.ts"],"version":"5.9.3"}