hic-pageflip 1.0.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/LICENSE +21 -0
- package/README.md +322 -0
- package/components/hic-pageflip-page.js +43 -0
- package/components/hic-pageflip.js +339 -0
- package/core/engines/engine-2d.js +470 -0
- package/core/engines/engine-3d.js +699 -0
- package/core/engines/engine-base.js +73 -0
- package/core/pageflip.js +657 -0
- package/core/utils/math.js +186 -0
- package/index.js +8 -0
- package/package.json +33 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML-in-Canvas 2D Pageflip Engine (HICPageflipEngine2D)
|
|
3
|
+
* Implements 2D geometric fold clipping, affine reflection matrices,
|
|
4
|
+
* fold drop shadows, spine shadows, and direct DOM element drawing via ctx.drawElementImage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseEngine } from './engine-base.js';
|
|
8
|
+
import { calculateFold, clamp } from '../utils/math.js';
|
|
9
|
+
|
|
10
|
+
export class HICPageflipEngine2D extends BaseEngine {
|
|
11
|
+
constructor(canvas, slides = []) {
|
|
12
|
+
super(canvas, slides);
|
|
13
|
+
this.ctx = canvas.getContext('2d', { alpha: true, desynchronized: true });
|
|
14
|
+
this.devicePixelRatio = 1;
|
|
15
|
+
this.resize();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
resize() {
|
|
19
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
20
|
+
this.devicePixelRatio = 1;
|
|
21
|
+
|
|
22
|
+
// Use CSS pixel dimensions directly for 1:1 context coordinate mapping
|
|
23
|
+
this.canvas.width = Math.round(rect.width) || this.pw;
|
|
24
|
+
this.canvas.height = Math.round(rect.height) || this.ph;
|
|
25
|
+
this.viewportWidth = this.canvas.width;
|
|
26
|
+
this.viewportHeight = this.canvas.height;
|
|
27
|
+
this.offsetX = this.canvas.width / 2;
|
|
28
|
+
this.offsetY = this.canvas.height / 2;
|
|
29
|
+
|
|
30
|
+
const pad = 40;
|
|
31
|
+
const availW = Math.max(100, this.canvas.width - pad);
|
|
32
|
+
const availH = Math.max(100, this.canvas.height - pad);
|
|
33
|
+
const spreadW = this.pw * 2;
|
|
34
|
+
const spreadH = this.ph;
|
|
35
|
+
|
|
36
|
+
this.scale = Math.min(availW / spreadW, availH / spreadH, 1);
|
|
37
|
+
this.zoom = 1.0;
|
|
38
|
+
this.panX = 0;
|
|
39
|
+
this.panY = 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
requestRender(callback) {
|
|
43
|
+
if (this.canvas && typeof this.canvas.requestPaint === 'function') {
|
|
44
|
+
this.canvas.requestPaint();
|
|
45
|
+
}
|
|
46
|
+
super.requestRender(callback);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Main render loop
|
|
51
|
+
*/
|
|
52
|
+
render(state) {
|
|
53
|
+
const [leftPage, rightPage] = state.currentSpread;
|
|
54
|
+
const totalPages = state.totalPages || this.slides.length;
|
|
55
|
+
const activePages = new Set();
|
|
56
|
+
|
|
57
|
+
// Once the pointer goes down (dragging) or during active fold animation, inert ALL pages
|
|
58
|
+
const isInteracting = state.isDragging || (state.activeFlip && !state.activeFlip.isPeek);
|
|
59
|
+
if (!isInteracting) {
|
|
60
|
+
if (leftPage > 0) activePages.add(leftPage);
|
|
61
|
+
if (rightPage <= totalPages) activePages.add(rightPage);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Dynamically inert all non-active pages
|
|
65
|
+
this.slides.forEach((s) => {
|
|
66
|
+
if (s.element) {
|
|
67
|
+
s.element.inert = !activePages.has(s.pageNum);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const ctx = this.ctx;
|
|
72
|
+
const dpr = this.devicePixelRatio;
|
|
73
|
+
const width = this.canvas.width / dpr;
|
|
74
|
+
const height = this.canvas.height / dpr;
|
|
75
|
+
|
|
76
|
+
ctx.save();
|
|
77
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
78
|
+
ctx.clearRect(0, 0, width, height);
|
|
79
|
+
|
|
80
|
+
// Apply main viewport transformations (center + zoom + pan)
|
|
81
|
+
ctx.translate(this.offsetX + this.panX, this.offsetY + this.panY);
|
|
82
|
+
ctx.scale(this.scale * this.zoom, this.scale * this.zoom);
|
|
83
|
+
|
|
84
|
+
// 1. Ambient shadow of the physical book block resting on the desk
|
|
85
|
+
this.renderBookShadow(ctx, state);
|
|
86
|
+
|
|
87
|
+
// 2. Render book pages, stationary gutter shadows, and turning flap on top
|
|
88
|
+
this.renderPages(ctx, state);
|
|
89
|
+
|
|
90
|
+
ctx.restore();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Ambient shadow of the physical book resting on the desk surface.
|
|
95
|
+
*/
|
|
96
|
+
renderBookShadow(ctx, state) {
|
|
97
|
+
const pw = this.pw;
|
|
98
|
+
const ph = this.ph;
|
|
99
|
+
const [leftPageNum, rightPageNum] = state.currentSpread;
|
|
100
|
+
const flip = state.activeFlip;
|
|
101
|
+
|
|
102
|
+
let leftX = leftPageNum === 0 ? 0 : -pw;
|
|
103
|
+
let rightX = rightPageNum > state.totalPages ? 0 : pw;
|
|
104
|
+
|
|
105
|
+
if (flip) {
|
|
106
|
+
if (flip.dir < 0 && leftPageNum - 2 <= 0) {
|
|
107
|
+
leftX = 0;
|
|
108
|
+
}
|
|
109
|
+
if (flip.dir > 0 && rightPageNum + 2 > state.totalPages) {
|
|
110
|
+
rightX = 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bookW = rightX - leftX;
|
|
115
|
+
if (bookW <= 0) return;
|
|
116
|
+
|
|
117
|
+
ctx.save();
|
|
118
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.55)';
|
|
119
|
+
ctx.shadowBlur = 38 * this.scale;
|
|
120
|
+
ctx.shadowOffsetX = 0;
|
|
121
|
+
ctx.shadowOffsetY = 16 * this.scale;
|
|
122
|
+
|
|
123
|
+
ctx.fillStyle = '#12141a';
|
|
124
|
+
ctx.fillRect(leftX, -ph / 2, bookW, ph);
|
|
125
|
+
ctx.restore();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Renders pages and dynamic page curl
|
|
130
|
+
*/
|
|
131
|
+
renderPages(ctx, state) {
|
|
132
|
+
const pw = this.pw;
|
|
133
|
+
const ph = this.ph;
|
|
134
|
+
const [leftPageNum, rightPageNum] = state.currentSpread;
|
|
135
|
+
const flip = state.activeFlip;
|
|
136
|
+
|
|
137
|
+
if (!flip) {
|
|
138
|
+
// Static double-page spread
|
|
139
|
+
if (leftPageNum > 0) {
|
|
140
|
+
this.drawPage(ctx, leftPageNum, -pw, -ph / 2, pw, ph);
|
|
141
|
+
}
|
|
142
|
+
if (rightPageNum <= state.totalPages) {
|
|
143
|
+
this.drawPage(ctx, rightPageNum, 0, -ph / 2, pw, ph);
|
|
144
|
+
}
|
|
145
|
+
// Gutter shadow between the two stationary pages
|
|
146
|
+
if (leftPageNum > 0 && rightPageNum <= state.totalPages) {
|
|
147
|
+
this.renderSpineShadow(ctx, state);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const fold = calculateFold(flip.px, flip.py, flip.sx, flip.sy, pw, ph);
|
|
153
|
+
|
|
154
|
+
if (flip.dir > 0) {
|
|
155
|
+
// ==========================================
|
|
156
|
+
// FORWARD FLIP: Right page turning to Left
|
|
157
|
+
// ==========================================
|
|
158
|
+
|
|
159
|
+
// 1. Current left page stays visible on the left desk
|
|
160
|
+
if (leftPageNum > 0) {
|
|
161
|
+
this.drawPage(ctx, leftPageNum, -pw, -ph / 2, pw, ph);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 2. Underneath right page (Page N+2) revealed as page is peeled back
|
|
165
|
+
const nextRightPage = rightPageNum + 2;
|
|
166
|
+
if (nextRightPage <= state.totalPages) {
|
|
167
|
+
this.drawPage(ctx, nextRightPage, 0, -ph / 2, pw, ph);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!fold) {
|
|
171
|
+
if (rightPageNum <= state.totalPages) {
|
|
172
|
+
this.drawPage(ctx, rightPageNum, 0, -ph / 2, pw, ph);
|
|
173
|
+
}
|
|
174
|
+
if (leftPageNum > 0) {
|
|
175
|
+
this.renderSpineShadow(ctx, state);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 3. Drop shadow cast by the fold crease onto the revealed page underneath
|
|
181
|
+
if (nextRightPage <= state.totalPages) {
|
|
182
|
+
this.drawFoldUnderShadow(ctx, fold, 0, -ph / 2, pw, ph);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 4. Flat unpeeled portion of Page N (contains spine: onDragSide = true)
|
|
186
|
+
if (rightPageNum <= state.totalPages) {
|
|
187
|
+
ctx.save();
|
|
188
|
+
ctx.beginPath();
|
|
189
|
+
ctx.rect(0, -ph / 2, pw, ph);
|
|
190
|
+
ctx.clip();
|
|
191
|
+
ctx.beginPath();
|
|
192
|
+
this.clipHalfPlane(ctx, fold, true);
|
|
193
|
+
ctx.clip();
|
|
194
|
+
this.drawPage(ctx, rightPageNum, 0, -ph / 2, pw, ph);
|
|
195
|
+
ctx.restore();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 5. Gutter spine shadow on the flat stationary spread UNDER the turning flap
|
|
199
|
+
if (leftPageNum > 0 && rightPageNum <= state.totalPages) {
|
|
200
|
+
this.renderSpineShadow(ctx, state);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 6. The Turning Flap (Back of turning page = Page N+1) rendered ON TOP
|
|
204
|
+
const backPageNum = rightPageNum + 1;
|
|
205
|
+
const theta = Math.atan2(fold.ny, fold.nx);
|
|
206
|
+
|
|
207
|
+
ctx.save();
|
|
208
|
+
// Clip to the dragged side of the fold (side containing P: onDragSide = true)
|
|
209
|
+
ctx.beginPath();
|
|
210
|
+
this.clipHalfPlane(ctx, fold, true);
|
|
211
|
+
ctx.clip();
|
|
212
|
+
|
|
213
|
+
// Apply reflection transformation across the fold crease line
|
|
214
|
+
ctx.translate(fold.cx, fold.cy);
|
|
215
|
+
ctx.rotate(theta);
|
|
216
|
+
ctx.scale(-1, 1);
|
|
217
|
+
ctx.rotate(-theta);
|
|
218
|
+
ctx.translate(-fold.cx, -fold.cy);
|
|
219
|
+
|
|
220
|
+
// Clip to the original right sheet boundary [0, -ph/2, pw, ph]
|
|
221
|
+
ctx.beginPath();
|
|
222
|
+
ctx.rect(0, -ph / 2, pw, ph);
|
|
223
|
+
ctx.clip();
|
|
224
|
+
|
|
225
|
+
// Draw Page N+1 on top
|
|
226
|
+
this.drawPageBack(ctx, backPageNum, 0, -ph / 2, pw, ph);
|
|
227
|
+
ctx.restore();
|
|
228
|
+
|
|
229
|
+
} else {
|
|
230
|
+
// ==========================================
|
|
231
|
+
// BACKWARD FLIP: Left page turning to Right
|
|
232
|
+
// ==========================================
|
|
233
|
+
|
|
234
|
+
// 1. Current right page stays visible on the right desk
|
|
235
|
+
if (rightPageNum <= state.totalPages) {
|
|
236
|
+
this.drawPage(ctx, rightPageNum, 0, -ph / 2, pw, ph);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 2. Underneath left page (Page N-3) revealed as left page is peeled back
|
|
240
|
+
const prevLeftPage = leftPageNum - 2;
|
|
241
|
+
if (prevLeftPage > 0) {
|
|
242
|
+
this.drawPage(ctx, prevLeftPage, -pw, -ph / 2, pw, ph);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!fold) {
|
|
246
|
+
if (leftPageNum > 0) {
|
|
247
|
+
this.drawPage(ctx, leftPageNum, -pw, -ph / 2, pw, ph);
|
|
248
|
+
}
|
|
249
|
+
if (leftPageNum > 0 && rightPageNum <= state.totalPages) {
|
|
250
|
+
this.renderSpineShadow(ctx, state);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 3. Drop shadow cast by fold crease onto revealed page underneath
|
|
256
|
+
if (prevLeftPage > 0) {
|
|
257
|
+
this.drawFoldUnderShadow(ctx, fold, -pw, -ph / 2, pw, ph);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 4. Flat unpeeled portion of Page N-1 (contains spine: onDragSide = true)
|
|
261
|
+
if (leftPageNum > 0) {
|
|
262
|
+
ctx.save();
|
|
263
|
+
ctx.beginPath();
|
|
264
|
+
ctx.rect(-pw, -ph / 2, pw, ph);
|
|
265
|
+
ctx.clip();
|
|
266
|
+
ctx.beginPath();
|
|
267
|
+
this.clipHalfPlane(ctx, fold, true);
|
|
268
|
+
ctx.clip();
|
|
269
|
+
this.drawPage(ctx, leftPageNum, -pw, -ph / 2, pw, ph);
|
|
270
|
+
ctx.restore();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 5. Gutter spine shadow on the flat stationary spread UNDER the turning flap
|
|
274
|
+
if (leftPageNum > 0 && rightPageNum <= state.totalPages) {
|
|
275
|
+
this.renderSpineShadow(ctx, state);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 6. The Turning Flap (Front of left sheet = Page N-1) rendered ON TOP
|
|
279
|
+
const frontPageNum = leftPageNum - 1;
|
|
280
|
+
const theta = Math.atan2(fold.ny, fold.nx);
|
|
281
|
+
|
|
282
|
+
ctx.save();
|
|
283
|
+
// Clip to the dragged side of the fold (side containing P: onDragSide = true)
|
|
284
|
+
ctx.beginPath();
|
|
285
|
+
this.clipHalfPlane(ctx, fold, true);
|
|
286
|
+
ctx.clip();
|
|
287
|
+
|
|
288
|
+
// Apply reflection transformation across the fold crease line
|
|
289
|
+
ctx.translate(fold.cx, fold.cy);
|
|
290
|
+
ctx.rotate(theta);
|
|
291
|
+
ctx.scale(-1, 1);
|
|
292
|
+
ctx.rotate(-theta);
|
|
293
|
+
ctx.translate(-fold.cx, -fold.cy);
|
|
294
|
+
|
|
295
|
+
// Clip to the left sheet boundary [-pw, -ph/2, pw, ph]
|
|
296
|
+
ctx.beginPath();
|
|
297
|
+
ctx.rect(-pw, -ph / 2, pw, ph);
|
|
298
|
+
ctx.clip();
|
|
299
|
+
|
|
300
|
+
// Draw Page N-1
|
|
301
|
+
this.drawPageFrontReflected(ctx, frontPageNum, -pw, -ph / 2, pw, ph);
|
|
302
|
+
ctx.restore();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Clips to one half of the plane divided by the fold crease line.
|
|
308
|
+
*/
|
|
309
|
+
clipHalfPlane(ctx, fold, onDragSide) {
|
|
310
|
+
const L = Math.max(this.pw, this.ph) * 6;
|
|
311
|
+
const tx = -fold.ny;
|
|
312
|
+
const ty = fold.nx;
|
|
313
|
+
const sign = onDragSide ? 1 : -1;
|
|
314
|
+
const nx = fold.nx * sign;
|
|
315
|
+
const ny = fold.ny * sign;
|
|
316
|
+
|
|
317
|
+
const p1 = { x: fold.cx - tx * L, y: fold.cy - ty * L };
|
|
318
|
+
const p2 = { x: fold.cx + tx * L, y: fold.cy + ty * L };
|
|
319
|
+
const p3 = { x: p2.x + nx * L, y: p2.y + ny * L };
|
|
320
|
+
const p4 = { x: p1.x + nx * L, y: p1.y + ny * L };
|
|
321
|
+
|
|
322
|
+
ctx.moveTo(p1.x, p1.y);
|
|
323
|
+
ctx.lineTo(p2.x, p2.y);
|
|
324
|
+
ctx.lineTo(p3.x, p3.y);
|
|
325
|
+
ctx.lineTo(p4.x, p4.y);
|
|
326
|
+
ctx.closePath();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Soft drop shadow under the fold crease onto the revealed page underneath.
|
|
331
|
+
*/
|
|
332
|
+
drawFoldUnderShadow(ctx, fold, rx, ry, rw, rh) {
|
|
333
|
+
ctx.save();
|
|
334
|
+
ctx.beginPath();
|
|
335
|
+
ctx.rect(rx, ry, rw, rh);
|
|
336
|
+
ctx.clip();
|
|
337
|
+
|
|
338
|
+
ctx.beginPath();
|
|
339
|
+
this.clipHalfPlane(ctx, fold, false);
|
|
340
|
+
ctx.clip();
|
|
341
|
+
|
|
342
|
+
const shadowWidth = clamp(fold.len * 0.16, 12, 45);
|
|
343
|
+
const grad = ctx.createLinearGradient(
|
|
344
|
+
fold.cx,
|
|
345
|
+
fold.cy,
|
|
346
|
+
fold.cx - fold.nx * shadowWidth,
|
|
347
|
+
fold.cy - fold.ny * shadowWidth
|
|
348
|
+
);
|
|
349
|
+
grad.addColorStop(0, 'rgba(0, 0, 0, 0.35)');
|
|
350
|
+
grad.addColorStop(0.35, 'rgba(0, 0, 0, 0.14)');
|
|
351
|
+
grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
352
|
+
|
|
353
|
+
ctx.fillStyle = grad;
|
|
354
|
+
ctx.fillRect(rx - 20, ry - 20, rw + 40, rh + 40);
|
|
355
|
+
ctx.restore();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Draws a flat DOM slide using ctx.drawElementImage.
|
|
360
|
+
*/
|
|
361
|
+
drawPage(ctx, pageNum, x, y, w, h) {
|
|
362
|
+
const slide = this.slides[pageNum - 1];
|
|
363
|
+
if (!slide || !slide.element) return;
|
|
364
|
+
|
|
365
|
+
const el = slide.element;
|
|
366
|
+
if (typeof ctx.drawElementImage === 'function') {
|
|
367
|
+
try {
|
|
368
|
+
ctx.save();
|
|
369
|
+
ctx.translate(x, y);
|
|
370
|
+
const transform = ctx.drawElementImage(el, 0, 0);
|
|
371
|
+
if (transform && el.style) {
|
|
372
|
+
el.style.transform = transform.toString();
|
|
373
|
+
}
|
|
374
|
+
ctx.restore();
|
|
375
|
+
} catch (err) {
|
|
376
|
+
// Ignore if paint record not ready
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Draws the backside of the turning page (Page N+1) onto the sheet [0, -ph/2, pw, ph].
|
|
383
|
+
*/
|
|
384
|
+
drawPageBack(ctx, pageNum, x, y, w, h) {
|
|
385
|
+
const slide = this.slides[pageNum - 1];
|
|
386
|
+
if (!slide || !slide.element) return;
|
|
387
|
+
|
|
388
|
+
const el = slide.element;
|
|
389
|
+
if (typeof ctx.drawElementImage === 'function') {
|
|
390
|
+
try {
|
|
391
|
+
ctx.save();
|
|
392
|
+
ctx.translate(x + w, y);
|
|
393
|
+
ctx.scale(-1, 1);
|
|
394
|
+
const transform = ctx.drawElementImage(el, 0, 0);
|
|
395
|
+
if (transform && el.style) {
|
|
396
|
+
el.style.transform = transform.toString();
|
|
397
|
+
}
|
|
398
|
+
ctx.restore();
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// Ignore
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Draws the front of the turning left page reflected onto the right side.
|
|
407
|
+
*/
|
|
408
|
+
drawPageFrontReflected(ctx, pageNum, x, y, w, h) {
|
|
409
|
+
const slide = this.slides[pageNum - 1];
|
|
410
|
+
if (!slide || !slide.element) return;
|
|
411
|
+
|
|
412
|
+
const el = slide.element;
|
|
413
|
+
if (typeof ctx.drawElementImage === 'function') {
|
|
414
|
+
try {
|
|
415
|
+
ctx.save();
|
|
416
|
+
ctx.translate(x, y);
|
|
417
|
+
ctx.scale(-1, 1);
|
|
418
|
+
ctx.translate(-w, 0);
|
|
419
|
+
const transform = ctx.drawElementImage(el, 0, 0);
|
|
420
|
+
if (transform && el.style) {
|
|
421
|
+
el.style.transform = transform.toString();
|
|
422
|
+
}
|
|
423
|
+
ctx.restore();
|
|
424
|
+
} catch (err) {
|
|
425
|
+
// Ignore
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Center spine crease shadow on the flat spread between stationary pages.
|
|
432
|
+
*/
|
|
433
|
+
renderSpineShadow(ctx, state) {
|
|
434
|
+
const ph = this.ph;
|
|
435
|
+
const spineWidth = 32;
|
|
436
|
+
const [leftPageNum, rightPageNum] = state.currentSpread;
|
|
437
|
+
|
|
438
|
+
ctx.save();
|
|
439
|
+
// Left side of spine
|
|
440
|
+
if (leftPageNum > 0) {
|
|
441
|
+
const leftGrad = ctx.createLinearGradient(-spineWidth, 0, 0, 0);
|
|
442
|
+
leftGrad.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
|
443
|
+
leftGrad.addColorStop(0.7, 'rgba(0, 0, 0, 0.14)');
|
|
444
|
+
leftGrad.addColorStop(1, 'rgba(0, 0, 0, 0.38)');
|
|
445
|
+
ctx.fillStyle = leftGrad;
|
|
446
|
+
ctx.fillRect(-spineWidth, -ph / 2, spineWidth, ph);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Right side of spine
|
|
450
|
+
if (rightPageNum <= state.totalPages) {
|
|
451
|
+
const rightGrad = ctx.createLinearGradient(0, 0, spineWidth, 0);
|
|
452
|
+
rightGrad.addColorStop(0, 'rgba(0, 0, 0, 0.38)');
|
|
453
|
+
rightGrad.addColorStop(0.3, 'rgba(0, 0, 0, 0.14)');
|
|
454
|
+
rightGrad.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
455
|
+
ctx.fillStyle = rightGrad;
|
|
456
|
+
ctx.fillRect(0, -ph / 2, spineWidth, ph);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Fine center line
|
|
460
|
+
if (leftPageNum > 0 && rightPageNum <= state.totalPages) {
|
|
461
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.45)';
|
|
462
|
+
ctx.lineWidth = 1;
|
|
463
|
+
ctx.beginPath();
|
|
464
|
+
ctx.moveTo(0, -ph / 2);
|
|
465
|
+
ctx.lineTo(0, ph / 2);
|
|
466
|
+
ctx.stroke();
|
|
467
|
+
}
|
|
468
|
+
ctx.restore();
|
|
469
|
+
}
|
|
470
|
+
}
|