panelui-native 0.86.1 → 0.88.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 +4 -2
- package/lib/module/components/animated-badge/index.js +451 -0
- package/lib/module/components/animated-badge/index.js.map +1 -0
- package/lib/module/components/bottom-sheet/index.js +17 -6
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/bubble-chart/index.js +1782 -0
- package/lib/module/components/bubble-chart/index.js.map +1 -0
- package/lib/module/components/feedback-dialog/index.js +474 -0
- package/lib/module/components/feedback-dialog/index.js.map +1 -0
- package/lib/module/components/pyramid-chart/index.js +1143 -0
- package/lib/module/components/pyramid-chart/index.js.map +1 -0
- package/lib/module/components/scroll-blur/index.js +363 -0
- package/lib/module/components/scroll-blur/index.js.map +1 -0
- package/lib/module/components/scroll-canvas/index.js +2 -0
- package/lib/module/components/scroll-canvas/index.js.map +1 -1
- package/lib/module/components/scroll-text/index.js +3 -0
- package/lib/module/components/scroll-text/index.js.map +1 -1
- package/lib/module/components/search-bar/index.js +753 -25
- package/lib/module/components/search-bar/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +21 -1
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-reveal-progress.js +21 -1
- package/lib/module/hooks/use-reveal-progress.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +24 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/animated-badge/index.d.ts +224 -0
- package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +1 -15
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bubble-chart/index.d.ts +481 -0
- package/lib/typescript/src/components/bubble-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/feedback-dialog/index.d.ts +156 -0
- package/lib/typescript/src/components/feedback-dialog/index.d.ts.map +1 -0
- package/lib/typescript/src/components/pyramid-chart/index.d.ts +328 -0
- package/lib/typescript/src/components/pyramid-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-blur/index.d.ts +113 -0
- package/lib/typescript/src/components/scroll-blur/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -1
- package/lib/typescript/src/components/search-bar/index.d.ts +350 -6
- package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts +6 -27
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +6 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +14 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/animated-badge/index.tsx +513 -0
- package/src/components/bottom-sheet/index.tsx +23 -8
- package/src/components/bubble-chart/index.tsx +2145 -0
- package/src/components/feedback-dialog/index.tsx +557 -0
- package/src/components/pyramid-chart/index.tsx +1360 -0
- package/src/components/scroll-blur/index.tsx +466 -0
- package/src/components/scroll-canvas/index.tsx +2 -1
- package/src/components/scroll-text/index.tsx +3 -3
- package/src/components/search-bar/index.tsx +933 -45
- package/src/hooks/use-keyboard-avoidance.ts +24 -1
- package/src/hooks/use-reveal-progress.ts +30 -1
- package/src/index.ts +72 -1
- package/src/utils/chart.ts +27 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollBlur — blurs the edges of a scroll container.
|
|
3
|
+
*
|
|
4
|
+
* Content that runs past a boundary reads better receding than being cut off,
|
|
5
|
+
* and a receding edge doubles as an affordance: an edge that is going soft is
|
|
6
|
+
* an edge with more content behind it.
|
|
7
|
+
*
|
|
8
|
+
* A blur says that where a fade cannot: a fade takes the content towards the
|
|
9
|
+
* colour behind the scroller, so it only works where that colour is known and
|
|
10
|
+
* flat. A blur takes it out of focus instead, which is true over a photograph,
|
|
11
|
+
* a gradient, or a list of coloured cards. It is also what belongs under
|
|
12
|
+
* something laid *over* the scroller — a button, a header, a search field —
|
|
13
|
+
* because the content passing beneath stays visible as shape and colour while
|
|
14
|
+
* losing the detail that would compete with the thing on top.
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <ScrollBlur>
|
|
18
|
+
* <ScrollView>…</ScrollView>
|
|
19
|
+
* </ScrollBlur>
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ## The ramp is a stack and a wash, because neither alone is smooth
|
|
23
|
+
*
|
|
24
|
+
* A blur that goes from nothing to full across a band needs a per-pixel blur
|
|
25
|
+
* radius, and there is no such thing on either platform — a blur view has one
|
|
26
|
+
* strength for its whole rectangle.
|
|
27
|
+
*
|
|
28
|
+
* So the ramp is built out of several of them: each layer covers a shorter
|
|
29
|
+
* span than the last, measured from the edge, and each blurs what the layer
|
|
30
|
+
* under it has already blurred. The spans are spaced on a curve rather than
|
|
31
|
+
* evenly, which puts most of the layers in the outer third where the blur is
|
|
32
|
+
* changing fastest and the steps would otherwise be widest.
|
|
33
|
+
*
|
|
34
|
+
* That alone is not enough. Every layer has a hard edge, and a stack of hard
|
|
35
|
+
* edges is a stack of visible lines however many there are. So a gradient of
|
|
36
|
+
* `color` is washed over the top — opaque at the outer edge, clear at the
|
|
37
|
+
* inner one. It hides the seams, and it is what makes the band read as one
|
|
38
|
+
* material rather than as a pile of rectangles: the content goes soft and
|
|
39
|
+
* fades into the surface at the same time, which is what the eye expects an
|
|
40
|
+
* edge to do.
|
|
41
|
+
*
|
|
42
|
+
* That wash is why `color` matters even when the blur is drawn. Give it the
|
|
43
|
+
* surface the scrollable actually sits on — a sheet, a card, the page — or the
|
|
44
|
+
* band fades towards a colour that is not there.
|
|
45
|
+
*
|
|
46
|
+
* ## Where it cannot blur, it fades
|
|
47
|
+
*
|
|
48
|
+
* A real blur needs a native view. `expo-blur` is optional, and Reduce
|
|
49
|
+
* Transparency is a preference that outranks the design, so both cases fall
|
|
50
|
+
* back to a gradient towards `color` — the same thing `ScrollFade` draws. A
|
|
51
|
+
* blur you cannot draw is better shown as a fade than as a hard edge, and far
|
|
52
|
+
* better than as a crash.
|
|
53
|
+
*
|
|
54
|
+
* Because of that fallback, pass `color` whenever the scroller does not sit on
|
|
55
|
+
* the theme background. It is unused in the blurred case and the whole effect
|
|
56
|
+
* in the other one.
|
|
57
|
+
*
|
|
58
|
+
* Scroll position, content size and viewport size are held in shared values
|
|
59
|
+
* and read on the UI thread, so scrolling never re-renders React.
|
|
60
|
+
*/
|
|
61
|
+
import {
|
|
62
|
+
Children,
|
|
63
|
+
isValidElement,
|
|
64
|
+
useMemo,
|
|
65
|
+
type ComponentType,
|
|
66
|
+
type ReactElement,
|
|
67
|
+
type ReactNode,
|
|
68
|
+
} from 'react';
|
|
69
|
+
import {
|
|
70
|
+
StyleSheet,
|
|
71
|
+
View,
|
|
72
|
+
type LayoutChangeEvent,
|
|
73
|
+
type ViewProps,
|
|
74
|
+
} from 'react-native';
|
|
75
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
76
|
+
import Animated, {
|
|
77
|
+
useAnimatedScrollHandler,
|
|
78
|
+
useAnimatedStyle,
|
|
79
|
+
useComposedEventHandler,
|
|
80
|
+
useDerivedValue,
|
|
81
|
+
useSharedValue,
|
|
82
|
+
type AnimatedScrollViewProps,
|
|
83
|
+
type DerivedValue,
|
|
84
|
+
} from 'react-native-reanimated';
|
|
85
|
+
import { useCSSVariable } from 'uniwind';
|
|
86
|
+
import { hasBlur, useReduceTransparency } from '../../primitives/scrim';
|
|
87
|
+
import { useThemeMode } from '../../theme/use-theme';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* `expo-blur`'s BlurView, or null when it is not installed. Resolved once at
|
|
91
|
+
* module load — the require is cheap and caching it avoids a try/catch on
|
|
92
|
+
* every render.
|
|
93
|
+
*/
|
|
94
|
+
const BlurView: ComponentType<{
|
|
95
|
+
intensity?: number;
|
|
96
|
+
tint?: ScrollBlurTint;
|
|
97
|
+
style?: unknown;
|
|
98
|
+
}> | null = (() => {
|
|
99
|
+
try {
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
101
|
+
const mod = require('expo-blur');
|
|
102
|
+
return (mod?.BlurView as ComponentType<{ intensity?: number }>) ?? null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
|
|
108
|
+
/** Past this many pixels from an edge, that edge's band is fully drawn. */
|
|
109
|
+
const DEFAULT_FADE_IN_DISTANCE = 24;
|
|
110
|
+
|
|
111
|
+
/** Which way the material tints. `default` follows the app's theme. */
|
|
112
|
+
export type ScrollBlurTint = 'light' | 'dark' | 'default';
|
|
113
|
+
|
|
114
|
+
export interface ScrollBlurProps extends ViewProps {
|
|
115
|
+
className?: string;
|
|
116
|
+
/** Depth of the blurred band in pixels. */
|
|
117
|
+
size?: number;
|
|
118
|
+
/** Which edges blur. */
|
|
119
|
+
edges?: 'both' | 'start' | 'end' | 'none';
|
|
120
|
+
/**
|
|
121
|
+
* Scroll axis. Inferred from the child's `horizontal` prop when omitted —
|
|
122
|
+
* pass it explicitly for children that scroll horizontally without that prop
|
|
123
|
+
* (a `FlatList` with `horizontal` set through `contentContainerStyle`, say).
|
|
124
|
+
*/
|
|
125
|
+
orientation?: 'horizontal' | 'vertical';
|
|
126
|
+
/**
|
|
127
|
+
* How many blur views make up the ramp. More is smoother and costs more; the
|
|
128
|
+
* band shows visible steps below four, and past eight nobody can tell.
|
|
129
|
+
*/
|
|
130
|
+
layers?: number;
|
|
131
|
+
/** Blur strength at the very edge, 0–100. The layers share it between them. */
|
|
132
|
+
intensity?: number;
|
|
133
|
+
/**
|
|
134
|
+
* Which way the material tints. Defaults to the app's theme rather than the
|
|
135
|
+
* phone's, so an app running dark on a light phone does not blur light.
|
|
136
|
+
*/
|
|
137
|
+
material?: ScrollBlurTint;
|
|
138
|
+
/**
|
|
139
|
+
* The colour the band fades towards — washed over the blur, and the whole
|
|
140
|
+
* effect where there is no blur to draw.
|
|
141
|
+
*
|
|
142
|
+
* Defaults to the theme's background. Pass the surface the scrollable
|
|
143
|
+
* actually sits on, or the band fades towards a colour that is not there.
|
|
144
|
+
*/
|
|
145
|
+
color?: string;
|
|
146
|
+
/**
|
|
147
|
+
* How opaque that wash gets at the outer edge, 0 to 1. Lower it to let more
|
|
148
|
+
* of the content show through the far end of the band; `0` leaves the blur
|
|
149
|
+
* bare, along with the seams between its layers.
|
|
150
|
+
*/
|
|
151
|
+
tint?: number;
|
|
152
|
+
/** Distance in pixels over which an edge comes in from clear to full. */
|
|
153
|
+
fadeInDistance?: number;
|
|
154
|
+
/** Set false to render the child with no bands at all. */
|
|
155
|
+
enabled?: boolean;
|
|
156
|
+
children?: ReactNode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface ScrollableProps {
|
|
160
|
+
horizontal?: boolean;
|
|
161
|
+
onScroll?: AnimatedScrollViewProps['onScroll'];
|
|
162
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
163
|
+
onContentSizeChange?: (width: number, height: number) => void;
|
|
164
|
+
scrollEventThrottle?: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function ScrollBlur({
|
|
168
|
+
className,
|
|
169
|
+
size = 80,
|
|
170
|
+
edges = 'both',
|
|
171
|
+
orientation,
|
|
172
|
+
layers = 8,
|
|
173
|
+
intensity = 56,
|
|
174
|
+
material = 'default',
|
|
175
|
+
tint = 0.95,
|
|
176
|
+
color,
|
|
177
|
+
fadeInDistance = DEFAULT_FADE_IN_DISTANCE,
|
|
178
|
+
enabled = true,
|
|
179
|
+
children,
|
|
180
|
+
...props
|
|
181
|
+
}: ScrollBlurProps) {
|
|
182
|
+
const offset = useSharedValue(0);
|
|
183
|
+
const contentLength = useSharedValue(0);
|
|
184
|
+
const viewportLength = useSharedValue(0);
|
|
185
|
+
|
|
186
|
+
const reduceTransparency = useReduceTransparency();
|
|
187
|
+
const { mode } = useThemeMode();
|
|
188
|
+
/*
|
|
189
|
+
* Not knowing yet counts as "do not blur": the answer is asynchronous and
|
|
190
|
+
* arriving a frame late costs nothing, while a blur flashed at somebody who
|
|
191
|
+
* switched Reduce Transparency on is the whole thing that setting is for.
|
|
192
|
+
*/
|
|
193
|
+
const blurring = hasBlur && BlurView !== null && reduceTransparency === false;
|
|
194
|
+
const materialTint: ScrollBlurTint = material === 'default' ? mode : material;
|
|
195
|
+
|
|
196
|
+
const themeBackground = useCSSVariable('--color-background');
|
|
197
|
+
const fadeColor =
|
|
198
|
+
color ?? (typeof themeBackground === 'string' ? themeBackground : '#000000');
|
|
199
|
+
|
|
200
|
+
const child = Children.only(children) as ReactElement<ScrollableProps>;
|
|
201
|
+
const horizontal =
|
|
202
|
+
orientation !== undefined
|
|
203
|
+
? orientation === 'horizontal'
|
|
204
|
+
: isValidElement(child) && !!child.props.horizontal;
|
|
205
|
+
|
|
206
|
+
// Reanimated can only drive a scroll handler on an animated component, and
|
|
207
|
+
// `Animated.ScrollView` is not necessarily what was passed in — the child may
|
|
208
|
+
// be a FlatList, a SectionList or a custom scrollable. Keyed on the element
|
|
209
|
+
// *type*, not the element: rebuilding the wrapper would remount the list.
|
|
210
|
+
const childType = isValidElement(child)
|
|
211
|
+
? (child.type as ComponentType<ScrollableProps>)
|
|
212
|
+
: null;
|
|
213
|
+
const AnimatedScrollable = useMemo(
|
|
214
|
+
() => (childType ? Animated.createAnimatedComponent(childType) : null),
|
|
215
|
+
[childType]
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
219
|
+
onScroll: (event) => {
|
|
220
|
+
const { contentOffset, contentSize, layoutMeasurement } = event;
|
|
221
|
+
offset.value = horizontal ? contentOffset.x : contentOffset.y;
|
|
222
|
+
contentLength.value = horizontal ? contentSize.width : contentSize.height;
|
|
223
|
+
viewportLength.value = horizontal
|
|
224
|
+
? layoutMeasurement.width
|
|
225
|
+
: layoutMeasurement.height;
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// A consumer `onScroll` is composed rather than dropped — but because the
|
|
230
|
+
// child is now an animated component it has to be an animated handler too.
|
|
231
|
+
const onScroll = useComposedEventHandler([
|
|
232
|
+
scrollHandler,
|
|
233
|
+
(child.props.onScroll as typeof scrollHandler | undefined) ?? null,
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
// Measured up front as well as on scroll, so an end edge with content behind
|
|
237
|
+
// it is drawn from the first frame rather than waiting for a scroll event.
|
|
238
|
+
const onLayout = (event: LayoutChangeEvent) => {
|
|
239
|
+
const { width, height } = event.nativeEvent.layout;
|
|
240
|
+
viewportLength.value = horizontal ? width : height;
|
|
241
|
+
child.props.onLayout?.(event);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const onContentSizeChange = (width: number, height: number) => {
|
|
245
|
+
contentLength.value = horizontal ? width : height;
|
|
246
|
+
child.props.onContentSizeChange?.(width, height);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const startOpacity = useDerivedValue(() =>
|
|
250
|
+
Math.min(offset.value / fadeInDistance, 1)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const endOpacity = useDerivedValue(() => {
|
|
254
|
+
// Nothing to blur towards when the content fits inside the viewport.
|
|
255
|
+
const remaining = contentLength.value - viewportLength.value - offset.value;
|
|
256
|
+
return Math.min(Math.max(remaining, 0) / fadeInDistance, 1);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const scrollable =
|
|
260
|
+
AnimatedScrollable && isValidElement(child) ? (
|
|
261
|
+
<AnimatedScrollable
|
|
262
|
+
{...child.props}
|
|
263
|
+
onScroll={onScroll}
|
|
264
|
+
onLayout={onLayout}
|
|
265
|
+
onContentSizeChange={onContentSizeChange}
|
|
266
|
+
scrollEventThrottle={child.props.scrollEventThrottle ?? 16}
|
|
267
|
+
/>
|
|
268
|
+
) : (
|
|
269
|
+
child
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (!enabled || edges === 'none') {
|
|
273
|
+
return (
|
|
274
|
+
<View {...props} className={className}>
|
|
275
|
+
{scrollable}
|
|
276
|
+
</View>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const edgeProps = {
|
|
281
|
+
size,
|
|
282
|
+
horizontal,
|
|
283
|
+
layers: Math.max(1, Math.round(layers)),
|
|
284
|
+
intensity,
|
|
285
|
+
material: materialTint,
|
|
286
|
+
tint: Math.max(0, Math.min(1, tint)),
|
|
287
|
+
blurring,
|
|
288
|
+
color: fadeColor,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<View {...props} className={className}>
|
|
293
|
+
{scrollable}
|
|
294
|
+
|
|
295
|
+
{edges !== 'end' ? (
|
|
296
|
+
<Edge {...edgeProps} opacity={startOpacity} edge="start" />
|
|
297
|
+
) : null}
|
|
298
|
+
{edges !== 'start' ? (
|
|
299
|
+
<Edge {...edgeProps} opacity={endOpacity} edge="end" />
|
|
300
|
+
) : null}
|
|
301
|
+
</View>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
ScrollBlur.displayName = 'ScrollBlur';
|
|
306
|
+
|
|
307
|
+
/** One edge's band: the stack of blur views, and the wash that joins them up. */
|
|
308
|
+
function Edge({
|
|
309
|
+
size,
|
|
310
|
+
horizontal,
|
|
311
|
+
layers,
|
|
312
|
+
intensity,
|
|
313
|
+
material,
|
|
314
|
+
tint,
|
|
315
|
+
blurring,
|
|
316
|
+
color,
|
|
317
|
+
opacity,
|
|
318
|
+
edge,
|
|
319
|
+
}: {
|
|
320
|
+
size: number;
|
|
321
|
+
horizontal: boolean;
|
|
322
|
+
layers: number;
|
|
323
|
+
intensity: number;
|
|
324
|
+
material: ScrollBlurTint;
|
|
325
|
+
tint: number;
|
|
326
|
+
blurring: boolean;
|
|
327
|
+
color: string;
|
|
328
|
+
opacity: DerivedValue<number>;
|
|
329
|
+
edge: 'start' | 'end';
|
|
330
|
+
}) {
|
|
331
|
+
const isStart = edge === 'start';
|
|
332
|
+
|
|
333
|
+
const position = horizontal
|
|
334
|
+
? { top: 0, bottom: 0, width: size, ...(isStart ? { left: 0 } : { right: 0 }) }
|
|
335
|
+
: { left: 0, right: 0, height: size, ...(isStart ? { top: 0 } : { bottom: 0 }) };
|
|
336
|
+
|
|
337
|
+
const style = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
338
|
+
|
|
339
|
+
/** How opaque the wash gets at the outer edge. Without a blur it is all of it. */
|
|
340
|
+
const full = blurring ? tint : 1;
|
|
341
|
+
|
|
342
|
+
const steps = useMemo(
|
|
343
|
+
() =>
|
|
344
|
+
Array.from({ length: layers }, (_unused, index) => {
|
|
345
|
+
/*
|
|
346
|
+
* Spans on a curve rather than evenly spaced. The blur's strength grows
|
|
347
|
+
* fastest near the edge, so that is where the steps between layers
|
|
348
|
+
* would be widest — squaring the fraction crowds most of the layers
|
|
349
|
+
* into the outer third and spreads the rest thin across the inner
|
|
350
|
+
* two, which is where nothing much is happening anyway.
|
|
351
|
+
*/
|
|
352
|
+
const fraction = (layers - index) / layers;
|
|
353
|
+
return {
|
|
354
|
+
key: index,
|
|
355
|
+
// Cubed rather than squared: with eight layers a square curve still
|
|
356
|
+
// leaves the outermost two nearly the same width, and two layers of
|
|
357
|
+
// the same width are one layer at twice the cost.
|
|
358
|
+
span: size * fraction * fraction * fraction,
|
|
359
|
+
/*
|
|
360
|
+
* The widest layer is the faintest, so the band starts from nothing
|
|
361
|
+
* instead of stepping up at its inner boundary. Squared, so the
|
|
362
|
+
* fade-in is gentle where the layers are widest and the eye has the
|
|
363
|
+
* most room to notice one arriving.
|
|
364
|
+
*/
|
|
365
|
+
opacity: ((index + 1) / layers) ** 2,
|
|
366
|
+
};
|
|
367
|
+
}),
|
|
368
|
+
[layers, size]
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const along = (span: number) =>
|
|
372
|
+
horizontal
|
|
373
|
+
? { top: 0, bottom: 0, width: span, ...(isStart ? { left: 0 } : { right: 0 }) }
|
|
374
|
+
: { left: 0, right: 0, height: span, ...(isStart ? { top: 0 } : { bottom: 0 }) };
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<Animated.View
|
|
378
|
+
pointerEvents="none"
|
|
379
|
+
style={[{ position: 'absolute', overflow: 'hidden' }, position, style]}
|
|
380
|
+
>
|
|
381
|
+
{blurring && BlurView
|
|
382
|
+
? steps.map((step) => (
|
|
383
|
+
<View
|
|
384
|
+
key={step.key}
|
|
385
|
+
style={[{ position: 'absolute', opacity: step.opacity }, along(step.span)]}
|
|
386
|
+
>
|
|
387
|
+
<BlurView
|
|
388
|
+
intensity={intensity / layers}
|
|
389
|
+
tint={material}
|
|
390
|
+
style={StyleSheet.absoluteFill}
|
|
391
|
+
/>
|
|
392
|
+
</View>
|
|
393
|
+
))
|
|
394
|
+
: null}
|
|
395
|
+
|
|
396
|
+
{/*
|
|
397
|
+
The wash. Over the blur it hides the seam every layer's hard edge
|
|
398
|
+
leaves and carries the content into the surface; without a blur to draw
|
|
399
|
+
it is the whole band, which is the fade this degrades to.
|
|
400
|
+
*/}
|
|
401
|
+
{tint > 0 || !blurring ? (
|
|
402
|
+
/*
|
|
403
|
+
* Three stops, not two. A straight line from opaque to clear spends
|
|
404
|
+
* half its length at alphas high enough to read as a flat panel, then
|
|
405
|
+
* gives out all at once — the band gets an edge of its own, which is
|
|
406
|
+
* the thing it exists to remove. The middle stop at just over a third
|
|
407
|
+
* of the full alpha, placed past the halfway point, bends it into the
|
|
408
|
+
* long tail an edge is supposed to have.
|
|
409
|
+
*/
|
|
410
|
+
<LinearGradient
|
|
411
|
+
colors={
|
|
412
|
+
isStart
|
|
413
|
+
? [
|
|
414
|
+
withAlpha(color, full),
|
|
415
|
+
withAlpha(color, full * 0.36),
|
|
416
|
+
withAlpha(color, 0),
|
|
417
|
+
]
|
|
418
|
+
: [
|
|
419
|
+
withAlpha(color, 0),
|
|
420
|
+
withAlpha(color, full * 0.36),
|
|
421
|
+
withAlpha(color, full),
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
locations={isStart ? [0, 0.55, 1] : [0, 0.45, 1]}
|
|
425
|
+
start={{ x: 0, y: 0 }}
|
|
426
|
+
end={horizontal ? { x: 1, y: 0 } : { x: 0, y: 1 }}
|
|
427
|
+
style={StyleSheet.absoluteFill}
|
|
428
|
+
/>
|
|
429
|
+
) : null}
|
|
430
|
+
</Animated.View>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Gradients need a transparent stop of the *same* colour — `transparent` is
|
|
436
|
+
* black at zero alpha on Android, which shows as a grey smear.
|
|
437
|
+
*
|
|
438
|
+
* Written out here rather than shared with `ScrollFade`, because each of these
|
|
439
|
+
* components is also copied into a project on its own and a helper reached for
|
|
440
|
+
* across two of them would arrive as a file nobody asked for.
|
|
441
|
+
*/
|
|
442
|
+
function withAlpha(color: string, alpha: number): string {
|
|
443
|
+
if (color.startsWith('#')) {
|
|
444
|
+
const hex = color.slice(1);
|
|
445
|
+
const full =
|
|
446
|
+
hex.length === 3
|
|
447
|
+
? hex
|
|
448
|
+
.split('')
|
|
449
|
+
.map((c) => c + c)
|
|
450
|
+
.join('')
|
|
451
|
+
: hex.slice(0, 6);
|
|
452
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
453
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
454
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
455
|
+
if (Number.isNaN(r + g + b)) return color;
|
|
456
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const channels = color.match(/rgba?\(([^)]+)\)/)?.[1];
|
|
460
|
+
if (channels) {
|
|
461
|
+
const [r, g, b] = channels.split(',').map((part) => part.trim());
|
|
462
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return color;
|
|
466
|
+
}
|
|
@@ -101,7 +101,7 @@ export function ScrollCanvas({
|
|
|
101
101
|
const reducedMotion = useReducedMotion();
|
|
102
102
|
const active = enabled && !reducedMotion;
|
|
103
103
|
|
|
104
|
-
const { ref, progress } = useRevealProgress({
|
|
104
|
+
const { ref, onLayout, progress } = useRevealProgress({
|
|
105
105
|
start,
|
|
106
106
|
end,
|
|
107
107
|
progress: external,
|
|
@@ -140,6 +140,7 @@ export function ScrollCanvas({
|
|
|
140
140
|
<View
|
|
141
141
|
{...props}
|
|
142
142
|
ref={ref}
|
|
143
|
+
onLayout={onLayout}
|
|
143
144
|
style={[{ aspectRatio }, style]}
|
|
144
145
|
className={cn('w-full overflow-hidden rounded-2xl bg-muted', className)}
|
|
145
146
|
>
|
|
@@ -106,7 +106,7 @@ export function ScrollText({
|
|
|
106
106
|
const reducedMotion = useReducedMotion();
|
|
107
107
|
const active = enabled && !reducedMotion;
|
|
108
108
|
|
|
109
|
-
const { ref, progress } = useRevealProgress({
|
|
109
|
+
const { ref, onLayout, progress } = useRevealProgress({
|
|
110
110
|
start,
|
|
111
111
|
end,
|
|
112
112
|
progress: external,
|
|
@@ -133,7 +133,7 @@ export function ScrollText({
|
|
|
133
133
|
|
|
134
134
|
if (asRow) {
|
|
135
135
|
return (
|
|
136
|
-
<View ref={ref} className="flex-row flex-wrap items-baseline">
|
|
136
|
+
<View ref={ref} onLayout={onLayout} className="flex-row flex-wrap items-baseline">
|
|
137
137
|
{tokens.map((token, index) => (
|
|
138
138
|
<RisingToken
|
|
139
139
|
key={`${index}-${token}`}
|
|
@@ -151,7 +151,7 @@ export function ScrollText({
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
return (
|
|
154
|
-
<View ref={ref}>
|
|
154
|
+
<View ref={ref} onLayout={onLayout}>
|
|
155
155
|
<Text {...textProps} className={className}>
|
|
156
156
|
{tokens.map((token, index) => (
|
|
157
157
|
<InkedToken
|