@objectifthunes/react-three-book 0.1.0 → 0.2.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 +214 -0
- package/dist/index.d.ts +132 -2
- package/dist/index.js +744 -583
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# @objectifthunes/react-three-book
|
|
2
|
+
|
|
3
|
+
A procedural, interactive 3D book for [React Three Fiber](https://docs.pmnd.rs/react-three-fiber) — drag pages to turn them, apply textures to every surface, and drop `<Book>` into your R3F scene like any other component.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://raw.githubusercontent.com/ObjectifThunes/react-three-book/main/docs/images/default.png" width="49%" alt="Closed book" />
|
|
7
|
+
<img src="https://raw.githubusercontent.com/ObjectifThunes/react-three-book/main/docs/images/open-half.png" width="49%" alt="Book opened halfway" />
|
|
8
|
+
</p>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://raw.githubusercontent.com/ObjectifThunes/react-three-book/main/docs/images/page-curl.png" width="49%" alt="Page mid-curl" />
|
|
11
|
+
<img src="https://raw.githubusercontent.com/ObjectifThunes/react-three-book/main/docs/images/demo-ui.png" width="49%" alt="Demo app with tweakable controls" />
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **`<Book>` R3F component** — handles init / update / dispose automatically; rebuilds cleanly via React's `key` prop.
|
|
17
|
+
- **`<BookInteraction>`** — declarative pointer-drag wiring for interactive page turning; auto-discovers the book from context.
|
|
18
|
+
- **Hooks** — `useBookControls`, `useAutoTurn`, `useBookState`, `useBookContent`, `usePageTurning`.
|
|
19
|
+
- **BookContext** — child components access the book instance without prop-drilling.
|
|
20
|
+
- **Per-surface textures** — assign a `THREE.Texture` (or `null`) to each cover side and page side independently.
|
|
21
|
+
- **Configurable geometry** — page/cover width, height, thickness, stiffness, color.
|
|
22
|
+
- **Texture utilities** — `createPageTexture`, `drawImageWithFit`, `loadImage` included.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @objectifthunes/react-three-book three @react-three/fiber react react-dom
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add @objectifthunes/react-three-book three @react-three/fiber react react-dom
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Peer dependencies: `three >= 0.150.0`, `react >= 18.0.0`, `@react-three/fiber >= 8.0.0`.
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { Canvas } from '@react-three/fiber';
|
|
40
|
+
import { OrbitControls } from '@react-three/drei';
|
|
41
|
+
import * as THREE from 'three';
|
|
42
|
+
import {
|
|
43
|
+
Book,
|
|
44
|
+
BookContent,
|
|
45
|
+
BookDirection,
|
|
46
|
+
BookInteraction,
|
|
47
|
+
StapleBookBinding,
|
|
48
|
+
useBookContent,
|
|
49
|
+
} from '@objectifthunes/react-three-book';
|
|
50
|
+
|
|
51
|
+
function Scene() {
|
|
52
|
+
const orbitRef = useRef(null);
|
|
53
|
+
|
|
54
|
+
const content = useBookContent(() => {
|
|
55
|
+
const c = new BookContent();
|
|
56
|
+
c.direction = BookDirection.LeftToRight;
|
|
57
|
+
c.covers.push(frontOuterTex, frontInnerTex, backInnerTex, backOuterTex);
|
|
58
|
+
c.pages.push(page1Tex, page2Tex, page3Tex, page4Tex);
|
|
59
|
+
return c;
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
<OrbitControls ref={orbitRef} />
|
|
65
|
+
<Book
|
|
66
|
+
content={content}
|
|
67
|
+
binding={new StapleBookBinding()}
|
|
68
|
+
castShadows
|
|
69
|
+
alignToGround
|
|
70
|
+
pagePaperSetup={{ width: 2, height: 3, thickness: 0.02, stiffness: 0.2, color: new THREE.Color(1, 1, 1), material: null }}
|
|
71
|
+
coverPaperSetup={{ width: 2.1, height: 3.1, thickness: 0.04, stiffness: 0.5, color: new THREE.Color(1, 1, 1), material: null }}
|
|
72
|
+
>
|
|
73
|
+
<BookInteraction orbitControlsRef={orbitRef} />
|
|
74
|
+
</Book>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function App() {
|
|
80
|
+
return (
|
|
81
|
+
<Canvas shadows camera={{ position: [0, 2, 5], fov: 45 }}>
|
|
82
|
+
<Scene />
|
|
83
|
+
</Canvas>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Triggering a Rebuild
|
|
89
|
+
|
|
90
|
+
Change the `key` prop — React unmounts and remounts `<Book>`, which runs a clean dispose → init cycle:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
<Book key={buildKey} content={content} binding={binding} ... />
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Imperative Access
|
|
97
|
+
|
|
98
|
+
Forward a ref to get the underlying `ThreeBook` instance:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
const bookRef = useRef<ThreeBook>(null);
|
|
102
|
+
<Book ref={bookRef} ... />
|
|
103
|
+
|
|
104
|
+
bookRef.current?.setOpenProgress(0.5);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Hooks
|
|
108
|
+
|
|
109
|
+
### `useBookControls(bookRef?)`
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
const { setOpenProgress, setOpenProgressByIndex, stopTurning } = useBookControls();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `useAutoTurn(bookRef?)`
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
const { turnNext, turnPrev, turnAll, startAutoTurning, cancelPendingAutoTurns } = useAutoTurn();
|
|
119
|
+
|
|
120
|
+
turnNext();
|
|
121
|
+
turnPrev();
|
|
122
|
+
turnAll(AutoTurnDirection.Next);
|
|
123
|
+
startAutoTurning(AutoTurnDirection.Next, settings, 5, 0.3);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `useBookState(bookRef?)`
|
|
127
|
+
|
|
128
|
+
Reactive snapshot updated every frame — triggers re-renders only when something actually changes:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const { isTurning, isIdle, isAutoTurning, paperCount } = useBookState();
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `useBookContent(factory, deps)`
|
|
135
|
+
|
|
136
|
+
Creates a `BookContent` and disposes its `THREE.Texture`s automatically when `deps` change or the component unmounts:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
const content = useBookContent(() => {
|
|
140
|
+
const c = new BookContent();
|
|
141
|
+
c.covers.push(myTexture);
|
|
142
|
+
c.pages.push(pageA, pageB);
|
|
143
|
+
return c;
|
|
144
|
+
}, [rebuildKey]);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Context
|
|
148
|
+
|
|
149
|
+
Any component rendered inside `<Book>` can access the instance without a ref:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
function PageButtons() {
|
|
153
|
+
const { turnNext, turnPrev } = useAutoTurn();
|
|
154
|
+
return (
|
|
155
|
+
<>
|
|
156
|
+
<button onClick={turnPrev}>◀</button>
|
|
157
|
+
<button onClick={turnNext}>▶</button>
|
|
158
|
+
</>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
<Book ...>
|
|
163
|
+
<BookInteraction />
|
|
164
|
+
<PageButtons />
|
|
165
|
+
</Book>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Texture Utilities
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { createPageTexture, drawImageWithFit, loadImage } from '@objectifthunes/react-three-book';
|
|
172
|
+
|
|
173
|
+
const tex = createPageTexture('#ff0000', 'Cover', myImage, 'cover', true);
|
|
174
|
+
|
|
175
|
+
const result = await loadImage(file); // { image, objectUrl } | null
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Content Model
|
|
179
|
+
|
|
180
|
+
**Covers** — 4 entries, one per surface:
|
|
181
|
+
|
|
182
|
+
| Index | Surface |
|
|
183
|
+
|-------|---------|
|
|
184
|
+
| 0 | Front outer |
|
|
185
|
+
| 1 | Front inner |
|
|
186
|
+
| 2 | Back inner |
|
|
187
|
+
| 3 | Back outer |
|
|
188
|
+
|
|
189
|
+
**Pages** — ordered list of page-side textures. Each entry can be a `THREE.Texture`, an `IPageContent` implementation, or `null` (renders the base paper color).
|
|
190
|
+
|
|
191
|
+
## Auto Turn
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { AutoTurnDirection, AutoTurnMode, AutoTurnSettings } from '@objectifthunes/react-three-book';
|
|
195
|
+
|
|
196
|
+
const { startAutoTurning } = useAutoTurn();
|
|
197
|
+
|
|
198
|
+
const settings = new AutoTurnSettings();
|
|
199
|
+
settings.mode = AutoTurnMode.Edge;
|
|
200
|
+
startAutoTurning(AutoTurnDirection.Next, settings, 3);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## API Surface
|
|
204
|
+
|
|
205
|
+
| Category | Exports |
|
|
206
|
+
|----------|---------|
|
|
207
|
+
| Components | `Book`, `BookInteraction` |
|
|
208
|
+
| Context | `BookContext`, `useBook`, `useRequiredBook` |
|
|
209
|
+
| Hooks | `useBookRef`, `useBookContent`, `useBookControls`, `useAutoTurn`, `useBookState`, `usePageTurning` |
|
|
210
|
+
| Texture utils | `createPageTexture`, `drawImageWithFit`, `loadImage` |
|
|
211
|
+
| Core | `ThreeBook`, `BookContent`, `BookDirection`, `Paper`, `PaperSetup` |
|
|
212
|
+
| Binding | `BookBinding`, `StapleBookBinding`, `StapleBookBound`, `StapleSetup` |
|
|
213
|
+
| Content | `IPageContent`, `PageContent`, `SpritePageContent2` |
|
|
214
|
+
| Auto turn | `AutoTurnDirection`, `AutoTurnMode`, `AutoTurnSettings`, `AutoTurnSetting` |
|
package/dist/index.d.ts
CHANGED
|
@@ -385,13 +385,16 @@ export declare interface BookState {
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
/**
|
|
388
|
-
* Creates a
|
|
388
|
+
* Creates a `THREE.CanvasTexture` suitable for use as a book page.
|
|
389
|
+
*
|
|
390
|
+
* When `pageWidth` and `pageHeight` are given the canvas matches the page
|
|
391
|
+
* aspect ratio (256 px per world unit). Otherwise defaults to 512×512.
|
|
389
392
|
*
|
|
390
393
|
* - Fills the background with `color`.
|
|
391
394
|
* - If `image` is provided, draws it using `fitMode` and `fullBleed`.
|
|
392
395
|
* - Otherwise, renders `label` as centred text (useful for debugging).
|
|
393
396
|
*/
|
|
394
|
-
export declare function createPageTexture(color: string, label: string, image: HTMLImageElement | null, fitMode: ImageFitMode, fullBleed: boolean): THREE.Texture;
|
|
397
|
+
export declare function createPageTexture(color: string, label: string, image: HTMLImageElement | null, fitMode: ImageFitMode, fullBleed: boolean, pageWidth?: number, pageHeight?: number): THREE.Texture;
|
|
395
398
|
|
|
396
399
|
/**
|
|
397
400
|
* Ported from Book.cs — Cylinder struct (lines ~3805-3902).
|
|
@@ -987,6 +990,9 @@ export declare class PaperUVMargin {
|
|
|
987
990
|
clone(): PaperUVMargin;
|
|
988
991
|
}
|
|
989
992
|
|
|
993
|
+
/** Pixels per world unit — used to compute canvas size from page dimensions. */
|
|
994
|
+
export declare const PX_PER_UNIT = 256;
|
|
995
|
+
|
|
990
996
|
export declare class RendererFactory {
|
|
991
997
|
private m_Root;
|
|
992
998
|
private m_CreateMeshCollider;
|
|
@@ -1093,6 +1099,119 @@ export declare class StapleSetup {
|
|
|
1093
1099
|
set quality(value: number);
|
|
1094
1100
|
}
|
|
1095
1101
|
|
|
1102
|
+
export declare class TextBlock {
|
|
1103
|
+
x: number;
|
|
1104
|
+
y: number;
|
|
1105
|
+
width: number;
|
|
1106
|
+
text: string;
|
|
1107
|
+
fontFamily: string;
|
|
1108
|
+
fontSize: number;
|
|
1109
|
+
fontWeight: 'normal' | 'bold';
|
|
1110
|
+
fontStyle: 'normal' | 'italic';
|
|
1111
|
+
color: string;
|
|
1112
|
+
lineHeight: number;
|
|
1113
|
+
textAlign: 'left' | 'center' | 'right';
|
|
1114
|
+
opacity: number;
|
|
1115
|
+
shadowColor: string;
|
|
1116
|
+
shadowBlur: number;
|
|
1117
|
+
constructor(options?: TextBlockOptions);
|
|
1118
|
+
private _font;
|
|
1119
|
+
/**
|
|
1120
|
+
* Word-wrap `text` into lines that fit within `width` pixels.
|
|
1121
|
+
* Respects explicit newlines.
|
|
1122
|
+
*/
|
|
1123
|
+
wrapLines(ctx: CanvasRenderingContext2D): string[];
|
|
1124
|
+
/** Total rendered height in canvas pixels. */
|
|
1125
|
+
measureHeight(ctx: CanvasRenderingContext2D): number;
|
|
1126
|
+
/** Returns true if the point (px, py) is within the text bounding box. */
|
|
1127
|
+
hitTest(ctx: CanvasRenderingContext2D, px: number, py: number): boolean;
|
|
1128
|
+
private _maxLineWidth;
|
|
1129
|
+
draw(ctx: CanvasRenderingContext2D): void;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* A styled text block rendered onto a 2D canvas.
|
|
1134
|
+
*
|
|
1135
|
+
* Positions and dimensions are in canvas pixels. Word-wrapping is handled
|
|
1136
|
+
* manually via `ctx.measureText()` when `width` > 0.
|
|
1137
|
+
*/
|
|
1138
|
+
export declare interface TextBlockOptions {
|
|
1139
|
+
/** Left edge of the text box in canvas pixels. */
|
|
1140
|
+
x?: number;
|
|
1141
|
+
/** Top edge of the text box in canvas pixels. */
|
|
1142
|
+
y?: number;
|
|
1143
|
+
/** Maximum width before wrapping. 0 = no wrapping (default 0). */
|
|
1144
|
+
width?: number;
|
|
1145
|
+
/** Text content. */
|
|
1146
|
+
text?: string;
|
|
1147
|
+
/** Font family (default 'Georgia'). */
|
|
1148
|
+
fontFamily?: string;
|
|
1149
|
+
/** Font size in canvas pixels (default 24). */
|
|
1150
|
+
fontSize?: number;
|
|
1151
|
+
/** Font weight (default 'normal'). */
|
|
1152
|
+
fontWeight?: 'normal' | 'bold';
|
|
1153
|
+
/** Font style (default 'normal'). */
|
|
1154
|
+
fontStyle?: 'normal' | 'italic';
|
|
1155
|
+
/** CSS fill colour (default '#222'). */
|
|
1156
|
+
color?: string;
|
|
1157
|
+
/** Line height multiplier (default 1.4). */
|
|
1158
|
+
lineHeight?: number;
|
|
1159
|
+
/** Text alignment within the box (default 'left'). */
|
|
1160
|
+
textAlign?: 'left' | 'center' | 'right';
|
|
1161
|
+
/** Opacity 0–1 (default 1). */
|
|
1162
|
+
opacity?: number;
|
|
1163
|
+
/** Optional text shadow colour for readability. */
|
|
1164
|
+
shadowColor?: string;
|
|
1165
|
+
/** Shadow blur radius in pixels (default 0). */
|
|
1166
|
+
shadowBlur?: number;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export declare class TextOverlayContent implements IPageContent {
|
|
1170
|
+
readonly canvas: HTMLCanvasElement;
|
|
1171
|
+
readonly texts: TextBlock[];
|
|
1172
|
+
private readonly ctx;
|
|
1173
|
+
private readonly _texture;
|
|
1174
|
+
private readonly _textureST;
|
|
1175
|
+
private _source;
|
|
1176
|
+
get texture(): THREE.Texture;
|
|
1177
|
+
get textureST(): THREE.Vector4;
|
|
1178
|
+
/** The base layer drawn beneath text blocks. */
|
|
1179
|
+
get source(): HTMLCanvasElement | HTMLImageElement | null;
|
|
1180
|
+
set source(v: HTMLCanvasElement | HTMLImageElement | null);
|
|
1181
|
+
constructor(options?: TextOverlayContentOptions);
|
|
1182
|
+
addText(options?: TextBlockOptions): TextBlock;
|
|
1183
|
+
removeText(text: TextBlock): void;
|
|
1184
|
+
/** Update a text block by index. Only provided fields are changed. */
|
|
1185
|
+
updateText(index: number, options: Partial<TextBlockOptions>): void;
|
|
1186
|
+
/**
|
|
1187
|
+
* Re-composite the canvas: source layer + text blocks.
|
|
1188
|
+
* Call every frame (or when content changes).
|
|
1189
|
+
*
|
|
1190
|
+
* @param root Optional THREE.Object3D to traverse for texture sync
|
|
1191
|
+
* (same pattern as SpriteScene — needed because three-book
|
|
1192
|
+
* clones material textures).
|
|
1193
|
+
*/
|
|
1194
|
+
update(root?: THREE.Object3D): void;
|
|
1195
|
+
/**
|
|
1196
|
+
* Traverse `root` and set `needsUpdate = true` on every material map whose
|
|
1197
|
+
* source image is this overlay's canvas.
|
|
1198
|
+
*/
|
|
1199
|
+
syncMaterials(root: THREE.Object3D): void;
|
|
1200
|
+
isPointOverUI(_textureCoord: THREE.Vector2): boolean;
|
|
1201
|
+
init(_bookContent: BookContent): void;
|
|
1202
|
+
setActive(_active: boolean): void;
|
|
1203
|
+
dispose(): void;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
export declare interface TextOverlayContentOptions {
|
|
1207
|
+
/** Canvas width in pixels (default 512). */
|
|
1208
|
+
width?: number;
|
|
1209
|
+
/** Canvas height in pixels (default 512). */
|
|
1210
|
+
height?: number;
|
|
1211
|
+
/** Source canvas or image to draw as the base layer. */
|
|
1212
|
+
source?: HTMLCanvasElement | HTMLImageElement | null;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1096
1215
|
export declare class ThreeBook extends THREE.Group {
|
|
1097
1216
|
private static s_Instances;
|
|
1098
1217
|
static get instances(): ThreeBook[];
|
|
@@ -1263,4 +1382,15 @@ export declare interface UsePageTurningOptions {
|
|
|
1263
1382
|
/** Returns the nearest ancestor Book instance. Throws outside a <Book> tree. */
|
|
1264
1383
|
export declare function useRequiredBook(): ThreeBook;
|
|
1265
1384
|
|
|
1385
|
+
/**
|
|
1386
|
+
* Creates and manages a TextOverlayContent instance with per-frame compositing.
|
|
1387
|
+
*
|
|
1388
|
+
* The returned overlay's canvas is re-composited every frame (source + text blocks).
|
|
1389
|
+
* Material sync is performed automatically if inside a `<Book>` tree.
|
|
1390
|
+
*
|
|
1391
|
+
* @param options Initial options (width, height, source).
|
|
1392
|
+
* @returns The TextOverlayContent instance (stable ref).
|
|
1393
|
+
*/
|
|
1394
|
+
export declare function useTextOverlay(options?: TextOverlayContentOptions): TextOverlayContent;
|
|
1395
|
+
|
|
1266
1396
|
export { }
|