panelui-native 0.82.0 → 0.83.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/lib/module/components/bottom-sheet/index.js +111 -16
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/image-generation/dot-field.js +66 -9
- package/lib/module/components/image-generation/dot-field.js.map +1 -1
- package/lib/module/components/image-generation/index.js +65 -23
- package/lib/module/components/image-generation/index.js.map +1 -1
- package/lib/module/native/index.js +24 -0
- package/lib/module/native/index.js.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +17 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/image-generation/dot-field.d.ts +36 -1
- package/lib/typescript/src/components/image-generation/dot-field.d.ts.map +1 -1
- package/lib/typescript/src/components/image-generation/index.d.ts +16 -8
- package/lib/typescript/src/components/image-generation/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +26 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/bottom-sheet/index.tsx +139 -16
- package/src/components/image-generation/dot-field.ts +65 -9
- package/src/components/image-generation/index.tsx +67 -18
- package/src/native/index.ts +45 -0
|
@@ -25,6 +25,15 @@ export const DOT_LIT_RADIUS = 1.9;
|
|
|
25
25
|
/** How visible the resting grid is, under everything. */
|
|
26
26
|
export const DOT_REST_OPACITY = 0.16;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* How visible the lit dots are, together.
|
|
30
|
+
*
|
|
31
|
+
* "Together" is the operative word: they are drawn as two layers fading into
|
|
32
|
+
* one another, and this is what the pair composites to, not what either one
|
|
33
|
+
* carries. {@link crossfadeAlphas} is what holds that true.
|
|
34
|
+
*/
|
|
35
|
+
export const LIT_OPACITY = 0.9;
|
|
36
|
+
|
|
28
37
|
/** How many still frames one loop is drawn as. */
|
|
29
38
|
export const FRAMES = 24;
|
|
30
39
|
|
|
@@ -118,11 +127,20 @@ export function influenceAt(distance: number, radius: number): number {
|
|
|
118
127
|
/**
|
|
119
128
|
* Where the light is at a phase of the loop, and how wide it reaches.
|
|
120
129
|
*
|
|
121
|
-
* `drift` wanders around the middle
|
|
122
|
-
*
|
|
123
|
-
* amplitudes because a light that reaches the corners
|
|
124
|
-
*
|
|
125
|
-
*
|
|
130
|
+
* `drift` wanders around the middle on a figure-eight: one horizontal pass to
|
|
131
|
+
* two vertical, so the path arrives back where it started and the loop has no
|
|
132
|
+
* seam in it. The amplitudes are small because a light that reaches the corners
|
|
133
|
+
* stops reading as one source.
|
|
134
|
+
*
|
|
135
|
+
* The two frequencies used not to divide into each other, on the reasoning that
|
|
136
|
+
* a path which never closes is one the eye cannot learn. It closes anyway —
|
|
137
|
+
* every animation here restarts at the end of its period — so all that bought
|
|
138
|
+
* was a jump of four normal steps, once a pass. A figure-eight is not a shape
|
|
139
|
+
* anybody follows over four seconds of soft light on a dot grid.
|
|
140
|
+
*
|
|
141
|
+
* `pulse` is a ring leaving the centre, and has expanded past the last dot
|
|
142
|
+
* before it restarts, so it fades out rather than snapping back. `scan` crosses
|
|
143
|
+
* as a band, which is the same maths with the light infinitely tall.
|
|
126
144
|
*/
|
|
127
145
|
function lightAt(
|
|
128
146
|
animation: DotFieldAnimation,
|
|
@@ -157,7 +175,7 @@ function lightAt(
|
|
|
157
175
|
|
|
158
176
|
return {
|
|
159
177
|
x: width / 2 + Math.sin(turn) * width * 0.26,
|
|
160
|
-
y: height / 2 + Math.cos(turn *
|
|
178
|
+
y: height / 2 + Math.cos(turn * 2) * height * 0.22,
|
|
161
179
|
radius: short * 0.42,
|
|
162
180
|
ring: 0,
|
|
163
181
|
};
|
|
@@ -169,19 +187,25 @@ function lightAt(
|
|
|
169
187
|
* Only the dots the light actually reaches are in it — the rest are already
|
|
170
188
|
* drawn by the resting grid underneath, so this is a fraction of the field
|
|
171
189
|
* rather than all of it.
|
|
190
|
+
*
|
|
191
|
+
* `points` is the same grid {@link anchors} would build, passed in by a caller
|
|
192
|
+
* that is about to ask for every frame of a loop. Building it here instead cost
|
|
193
|
+
* a fresh array of several hundred pairs per frame, to arrive at the identical
|
|
194
|
+
* grid twenty-four times over.
|
|
172
195
|
*/
|
|
173
196
|
export function litPath(
|
|
174
197
|
width: number,
|
|
175
198
|
height: number,
|
|
176
199
|
phase: number,
|
|
177
|
-
animation: DotFieldAnimation = 'drift'
|
|
200
|
+
animation: DotFieldAnimation = 'drift',
|
|
201
|
+
points?: [number, number][]
|
|
178
202
|
): string {
|
|
179
203
|
if (width <= 0 || height <= 0) return '';
|
|
180
204
|
|
|
181
205
|
const light = lightAt(animation, phase, width, height);
|
|
182
206
|
let path = '';
|
|
183
207
|
|
|
184
|
-
for (const [x, y] of anchors(width, height)) {
|
|
208
|
+
for (const [x, y] of points ?? anchors(width, height)) {
|
|
185
209
|
const deltaX = x - light.x;
|
|
186
210
|
const deltaY = Number.isNaN(light.y) ? 0 : y - light.y;
|
|
187
211
|
const distance = Math.abs(
|
|
@@ -206,8 +230,9 @@ export function litFrames(
|
|
|
206
230
|
height: number,
|
|
207
231
|
animation: DotFieldAnimation = 'drift'
|
|
208
232
|
): string[] {
|
|
233
|
+
const points = width > 0 && height > 0 ? anchors(width, height) : [];
|
|
209
234
|
return Array.from({ length: FRAMES }, (_unused, index) =>
|
|
210
|
-
litPath(width, height, index / FRAMES, animation)
|
|
235
|
+
litPath(width, height, index / FRAMES, animation, points)
|
|
211
236
|
);
|
|
212
237
|
}
|
|
213
238
|
|
|
@@ -224,3 +249,34 @@ export function frameAt(time: number, animation: DotFieldAnimation): number {
|
|
|
224
249
|
const phase = (time % PERIOD[animation]) / PERIOD[animation];
|
|
225
250
|
return Math.min(FRAMES - 1, Math.floor(phase * FRAMES));
|
|
226
251
|
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Where a moment falls in the loop, as a frame index with its fraction kept.
|
|
255
|
+
*
|
|
256
|
+
* {@link frameAt} rounds this down, and rounding it down is what made the field
|
|
257
|
+
* a flipbook: twenty-four pictures spread over the period, which on the slowest
|
|
258
|
+
* animation is a new one every 175ms however fast the screen refreshes. The
|
|
259
|
+
* fraction is what the two layers cross-fade on, so the light moves at the rate
|
|
260
|
+
* the display can draw rather than the rate the frames were built at.
|
|
261
|
+
*/
|
|
262
|
+
export function framePhase(time: number, animation: DotFieldAnimation): number {
|
|
263
|
+
'worklet';
|
|
264
|
+
return ((time % PERIOD[animation]) / PERIOD[animation]) * FRAMES;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* What the outgoing and incoming layers are worth, a fraction `t` of the way
|
|
269
|
+
* from one frame to the next.
|
|
270
|
+
*
|
|
271
|
+
* Not `[1 - t, t]`. The layers are drawn over one another, so a dot lit in both
|
|
272
|
+
* of them composites to `1 - (1-a)(1-b)`, and two half-strength copies of it
|
|
273
|
+
* come to 0.70 rather than 0.90 — the field dips a fifth in the middle of every
|
|
274
|
+
* step, which six steps a second turns into a flicker. So only the outgoing
|
|
275
|
+
* layer ramps, and the incoming one is solved for: whatever leaves the pair at
|
|
276
|
+
* {@link LIT_OPACITY} the whole way across.
|
|
277
|
+
*/
|
|
278
|
+
export function crossfadeAlphas(t: number): [number, number] {
|
|
279
|
+
'worklet';
|
|
280
|
+
const out = LIT_OPACITY * (1 - t);
|
|
281
|
+
return [out, 1 - (1 - LIT_OPACITY) / (1 - out)];
|
|
282
|
+
}
|
|
@@ -15,14 +15,22 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ## The dot field
|
|
17
17
|
*
|
|
18
|
-
* A grid of dots with a soft
|
|
19
|
-
*
|
|
20
|
-
* costs twenty-two worklets a frame rather than five hundred — the dots inside
|
|
21
|
-
* a column never move independently of it, and at this scale nobody can tell.
|
|
18
|
+
* A grid of dots with a soft light moving through it, drawn as three layers:
|
|
19
|
+
* the field at rest, and two copies of the lit dots fading into one another.
|
|
22
20
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* Every picture the loop will ever draw is built when the box is measured, so
|
|
22
|
+
* running it hands over a string that already exists. That leaves twenty-four
|
|
23
|
+
* pictures to spread across the pass, which on its own is a flipbook — a new
|
|
24
|
+
* one every 175ms, however fast the screen refreshes. The two layers are what
|
|
25
|
+
* fills the gap: one holds the frame being left and one the frame being
|
|
26
|
+
* arrived at, and only their opacities move, which the compositor animates at
|
|
27
|
+
* the display's own rate without the drawing being touched.
|
|
28
|
+
*
|
|
29
|
+
* They are opacities on views rather than on the dots, and that is not a
|
|
30
|
+
* detail. A value that changes every frame sitting beside the path string
|
|
31
|
+
* pushes the path every frame too, and every push of a path is a re-parse of
|
|
32
|
+
* several hundred subpaths — which is the cost this whole arrangement exists
|
|
33
|
+
* to avoid. Kept apart, the path is pushed only when it actually changes.
|
|
26
34
|
*
|
|
27
35
|
* ## What arrives, and when
|
|
28
36
|
*
|
|
@@ -34,11 +42,11 @@
|
|
|
34
42
|
*
|
|
35
43
|
* ## Reduced motion
|
|
36
44
|
*
|
|
37
|
-
* The
|
|
45
|
+
* The light stops, and the field holds wherever the clock was left. A
|
|
38
46
|
* placeholder that shows nothing is indistinguishable from a component that
|
|
39
47
|
* failed to load, so this is a quieter picture rather than an empty one.
|
|
40
48
|
*/
|
|
41
|
-
import { useEffect,
|
|
49
|
+
import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
42
50
|
import {
|
|
43
51
|
Pressable,
|
|
44
52
|
StyleSheet,
|
|
@@ -61,13 +69,14 @@ import Animated, {
|
|
|
61
69
|
} from 'react-native-reanimated';
|
|
62
70
|
import { tv } from 'tailwind-variants';
|
|
63
71
|
import { useCSSVariable } from 'uniwind';
|
|
64
|
-
import { useDirectionSign } from '../../hooks/use-direction';
|
|
65
72
|
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from '../../icons';
|
|
66
73
|
import { Text } from '../../primitives/text';
|
|
67
74
|
import { cn } from '../../utils/cn';
|
|
68
75
|
import {
|
|
69
76
|
DOT_REST_OPACITY,
|
|
70
|
-
|
|
77
|
+
FRAMES,
|
|
78
|
+
crossfadeAlphas,
|
|
79
|
+
framePhase,
|
|
71
80
|
gridPath,
|
|
72
81
|
litFrames,
|
|
73
82
|
type DotFieldAnimation,
|
|
@@ -241,8 +250,21 @@ function ImageGenerationField({
|
|
|
241
250
|
* light is somewhere legible rather than dead centre or absent, which is the
|
|
242
251
|
* difference between "not animating" and "failed to load".
|
|
243
252
|
*/
|
|
244
|
-
const
|
|
245
|
-
d: frames[
|
|
253
|
+
const slotA = useAnimatedProps(() => ({
|
|
254
|
+
d: frames[Math.floor(framePhase(clock.value, animation))] ?? '',
|
|
255
|
+
}));
|
|
256
|
+
const slotB = useAnimatedProps(() => ({
|
|
257
|
+
d: frames[(Math.floor(framePhase(clock.value, animation)) + 1) % FRAMES] ?? '',
|
|
258
|
+
}));
|
|
259
|
+
|
|
260
|
+
// Only these move between one frame and the next, and a view's opacity is a
|
|
261
|
+
// layer alpha — so the drawing underneath is composited again rather than
|
|
262
|
+
// drawn again.
|
|
263
|
+
const fadeOut = useAnimatedStyle(() => ({
|
|
264
|
+
opacity: crossfadeAlphas(framePhase(clock.value, animation) % 1)[0],
|
|
265
|
+
}));
|
|
266
|
+
const fadeIn = useAnimatedStyle(() => ({
|
|
267
|
+
opacity: crossfadeAlphas(framePhase(clock.value, animation) % 1)[1],
|
|
246
268
|
}));
|
|
247
269
|
|
|
248
270
|
const onLayout = (event: LayoutChangeEvent) => {
|
|
@@ -261,14 +283,41 @@ function ImageGenerationField({
|
|
|
261
283
|
{...props}
|
|
262
284
|
>
|
|
263
285
|
{grid ? (
|
|
264
|
-
|
|
286
|
+
<>
|
|
265
287
|
{/* The field at rest, under everything and never touched again. */}
|
|
266
|
-
<
|
|
288
|
+
<Svg
|
|
289
|
+
width={size.width}
|
|
290
|
+
height={size.height}
|
|
291
|
+
style={StyleSheet.absoluteFill}
|
|
292
|
+
>
|
|
293
|
+
<Path d={grid} fill={color} fillOpacity={DOT_REST_OPACITY} />
|
|
294
|
+
</Svg>
|
|
267
295
|
{/* The dots the light has reached, drawn larger over their own
|
|
268
296
|
resting copies rather than instead of them — so a dot brightens
|
|
269
|
-
in place instead of appearing to move.
|
|
270
|
-
|
|
271
|
-
|
|
297
|
+
in place instead of appearing to move.
|
|
298
|
+
|
|
299
|
+
Two of them: the frame being left and the frame being arrived at.
|
|
300
|
+
Each carries its dots at full strength and the pair is brought to
|
|
301
|
+
LIT_OPACITY by the views, because what fades has to be the layer
|
|
302
|
+
and not the fill — see crossfadeAlphas for why the ramps are not
|
|
303
|
+
simply t and 1 - t. */}
|
|
304
|
+
<Animated.View
|
|
305
|
+
style={[StyleSheet.absoluteFill, fadeOut]}
|
|
306
|
+
pointerEvents="none"
|
|
307
|
+
>
|
|
308
|
+
<Svg width={size.width} height={size.height}>
|
|
309
|
+
<AnimatedPath animatedProps={slotA} fill={color} />
|
|
310
|
+
</Svg>
|
|
311
|
+
</Animated.View>
|
|
312
|
+
<Animated.View
|
|
313
|
+
style={[StyleSheet.absoluteFill, fadeIn]}
|
|
314
|
+
pointerEvents="none"
|
|
315
|
+
>
|
|
316
|
+
<Svg width={size.width} height={size.height}>
|
|
317
|
+
<AnimatedPath animatedProps={slotB} fill={color} />
|
|
318
|
+
</Svg>
|
|
319
|
+
</Animated.View>
|
|
320
|
+
</>
|
|
272
321
|
) : null}
|
|
273
322
|
</View>
|
|
274
323
|
);
|
package/src/native/index.ts
CHANGED
|
@@ -145,6 +145,20 @@ interface SwiftUIModifiers {
|
|
|
145
145
|
cornerRadius?: number
|
|
146
146
|
) => unknown;
|
|
147
147
|
controlSize: (size: 'mini' | 'small' | 'regular' | 'large' | 'extraLarge') => unknown;
|
|
148
|
+
/**
|
|
149
|
+
* Paints a sheet's own surface, rather than a background behind its content.
|
|
150
|
+
*
|
|
151
|
+
* It is the only way out of the material a sheet is drawn in by default, and
|
|
152
|
+
* it reaches what an ordinary background cannot: the grabber's strip at the
|
|
153
|
+
* top and the safe-area inset at the bottom, which belong to the sheet's
|
|
154
|
+
* chrome and not to anything hosted inside it.
|
|
155
|
+
*
|
|
156
|
+
* **iOS 16.4 and up.** Below that it is inert and the sheet keeps its
|
|
157
|
+
* material — the same shape as glass being inert below iOS 26, and the same
|
|
158
|
+
* trap: indistinguishable from the prop not working. Check the OS before
|
|
159
|
+
* changing any code on a report of "the colour did nothing".
|
|
160
|
+
*/
|
|
161
|
+
presentationBackground: (color: string) => unknown;
|
|
148
162
|
}
|
|
149
163
|
|
|
150
164
|
let modifiersResolved = false;
|
|
@@ -165,3 +179,34 @@ export function getSwiftUIModifiers(): SwiftUIModifiers | null {
|
|
|
165
179
|
|
|
166
180
|
return modifiers;
|
|
167
181
|
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The Compose view modifiers — the other half of the same door.
|
|
185
|
+
*
|
|
186
|
+
* Android's toolkit has its own modifier system and its own vocabulary, so a
|
|
187
|
+
* modifier built for one platform is not a modifier the other can read. Where
|
|
188
|
+
* both are asked the same question the answers are written separately, here and
|
|
189
|
+
* above, rather than one being sent to both.
|
|
190
|
+
*/
|
|
191
|
+
interface ComposeModifiers {
|
|
192
|
+
background: (color: string) => unknown;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let composeResolved = false;
|
|
196
|
+
let compose: ComposeModifiers | null = null;
|
|
197
|
+
|
|
198
|
+
export function getComposeModifiers(): ComposeModifiers | null {
|
|
199
|
+
if (composeResolved) return compose;
|
|
200
|
+
composeResolved = true;
|
|
201
|
+
|
|
202
|
+
if (Platform.OS !== 'android') return null;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
206
|
+
compose = require('@expo/ui/jetpack-compose/modifiers') as ComposeModifiers;
|
|
207
|
+
} catch {
|
|
208
|
+
compose = null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return compose;
|
|
212
|
+
}
|