panelui-native 0.81.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 +189 -21
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +13 -0
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/button-group/index.js +3 -0
- package/lib/module/components/button-group/index.js.map +1 -1
- package/lib/module/components/image-generation/dot-field.js +267 -0
- package/lib/module/components/image-generation/dot-field.js.map +1 -0
- package/lib/module/components/image-generation/index.js +459 -0
- package/lib/module/components/image-generation/index.js.map +1 -0
- package/lib/module/components/kpi/index.js +2 -4
- package/lib/module/components/kpi/index.js.map +1 -1
- package/lib/module/components/menu/index.js +8 -16
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +340 -33
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/select/index.js +127 -23
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slide-button/index.js +610 -0
- package/lib/module/components/slide-button/index.js.map +1 -0
- package/lib/module/components/slide-button/slide-button-track.js +98 -0
- package/lib/module/components/slide-button/slide-button-track.js.map +1 -0
- package/lib/module/icons/index.js +553 -1934
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js +24 -0
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/portal.js +15 -1
- package/lib/module/primitives/portal.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/button/index.d.ts +12 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button-group/index.d.ts +9 -0
- package/lib/typescript/src/components/button-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/image-generation/dot-field.d.ts +96 -0
- package/lib/typescript/src/components/image-generation/dot-field.d.ts.map +1 -0
- package/lib/typescript/src/components/image-generation/index.d.ts +134 -0
- package/lib/typescript/src/components/image-generation/index.d.ts.map +1 -0
- package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +194 -10
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/select/index.d.ts +75 -2
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slide-button/index.d.ts +317 -0
- package/lib/typescript/src/components/slide-button/index.d.ts.map +1 -0
- package/lib/typescript/src/components/slide-button/slide-button-track.d.ts +70 -0
- package/lib/typescript/src/components/slide-button/slide-button-track.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +136 -189
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/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/lib/typescript/src/primitives/portal.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/components/bottom-sheet/index.tsx +223 -20
- package/src/components/button/index.tsx +10 -2
- package/src/components/button-group/index.tsx +1 -0
- package/src/components/image-generation/dot-field.ts +282 -0
- package/src/components/image-generation/index.tsx +537 -0
- package/src/components/kpi/index.tsx +2 -4
- package/src/components/menu/index.tsx +10 -15
- package/src/components/panelside/index.tsx +498 -33
- package/src/components/select/index.tsx +158 -31
- package/src/components/slide-button/index.tsx +681 -0
- package/src/components/slide-button/slide-button-track.ts +100 -0
- package/src/icons/index.tsx +338 -1327
- package/src/index.ts +28 -0
- package/src/native/index.ts +45 -0
- package/src/primitives/portal.tsx +15 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dot field's drawing, kept out of the component so it can be checked
|
|
3
|
+
* without a renderer — the same split `progress-button-hold` uses.
|
|
4
|
+
*
|
|
5
|
+
* ## Nothing is built while it is running
|
|
6
|
+
*
|
|
7
|
+
* The field is hundreds of dots and it is on screen for as long as a model
|
|
8
|
+
* takes to answer, on a page that may be showing several at once. So every
|
|
9
|
+
* frame it will ever draw is built when the box is measured, and playing it is
|
|
10
|
+
* handing react-native-svg a string that already exists.
|
|
11
|
+
*
|
|
12
|
+
* Two earlier versions did this the obvious ways and both stalled a page of
|
|
13
|
+
* them on scroll: one rebuilt every dot as a path string on every frame, the
|
|
14
|
+
* other clipped a moving shape of light to a path with hundreds of subpaths in
|
|
15
|
+
* it, which is re-rasterized every time the view is drawn.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Points between one dot and the next. */
|
|
19
|
+
export const DOT_GAP = 10;
|
|
20
|
+
|
|
21
|
+
/** The dot's radius where the light is not, and where it is strongest. */
|
|
22
|
+
export const DOT_RADIUS = 1;
|
|
23
|
+
export const DOT_LIT_RADIUS = 1.9;
|
|
24
|
+
|
|
25
|
+
/** How visible the resting grid is, under everything. */
|
|
26
|
+
export const DOT_REST_OPACITY = 0.16;
|
|
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
|
+
|
|
37
|
+
/** How many still frames one loop is drawn as. */
|
|
38
|
+
export const FRAMES = 24;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The most dots the field will draw, whatever box it is given.
|
|
42
|
+
*
|
|
43
|
+
* The grid opens up rather than the count running away: a full-width field on
|
|
44
|
+
* a tablet gets the same picture drawn coarser, which costs the same and looks
|
|
45
|
+
* the same at arm's length.
|
|
46
|
+
*/
|
|
47
|
+
const MAX_DOTS = 700;
|
|
48
|
+
|
|
49
|
+
/** How the light moves through the field. */
|
|
50
|
+
export type DotFieldAnimation = 'drift' | 'pulse' | 'scan';
|
|
51
|
+
|
|
52
|
+
/** The gap this box is drawn at, opened up if the grid would be too dense. */
|
|
53
|
+
export function gapFor(width: number, height: number): number {
|
|
54
|
+
if (width <= 0 || height <= 0) return DOT_GAP;
|
|
55
|
+
const cells = (width / DOT_GAP + 1) * (height / DOT_GAP + 1);
|
|
56
|
+
return cells > MAX_DOTS ? DOT_GAP * Math.sqrt(cells / MAX_DOTS) : DOT_GAP;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A circle as an SVG subpath, which is two half arcs. */
|
|
60
|
+
function circle(x: number, y: number, r: number): string {
|
|
61
|
+
const d = (r * 2).toFixed(2);
|
|
62
|
+
return `M${(x - r).toFixed(2)},${y.toFixed(2)}a${r},${r} 0 1,0 ${d},0a${r},${r} 0 1,0 -${d},0`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Every dot's anchor, in reading order.
|
|
67
|
+
*
|
|
68
|
+
* Laid out inside the box by the widest a dot can be drawn, and counted with
|
|
69
|
+
* `floor` rather than `ceil`, so no part of any dot falls outside it.
|
|
70
|
+
*
|
|
71
|
+
* Counted with `ceil` the grid overhangs by up to half a gap on every side.
|
|
72
|
+
* That is invisible while the light stays near the middle and obvious the
|
|
73
|
+
* moment it reaches an edge — which the scanning band does by design, and a
|
|
74
|
+
* short wide box does simply by being short. The dots then draw outside the
|
|
75
|
+
* card, which looks like the animation has escaped it.
|
|
76
|
+
*
|
|
77
|
+
* The row and column are still centred in whatever is left over, because laid
|
|
78
|
+
* out from the origin the field ends with a strip of nothing down one edge.
|
|
79
|
+
*/
|
|
80
|
+
function anchors(width: number, height: number): [number, number][] {
|
|
81
|
+
const gap = gapFor(width, height);
|
|
82
|
+
const margin = DOT_LIT_RADIUS;
|
|
83
|
+
const innerWidth = Math.max(0, width - margin * 2);
|
|
84
|
+
const innerHeight = Math.max(0, height - margin * 2);
|
|
85
|
+
|
|
86
|
+
const columns = Math.floor(innerWidth / gap) + 1;
|
|
87
|
+
const rows = Math.floor(innerHeight / gap) + 1;
|
|
88
|
+
const offsetX = margin + (innerWidth - (columns - 1) * gap) / 2;
|
|
89
|
+
const offsetY = margin + (innerHeight - (rows - 1) * gap) / 2;
|
|
90
|
+
|
|
91
|
+
const out: [number, number][] = [];
|
|
92
|
+
for (let row = 0; row < rows; row += 1) {
|
|
93
|
+
for (let column = 0; column < columns; column += 1) {
|
|
94
|
+
out.push([offsetX + column * gap, offsetY + row * gap]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The resting grid as one path. Drawn once, under the light. */
|
|
101
|
+
export function gridPath(width: number, height: number): string {
|
|
102
|
+
if (width <= 0 || height <= 0) return '';
|
|
103
|
+
let path = '';
|
|
104
|
+
for (const [x, y] of anchors(width, height)) path += circle(x, y, DOT_RADIUS);
|
|
105
|
+
return path;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** How many dots {@link gridPath} would draw for a box. */
|
|
109
|
+
export function dotCount(width: number, height: number): number {
|
|
110
|
+
if (width <= 0 || height <= 0) return 0;
|
|
111
|
+
return anchors(width, height).length;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* How strongly a dot at `distance` from the light is lit.
|
|
116
|
+
*
|
|
117
|
+
* Smoothstep rather than a linear ramp: linear gives the light a hard rim,
|
|
118
|
+
* because the eye finds the discontinuity in the first derivative. This one
|
|
119
|
+
* arrives and leaves at zero slope, so the light has no edge.
|
|
120
|
+
*/
|
|
121
|
+
export function influenceAt(distance: number, radius: number): number {
|
|
122
|
+
if (radius <= 0) return 0;
|
|
123
|
+
const proximity = Math.max(0, 1 - distance / radius);
|
|
124
|
+
return proximity * proximity * (3 - 2 * proximity);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Where the light is at a phase of the loop, and how wide it reaches.
|
|
129
|
+
*
|
|
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.
|
|
144
|
+
*/
|
|
145
|
+
function lightAt(
|
|
146
|
+
animation: DotFieldAnimation,
|
|
147
|
+
phase: number,
|
|
148
|
+
width: number,
|
|
149
|
+
height: number
|
|
150
|
+
): { x: number; y: number; radius: number; ring: number } {
|
|
151
|
+
const short = Math.min(width, height);
|
|
152
|
+
const turn = phase * Math.PI * 2;
|
|
153
|
+
|
|
154
|
+
if (animation === 'pulse') {
|
|
155
|
+
const reach = Math.sqrt(width * width + height * height) / 2;
|
|
156
|
+
return { x: width / 2, y: height / 2, radius: short * 0.3, ring: phase * reach };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (animation === 'scan') {
|
|
160
|
+
// A band: infinitely tall, so only the horizontal distance counts.
|
|
161
|
+
//
|
|
162
|
+
// It starts and ends half a band clear of the edges rather than a whole
|
|
163
|
+
// one — far enough to ramp in and out, near enough that some of it is
|
|
164
|
+
// always on the field. Travelling fully off, the loop spends several
|
|
165
|
+
// frames drawing nothing at all, which reads as the component having
|
|
166
|
+
// stopped rather than as a band that has passed.
|
|
167
|
+
const radius = width * 0.28;
|
|
168
|
+
return {
|
|
169
|
+
x: -radius * 0.5 + phase * (width + radius),
|
|
170
|
+
y: Number.NaN,
|
|
171
|
+
radius,
|
|
172
|
+
ring: 0,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
x: width / 2 + Math.sin(turn) * width * 0.26,
|
|
178
|
+
y: height / 2 + Math.cos(turn * 2) * height * 0.22,
|
|
179
|
+
radius: short * 0.42,
|
|
180
|
+
ring: 0,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The lit dots at one phase of the loop, as a path.
|
|
186
|
+
*
|
|
187
|
+
* Only the dots the light actually reaches are in it — the rest are already
|
|
188
|
+
* drawn by the resting grid underneath, so this is a fraction of the field
|
|
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.
|
|
195
|
+
*/
|
|
196
|
+
export function litPath(
|
|
197
|
+
width: number,
|
|
198
|
+
height: number,
|
|
199
|
+
phase: number,
|
|
200
|
+
animation: DotFieldAnimation = 'drift',
|
|
201
|
+
points?: [number, number][]
|
|
202
|
+
): string {
|
|
203
|
+
if (width <= 0 || height <= 0) return '';
|
|
204
|
+
|
|
205
|
+
const light = lightAt(animation, phase, width, height);
|
|
206
|
+
let path = '';
|
|
207
|
+
|
|
208
|
+
for (const [x, y] of points ?? anchors(width, height)) {
|
|
209
|
+
const deltaX = x - light.x;
|
|
210
|
+
const deltaY = Number.isNaN(light.y) ? 0 : y - light.y;
|
|
211
|
+
const distance = Math.abs(
|
|
212
|
+
Math.sqrt(deltaX * deltaX + deltaY * deltaY) - light.ring
|
|
213
|
+
);
|
|
214
|
+
const influence = influenceAt(distance, light.radius);
|
|
215
|
+
if (influence <= 0.02) continue;
|
|
216
|
+
path += circle(x, y, DOT_RADIUS + influence * (DOT_LIT_RADIUS - DOT_RADIUS));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return path;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Every frame of the loop, built once.
|
|
224
|
+
*
|
|
225
|
+
* A still frame the light is somewhere legible in is also what a paused or
|
|
226
|
+
* reduced-motion field shows, so there is no separate code path for holding it.
|
|
227
|
+
*/
|
|
228
|
+
export function litFrames(
|
|
229
|
+
width: number,
|
|
230
|
+
height: number,
|
|
231
|
+
animation: DotFieldAnimation = 'drift'
|
|
232
|
+
): string[] {
|
|
233
|
+
const points = width > 0 && height > 0 ? anchors(width, height) : [];
|
|
234
|
+
return Array.from({ length: FRAMES }, (_unused, index) =>
|
|
235
|
+
litPath(width, height, index / FRAMES, animation, points)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** One pass of the loop, in milliseconds, per animation. */
|
|
240
|
+
export const PERIOD: Record<DotFieldAnimation, number> = {
|
|
241
|
+
drift: 4200,
|
|
242
|
+
pulse: 2200,
|
|
243
|
+
scan: 1900,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/** Which frame a moment falls on. */
|
|
247
|
+
export function frameAt(time: number, animation: DotFieldAnimation): number {
|
|
248
|
+
'worklet';
|
|
249
|
+
const phase = (time % PERIOD[animation]) / PERIOD[animation];
|
|
250
|
+
return Math.min(FRAMES - 1, Math.floor(phase * FRAMES));
|
|
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
|
+
}
|