@toneflix/flipbook-native 0.2.0 → 0.2.2
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 +2 -2
- package/lib/Cover.d.ts +13 -0
- package/lib/Cover.js +122 -0
- package/lib/Flipbook.js +2 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/look.js +5 -5
- package/lib/pages.d.ts +4 -11
- package/lib/pages.js +2 -25
- package/package.json +7 -3
- package/src/Cover.tsx +416 -0
- package/src/Flipbook.tsx +2 -1
- package/src/index.ts +2 -12
- package/src/look.tsx +5 -5
- package/src/pages.tsx +2 -95
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @toneflix/flipbook-native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An expressive flipbook for creating personalised and interactive reading and photo viewing experiences, for React Native and Expo.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx expo install @toneflix/flipbook-native react-native-svg expo-screen-orientation
|
|
@@ -21,4 +21,4 @@ export function Book({ book }) {
|
|
|
21
21
|
}
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
See the [documentation](https://toneflix.github.io/flipbook/
|
|
24
|
+
See the [documentation](https://toneflix.github.io/flipbook/frameworks/react-native).
|
package/lib/Cover.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type FlipbookBook } from '@toneflix/flipbook-core';
|
|
2
|
+
export interface CoverViewProps {
|
|
3
|
+
book: Pick<FlipbookBook, 'title' | 'subtitle' | 'theme' | 'cover' | 'kicker' | 'coverDesign' | 'coverText'>;
|
|
4
|
+
width: number;
|
|
5
|
+
showImages?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** The front cover, laid out by the book's `coverDesign`. */
|
|
8
|
+
export declare function CoverView({ book, width, showImages }: CoverViewProps): import("react").JSX.Element;
|
|
9
|
+
/** The back cover: the same board, with the imprint. */
|
|
10
|
+
export declare function BackCoverView({ book, width }: {
|
|
11
|
+
book: Pick<FlipbookBook, 'theme' | 'imprint' | 'coverDesign'>;
|
|
12
|
+
width: number;
|
|
13
|
+
}): import("react").JSX.Element;
|
package/lib/Cover.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import { Image, StyleSheet, Text, View } from 'react-native';
|
|
4
|
+
import Svg, { ClipPath, Defs, Ellipse, Image as SvgImage, Path, RadialGradient, Rect, Stop } from 'react-native-svg';
|
|
5
|
+
import { COVER_THEMES, DECO, FLEURON, coverHasText } from '@toneflix/flipbook-core';
|
|
6
|
+
import { Gradient } from './Gradient';
|
|
7
|
+
import { PAPERS, useSerif } from './look';
|
|
8
|
+
import { Ornament, PAGE_RATIO } from './pages';
|
|
9
|
+
const CORNERS = ['tl', 'tr', 'bl', 'br'];
|
|
10
|
+
const MIRROR = {
|
|
11
|
+
tl: undefined,
|
|
12
|
+
tr: [{ scaleX: -1 }],
|
|
13
|
+
bl: [{ scaleY: -1 }],
|
|
14
|
+
br: [{ scaleX: -1 }, { scaleY: -1 }],
|
|
15
|
+
};
|
|
16
|
+
function Art({ art, color, style }) {
|
|
17
|
+
return (_jsxs(Svg, { viewBox: art.viewBox, preserveAspectRatio: "none", style: style, pointerEvents: "none", children: [art.strokes.map(({ d, width, opacity = 1 }) => (_jsx(Path, { d: d, fill: "none", stroke: color, strokeWidth: width, strokeOpacity: opacity }, d))), art.fills.map((d) => (_jsx(Path, { d: d, fill: color }, d)))] }));
|
|
18
|
+
}
|
|
19
|
+
/** Gilt rules and a flourish in each corner. */
|
|
20
|
+
function GiltFrame({ unit, color }) {
|
|
21
|
+
const size = 18 * unit;
|
|
22
|
+
const at = 3.6 * unit;
|
|
23
|
+
const place = {
|
|
24
|
+
tl: { top: at, left: at },
|
|
25
|
+
tr: { top: at, right: at },
|
|
26
|
+
bl: { bottom: at, left: at },
|
|
27
|
+
br: { bottom: at, right: at },
|
|
28
|
+
};
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsx(View, { pointerEvents: "none", style: [styles.fill, { margin: 5 * unit, borderWidth: 0.35 * unit, borderColor: color, opacity: 0.85 }] }), _jsx(View, { pointerEvents: "none", style: [styles.fill, { margin: 6.6 * unit, borderWidth: 0.15 * unit, borderColor: color, opacity: 0.85 }] }), CORNERS.map((corner) => (_jsx(Art, { art: FLEURON, color: color, style: { position: 'absolute', width: size, height: size, ...place[corner], transform: MIRROR[corner] } }, corner)))] }));
|
|
30
|
+
}
|
|
31
|
+
/** A photo clipped to an oval or an arch, ruled in the ink. */
|
|
32
|
+
function ShapedPhoto({ photo, shape, width, height, unit, color, show, }) {
|
|
33
|
+
const id = `c${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
34
|
+
const r = width / 2;
|
|
35
|
+
const arch = `M0 ${height}V${r}A${r} ${r} 0 0 1 ${width} ${r}V${height}Z`;
|
|
36
|
+
const outline = shape === 'oval' ? _jsx(Ellipse, { cx: r, cy: height / 2, rx: r - 0.3 * unit, ry: height / 2 - 0.3 * unit }) : _jsx(Path, { d: arch });
|
|
37
|
+
return (_jsxs(Svg, { width: width, height: height, children: [_jsx(Defs, { children: _jsx(ClipPath, { id: id, children: outline }) }), show ? (_jsx(SvgImage, { href: { uri: photo.src }, width: width, height: height, preserveAspectRatio: "xMidYMid slice", clipPath: `url(#${id})` })) : (_jsx(Rect, { width: width, height: height, fill: color, fillOpacity: 0.15, clipPath: `url(#${id})` })), shape === 'oval' ? (_jsx(Ellipse, { cx: r, cy: height / 2, rx: r - 0.3 * unit, ry: height / 2 - 0.3 * unit, fill: "none", stroke: color, strokeWidth: 0.6 * unit })) : (_jsx(Path, { d: arch, fill: "none", stroke: color, strokeWidth: 1 * unit }))] }));
|
|
38
|
+
}
|
|
39
|
+
const Photo = ({ photo, show }) => show ? _jsx(Image, { source: { uri: photo.src }, style: StyleSheet.absoluteFill, resizeMode: "cover", "aria-label": photo.alt ?? undefined }) : null;
|
|
40
|
+
/** Cover board: cloth, light, the hinge shadow along the spine and a grain finish. */
|
|
41
|
+
function Board({ book, width, hinge, children }) {
|
|
42
|
+
const theme = COVER_THEMES[book.theme ?? 'classic'];
|
|
43
|
+
const id = useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
44
|
+
const shade = (a, offset) => ({ offset, color: '#000', opacity: a });
|
|
45
|
+
return (_jsxs(View, { style: [styles.board, { width, height: width / PAGE_RATIO, backgroundColor: theme.cloth }], children: [children, _jsxs(Svg, { style: StyleSheet.absoluteFill, width: "100%", height: "100%", pointerEvents: "none", children: [_jsxs(Defs, { children: [_jsxs(RadialGradient, { id: `sheen${id}`, cx: "30%", cy: "20%", rx: "45%", ry: "45%", children: [_jsx(Stop, { offset: "0", stopColor: "#fff", stopOpacity: 0.1 }), _jsx(Stop, { offset: "1", stopColor: "#fff", stopOpacity: 0 })] }), _jsxs(RadialGradient, { id: `edge${id}`, cx: "50%", cy: "50%", rx: "70%", ry: "70%", children: [_jsx(Stop, { offset: "0.6", stopColor: "#000", stopOpacity: 0 }), _jsx(Stop, { offset: "1", stopColor: "#000", stopOpacity: 0.25 })] })] }), _jsx(Rect, { width: "100%", height: "100%", fill: `url(#sheen${id})` }), _jsx(Rect, { width: "100%", height: "100%", fill: `url(#edge${id})` })] }), _jsx(Image, { source: PAPERS.ivory.grain, resizeMode: "repeat", pointerEvents: "none", style: [StyleSheet.absoluteFill, { opacity: 0.25, mixBlendMode: 'soft-light' }] }), _jsx(Gradient, { to: hinge === 'left' ? 'right' : 'left', style: [styles.hinge, hinge === 'left' ? styles.left : styles.right, { width: width * 0.04 }], stops: [shade(0.35, 0), shade(0.1, 0.5), shade(0, 1)] })] }));
|
|
46
|
+
}
|
|
47
|
+
/** The front cover, laid out by the book's `coverDesign`. */
|
|
48
|
+
export function CoverView({ book, width, showImages = true }) {
|
|
49
|
+
const color = COVER_THEMES[book.theme ?? 'classic'].ink;
|
|
50
|
+
const serif = useSerif();
|
|
51
|
+
const unit = width / 100;
|
|
52
|
+
const text = coverHasText(book);
|
|
53
|
+
const stamped = {
|
|
54
|
+
textShadowColor: 'rgba(0,0,0,0.35)',
|
|
55
|
+
textShadowOffset: { width: 0, height: 0.15 * unit },
|
|
56
|
+
textShadowRadius: 0,
|
|
57
|
+
};
|
|
58
|
+
const kicker = (ink = color) => book.kicker ? (_jsx(Text, { style: [styles.kicker, { fontSize: 2.6 * unit, letterSpacing: 0.78 * unit, color: ink }], children: book.kicker.toUpperCase() })) : null;
|
|
59
|
+
const title = (size, ink = color, extra) => (_jsx(Text, { style: [
|
|
60
|
+
serif.upright,
|
|
61
|
+
styles.center,
|
|
62
|
+
stamped,
|
|
63
|
+
{ marginTop: 4 * unit, fontSize: size * unit, lineHeight: size * 1.08 * unit, fontWeight: '500', color: ink },
|
|
64
|
+
extra,
|
|
65
|
+
], children: book.title }));
|
|
66
|
+
const subtitle = (size = 4, ink = color) => book.subtitle ? (_jsx(Text, { style: [
|
|
67
|
+
serif.italic,
|
|
68
|
+
styles.center,
|
|
69
|
+
{ marginTop: 3 * unit, fontSize: size * unit, lineHeight: size * 1.35 * unit, color: ink, opacity: 0.85 },
|
|
70
|
+
], children: book.subtitle })) : null;
|
|
71
|
+
const designs = {
|
|
72
|
+
classic: () => (_jsxs(View, { style: [styles.panel, { margin: 6 * unit, paddingVertical: 8 * unit, paddingHorizontal: 6 * unit, borderColor: color }], children: [text && kicker(), text && title(9), book.cover ? (_jsx(View, { style: [styles.coverPhoto, { marginTop: 7 * unit, borderWidth: 0.8 * unit, borderColor: color }], children: _jsx(Photo, { photo: book.cover, show: showImages }) })) : (text && subtitle())] })),
|
|
73
|
+
gilt: () => (_jsxs(_Fragment, { children: [_jsx(GiltFrame, { unit: unit, color: color }), _jsxs(View, { style: [styles.stack, { top: 14 * unit, bottom: 14 * unit, left: 12 * unit, right: 12 * unit }], children: [text && kicker(), text && title(8), book.cover && (_jsx(View, { style: { marginTop: 6 * unit }, children: _jsx(ShapedPhoto, { photo: book.cover, shape: "oval", width: 52 * unit, height: 64 * unit, unit: unit, color: color, show: showImages }) })), text && subtitle()] })] })),
|
|
74
|
+
window: () => (_jsxs(_Fragment, { children: [_jsxs(View, { style: [styles.window, { top: 10 * unit, left: 12 * unit, right: 12 * unit, height: 76 * unit, borderRadius: 0.6 * unit }], children: [book.cover ? (_jsx(Photo, { photo: book.cover, show: showImages })) : (_jsx(View, { style: [styles.fill, styles.centered], children: _jsx(Ornament, { size: 16 * unit, color: color }) })), _jsx(Gradient, { to: "bottom", style: [styles.edge, { top: 0, height: 4 * unit }], stops: [
|
|
75
|
+
{ offset: 0, color: '#000', opacity: 0.55 },
|
|
76
|
+
{ offset: 1, color: '#000', opacity: 0 },
|
|
77
|
+
] }), _jsx(Gradient, { to: "right", style: [styles.edgeV, { left: 0, width: 2 * unit }], stops: [
|
|
78
|
+
{ offset: 0, color: '#000', opacity: 0.3 },
|
|
79
|
+
{ offset: 1, color: '#000', opacity: 0 },
|
|
80
|
+
] }), _jsx(Gradient, { to: "left", style: [styles.edgeV, { right: 0, width: 2 * unit }], stops: [
|
|
81
|
+
{ offset: 0, color: '#000', opacity: 0.25 },
|
|
82
|
+
{ offset: 1, color: '#000', opacity: 0 },
|
|
83
|
+
] })] }), text && (_jsxs(View, { style: [styles.stack, { top: 90 * unit, bottom: 8 * unit, left: 10 * unit, right: 10 * unit }], children: [kicker(), title(7.5, color, { textShadowColor: 'rgba(0,0,0,0.45)', textShadowOffset: { width: 0, height: -0.15 * unit } }), subtitle(3.4)] }))] })),
|
|
84
|
+
label: () => (_jsxs(_Fragment, { children: [book.cover && (_jsxs(View, { style: [styles.print, { top: 11 * unit, left: 16 * unit, right: 16 * unit, height: 62 * unit, padding: 2.4 * unit }], children: [_jsx(View, { style: [styles.flex, styles.clip], children: _jsx(Photo, { photo: book.cover, show: showImages }) }), _jsx(Svg, { style: StyleSheet.absoluteFill, viewBox: "0 0 100 100", preserveAspectRatio: "none", pointerEvents: "none", children: _jsx(Path, { d: "M0 0H9L0 9Z M100 0V9L91 0Z M0 100V91L9 100Z M100 100H91L100 91Z", fill: "#1f1b18" }) })] })), text && (_jsxs(View, { style: [styles.label, { top: (book.cover ? 84 : 44) * unit, left: 18 * unit, right: 18 * unit, padding: 4 * unit }], children: [_jsx(View, { pointerEvents: "none", style: [styles.fill, { margin: 1.2 * unit, borderWidth: 0.2 * unit, borderColor: 'rgba(60,45,30,0.5)' }] }), kicker('#3a2f25'), title(6.4, '#3a2f25', { marginTop: 1 * unit, textShadowColor: 'transparent' }), subtitle(3, '#3a2f25')] }))] })),
|
|
85
|
+
deco: () => (_jsxs(_Fragment, { children: [_jsx(Art, { art: DECO, color: color, style: StyleSheet.absoluteFill }), _jsxs(View, { style: [styles.stack, { top: 17 * unit, bottom: 10 * unit, left: 14 * unit, right: 14 * unit, justifyContent: 'flex-start' }], children: [text && kicker(), book.cover && (_jsx(View, { style: { marginTop: 4 * unit }, children: _jsx(ShapedPhoto, { photo: book.cover, shape: "arch", width: 56 * unit, height: 62 * unit, unit: unit, color: color, show: showImages }) })), text && title(6.6, color, { marginTop: 7 * unit, fontWeight: '400', letterSpacing: 0.8 * unit, textTransform: 'uppercase' }), text && subtitle(3.4)] })] })),
|
|
86
|
+
image: () => (_jsxs(_Fragment, { children: [book.cover && _jsx(Photo, { photo: book.cover, show: showImages }), book.cover && (_jsx(View, { pointerEvents: "none", style: [StyleSheet.absoluteFill, { backgroundColor: color, opacity: 0.18, mixBlendMode: 'soft-light' }] })), text &&
|
|
87
|
+
(book.cover ? (_jsxs(View, { style: [styles.caption, { paddingTop: 26 * unit, paddingHorizontal: 10 * unit, paddingBottom: 10 * unit }], children: [_jsx(Gradient, { to: "top", stops: [
|
|
88
|
+
{ offset: 0, color: '#000', opacity: 0.65 },
|
|
89
|
+
{ offset: 0.55, color: '#000', opacity: 0.25 },
|
|
90
|
+
{ offset: 1, color: '#000', opacity: 0 },
|
|
91
|
+
] }), kicker('#fff'), title(9, '#fff', { textShadowColor: 'rgba(0,0,0,0.5)', textShadowRadius: 1.2 * unit }), subtitle(4, '#fff')] })) : (_jsxs(View, { style: [styles.fill, styles.centered, { padding: 10 * unit }], children: [kicker(), title(9), subtitle()] })))] })),
|
|
92
|
+
};
|
|
93
|
+
return (_jsx(Board, { book: book, width: width, hinge: "left", children: designs[book.coverDesign ?? 'classic']() }));
|
|
94
|
+
}
|
|
95
|
+
/** The back cover: the same board, with the imprint. */
|
|
96
|
+
export function BackCoverView({ book, width }) {
|
|
97
|
+
const color = COVER_THEMES[book.theme ?? 'classic'].ink;
|
|
98
|
+
const unit = width / 100;
|
|
99
|
+
return (_jsxs(Board, { book: book, width: width, hinge: "right", children: [book.coverDesign === 'gilt' && _jsx(GiltFrame, { unit: unit, color: color }), _jsxs(View, { style: [styles.imprint, { bottom: 10 * unit }], children: [_jsx(Ornament, { size: 9 * unit, color: color }), book.imprint ? (_jsx(Text, { style: [styles.kicker, { marginTop: 2 * unit, fontSize: 2.4 * unit, letterSpacing: 0.72 * unit, color, opacity: 0.6 }], children: book.imprint.toUpperCase() })) : null] })] }));
|
|
100
|
+
}
|
|
101
|
+
const styles = StyleSheet.create({
|
|
102
|
+
board: { overflow: 'hidden' },
|
|
103
|
+
fill: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 },
|
|
104
|
+
flex: { flex: 1 },
|
|
105
|
+
clip: { overflow: 'hidden' },
|
|
106
|
+
centered: { alignItems: 'center', justifyContent: 'center' },
|
|
107
|
+
center: { textAlign: 'center' },
|
|
108
|
+
hinge: { position: 'absolute', top: 0, bottom: 0 },
|
|
109
|
+
left: { left: 0 },
|
|
110
|
+
right: { right: 0 },
|
|
111
|
+
panel: { flex: 1, alignItems: 'center', borderWidth: StyleSheet.hairlineWidth },
|
|
112
|
+
kicker: { textAlign: 'center', opacity: 0.7 },
|
|
113
|
+
coverPhoto: { width: '100%', flex: 1, maxHeight: '62%', overflow: 'hidden' },
|
|
114
|
+
stack: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
|
|
115
|
+
window: { position: 'absolute', overflow: 'hidden', backgroundColor: 'rgba(0,0,0,0.12)' },
|
|
116
|
+
edge: { position: 'absolute', left: 0, right: 0 },
|
|
117
|
+
edgeV: { position: 'absolute', top: 0, bottom: 0 },
|
|
118
|
+
print: { position: 'absolute', backgroundColor: '#f7f2e7', transform: [{ rotate: '-2.5deg' }], boxShadow: '0 3px 8px rgba(0,0,0,0.4)' },
|
|
119
|
+
label: { position: 'absolute', alignItems: 'center', backgroundColor: '#f4eddc', boxShadow: '0 1px 4px rgba(0,0,0,0.3)' },
|
|
120
|
+
caption: { position: 'absolute', left: 0, right: 0, bottom: 0, alignItems: 'center' },
|
|
121
|
+
imprint: { position: 'absolute', left: 0, right: 0, alignItems: 'center' },
|
|
122
|
+
});
|
package/lib/Flipbook.js
CHANGED
|
@@ -6,7 +6,8 @@ import { BookLayout, COVER_THEMES, DEFAULT_LABELS, FlipbookReader, } from '@tone
|
|
|
6
6
|
import { FoldLayout } from './geometry';
|
|
7
7
|
import { Gradient } from './Gradient';
|
|
8
8
|
import { BookLookProvider } from './look';
|
|
9
|
-
import { BackCoverView,
|
|
9
|
+
import { BackCoverView, CoverView } from './Cover';
|
|
10
|
+
import { BlankPageView, ColophonView, PAGE_RATIO, PageView, TitlePageView } from './pages';
|
|
10
11
|
import { lockLandscape } from './orientation';
|
|
11
12
|
/** One face's content. Memoised so turning a page only moves its wrappers. */
|
|
12
13
|
const FaceContent = memo(function FaceContent({ face, book, width, side, }) {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from '@toneflix/flipbook-core';
|
|
2
2
|
export { Flipbook, type FlipbookProps, type FlipbookHandle, type FlipbookPlace } from './Flipbook';
|
|
3
|
-
export { PageView, Paper,
|
|
3
|
+
export { PageView, Paper, TitlePageView, ColophonView, BlankPageView, PAGE_RATIO, type PageViewProps } from './pages';
|
|
4
|
+
export { CoverView, BackCoverView, type CoverViewProps } from './Cover';
|
|
4
5
|
export { PrintedPhoto, type PrintedPhotoProps } from './PrintedPhoto';
|
|
5
6
|
export { BookLookProvider, useBookLook, usePaper, useSerif, ink, PAPERS, type BookLook, type BookFonts, type PaperLook } from './look';
|
|
6
7
|
export { Gradient, type GradientStop } from './Gradient';
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from '@toneflix/flipbook-core';
|
|
2
2
|
export { Flipbook } from './Flipbook';
|
|
3
|
-
export { PageView, Paper,
|
|
3
|
+
export { PageView, Paper, TitlePageView, ColophonView, BlankPageView, PAGE_RATIO } from './pages';
|
|
4
|
+
export { CoverView, BackCoverView } from './Cover';
|
|
4
5
|
export { PrintedPhoto } from './PrintedPhoto';
|
|
5
6
|
export { BookLookProvider, useBookLook, usePaper, useSerif, ink, PAPERS } from './look';
|
|
6
7
|
export { Gradient } from './Gradient';
|
package/lib/look.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext } from 'react';
|
|
3
3
|
import { Platform } from 'react-native';
|
|
4
|
-
const fine = { texture: require('../assets/textures/paper-fine
|
|
4
|
+
const fine = { texture: require('../assets/textures/paper-fine.webp'), grain: require('../assets/textures/grain-fine.webp') };
|
|
5
5
|
const ivory = {
|
|
6
6
|
background: '#fdf9f0',
|
|
7
7
|
ink: [61, 55, 47],
|
|
@@ -25,9 +25,9 @@ export const PAPERS = {
|
|
|
25
25
|
plateTint: 'rgba(90,70,40,0.03)',
|
|
26
26
|
plateLow: 'rgba(80,50,20,0.16)',
|
|
27
27
|
plateHigh: 'rgba(255,245,225,0.35)',
|
|
28
|
-
texture: require('../assets/textures/paper-kraft
|
|
28
|
+
texture: require('../assets/textures/paper-kraft.webp'),
|
|
29
29
|
textureOpacity: 0.45,
|
|
30
|
-
grain: require('../assets/textures/grain-kraft
|
|
30
|
+
grain: require('../assets/textures/grain-kraft.webp'),
|
|
31
31
|
grainOpacity: 0.35,
|
|
32
32
|
light: 'rgba(255,255,255,0.18)',
|
|
33
33
|
vignette: 'rgba(80,50,20,0.12)',
|
|
@@ -39,9 +39,9 @@ export const PAPERS = {
|
|
|
39
39
|
plateTint: 'rgba(0,0,0,0.08)',
|
|
40
40
|
plateLow: 'rgba(0,0,0,0.35)',
|
|
41
41
|
plateHigh: 'rgba(255,255,255,0.07)',
|
|
42
|
-
texture: require('../assets/textures/paper-charcoal
|
|
42
|
+
texture: require('../assets/textures/paper-charcoal.webp'),
|
|
43
43
|
textureOpacity: 0.25,
|
|
44
|
-
grain: require('../assets/textures/grain-charcoal
|
|
44
|
+
grain: require('../assets/textures/grain-charcoal.webp'),
|
|
45
45
|
grainOpacity: 0.22,
|
|
46
46
|
light: 'rgba(255,255,255,0.05)',
|
|
47
47
|
vignette: 'rgba(0,0,0,0.25)',
|
package/lib/pages.d.ts
CHANGED
|
@@ -29,17 +29,6 @@ export declare function Paper({ width, side, children, style }: {
|
|
|
29
29
|
children?: ReactNode;
|
|
30
30
|
style?: ViewStyle;
|
|
31
31
|
}): import("react").JSX.Element;
|
|
32
|
-
export interface CoverViewProps {
|
|
33
|
-
book: Pick<FlipbookBook, 'title' | 'subtitle' | 'theme' | 'cover' | 'kicker'>;
|
|
34
|
-
width: number;
|
|
35
|
-
showImages?: boolean;
|
|
36
|
-
}
|
|
37
|
-
/** The front cover: cloth with the title and an inset photo. */
|
|
38
|
-
export declare function CoverView({ book, width, showImages }: CoverViewProps): import("react").JSX.Element;
|
|
39
|
-
export declare function BackCoverView({ book, width }: {
|
|
40
|
-
book: Pick<FlipbookBook, 'theme' | 'imprint'>;
|
|
41
|
-
width: number;
|
|
42
|
-
}): import("react").JSX.Element;
|
|
43
32
|
/** Inside the front cover: a title page. */
|
|
44
33
|
export declare function TitlePageView({ book, width, side, }: {
|
|
45
34
|
book: Pick<FlipbookBook, 'title' | 'subtitle' | 'author' | 'year'>;
|
|
@@ -56,4 +45,8 @@ export declare function BlankPageView({ width, side }: {
|
|
|
56
45
|
width: number;
|
|
57
46
|
side?: Side;
|
|
58
47
|
}): import("react").JSX.Element;
|
|
48
|
+
export declare function Ornament({ size, color }: {
|
|
49
|
+
size: number;
|
|
50
|
+
color: string;
|
|
51
|
+
}): import("react").JSX.Element;
|
|
59
52
|
export {};
|
package/lib/pages.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useId } from 'react';
|
|
3
3
|
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
4
4
|
import Svg, { Circle, Defs, Path, RadialGradient, Rect, Stop } from 'react-native-svg';
|
|
5
|
-
import {
|
|
5
|
+
import { PLACED_LAYOUTS, photoFrame } from '@toneflix/flipbook-core';
|
|
6
6
|
import { Gradient } from './Gradient';
|
|
7
7
|
import { ink, useBookLook, usePaper, useSerif } from './look';
|
|
8
8
|
import { PrintedPhoto } from './PrintedPhoto';
|
|
@@ -67,29 +67,6 @@ function Gutter({ side }) {
|
|
|
67
67
|
const sheen = (a, offset) => ({ offset, color: 'rgb(255,250,240)', opacity: a });
|
|
68
68
|
return (_jsxs(_Fragment, { children: [_jsx(Gradient, { to: to, stops: [shade(0.5, 0), shade(0.3, 0.015), shade(0.14, 0.04), shade(0.05, 0.08), shade(0, 0.15), shade(0, 0.94), shade(0.07, 1)] }), _jsx(Gradient, { to: to, stops: [sheen(0, 0.06), sheen(0.07, 0.13), sheen(0.02, 0.26), sheen(0, 0.4)] })] }));
|
|
69
69
|
}
|
|
70
|
-
/** Cloth for a cover, with the hinge shadow along the spine. */
|
|
71
|
-
function Cloth({ book, width, hinge, children }) {
|
|
72
|
-
const theme = COVER_THEMES[book.theme ?? 'classic'];
|
|
73
|
-
const id = useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
74
|
-
const shade = (a, offset) => ({ offset, color: '#000', opacity: a });
|
|
75
|
-
return (_jsxs(View, { style: [styles.page, { width, height: width / PAGE_RATIO, backgroundColor: theme.cloth }], children: [_jsxs(Svg, { style: StyleSheet.absoluteFill, width: "100%", height: "100%", pointerEvents: "none", children: [_jsx(Defs, { children: _jsxs(RadialGradient, { id: `sheen${id}`, cx: "30%", cy: "20%", rx: "45%", ry: "45%", children: [_jsx(Stop, { offset: "0", stopColor: "#fff", stopOpacity: 0.08 }), _jsx(Stop, { offset: "1", stopColor: "#fff", stopOpacity: 0 })] }) }), _jsx(Rect, { width: "100%", height: "100%", fill: `url(#sheen${id})` })] }), _jsx(Gradient, { to: hinge === 'left' ? 'right' : 'left', style: [styles.hinge, hinge === 'left' ? styles.left : styles.right, { width: width * 0.04 }], stops: [shade(0.35, 0), shade(0.1, 0.5), shade(0, 1)] }), children] }));
|
|
76
|
-
}
|
|
77
|
-
/** The front cover: cloth with the title and an inset photo. */
|
|
78
|
-
export function CoverView({ book, width, showImages = true }) {
|
|
79
|
-
const color = COVER_THEMES[book.theme ?? 'classic'].ink;
|
|
80
|
-
const serif = useSerif();
|
|
81
|
-
const unit = width / 100;
|
|
82
|
-
return (_jsx(Cloth, { book: book, width: width, hinge: "left", children: _jsxs(View, { style: [styles.panel, { margin: 6 * unit, paddingVertical: 8 * unit, paddingHorizontal: 6 * unit, borderColor: color }], children: [book.kicker ? (_jsx(Text, { style: [styles.kicker, { fontSize: 2.6 * unit, letterSpacing: 0.78 * unit, color }], children: book.kicker.toUpperCase() })) : null, _jsx(Text, { style: [
|
|
83
|
-
serif.upright,
|
|
84
|
-
styles.center,
|
|
85
|
-
{ marginTop: 5 * unit, fontSize: 9 * unit, lineHeight: 9.5 * unit, fontWeight: '500', color },
|
|
86
|
-
], children: book.title }), book.cover ? (_jsx(View, { style: [styles.coverPhoto, { marginTop: 7 * unit, borderWidth: 0.8 * unit, borderColor: color }], children: showImages && (_jsx(Image, { source: { uri: book.cover.src }, style: StyleSheet.absoluteFill, resizeMode: "cover", "aria-label": book.cover.alt ?? undefined })) })) : book.subtitle ? (_jsx(Text, { style: [serif.italic, styles.center, { marginTop: 6 * unit, fontSize: 4 * unit, color, opacity: 0.8 }], children: book.subtitle })) : null] }) }));
|
|
87
|
-
}
|
|
88
|
-
export function BackCoverView({ book, width }) {
|
|
89
|
-
const color = COVER_THEMES[book.theme ?? 'classic'].ink;
|
|
90
|
-
const unit = width / 100;
|
|
91
|
-
return (_jsx(Cloth, { book: book, width: width, hinge: "right", children: _jsxs(View, { style: [styles.imprint, { bottom: 10 * unit }], children: [_jsx(Ornament, { size: 9 * unit, color: color }), book.imprint ? (_jsx(Text, { style: [styles.kicker, { marginTop: 2 * unit, fontSize: 2.4 * unit, letterSpacing: 0.72 * unit, color, opacity: 0.6 }], children: book.imprint.toUpperCase() })) : null] }) }));
|
|
92
|
-
}
|
|
93
70
|
/** Inside the front cover: a title page. */
|
|
94
71
|
export function TitlePageView({ book, width, side, }) {
|
|
95
72
|
const look = usePaper();
|
|
@@ -108,7 +85,7 @@ export function ColophonView({ book, width, side }) {
|
|
|
108
85
|
export function BlankPageView({ width, side }) {
|
|
109
86
|
return _jsx(Paper, { width: width, side: side });
|
|
110
87
|
}
|
|
111
|
-
function Ornament({ size, color }) {
|
|
88
|
+
export function Ornament({ size, color }) {
|
|
112
89
|
return (_jsxs(Svg, { viewBox: "0 0 40 40", width: size, height: size, children: [_jsx(Circle, { cx: "20", cy: "20", r: "18.5", fill: "none", stroke: color, strokeOpacity: 0.6 }), _jsx(Path, { d: "M20 9 L 27 20 L 20 31 L 13 20 Z", fill: "none", stroke: color, strokeWidth: 1.4, strokeLinejoin: "round" }), _jsx(Circle, { cx: "20", cy: "20", r: "2.2", fill: color })] }));
|
|
113
90
|
}
|
|
114
91
|
const styles = StyleSheet.create({
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toneflix/flipbook-native",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "An expressive flipbook for creating personalised and interactive reading and photo viewing experiences, for React Native and Expo.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://toneflix.github.io/flipbook/frameworks/react-native",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/toneflix/flipbook/issues"
|
|
9
|
+
},
|
|
6
10
|
"repository": {
|
|
7
11
|
"type": "git",
|
|
8
12
|
"url": "https://github.com/toneflix/flipbook",
|
|
@@ -25,7 +29,7 @@
|
|
|
25
29
|
"assets"
|
|
26
30
|
],
|
|
27
31
|
"dependencies": {
|
|
28
|
-
"@toneflix/flipbook-core": "^0.2.
|
|
32
|
+
"@toneflix/flipbook-core": "^0.2.2"
|
|
29
33
|
},
|
|
30
34
|
"peerDependencies": {
|
|
31
35
|
"expo-screen-orientation": "*",
|
package/src/Cover.tsx
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { useId, type ReactNode } from 'react'
|
|
2
|
+
import { Image, StyleSheet, Text, View, type TextStyle, type ViewStyle } from 'react-native'
|
|
3
|
+
import Svg, { ClipPath, Defs, Ellipse, Image as SvgImage, Path, RadialGradient, Rect, Stop } from 'react-native-svg'
|
|
4
|
+
import { COVER_THEMES, DECO, FLEURON, coverHasText, type CoverArt, type FlipbookBook, type FlipbookPhoto } from '@toneflix/flipbook-core'
|
|
5
|
+
import { Gradient } from './Gradient'
|
|
6
|
+
import { PAPERS, useSerif } from './look'
|
|
7
|
+
import { Ornament, PAGE_RATIO } from './pages'
|
|
8
|
+
|
|
9
|
+
type Side = 'left' | 'right'
|
|
10
|
+
type Corner = 'tl' | 'tr' | 'bl' | 'br'
|
|
11
|
+
|
|
12
|
+
const CORNERS: Corner[] = ['tl', 'tr', 'bl', 'br']
|
|
13
|
+
const MIRROR: Record<Corner, ViewStyle['transform']> = {
|
|
14
|
+
tl: undefined,
|
|
15
|
+
tr: [{ scaleX: -1 }],
|
|
16
|
+
bl: [{ scaleY: -1 }],
|
|
17
|
+
br: [{ scaleX: -1 }, { scaleY: -1 }],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Art({ art, color, style }: { art: CoverArt; color: string; style: ViewStyle }) {
|
|
21
|
+
return (
|
|
22
|
+
<Svg viewBox={art.viewBox} preserveAspectRatio="none" style={style} pointerEvents="none">
|
|
23
|
+
{art.strokes.map(({ d, width, opacity = 1 }) => (
|
|
24
|
+
<Path key={d} d={d} fill="none" stroke={color} strokeWidth={width} strokeOpacity={opacity} />
|
|
25
|
+
))}
|
|
26
|
+
{art.fills.map((d) => (
|
|
27
|
+
<Path key={d} d={d} fill={color} />
|
|
28
|
+
))}
|
|
29
|
+
</Svg>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Gilt rules and a flourish in each corner. */
|
|
34
|
+
function GiltFrame({ unit, color }: { unit: number; color: string }) {
|
|
35
|
+
const size = 18 * unit
|
|
36
|
+
const at = 3.6 * unit
|
|
37
|
+
const place: Record<Corner, ViewStyle> = {
|
|
38
|
+
tl: { top: at, left: at },
|
|
39
|
+
tr: { top: at, right: at },
|
|
40
|
+
bl: { bottom: at, left: at },
|
|
41
|
+
br: { bottom: at, right: at },
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<View pointerEvents="none" style={[styles.fill, { margin: 5 * unit, borderWidth: 0.35 * unit, borderColor: color, opacity: 0.85 }]} />
|
|
47
|
+
<View
|
|
48
|
+
pointerEvents="none"
|
|
49
|
+
style={[styles.fill, { margin: 6.6 * unit, borderWidth: 0.15 * unit, borderColor: color, opacity: 0.85 }]}
|
|
50
|
+
/>
|
|
51
|
+
{CORNERS.map((corner) => (
|
|
52
|
+
<Art
|
|
53
|
+
key={corner}
|
|
54
|
+
art={FLEURON}
|
|
55
|
+
color={color}
|
|
56
|
+
style={{ position: 'absolute', width: size, height: size, ...place[corner], transform: MIRROR[corner] }}
|
|
57
|
+
/>
|
|
58
|
+
))}
|
|
59
|
+
</>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A photo clipped to an oval or an arch, ruled in the ink. */
|
|
64
|
+
function ShapedPhoto({
|
|
65
|
+
photo,
|
|
66
|
+
shape,
|
|
67
|
+
width,
|
|
68
|
+
height,
|
|
69
|
+
unit,
|
|
70
|
+
color,
|
|
71
|
+
show,
|
|
72
|
+
}: {
|
|
73
|
+
photo: FlipbookPhoto
|
|
74
|
+
shape: 'oval' | 'arch'
|
|
75
|
+
width: number
|
|
76
|
+
height: number
|
|
77
|
+
unit: number
|
|
78
|
+
color: string
|
|
79
|
+
show: boolean
|
|
80
|
+
}) {
|
|
81
|
+
const id = `c${useId().replace(/[^a-zA-Z0-9]/g, '')}`
|
|
82
|
+
const r = width / 2
|
|
83
|
+
const arch = `M0 ${height}V${r}A${r} ${r} 0 0 1 ${width} ${r}V${height}Z`
|
|
84
|
+
const outline = shape === 'oval' ? <Ellipse cx={r} cy={height / 2} rx={r - 0.3 * unit} ry={height / 2 - 0.3 * unit} /> : <Path d={arch} />
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Svg width={width} height={height}>
|
|
88
|
+
<Defs>
|
|
89
|
+
<ClipPath id={id}>{outline}</ClipPath>
|
|
90
|
+
</Defs>
|
|
91
|
+
{show ? (
|
|
92
|
+
<SvgImage href={{ uri: photo.src }} width={width} height={height} preserveAspectRatio="xMidYMid slice" clipPath={`url(#${id})`} />
|
|
93
|
+
) : (
|
|
94
|
+
<Rect width={width} height={height} fill={color} fillOpacity={0.15} clipPath={`url(#${id})`} />
|
|
95
|
+
)}
|
|
96
|
+
{shape === 'oval' ? (
|
|
97
|
+
<Ellipse
|
|
98
|
+
cx={r}
|
|
99
|
+
cy={height / 2}
|
|
100
|
+
rx={r - 0.3 * unit}
|
|
101
|
+
ry={height / 2 - 0.3 * unit}
|
|
102
|
+
fill="none"
|
|
103
|
+
stroke={color}
|
|
104
|
+
strokeWidth={0.6 * unit}
|
|
105
|
+
/>
|
|
106
|
+
) : (
|
|
107
|
+
<Path d={arch} fill="none" stroke={color} strokeWidth={1 * unit} />
|
|
108
|
+
)}
|
|
109
|
+
</Svg>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const Photo = ({ photo, show }: { photo: FlipbookPhoto; show: boolean }) =>
|
|
114
|
+
show ? <Image source={{ uri: photo.src }} style={StyleSheet.absoluteFill} resizeMode="cover" aria-label={photo.alt ?? undefined} /> : null
|
|
115
|
+
|
|
116
|
+
/** Cover board: cloth, light, the hinge shadow along the spine and a grain finish. */
|
|
117
|
+
function Board({ book, width, hinge, children }: { book: Pick<FlipbookBook, 'theme'>; width: number; hinge: Side; children?: ReactNode }) {
|
|
118
|
+
const theme = COVER_THEMES[book.theme ?? 'classic']
|
|
119
|
+
const id = useId().replace(/[^a-zA-Z0-9]/g, '')
|
|
120
|
+
const shade = (a: number, offset: number) => ({ offset, color: '#000', opacity: a })
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<View style={[styles.board, { width, height: width / PAGE_RATIO, backgroundColor: theme.cloth }]}>
|
|
124
|
+
{children}
|
|
125
|
+
<Svg style={StyleSheet.absoluteFill} width="100%" height="100%" pointerEvents="none">
|
|
126
|
+
<Defs>
|
|
127
|
+
<RadialGradient id={`sheen${id}`} cx="30%" cy="20%" rx="45%" ry="45%">
|
|
128
|
+
<Stop offset="0" stopColor="#fff" stopOpacity={0.1} />
|
|
129
|
+
<Stop offset="1" stopColor="#fff" stopOpacity={0} />
|
|
130
|
+
</RadialGradient>
|
|
131
|
+
<RadialGradient id={`edge${id}`} cx="50%" cy="50%" rx="70%" ry="70%">
|
|
132
|
+
<Stop offset="0.6" stopColor="#000" stopOpacity={0} />
|
|
133
|
+
<Stop offset="1" stopColor="#000" stopOpacity={0.25} />
|
|
134
|
+
</RadialGradient>
|
|
135
|
+
</Defs>
|
|
136
|
+
<Rect width="100%" height="100%" fill={`url(#sheen${id})`} />
|
|
137
|
+
<Rect width="100%" height="100%" fill={`url(#edge${id})`} />
|
|
138
|
+
</Svg>
|
|
139
|
+
<Image
|
|
140
|
+
source={PAPERS.ivory.grain}
|
|
141
|
+
resizeMode="repeat"
|
|
142
|
+
pointerEvents="none"
|
|
143
|
+
style={[StyleSheet.absoluteFill, { opacity: 0.25, mixBlendMode: 'soft-light' }]}
|
|
144
|
+
/>
|
|
145
|
+
<Gradient
|
|
146
|
+
to={hinge === 'left' ? 'right' : 'left'}
|
|
147
|
+
style={[styles.hinge, hinge === 'left' ? styles.left : styles.right, { width: width * 0.04 }]}
|
|
148
|
+
stops={[shade(0.35, 0), shade(0.1, 0.5), shade(0, 1)]}
|
|
149
|
+
/>
|
|
150
|
+
</View>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface CoverViewProps {
|
|
155
|
+
book: Pick<FlipbookBook, 'title' | 'subtitle' | 'theme' | 'cover' | 'kicker' | 'coverDesign' | 'coverText'>
|
|
156
|
+
width: number
|
|
157
|
+
showImages?: boolean
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The front cover, laid out by the book's `coverDesign`. */
|
|
161
|
+
export function CoverView({ book, width, showImages = true }: CoverViewProps) {
|
|
162
|
+
const color = COVER_THEMES[book.theme ?? 'classic'].ink
|
|
163
|
+
const serif = useSerif()
|
|
164
|
+
const unit = width / 100
|
|
165
|
+
const text = coverHasText(book)
|
|
166
|
+
const stamped: TextStyle = {
|
|
167
|
+
textShadowColor: 'rgba(0,0,0,0.35)',
|
|
168
|
+
textShadowOffset: { width: 0, height: 0.15 * unit },
|
|
169
|
+
textShadowRadius: 0,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const kicker = (ink = color) =>
|
|
173
|
+
book.kicker ? (
|
|
174
|
+
<Text style={[styles.kicker, { fontSize: 2.6 * unit, letterSpacing: 0.78 * unit, color: ink }]}>{book.kicker.toUpperCase()}</Text>
|
|
175
|
+
) : null
|
|
176
|
+
const title = (size: number, ink = color, extra?: TextStyle) => (
|
|
177
|
+
<Text
|
|
178
|
+
style={[
|
|
179
|
+
serif.upright,
|
|
180
|
+
styles.center,
|
|
181
|
+
stamped,
|
|
182
|
+
{ marginTop: 4 * unit, fontSize: size * unit, lineHeight: size * 1.08 * unit, fontWeight: '500', color: ink },
|
|
183
|
+
extra,
|
|
184
|
+
]}
|
|
185
|
+
>
|
|
186
|
+
{book.title}
|
|
187
|
+
</Text>
|
|
188
|
+
)
|
|
189
|
+
const subtitle = (size = 4, ink = color) =>
|
|
190
|
+
book.subtitle ? (
|
|
191
|
+
<Text
|
|
192
|
+
style={[
|
|
193
|
+
serif.italic,
|
|
194
|
+
styles.center,
|
|
195
|
+
{ marginTop: 3 * unit, fontSize: size * unit, lineHeight: size * 1.35 * unit, color: ink, opacity: 0.85 },
|
|
196
|
+
]}
|
|
197
|
+
>
|
|
198
|
+
{book.subtitle}
|
|
199
|
+
</Text>
|
|
200
|
+
) : null
|
|
201
|
+
|
|
202
|
+
const designs = {
|
|
203
|
+
classic: () => (
|
|
204
|
+
<View style={[styles.panel, { margin: 6 * unit, paddingVertical: 8 * unit, paddingHorizontal: 6 * unit, borderColor: color }]}>
|
|
205
|
+
{text && kicker()}
|
|
206
|
+
{text && title(9)}
|
|
207
|
+
{book.cover ? (
|
|
208
|
+
<View style={[styles.coverPhoto, { marginTop: 7 * unit, borderWidth: 0.8 * unit, borderColor: color }]}>
|
|
209
|
+
<Photo photo={book.cover} show={showImages} />
|
|
210
|
+
</View>
|
|
211
|
+
) : (
|
|
212
|
+
text && subtitle()
|
|
213
|
+
)}
|
|
214
|
+
</View>
|
|
215
|
+
),
|
|
216
|
+
gilt: () => (
|
|
217
|
+
<>
|
|
218
|
+
<GiltFrame unit={unit} color={color} />
|
|
219
|
+
<View style={[styles.stack, { top: 14 * unit, bottom: 14 * unit, left: 12 * unit, right: 12 * unit }]}>
|
|
220
|
+
{text && kicker()}
|
|
221
|
+
{text && title(8)}
|
|
222
|
+
{book.cover && (
|
|
223
|
+
<View style={{ marginTop: 6 * unit }}>
|
|
224
|
+
<ShapedPhoto
|
|
225
|
+
photo={book.cover}
|
|
226
|
+
shape="oval"
|
|
227
|
+
width={52 * unit}
|
|
228
|
+
height={64 * unit}
|
|
229
|
+
unit={unit}
|
|
230
|
+
color={color}
|
|
231
|
+
show={showImages}
|
|
232
|
+
/>
|
|
233
|
+
</View>
|
|
234
|
+
)}
|
|
235
|
+
{text && subtitle()}
|
|
236
|
+
</View>
|
|
237
|
+
</>
|
|
238
|
+
),
|
|
239
|
+
window: () => (
|
|
240
|
+
<>
|
|
241
|
+
<View style={[styles.window, { top: 10 * unit, left: 12 * unit, right: 12 * unit, height: 76 * unit, borderRadius: 0.6 * unit }]}>
|
|
242
|
+
{book.cover ? (
|
|
243
|
+
<Photo photo={book.cover} show={showImages} />
|
|
244
|
+
) : (
|
|
245
|
+
<View style={[styles.fill, styles.centered]}>
|
|
246
|
+
<Ornament size={16 * unit} color={color} />
|
|
247
|
+
</View>
|
|
248
|
+
)}
|
|
249
|
+
<Gradient
|
|
250
|
+
to="bottom"
|
|
251
|
+
style={[styles.edge, { top: 0, height: 4 * unit }]}
|
|
252
|
+
stops={[
|
|
253
|
+
{ offset: 0, color: '#000', opacity: 0.55 },
|
|
254
|
+
{ offset: 1, color: '#000', opacity: 0 },
|
|
255
|
+
]}
|
|
256
|
+
/>
|
|
257
|
+
<Gradient
|
|
258
|
+
to="right"
|
|
259
|
+
style={[styles.edgeV, { left: 0, width: 2 * unit }]}
|
|
260
|
+
stops={[
|
|
261
|
+
{ offset: 0, color: '#000', opacity: 0.3 },
|
|
262
|
+
{ offset: 1, color: '#000', opacity: 0 },
|
|
263
|
+
]}
|
|
264
|
+
/>
|
|
265
|
+
<Gradient
|
|
266
|
+
to="left"
|
|
267
|
+
style={[styles.edgeV, { right: 0, width: 2 * unit }]}
|
|
268
|
+
stops={[
|
|
269
|
+
{ offset: 0, color: '#000', opacity: 0.25 },
|
|
270
|
+
{ offset: 1, color: '#000', opacity: 0 },
|
|
271
|
+
]}
|
|
272
|
+
/>
|
|
273
|
+
</View>
|
|
274
|
+
{text && (
|
|
275
|
+
<View style={[styles.stack, { top: 90 * unit, bottom: 8 * unit, left: 10 * unit, right: 10 * unit }]}>
|
|
276
|
+
{kicker()}
|
|
277
|
+
{title(7.5, color, { textShadowColor: 'rgba(0,0,0,0.45)', textShadowOffset: { width: 0, height: -0.15 * unit } })}
|
|
278
|
+
{subtitle(3.4)}
|
|
279
|
+
</View>
|
|
280
|
+
)}
|
|
281
|
+
</>
|
|
282
|
+
),
|
|
283
|
+
label: () => (
|
|
284
|
+
<>
|
|
285
|
+
{book.cover && (
|
|
286
|
+
<View style={[styles.print, { top: 11 * unit, left: 16 * unit, right: 16 * unit, height: 62 * unit, padding: 2.4 * unit }]}>
|
|
287
|
+
<View style={[styles.flex, styles.clip]}>
|
|
288
|
+
<Photo photo={book.cover} show={showImages} />
|
|
289
|
+
</View>
|
|
290
|
+
<Svg style={StyleSheet.absoluteFill} viewBox="0 0 100 100" preserveAspectRatio="none" pointerEvents="none">
|
|
291
|
+
<Path d="M0 0H9L0 9Z M100 0V9L91 0Z M0 100V91L9 100Z M100 100H91L100 91Z" fill="#1f1b18" />
|
|
292
|
+
</Svg>
|
|
293
|
+
</View>
|
|
294
|
+
)}
|
|
295
|
+
{text && (
|
|
296
|
+
<View style={[styles.label, { top: (book.cover ? 84 : 44) * unit, left: 18 * unit, right: 18 * unit, padding: 4 * unit }]}>
|
|
297
|
+
<View
|
|
298
|
+
pointerEvents="none"
|
|
299
|
+
style={[styles.fill, { margin: 1.2 * unit, borderWidth: 0.2 * unit, borderColor: 'rgba(60,45,30,0.5)' }]}
|
|
300
|
+
/>
|
|
301
|
+
{kicker('#3a2f25')}
|
|
302
|
+
{title(6.4, '#3a2f25', { marginTop: 1 * unit, textShadowColor: 'transparent' })}
|
|
303
|
+
{subtitle(3, '#3a2f25')}
|
|
304
|
+
</View>
|
|
305
|
+
)}
|
|
306
|
+
</>
|
|
307
|
+
),
|
|
308
|
+
deco: () => (
|
|
309
|
+
<>
|
|
310
|
+
<Art art={DECO} color={color} style={StyleSheet.absoluteFill as ViewStyle} />
|
|
311
|
+
<View
|
|
312
|
+
style={[styles.stack, { top: 17 * unit, bottom: 10 * unit, left: 14 * unit, right: 14 * unit, justifyContent: 'flex-start' }]}
|
|
313
|
+
>
|
|
314
|
+
{text && kicker()}
|
|
315
|
+
{book.cover && (
|
|
316
|
+
<View style={{ marginTop: 4 * unit }}>
|
|
317
|
+
<ShapedPhoto
|
|
318
|
+
photo={book.cover}
|
|
319
|
+
shape="arch"
|
|
320
|
+
width={56 * unit}
|
|
321
|
+
height={62 * unit}
|
|
322
|
+
unit={unit}
|
|
323
|
+
color={color}
|
|
324
|
+
show={showImages}
|
|
325
|
+
/>
|
|
326
|
+
</View>
|
|
327
|
+
)}
|
|
328
|
+
{text && title(6.6, color, { marginTop: 7 * unit, fontWeight: '400', letterSpacing: 0.8 * unit, textTransform: 'uppercase' })}
|
|
329
|
+
{text && subtitle(3.4)}
|
|
330
|
+
</View>
|
|
331
|
+
</>
|
|
332
|
+
),
|
|
333
|
+
image: () => (
|
|
334
|
+
<>
|
|
335
|
+
{book.cover && <Photo photo={book.cover} show={showImages} />}
|
|
336
|
+
{book.cover && (
|
|
337
|
+
<View
|
|
338
|
+
pointerEvents="none"
|
|
339
|
+
style={[StyleSheet.absoluteFill, { backgroundColor: color, opacity: 0.18, mixBlendMode: 'soft-light' }]}
|
|
340
|
+
/>
|
|
341
|
+
)}
|
|
342
|
+
{text &&
|
|
343
|
+
(book.cover ? (
|
|
344
|
+
<View style={[styles.caption, { paddingTop: 26 * unit, paddingHorizontal: 10 * unit, paddingBottom: 10 * unit }]}>
|
|
345
|
+
<Gradient
|
|
346
|
+
to="top"
|
|
347
|
+
stops={[
|
|
348
|
+
{ offset: 0, color: '#000', opacity: 0.65 },
|
|
349
|
+
{ offset: 0.55, color: '#000', opacity: 0.25 },
|
|
350
|
+
{ offset: 1, color: '#000', opacity: 0 },
|
|
351
|
+
]}
|
|
352
|
+
/>
|
|
353
|
+
{kicker('#fff')}
|
|
354
|
+
{title(9, '#fff', { textShadowColor: 'rgba(0,0,0,0.5)', textShadowRadius: 1.2 * unit })}
|
|
355
|
+
{subtitle(4, '#fff')}
|
|
356
|
+
</View>
|
|
357
|
+
) : (
|
|
358
|
+
<View style={[styles.fill, styles.centered, { padding: 10 * unit }]}>
|
|
359
|
+
{kicker()}
|
|
360
|
+
{title(9)}
|
|
361
|
+
{subtitle()}
|
|
362
|
+
</View>
|
|
363
|
+
))}
|
|
364
|
+
</>
|
|
365
|
+
),
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<Board book={book} width={width} hinge="left">
|
|
370
|
+
{designs[book.coverDesign ?? 'classic']()}
|
|
371
|
+
</Board>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** The back cover: the same board, with the imprint. */
|
|
376
|
+
export function BackCoverView({ book, width }: { book: Pick<FlipbookBook, 'theme' | 'imprint' | 'coverDesign'>; width: number }) {
|
|
377
|
+
const color = COVER_THEMES[book.theme ?? 'classic'].ink
|
|
378
|
+
const unit = width / 100
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<Board book={book} width={width} hinge="right">
|
|
382
|
+
{book.coverDesign === 'gilt' && <GiltFrame unit={unit} color={color} />}
|
|
383
|
+
<View style={[styles.imprint, { bottom: 10 * unit }]}>
|
|
384
|
+
<Ornament size={9 * unit} color={color} />
|
|
385
|
+
{book.imprint ? (
|
|
386
|
+
<Text style={[styles.kicker, { marginTop: 2 * unit, fontSize: 2.4 * unit, letterSpacing: 0.72 * unit, color, opacity: 0.6 }]}>
|
|
387
|
+
{book.imprint.toUpperCase()}
|
|
388
|
+
</Text>
|
|
389
|
+
) : null}
|
|
390
|
+
</View>
|
|
391
|
+
</Board>
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const styles = StyleSheet.create({
|
|
396
|
+
board: { overflow: 'hidden' },
|
|
397
|
+
fill: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 },
|
|
398
|
+
flex: { flex: 1 },
|
|
399
|
+
clip: { overflow: 'hidden' },
|
|
400
|
+
centered: { alignItems: 'center', justifyContent: 'center' },
|
|
401
|
+
center: { textAlign: 'center' },
|
|
402
|
+
hinge: { position: 'absolute', top: 0, bottom: 0 },
|
|
403
|
+
left: { left: 0 },
|
|
404
|
+
right: { right: 0 },
|
|
405
|
+
panel: { flex: 1, alignItems: 'center', borderWidth: StyleSheet.hairlineWidth },
|
|
406
|
+
kicker: { textAlign: 'center', opacity: 0.7 },
|
|
407
|
+
coverPhoto: { width: '100%', flex: 1, maxHeight: '62%', overflow: 'hidden' },
|
|
408
|
+
stack: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
|
|
409
|
+
window: { position: 'absolute', overflow: 'hidden', backgroundColor: 'rgba(0,0,0,0.12)' },
|
|
410
|
+
edge: { position: 'absolute', left: 0, right: 0 },
|
|
411
|
+
edgeV: { position: 'absolute', top: 0, bottom: 0 },
|
|
412
|
+
print: { position: 'absolute', backgroundColor: '#f7f2e7', transform: [{ rotate: '-2.5deg' }], boxShadow: '0 3px 8px rgba(0,0,0,0.4)' },
|
|
413
|
+
label: { position: 'absolute', alignItems: 'center', backgroundColor: '#f4eddc', boxShadow: '0 1px 4px rgba(0,0,0,0.3)' },
|
|
414
|
+
caption: { position: 'absolute', left: 0, right: 0, bottom: 0, alignItems: 'center' },
|
|
415
|
+
imprint: { position: 'absolute', left: 0, right: 0, alignItems: 'center' },
|
|
416
|
+
})
|
package/src/Flipbook.tsx
CHANGED
|
@@ -41,7 +41,8 @@ import {
|
|
|
41
41
|
import { FoldLayout, type Band, type Role } from './geometry'
|
|
42
42
|
import { Gradient } from './Gradient'
|
|
43
43
|
import { BookLookProvider, type BookFonts } from './look'
|
|
44
|
-
import { BackCoverView,
|
|
44
|
+
import { BackCoverView, CoverView } from './Cover'
|
|
45
|
+
import { BlankPageView, ColophonView, PAGE_RATIO, PageView, TitlePageView } from './pages'
|
|
45
46
|
import { lockLandscape } from './orientation'
|
|
46
47
|
|
|
47
48
|
export interface FlipbookPlace {
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
export * from '@toneflix/flipbook-core'
|
|
2
2
|
export { Flipbook, type FlipbookProps, type FlipbookHandle, type FlipbookPlace } from './Flipbook'
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
Paper,
|
|
6
|
-
CoverView,
|
|
7
|
-
BackCoverView,
|
|
8
|
-
TitlePageView,
|
|
9
|
-
ColophonView,
|
|
10
|
-
BlankPageView,
|
|
11
|
-
PAGE_RATIO,
|
|
12
|
-
type PageViewProps,
|
|
13
|
-
type CoverViewProps,
|
|
14
|
-
} from './pages'
|
|
3
|
+
export { PageView, Paper, TitlePageView, ColophonView, BlankPageView, PAGE_RATIO, type PageViewProps } from './pages'
|
|
4
|
+
export { CoverView, BackCoverView, type CoverViewProps } from './Cover'
|
|
15
5
|
export { PrintedPhoto, type PrintedPhotoProps } from './PrintedPhoto'
|
|
16
6
|
export { BookLookProvider, useBookLook, usePaper, useSerif, ink, PAPERS, type BookLook, type BookFonts, type PaperLook } from './look'
|
|
17
7
|
export { Gradient, type GradientStop } from './Gradient'
|
package/src/look.tsx
CHANGED
|
@@ -20,7 +20,7 @@ export interface PaperLook {
|
|
|
20
20
|
vignette: string
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const fine = { texture: require('../assets/textures/paper-fine
|
|
23
|
+
const fine = { texture: require('../assets/textures/paper-fine.webp'), grain: require('../assets/textures/grain-fine.webp') }
|
|
24
24
|
|
|
25
25
|
const ivory: PaperLook = {
|
|
26
26
|
background: '#fdf9f0',
|
|
@@ -46,9 +46,9 @@ export const PAPERS: Record<PaperColor, PaperLook> = {
|
|
|
46
46
|
plateTint: 'rgba(90,70,40,0.03)',
|
|
47
47
|
plateLow: 'rgba(80,50,20,0.16)',
|
|
48
48
|
plateHigh: 'rgba(255,245,225,0.35)',
|
|
49
|
-
texture: require('../assets/textures/paper-kraft
|
|
49
|
+
texture: require('../assets/textures/paper-kraft.webp'),
|
|
50
50
|
textureOpacity: 0.45,
|
|
51
|
-
grain: require('../assets/textures/grain-kraft
|
|
51
|
+
grain: require('../assets/textures/grain-kraft.webp'),
|
|
52
52
|
grainOpacity: 0.35,
|
|
53
53
|
light: 'rgba(255,255,255,0.18)',
|
|
54
54
|
vignette: 'rgba(80,50,20,0.12)',
|
|
@@ -60,9 +60,9 @@ export const PAPERS: Record<PaperColor, PaperLook> = {
|
|
|
60
60
|
plateTint: 'rgba(0,0,0,0.08)',
|
|
61
61
|
plateLow: 'rgba(0,0,0,0.35)',
|
|
62
62
|
plateHigh: 'rgba(255,255,255,0.07)',
|
|
63
|
-
texture: require('../assets/textures/paper-charcoal
|
|
63
|
+
texture: require('../assets/textures/paper-charcoal.webp'),
|
|
64
64
|
textureOpacity: 0.25,
|
|
65
|
-
grain: require('../assets/textures/grain-charcoal
|
|
65
|
+
grain: require('../assets/textures/grain-charcoal.webp'),
|
|
66
66
|
grainOpacity: 0.22,
|
|
67
67
|
light: 'rgba(255,255,255,0.05)',
|
|
68
68
|
vignette: 'rgba(0,0,0,0.25)',
|
package/src/pages.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useId, type ReactNode } from 'react'
|
|
2
2
|
import { Image, Pressable, StyleSheet, Text, View, type ViewStyle } from 'react-native'
|
|
3
3
|
import Svg, { Circle, Defs, Path, RadialGradient, Rect, Stop } from 'react-native-svg'
|
|
4
|
-
import {
|
|
4
|
+
import { PLACED_LAYOUTS, photoFrame, type FlipbookBook, type FlipbookPage, type PageBox } from '@toneflix/flipbook-core'
|
|
5
5
|
import { Gradient } from './Gradient'
|
|
6
6
|
import { ink, useBookLook, usePaper, useSerif, type PaperLook } from './look'
|
|
7
7
|
import { PrintedPhoto } from './PrintedPhoto'
|
|
@@ -250,99 +250,6 @@ function Gutter({ side }: { side: Side }) {
|
|
|
250
250
|
)
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
/** Cloth for a cover, with the hinge shadow along the spine. */
|
|
254
|
-
function Cloth({ book, width, hinge, children }: { book: Pick<FlipbookBook, 'theme'>; width: number; hinge: Side; children?: ReactNode }) {
|
|
255
|
-
const theme = COVER_THEMES[book.theme ?? 'classic']
|
|
256
|
-
const id = useId().replace(/[^a-zA-Z0-9]/g, '')
|
|
257
|
-
const shade = (a: number, offset: number) => ({ offset, color: '#000', opacity: a })
|
|
258
|
-
|
|
259
|
-
return (
|
|
260
|
-
<View style={[styles.page, { width, height: width / PAGE_RATIO, backgroundColor: theme.cloth }]}>
|
|
261
|
-
<Svg style={StyleSheet.absoluteFill} width="100%" height="100%" pointerEvents="none">
|
|
262
|
-
<Defs>
|
|
263
|
-
<RadialGradient id={`sheen${id}`} cx="30%" cy="20%" rx="45%" ry="45%">
|
|
264
|
-
<Stop offset="0" stopColor="#fff" stopOpacity={0.08} />
|
|
265
|
-
<Stop offset="1" stopColor="#fff" stopOpacity={0} />
|
|
266
|
-
</RadialGradient>
|
|
267
|
-
</Defs>
|
|
268
|
-
<Rect width="100%" height="100%" fill={`url(#sheen${id})`} />
|
|
269
|
-
</Svg>
|
|
270
|
-
<Gradient
|
|
271
|
-
to={hinge === 'left' ? 'right' : 'left'}
|
|
272
|
-
style={[styles.hinge, hinge === 'left' ? styles.left : styles.right, { width: width * 0.04 }]}
|
|
273
|
-
stops={[shade(0.35, 0), shade(0.1, 0.5), shade(0, 1)]}
|
|
274
|
-
/>
|
|
275
|
-
{children}
|
|
276
|
-
</View>
|
|
277
|
-
)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export interface CoverViewProps {
|
|
281
|
-
book: Pick<FlipbookBook, 'title' | 'subtitle' | 'theme' | 'cover' | 'kicker'>
|
|
282
|
-
width: number
|
|
283
|
-
showImages?: boolean
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** The front cover: cloth with the title and an inset photo. */
|
|
287
|
-
export function CoverView({ book, width, showImages = true }: CoverViewProps) {
|
|
288
|
-
const color = COVER_THEMES[book.theme ?? 'classic'].ink
|
|
289
|
-
const serif = useSerif()
|
|
290
|
-
const unit = width / 100
|
|
291
|
-
|
|
292
|
-
return (
|
|
293
|
-
<Cloth book={book} width={width} hinge="left">
|
|
294
|
-
<View style={[styles.panel, { margin: 6 * unit, paddingVertical: 8 * unit, paddingHorizontal: 6 * unit, borderColor: color }]}>
|
|
295
|
-
{book.kicker ? (
|
|
296
|
-
<Text style={[styles.kicker, { fontSize: 2.6 * unit, letterSpacing: 0.78 * unit, color }]}>{book.kicker.toUpperCase()}</Text>
|
|
297
|
-
) : null}
|
|
298
|
-
<Text
|
|
299
|
-
style={[
|
|
300
|
-
serif.upright,
|
|
301
|
-
styles.center,
|
|
302
|
-
{ marginTop: 5 * unit, fontSize: 9 * unit, lineHeight: 9.5 * unit, fontWeight: '500', color },
|
|
303
|
-
]}
|
|
304
|
-
>
|
|
305
|
-
{book.title}
|
|
306
|
-
</Text>
|
|
307
|
-
{book.cover ? (
|
|
308
|
-
<View style={[styles.coverPhoto, { marginTop: 7 * unit, borderWidth: 0.8 * unit, borderColor: color }]}>
|
|
309
|
-
{showImages && (
|
|
310
|
-
<Image
|
|
311
|
-
source={{ uri: book.cover.src }}
|
|
312
|
-
style={StyleSheet.absoluteFill}
|
|
313
|
-
resizeMode="cover"
|
|
314
|
-
aria-label={book.cover.alt ?? undefined}
|
|
315
|
-
/>
|
|
316
|
-
)}
|
|
317
|
-
</View>
|
|
318
|
-
) : book.subtitle ? (
|
|
319
|
-
<Text style={[serif.italic, styles.center, { marginTop: 6 * unit, fontSize: 4 * unit, color, opacity: 0.8 }]}>
|
|
320
|
-
{book.subtitle}
|
|
321
|
-
</Text>
|
|
322
|
-
) : null}
|
|
323
|
-
</View>
|
|
324
|
-
</Cloth>
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function BackCoverView({ book, width }: { book: Pick<FlipbookBook, 'theme' | 'imprint'>; width: number }) {
|
|
329
|
-
const color = COVER_THEMES[book.theme ?? 'classic'].ink
|
|
330
|
-
const unit = width / 100
|
|
331
|
-
|
|
332
|
-
return (
|
|
333
|
-
<Cloth book={book} width={width} hinge="right">
|
|
334
|
-
<View style={[styles.imprint, { bottom: 10 * unit }]}>
|
|
335
|
-
<Ornament size={9 * unit} color={color} />
|
|
336
|
-
{book.imprint ? (
|
|
337
|
-
<Text style={[styles.kicker, { marginTop: 2 * unit, fontSize: 2.4 * unit, letterSpacing: 0.72 * unit, color, opacity: 0.6 }]}>
|
|
338
|
-
{book.imprint.toUpperCase()}
|
|
339
|
-
</Text>
|
|
340
|
-
) : null}
|
|
341
|
-
</View>
|
|
342
|
-
</Cloth>
|
|
343
|
-
)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
253
|
/** Inside the front cover: a title page. */
|
|
347
254
|
export function TitlePageView({
|
|
348
255
|
book,
|
|
@@ -406,7 +313,7 @@ export function BlankPageView({ width, side }: { width: number; side?: Side }) {
|
|
|
406
313
|
return <Paper width={width} side={side} />
|
|
407
314
|
}
|
|
408
315
|
|
|
409
|
-
function Ornament({ size, color }: { size: number; color: string }) {
|
|
316
|
+
export function Ornament({ size, color }: { size: number; color: string }) {
|
|
410
317
|
return (
|
|
411
318
|
<Svg viewBox="0 0 40 40" width={size} height={size}>
|
|
412
319
|
<Circle cx="20" cy="20" r="18.5" fill="none" stroke={color} strokeOpacity={0.6} />
|