@flux-ui/visuals 3.6.1 → 3.7.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flux-ui/visuals",
3
3
  "description": "Visual effect components for the Flux UI library.",
4
- "version": "3.6.1",
4
+ "version": "3.7.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/basmilius",
@@ -48,10 +48,10 @@
48
48
  "module": "./dist/index.js",
49
49
  "types": "./dist/index.d.ts",
50
50
  "dependencies": {
51
- "@basmilius/common": "^3.52.0",
52
- "@basmilius/utils": "^3.52.0",
53
- "@flux-ui/internals": "3.6.1",
54
- "@flux-ui/types": "3.6.1",
51
+ "@basmilius/common": "^3.53.0",
52
+ "@basmilius/utils": "^3.53.0",
53
+ "@flux-ui/internals": "3.7.0",
54
+ "@flux-ui/types": "3.7.0",
55
55
  "clsx": "^2.1.1",
56
56
  "rough-notation": "^0.5.1"
57
57
  },
@@ -59,9 +59,9 @@
59
59
  "vue": "^3.6.0-beta.17"
60
60
  },
61
61
  "devDependencies": {
62
- "@basmilius/vite-preset": "^3.52.0",
62
+ "@basmilius/vite-preset": "^3.53.0",
63
63
  "@types/node": "^26.1.1",
64
- "@vitejs/plugin-vue": "^6.0.7",
64
+ "@vitejs/plugin-vue": "^6.0.8",
65
65
  "@vue/tsconfig": "^0.9.1",
66
66
  "sass-embedded": "^1.100.0",
67
67
  "typescript": "6.0.3",
@@ -32,14 +32,13 @@
32
32
  }>();
33
33
 
34
34
  const canvasRef = useTemplateRef('canvas');
35
- const componentId = useComponentId();
36
- const inView = useInView(canvasRef, {initial: true});
37
-
38
35
  const contextRef = ref<CanvasRenderingContext2D>();
39
36
  const animationFrame = ref(0);
40
37
  const tick = ref(0);
41
38
  const size = ref<{ width: number; height: number; } | null>(null);
42
39
 
40
+ const componentId = useComponentId();
41
+ const inView = useInView(canvasRef, {initial: true});
43
42
  const reducedMotion = prefersReducedMotion();
44
43
 
45
44
  const polygons = computed(() => {
@@ -72,6 +71,45 @@
72
71
  return polygons;
73
72
  });
74
73
 
74
+ watch(canvasRef, (canvas, _, onCleanup) => {
75
+ if (!canvas) {
76
+ contextRef.value = undefined;
77
+ size.value = null;
78
+ return;
79
+ }
80
+
81
+ contextRef.value = canvas.getContext('2d', {
82
+ alpha: true,
83
+ colorSpace: 'display-p3'
84
+ })!;
85
+
86
+ if (typeof ResizeObserver === 'undefined') {
87
+ size.value = {width: canvas.offsetWidth, height: canvas.offsetHeight};
88
+ canvas.width = canvas.offsetWidth;
89
+ canvas.height = canvas.offsetHeight;
90
+ return;
91
+ }
92
+
93
+ const observer = new ResizeObserver(() => {
94
+ const width = canvas.offsetWidth;
95
+ const height = canvas.offsetHeight;
96
+
97
+ if (!width || !height || (size.value?.width === width && size.value?.height === height)) {
98
+ return;
99
+ }
100
+
101
+ canvas.width = width;
102
+ canvas.height = height;
103
+ size.value = {width, height};
104
+ });
105
+
106
+ observer.observe(canvas);
107
+
108
+ onCleanup(() => observer.disconnect());
109
+ }, {immediate: true});
110
+
111
+ watch([polygons, () => opacity, size, inView], () => restart());
112
+
75
113
  onBeforeUnmount(() => cancel());
76
114
 
77
115
  function cancel(): void {
@@ -145,43 +183,4 @@
145
183
 
146
184
  schedule();
147
185
  }
148
-
149
- watch(canvasRef, (canvas, _, onCleanup) => {
150
- if (!canvas) {
151
- contextRef.value = undefined;
152
- size.value = null;
153
- return;
154
- }
155
-
156
- contextRef.value = canvas.getContext('2d', {
157
- alpha: true,
158
- colorSpace: 'display-p3'
159
- })!;
160
-
161
- if (typeof ResizeObserver === 'undefined') {
162
- size.value = {width: canvas.offsetWidth, height: canvas.offsetHeight};
163
- canvas.width = canvas.offsetWidth;
164
- canvas.height = canvas.offsetHeight;
165
- return;
166
- }
167
-
168
- const observer = new ResizeObserver(() => {
169
- const width = canvas.offsetWidth;
170
- const height = canvas.offsetHeight;
171
-
172
- if (!width || !height || (size.value?.width === width && size.value?.height === height)) {
173
- return;
174
- }
175
-
176
- canvas.width = width;
177
- canvas.height = height;
178
- size.value = {width, height};
179
- });
180
-
181
- observer.observe(canvas);
182
-
183
- onCleanup(() => observer.disconnect());
184
- }, {immediate: true});
185
-
186
- watch([polygons, () => opacity, size, inView], () => restart());
187
186
  </script>
@@ -49,60 +49,6 @@
49
49
  return context.getImageData(0, 0, 1, 1).data;
50
50
  });
51
51
 
52
- function draw(context: CanvasRenderingContext2D, width: number, height: number, columns: number, rows: number, squares: Float32Array, dpr: number): void {
53
- context.clearRect(0, 0, width * dpr, height * dpr);
54
-
55
- const [r, g, b] = unref(rgb);
56
-
57
- for (let i = 0; i < columns; ++i) {
58
- for (let j = 0; j < rows; ++j) {
59
- const opacity = squares[i * rows + j];
60
- context.fillStyle = `rgb(${r} ${g} ${b} / ${opacity})`;
61
- context.fillRect(
62
- (i * (size + gap) + width / 2 - (columns / 2 * (size + gap) - gap / 2)) * dpr,
63
- (j * (size + gap) + height / 2 - (rows / 2 * (size + gap) - gap / 2)) * dpr,
64
- size * dpr,
65
- size * dpr
66
- );
67
- }
68
- }
69
- }
70
-
71
- function setup(canvas: HTMLCanvasElement) {
72
- const width = canvas.clientWidth;
73
- const height = canvas.clientHeight;
74
- const dpr = window.devicePixelRatio || 1;
75
- canvas.width = width * dpr;
76
- canvas.height = height * dpr;
77
- canvas.style.width = `${width}px`;
78
- canvas.style.height = `${height}px`;
79
-
80
- const columns = Math.ceil(width / (size + gap));
81
- const rows = Math.ceil(height / (size + gap));
82
- const squares = new Float32Array(columns * rows);
83
-
84
- for (let i = 0; i < squares.length; ++i) {
85
- squares[i] = mulberry.next() * maxOpacity;
86
- }
87
-
88
- return {
89
- width,
90
- height,
91
- columns,
92
- rows,
93
- squares,
94
- dpr
95
- };
96
- }
97
-
98
- function tick(squares: Float32Array, delta: number): void {
99
- for (let i = 0; i < squares.length; ++i) {
100
- if (mulberry.next() < flickerChance * delta) {
101
- squares[i] = mulberry.next() * maxOpacity;
102
- }
103
- }
104
- }
105
-
106
52
  watch([canvasRef, inView], ([canvas, inView], _, onCleanup) => {
107
53
  if (!canvas || !inView) {
108
54
  return;
@@ -156,4 +102,58 @@
156
102
  cancelAnimationFrame(frame);
157
103
  });
158
104
  }, {immediate: true});
105
+
106
+ function draw(context: CanvasRenderingContext2D, width: number, height: number, columns: number, rows: number, squares: Float32Array, dpr: number): void {
107
+ context.clearRect(0, 0, width * dpr, height * dpr);
108
+
109
+ const [r, g, b] = unref(rgb);
110
+
111
+ for (let i = 0; i < columns; ++i) {
112
+ for (let j = 0; j < rows; ++j) {
113
+ const opacity = squares[i * rows + j];
114
+ context.fillStyle = `rgb(${r} ${g} ${b} / ${opacity})`;
115
+ context.fillRect(
116
+ (i * (size + gap) + width / 2 - (columns / 2 * (size + gap) - gap / 2)) * dpr,
117
+ (j * (size + gap) + height / 2 - (rows / 2 * (size + gap) - gap / 2)) * dpr,
118
+ size * dpr,
119
+ size * dpr
120
+ );
121
+ }
122
+ }
123
+ }
124
+
125
+ function setup(canvas: HTMLCanvasElement) {
126
+ const width = canvas.clientWidth;
127
+ const height = canvas.clientHeight;
128
+ const dpr = window.devicePixelRatio || 1;
129
+ canvas.width = width * dpr;
130
+ canvas.height = height * dpr;
131
+ canvas.style.width = `${width}px`;
132
+ canvas.style.height = `${height}px`;
133
+
134
+ const columns = Math.ceil(width / (size + gap));
135
+ const rows = Math.ceil(height / (size + gap));
136
+ const squares = new Float32Array(columns * rows);
137
+
138
+ for (let i = 0; i < squares.length; ++i) {
139
+ squares[i] = mulberry.next() * maxOpacity;
140
+ }
141
+
142
+ return {
143
+ width,
144
+ height,
145
+ columns,
146
+ rows,
147
+ squares,
148
+ dpr
149
+ };
150
+ }
151
+
152
+ function tick(squares: Float32Array, delta: number): void {
153
+ for (let i = 0; i < squares.length; ++i) {
154
+ if (mulberry.next() < flickerChance * delta) {
155
+ squares[i] = mulberry.next() * maxOpacity;
156
+ }
157
+ }
158
+ }
159
159
  </script>
@@ -48,10 +48,10 @@
48
48
  default(): any;
49
49
  }>();
50
50
 
51
- const group = useHighlighterGroupInjection();
52
-
53
51
  const targetRef = useTemplateRef('target');
54
52
 
53
+ const group = useHighlighterGroupInjection();
54
+
55
55
  // Standalone in-view tracking. In a group the parent owns the reveal timing,
56
56
  // so no observer is set up at all.
57
57
  const inView = group ? shallowRef(true) : useInView(targetRef, {initial: !whenInView});
@@ -71,6 +71,39 @@
71
71
  let shownTimer: number | undefined;
72
72
  let revealed = false;
73
73
 
74
+ // The annotation type is immutable, so any prop change rebuilds it from scratch.
75
+ watch([effectiveVariant, effectiveColor, effectiveStrokeWidth, effectiveAnimationDuration, effectiveIterations, effectivePadding, effectiveMultiline], () => build());
76
+
77
+ // Standalone reveal once the element scrolls into view (whenInView).
78
+ watch(inView, () => {
79
+ if (!group) {
80
+ reveal();
81
+ }
82
+ });
83
+
84
+ onMounted(() => {
85
+ const element = targetRef.value;
86
+
87
+ if (group && element) {
88
+ entry = {element, getAnnotation: () => annotation};
89
+ group.add(entry);
90
+ }
91
+
92
+ build();
93
+ });
94
+
95
+ onBeforeUnmount(() => {
96
+ if (group && entry) {
97
+ group.remove(entry);
98
+ entry = null;
99
+ }
100
+
101
+ window.clearTimeout(shownTimer);
102
+ stopSettleWatch();
103
+ annotation?.remove();
104
+ annotation = null;
105
+ });
106
+
74
107
  // rough-notation has no completion callback, so shown is emitted after the
75
108
  // draw animation's duration, or immediately when it draws without animation.
76
109
  function emitShown(): void {
@@ -177,39 +210,6 @@
177
210
  observer.observe(document.body);
178
211
  }
179
212
 
180
- onMounted(() => {
181
- const element = targetRef.value;
182
-
183
- if (group && element) {
184
- entry = {element, getAnnotation: () => annotation};
185
- group.add(entry);
186
- }
187
-
188
- build();
189
- });
190
-
191
- // The annotation type is immutable, so any prop change rebuilds it from scratch.
192
- watch([effectiveVariant, effectiveColor, effectiveStrokeWidth, effectiveAnimationDuration, effectiveIterations, effectivePadding, effectiveMultiline], () => build());
193
-
194
- // Standalone reveal once the element scrolls into view (whenInView).
195
- watch(inView, () => {
196
- if (!group) {
197
- reveal();
198
- }
199
- });
200
-
201
- onBeforeUnmount(() => {
202
- if (group && entry) {
203
- group.remove(entry);
204
- entry = null;
205
- }
206
-
207
- window.clearTimeout(shownTimer);
208
- stopSettleWatch();
209
- annotation?.remove();
210
- annotation = null;
211
- });
212
-
213
213
  defineExpose({
214
214
  hide,
215
215
  replay,
@@ -48,6 +48,62 @@
48
48
 
49
49
  const CUBIC_BEZIER_PATTERN = /^cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)$/;
50
50
 
51
+ // Fallback for an unrecognised easing value, matching the --swift-out default.
52
+ const swiftOutEasing = cubicBezier(0.55, 0, 0.1, 1);
53
+
54
+ const labelRef = useTemplateRef('label');
55
+
56
+ let frame = 0;
57
+ let current = animateOnMount ? 0 : value;
58
+
59
+ // Resolve the easing prop to a plain (t) => number function the tween can
60
+ // sample: functions pass through, keywords map to the table above and a
61
+ // cubic-bezier(...) string is parsed into a solver, falling back to swift-out.
62
+ const easingFunction = computed<NumberFlowEasingFunction>(() => {
63
+ if (typeof easing === 'function') {
64
+ return easing;
65
+ }
66
+
67
+ const keyword = EASINGS[easing as NumberFlowEasingKeyword];
68
+
69
+ if (keyword) {
70
+ return keyword;
71
+ }
72
+
73
+ return parseCubicBezier(easing) ?? swiftOutEasing;
74
+ });
75
+
76
+ // Default to whole numbers so a mid-tween value never sprouts stray decimals.
77
+ // Currency, percentage and fractional displays opt in through `format`.
78
+ const formatter = computed(() => new Intl.NumberFormat(locale, format ?? {maximumFractionDigits: 0}));
79
+
80
+ // Rendered once for SSR / first paint only. The engine owns the span's text
81
+ // after mount, so this must NOT be reactive - a reactive {{ value }} would make
82
+ // Vue re-patch the text node every frame and wipe the tween. The accessible
83
+ // name stays current through the reactive :aria-label binding.
84
+ const initialText = formatter.value.format(animateOnMount ? 0 : value);
85
+
86
+ // Screen readers always read the final, settled value rather than the
87
+ // intermediate frames streaming past on screen.
88
+ const accessibleValue = computed(() => formatter.value.format(value));
89
+
90
+ // Tween from wherever the display currently sits, so a value that changes
91
+ // mid-tween keeps rolling smoothly instead of jumping.
92
+ watch(() => value, next => tween(current, next));
93
+
94
+ // Re-render in place when the locale or format changes.
95
+ watch(formatter, () => render(current));
96
+
97
+ onMounted(() => {
98
+ if (animateOnMount) {
99
+ tween(0, value);
100
+ } else {
101
+ render(value);
102
+ }
103
+ });
104
+
105
+ onBeforeUnmount(cancel);
106
+
51
107
  // Evaluate a cubic-bezier(x1, y1, x2, y2) timing function in JS. The control
52
108
  // points describe x(t) and y(t); for a given progress we need y at the t where
53
109
  // x(t) === progress. x is solved with Newton-Raphson and a bisection fallback,
@@ -132,45 +188,6 @@
132
188
  return cubicBezier(Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4]));
133
189
  }
134
190
 
135
- // Fallback for an unrecognised easing value, matching the --swift-out default.
136
- const swiftOutEasing = cubicBezier(0.55, 0, 0.1, 1);
137
-
138
- // Resolve the easing prop to a plain (t) => number function the tween can
139
- // sample: functions pass through, keywords map to the table above and a
140
- // cubic-bezier(...) string is parsed into a solver, falling back to swift-out.
141
- const easingFunction = computed<NumberFlowEasingFunction>(() => {
142
- if (typeof easing === 'function') {
143
- return easing;
144
- }
145
-
146
- const keyword = EASINGS[easing as NumberFlowEasingKeyword];
147
-
148
- if (keyword) {
149
- return keyword;
150
- }
151
-
152
- return parseCubicBezier(easing) ?? swiftOutEasing;
153
- });
154
-
155
- // Default to whole numbers so a mid-tween value never sprouts stray decimals.
156
- // Currency, percentage and fractional displays opt in through `format`.
157
- const formatter = computed(() => new Intl.NumberFormat(locale, format ?? {maximumFractionDigits: 0}));
158
-
159
- // Rendered once for SSR / first paint only. The engine owns the span's text
160
- // after mount, so this must NOT be reactive - a reactive {{ value }} would make
161
- // Vue re-patch the text node every frame and wipe the tween. The accessible
162
- // name stays current through the reactive :aria-label binding.
163
- const initialText = formatter.value.format(animateOnMount ? 0 : value);
164
-
165
- // Screen readers always read the final, settled value rather than the
166
- // intermediate frames streaming past on screen.
167
- const accessibleValue = computed(() => formatter.value.format(value));
168
-
169
- const labelRef = useTemplateRef('label');
170
-
171
- let frame = 0;
172
- let current = animateOnMount ? 0 : value;
173
-
174
191
  function render(next: number): void {
175
192
  current = next;
176
193
 
@@ -215,21 +232,4 @@
215
232
 
216
233
  frame = requestAnimationFrame(step);
217
234
  }
218
-
219
- onMounted(() => {
220
- if (animateOnMount) {
221
- tween(0, value);
222
- } else {
223
- render(value);
224
- }
225
- });
226
-
227
- // Tween from wherever the display currently sits, so a value that changes
228
- // mid-tween keeps rolling smoothly instead of jumping.
229
- watch(() => value, next => tween(current, next));
230
-
231
- // Re-render in place when the locale or format changes.
232
- watch(formatter, () => render(current));
233
-
234
- onBeforeUnmount(cancel);
235
235
  </script>
@@ -82,13 +82,35 @@
82
82
  let revertTimeout: number | undefined;
83
83
  let restingText: string | undefined;
84
84
 
85
- const glyph = (char: string): string => char === ' ' ? NBSP : char;
85
+ watch(() => text, value => set(value));
86
+
87
+ onMounted(() => {
88
+ const element = labelRef.value;
89
+
90
+ if (element) {
91
+ buildSlotText(element, text);
92
+ }
93
+ });
94
+
95
+ onBeforeUnmount(() => {
96
+ window.clearTimeout(revertTimeout);
97
+
98
+ const element = labelRef.value;
99
+
100
+ if (element) {
101
+ clearSlotText(element, text);
102
+ }
103
+ });
104
+
105
+ function glyph(char: string): string {
106
+ return char === ' ' ? NBSP : char;
107
+ }
86
108
 
87
109
  // Sweep the hue across the line so the roll lands as a chromatic spectrum.
88
- const chromaticColor = (index: number, total: number): string => {
110
+ function chromaticColor(index: number, total: number): string {
89
111
  const t = total <= 1 ? 0 : index / (total - 1);
90
112
  return `hsl(${(t * 320) % 360} 92% 60%)`;
91
- };
113
+ }
92
114
 
93
115
  function baseOptions(): AnimateOptions {
94
116
  return {
@@ -430,26 +452,6 @@
430
452
  }, revertAfter);
431
453
  }
432
454
 
433
- onMounted(() => {
434
- const element = labelRef.value;
435
-
436
- if (element) {
437
- buildSlotText(element, text);
438
- }
439
- });
440
-
441
- watch(() => text, value => set(value));
442
-
443
- onBeforeUnmount(() => {
444
- window.clearTimeout(revertTimeout);
445
-
446
- const element = labelRef.value;
447
-
448
- if (element) {
449
- clearSlotText(element, text);
450
- }
451
- });
452
-
453
455
  defineExpose({
454
456
  flash,
455
457
  set
@@ -22,6 +22,10 @@
22
22
  fixed: boolean;
23
23
  };
24
24
 
25
+ const emit = defineEmits<{
26
+ finished: [];
27
+ }>();
28
+
25
29
  const {
26
30
  text,
27
31
  characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
@@ -38,10 +42,6 @@
38
42
  readonly stagger?: number;
39
43
  }>();
40
44
 
41
- const emit = defineEmits<{
42
- finished: [];
43
- }>();
44
-
45
45
  // Rendered once for SSR / first paint only. The engine owns the span's text
46
46
  // after mount, so this must NOT be reactive - a reactive {{ text }} would make
47
47
  // Vue re-patch the text node every frame and wipe the scramble. The accessible
@@ -53,6 +53,10 @@
53
53
  let frame = 0;
54
54
  let currentText = text;
55
55
 
56
+ watch(() => text, value => set(value));
57
+
58
+ onBeforeUnmount(cancel);
59
+
56
60
  function cancel(): void {
57
61
  if (frame) {
58
62
  cancelAnimationFrame(frame);
@@ -157,10 +161,6 @@
157
161
  scramble(element, currentText, currentText, true);
158
162
  }
159
163
 
160
- watch(() => text, value => set(value));
161
-
162
- onBeforeUnmount(cancel);
163
-
164
164
  defineExpose({
165
165
  replay,
166
166
  set