@flux-ui/visuals 0.0.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 +32 -0
- package/dist/component/FluxVisualAnimatedColors.vue.d.ts +10 -0
- package/dist/component/FluxVisualAttention.vue.d.ts +40 -0
- package/dist/component/FluxVisualBorderBeam.vue.d.ts +31 -0
- package/dist/component/FluxVisualBorderShine.vue.d.ts +53 -0
- package/dist/component/FluxVisualDotPattern.vue.d.ts +11 -0
- package/dist/component/FluxVisualFlickeringGrid.vue.d.ts +10 -0
- package/dist/component/FluxVisualGridPattern.vue.d.ts +10 -0
- package/dist/component/FluxVisualHighlighter.vue.d.ts +36 -0
- package/dist/component/FluxVisualHighlighterGroup.vue.d.ts +13 -0
- package/dist/component/FluxVisualNoise.vue.d.ts +9 -0
- package/dist/component/FluxVisualNumberFlow.vue.d.ts +14 -0
- package/dist/component/FluxVisualPing.vue.d.ts +8 -0
- package/dist/component/FluxVisualSlotText.vue.d.ts +41 -0
- package/dist/component/FluxVisualTextScramble.vue.d.ts +20 -0
- package/dist/component/FluxVisualTextShimmer.vue.d.ts +18 -0
- package/dist/component/index.d.ts +15 -0
- package/dist/composable/private/index.d.ts +2 -0
- package/dist/composable/private/useBorderBeamPulse.d.ts +21 -0
- package/dist/composable/private/useHighlighterGroup.d.ts +51 -0
- package/dist/index.css +1110 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12320 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/package.json +71 -0
- package/src/component/FluxVisualAnimatedColors.vue +187 -0
- package/src/component/FluxVisualAttention.vue +86 -0
- package/src/component/FluxVisualBorderBeam.vue +171 -0
- package/src/component/FluxVisualBorderShine.vue +36 -0
- package/src/component/FluxVisualDotPattern.vue +119 -0
- package/src/component/FluxVisualFlickeringGrid.vue +159 -0
- package/src/component/FluxVisualGridPattern.vue +128 -0
- package/src/component/FluxVisualHighlighter.vue +217 -0
- package/src/component/FluxVisualHighlighterGroup.vue +20 -0
- package/src/component/FluxVisualNoise.vue +31 -0
- package/src/component/FluxVisualNumberFlow.vue +235 -0
- package/src/component/FluxVisualPing.vue +26 -0
- package/src/component/FluxVisualSlotText.vue +457 -0
- package/src/component/FluxVisualTextScramble.vue +168 -0
- package/src/component/FluxVisualTextShimmer.vue +34 -0
- package/src/component/index.ts +15 -0
- package/src/composable/private/index.ts +2 -0
- package/src/composable/private/useBorderBeamPulse.ts +203 -0
- package/src/composable/private/useHighlighterGroup.ts +162 -0
- package/src/css/component/Attention.module.scss +104 -0
- package/src/css/component/BorderBeam.module.scss +1178 -0
- package/src/css/component/Highlighter.module.scss +7 -0
- package/src/css/component/Noise.module.scss +80 -0
- package/src/css/component/NumberFlow.module.scss +5 -0
- package/src/css/component/PatternGlow.module.scss +19 -0
- package/src/css/component/Ping.module.scss +44 -0
- package/src/css/component/SlotText.module.scss +43 -0
- package/src/css/component/TextScramble.module.scss +5 -0
- package/src/css/component/TextShimmer.module.scss +54 -0
- package/src/css/component/Visual.module.scss +71 -0
- package/src/index.ts +1 -0
- package/src/types.d.ts +13 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<canvas
|
|
3
|
+
ref="canvas"
|
|
4
|
+
aria-hidden="true"
|
|
5
|
+
:class="$style.flickeringGrid"/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script
|
|
9
|
+
lang="ts"
|
|
10
|
+
setup>
|
|
11
|
+
import { mulberry32 } from '@basmilius/utils';
|
|
12
|
+
import { prefersReducedMotion, useInView } from '@flux-ui/internals';
|
|
13
|
+
import { computed, unref, useTemplateRef, watch } from 'vue';
|
|
14
|
+
import $style from '~flux/visuals/css/component/Visual.module.scss';
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
color = '#1d4ed8',
|
|
18
|
+
flickerChance = 0.15,
|
|
19
|
+
gap = 6,
|
|
20
|
+
maxOpacity = 0.3,
|
|
21
|
+
size = 3
|
|
22
|
+
} = defineProps<{
|
|
23
|
+
readonly color?: string;
|
|
24
|
+
readonly flickerChance?: number;
|
|
25
|
+
readonly gap?: number;
|
|
26
|
+
readonly maxOpacity?: number;
|
|
27
|
+
readonly size?: number;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const canvasRef = useTemplateRef('canvas');
|
|
31
|
+
|
|
32
|
+
const inView = useInView(canvasRef);
|
|
33
|
+
|
|
34
|
+
const mulberry = mulberry32(13);
|
|
35
|
+
|
|
36
|
+
const rgb = computed(() => {
|
|
37
|
+
const canvas = document.createElement('canvas');
|
|
38
|
+
canvas.width = canvas.height = 1;
|
|
39
|
+
|
|
40
|
+
const context = canvas.getContext('2d');
|
|
41
|
+
|
|
42
|
+
if (!context) {
|
|
43
|
+
return [0, 0, 0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
context.fillStyle = color;
|
|
47
|
+
context.fillRect(0, 0, 1, 1);
|
|
48
|
+
|
|
49
|
+
return context.getImageData(0, 0, 1, 1).data;
|
|
50
|
+
});
|
|
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
|
+
watch([canvasRef, inView], ([canvas, inView], _, onCleanup) => {
|
|
107
|
+
if (!canvas || !inView) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const context = canvas.getContext('2d');
|
|
112
|
+
|
|
113
|
+
if (!context) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let frame = 0;
|
|
118
|
+
let lastTime = 0;
|
|
119
|
+
let {width, height, columns, rows, squares, dpr} = setup(canvas);
|
|
120
|
+
|
|
121
|
+
const reducedMotion = prefersReducedMotion();
|
|
122
|
+
|
|
123
|
+
const onResize = () => {
|
|
124
|
+
({width, height, columns, rows, squares, dpr} = setup(canvas));
|
|
125
|
+
|
|
126
|
+
if (reducedMotion) {
|
|
127
|
+
draw(context, width, height, columns, rows, squares, dpr);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
window.addEventListener('resize', onResize, {passive: true});
|
|
132
|
+
|
|
133
|
+
if (reducedMotion) {
|
|
134
|
+
draw(context, width, height, columns, rows, squares, dpr);
|
|
135
|
+
|
|
136
|
+
onCleanup(() => {
|
|
137
|
+
window.removeEventListener('resize', onResize);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const animate = (time: number): void => {
|
|
144
|
+
const delta = lastTime > 0 ? (time - lastTime) / 1000 : 0;
|
|
145
|
+
lastTime = time;
|
|
146
|
+
|
|
147
|
+
tick(squares, delta);
|
|
148
|
+
draw(context, width, height, columns, rows, squares, dpr);
|
|
149
|
+
frame = requestAnimationFrame(animate);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
frame = requestAnimationFrame(animate);
|
|
153
|
+
|
|
154
|
+
onCleanup(() => {
|
|
155
|
+
window.removeEventListener('resize', onResize);
|
|
156
|
+
cancelAnimationFrame(frame);
|
|
157
|
+
});
|
|
158
|
+
}, {immediate: true});
|
|
159
|
+
</script>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
ref="root"
|
|
4
|
+
aria-hidden="true"
|
|
5
|
+
:class="$style.gridPattern">
|
|
6
|
+
<defs>
|
|
7
|
+
<pattern
|
|
8
|
+
:id="id"
|
|
9
|
+
:width="width"
|
|
10
|
+
:height="height"
|
|
11
|
+
patternUnits="userSpaceOnUse"
|
|
12
|
+
:x="-1"
|
|
13
|
+
:y="-1">
|
|
14
|
+
<path
|
|
15
|
+
:d="`M.5 ${height}V.5H${width}`"
|
|
16
|
+
fill="none"
|
|
17
|
+
:stroke-dasharray="strokeDasharray"/>
|
|
18
|
+
</pattern>
|
|
19
|
+
|
|
20
|
+
<pattern
|
|
21
|
+
v-if="glow"
|
|
22
|
+
:id="glowId"
|
|
23
|
+
:width="width"
|
|
24
|
+
:height="height"
|
|
25
|
+
patternUnits="userSpaceOnUse"
|
|
26
|
+
:x="-1"
|
|
27
|
+
:y="-1">
|
|
28
|
+
<path
|
|
29
|
+
:class="$glow.glowLine"
|
|
30
|
+
:d="`M.5 ${height}V.5H${width}`"
|
|
31
|
+
fill="none"
|
|
32
|
+
:stroke-dasharray="strokeDasharray"/>
|
|
33
|
+
</pattern>
|
|
34
|
+
</defs>
|
|
35
|
+
|
|
36
|
+
<rect
|
|
37
|
+
width="100%"
|
|
38
|
+
height="100%"
|
|
39
|
+
stroke-width="0"
|
|
40
|
+
:fill="`url(#${id})`"/>
|
|
41
|
+
|
|
42
|
+
<svg
|
|
43
|
+
v-if="squares?.length"
|
|
44
|
+
style="overflow: visible;">
|
|
45
|
+
<rect
|
|
46
|
+
v-for="[x, y] of squares"
|
|
47
|
+
:key="`${x}-${y}`"
|
|
48
|
+
:width="width - 1"
|
|
49
|
+
:height="height - 1"
|
|
50
|
+
:x="x * width"
|
|
51
|
+
:y="y * height"
|
|
52
|
+
stroke-width="0"/>
|
|
53
|
+
</svg>
|
|
54
|
+
|
|
55
|
+
<rect
|
|
56
|
+
v-if="glow"
|
|
57
|
+
:class="[$glow.glowLayer, active && $glow.isActive]"
|
|
58
|
+
width="100%"
|
|
59
|
+
height="100%"
|
|
60
|
+
stroke-width="0"
|
|
61
|
+
:fill="`url(#${glowId})`"/>
|
|
62
|
+
</svg>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<script
|
|
66
|
+
lang="ts"
|
|
67
|
+
setup>
|
|
68
|
+
import { ref, unref, useId, useTemplateRef, watch } from 'vue';
|
|
69
|
+
import $glow from '~flux/visuals/css/component/PatternGlow.module.scss';
|
|
70
|
+
import $style from '~flux/visuals/css/component/Visual.module.scss';
|
|
71
|
+
|
|
72
|
+
const {
|
|
73
|
+
width = 42,
|
|
74
|
+
height = 42,
|
|
75
|
+
strokeDasharray = 0,
|
|
76
|
+
squares,
|
|
77
|
+
glow = false
|
|
78
|
+
} = defineProps<{
|
|
79
|
+
readonly width?: number;
|
|
80
|
+
readonly height?: number;
|
|
81
|
+
readonly strokeDasharray?: number | string;
|
|
82
|
+
readonly squares?: Array<[x: number, y: number]>;
|
|
83
|
+
readonly glow?: boolean;
|
|
84
|
+
}>();
|
|
85
|
+
|
|
86
|
+
const id = useId();
|
|
87
|
+
const glowId = `${id}-glow`;
|
|
88
|
+
const rootRef = useTemplateRef<SVGSVGElement>('root');
|
|
89
|
+
const active = ref(false);
|
|
90
|
+
|
|
91
|
+
// The pattern svg has pointer-events: none, so the cursor is tracked on the
|
|
92
|
+
// parent scroll/overflow container instead. The position is written straight
|
|
93
|
+
// to CSS custom properties to avoid a re-render on every pointermove.
|
|
94
|
+
watch([rootRef, () => glow], (_, __, onCleanup) => {
|
|
95
|
+
const root = unref(rootRef);
|
|
96
|
+
|
|
97
|
+
if (!glow || !root || !root.parentElement) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parent = root.parentElement;
|
|
102
|
+
|
|
103
|
+
const onPointerMove = (event: PointerEvent): void => {
|
|
104
|
+
const rect = parent.getBoundingClientRect();
|
|
105
|
+
root.style.setProperty('--pattern-glow-x', `${event.clientX - rect.left}px`);
|
|
106
|
+
root.style.setProperty('--pattern-glow-y', `${event.clientY - rect.top}px`);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const onPointerEnter = (): void => {
|
|
110
|
+
active.value = true;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const onPointerLeave = (): void => {
|
|
114
|
+
active.value = false;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
parent.addEventListener('pointermove', onPointerMove, {passive: true});
|
|
118
|
+
parent.addEventListener('pointerenter', onPointerEnter);
|
|
119
|
+
parent.addEventListener('pointerleave', onPointerLeave);
|
|
120
|
+
|
|
121
|
+
onCleanup(() => {
|
|
122
|
+
active.value = false;
|
|
123
|
+
parent.removeEventListener('pointermove', onPointerMove);
|
|
124
|
+
parent.removeEventListener('pointerenter', onPointerEnter);
|
|
125
|
+
parent.removeEventListener('pointerleave', onPointerLeave);
|
|
126
|
+
});
|
|
127
|
+
}, {immediate: true});
|
|
128
|
+
</script>
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
ref="target"
|
|
4
|
+
:class="$style.highlighter">
|
|
5
|
+
<slot/>
|
|
6
|
+
</span>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script
|
|
10
|
+
lang="ts"
|
|
11
|
+
setup>
|
|
12
|
+
import { prefersReducedMotion, useInView } from '@flux-ui/internals';
|
|
13
|
+
import { annotate } from 'rough-notation';
|
|
14
|
+
import { computed, onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch } from 'vue';
|
|
15
|
+
import { useHighlighterGroupInjection, type HighlighterGroupEntry, type HighlighterVariant } from '~flux/visuals/composable/private';
|
|
16
|
+
import $style from '~flux/visuals/css/component/Highlighter.module.scss';
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
hidden: [];
|
|
20
|
+
shown: [];
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
// The annotation props deliberately have no destructure defaults: an unset
|
|
24
|
+
// prop must stay distinguishable from an explicit one, so an enclosing group
|
|
25
|
+
// can supply the value instead. The effective* computeds resolve the chain.
|
|
26
|
+
const {
|
|
27
|
+
variant,
|
|
28
|
+
color,
|
|
29
|
+
strokeWidth,
|
|
30
|
+
animationDuration,
|
|
31
|
+
iterations,
|
|
32
|
+
padding,
|
|
33
|
+
multiline,
|
|
34
|
+
whenInView = false
|
|
35
|
+
} = defineProps<{
|
|
36
|
+
readonly variant?: HighlighterVariant;
|
|
37
|
+
readonly color?: string;
|
|
38
|
+
readonly strokeWidth?: number;
|
|
39
|
+
readonly animationDuration?: number;
|
|
40
|
+
readonly iterations?: number;
|
|
41
|
+
readonly padding?: number;
|
|
42
|
+
readonly multiline?: boolean;
|
|
43
|
+
readonly whenInView?: boolean;
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
defineSlots<{
|
|
47
|
+
default(): any;
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
const group = useHighlighterGroupInjection();
|
|
51
|
+
|
|
52
|
+
const targetRef = useTemplateRef('target');
|
|
53
|
+
|
|
54
|
+
// Standalone in-view tracking. In a group the parent owns the reveal timing,
|
|
55
|
+
// so no observer is set up at all.
|
|
56
|
+
const inView = group ? shallowRef(true) : useInView(targetRef, {initial: !whenInView});
|
|
57
|
+
|
|
58
|
+
const effectiveVariant = computed(() => variant ?? group?.defaults.variant ?? 'highlight');
|
|
59
|
+
const effectiveColor = computed(() => color ?? group?.defaults.color ?? 'var(--warning-200)');
|
|
60
|
+
const effectiveStrokeWidth = computed(() => strokeWidth ?? group?.defaults.strokeWidth ?? 1.5);
|
|
61
|
+
const effectiveAnimationDuration = computed(() => animationDuration ?? group?.defaults.animationDuration ?? 500);
|
|
62
|
+
const effectiveIterations = computed(() => iterations ?? group?.defaults.iterations ?? 2);
|
|
63
|
+
const effectivePadding = computed(() => padding ?? group?.defaults.padding ?? 2);
|
|
64
|
+
const effectiveMultiline = computed(() => multiline ?? group?.defaults.multiline ?? true);
|
|
65
|
+
|
|
66
|
+
let annotation: ReturnType<typeof annotate> | null = null;
|
|
67
|
+
let entry: HighlighterGroupEntry | null = null;
|
|
68
|
+
let observer: ResizeObserver | null = null;
|
|
69
|
+
let settleTimer: number | undefined;
|
|
70
|
+
let shownTimer: number | undefined;
|
|
71
|
+
let revealed = false;
|
|
72
|
+
|
|
73
|
+
// rough-notation has no completion callback, so shown is emitted after the
|
|
74
|
+
// draw animation's duration, or immediately when it draws without animation.
|
|
75
|
+
function emitShown(): void {
|
|
76
|
+
window.clearTimeout(shownTimer);
|
|
77
|
+
|
|
78
|
+
if (prefersReducedMotion()) {
|
|
79
|
+
emit('shown');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
shownTimer = window.setTimeout(() => emit('shown'), effectiveAnimationDuration.value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function show(): void {
|
|
87
|
+
if (!annotation) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
revealed = true;
|
|
92
|
+
stopSettleWatch();
|
|
93
|
+
annotation.show();
|
|
94
|
+
emitShown();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hide(): void {
|
|
98
|
+
if (!annotation) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
window.clearTimeout(shownTimer);
|
|
103
|
+
annotation.hide();
|
|
104
|
+
emit('hidden');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function replay(): void {
|
|
108
|
+
if (!annotation) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
annotation.hide();
|
|
113
|
+
annotation.show();
|
|
114
|
+
emitShown();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Standalone: draw once, with the intro animation, on the final geometry.
|
|
118
|
+
// rough-notation keeps the drawn annotation aligned on later resizes itself.
|
|
119
|
+
function reveal(): void {
|
|
120
|
+
if (revealed || !annotation || !inView.value) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
show();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function stopSettleWatch(): void {
|
|
128
|
+
window.clearTimeout(settleTimer);
|
|
129
|
+
observer?.disconnect();
|
|
130
|
+
observer = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function build(): void {
|
|
134
|
+
annotation?.remove();
|
|
135
|
+
annotation = null;
|
|
136
|
+
stopSettleWatch();
|
|
137
|
+
revealed = false;
|
|
138
|
+
|
|
139
|
+
const element = targetRef.value;
|
|
140
|
+
|
|
141
|
+
if (!element) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
annotation = annotate(element, {
|
|
146
|
+
type: effectiveVariant.value,
|
|
147
|
+
color: effectiveColor.value,
|
|
148
|
+
strokeWidth: effectiveStrokeWidth.value,
|
|
149
|
+
animationDuration: effectiveAnimationDuration.value,
|
|
150
|
+
iterations: effectiveIterations.value,
|
|
151
|
+
padding: effectivePadding.value,
|
|
152
|
+
multiline: effectiveMultiline.value,
|
|
153
|
+
animate: !prefersReducedMotion()
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// In a group the parent collects the annotations and drives the cascade.
|
|
157
|
+
if (group) {
|
|
158
|
+
group.notify();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
163
|
+
reveal();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The surrounding chrome (preview panels, web-font swaps) can reflow right
|
|
168
|
+
// after mount, which would otherwise play the intro animation where the
|
|
169
|
+
// text sat *before* it settled. Debounce until the layout is quiet, then
|
|
170
|
+
// draw on the final geometry.
|
|
171
|
+
observer = new ResizeObserver(() => {
|
|
172
|
+
window.clearTimeout(settleTimer);
|
|
173
|
+
settleTimer = window.setTimeout(() => reveal(), 80);
|
|
174
|
+
});
|
|
175
|
+
observer.observe(element);
|
|
176
|
+
observer.observe(document.body);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
onMounted(() => {
|
|
180
|
+
const element = targetRef.value;
|
|
181
|
+
|
|
182
|
+
if (group && element) {
|
|
183
|
+
entry = {element, getAnnotation: () => annotation};
|
|
184
|
+
group.add(entry);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
build();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// The annotation type is immutable, so any prop change rebuilds it from scratch.
|
|
191
|
+
watch([effectiveVariant, effectiveColor, effectiveStrokeWidth, effectiveAnimationDuration, effectiveIterations, effectivePadding, effectiveMultiline], () => build());
|
|
192
|
+
|
|
193
|
+
// Standalone reveal once the element scrolls into view (whenInView).
|
|
194
|
+
watch(inView, () => {
|
|
195
|
+
if (!group) {
|
|
196
|
+
reveal();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
onBeforeUnmount(() => {
|
|
201
|
+
if (group && entry) {
|
|
202
|
+
group.remove(entry);
|
|
203
|
+
entry = null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
window.clearTimeout(shownTimer);
|
|
207
|
+
stopSettleWatch();
|
|
208
|
+
annotation?.remove();
|
|
209
|
+
annotation = null;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
defineExpose({
|
|
213
|
+
hide,
|
|
214
|
+
replay,
|
|
215
|
+
show
|
|
216
|
+
});
|
|
217
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span :class="$style.highlighterGroup">
|
|
3
|
+
<slot/>
|
|
4
|
+
</span>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script
|
|
8
|
+
lang="ts"
|
|
9
|
+
setup>
|
|
10
|
+
import { useHighlighterGroup, type HighlighterGroupProps } from '~flux/visuals/composable/private';
|
|
11
|
+
import $style from '~flux/visuals/css/component/Highlighter.module.scss';
|
|
12
|
+
|
|
13
|
+
const props = defineProps<HighlighterGroupProps>();
|
|
14
|
+
|
|
15
|
+
defineSlots<{
|
|
16
|
+
default(): any;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
useHighlighterGroup(props);
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
aria-hidden="true"
|
|
4
|
+
:class="clsx($style.noise, animated && $style.animated)"
|
|
5
|
+
:style="style"/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script
|
|
9
|
+
lang="ts"
|
|
10
|
+
setup>
|
|
11
|
+
import { clsx } from 'clsx';
|
|
12
|
+
import { computed } from 'vue';
|
|
13
|
+
import $style from '~flux/visuals/css/component/Noise.module.scss';
|
|
14
|
+
|
|
15
|
+
type NoiseBlendMode = 'normal' | 'multiply' | 'screen' | 'overlay' | 'soft-light' | 'plus-lighter';
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
animated = false,
|
|
19
|
+
blend = 'overlay',
|
|
20
|
+
opacity = 0.05
|
|
21
|
+
} = defineProps<{
|
|
22
|
+
readonly animated?: boolean;
|
|
23
|
+
readonly blend?: NoiseBlendMode;
|
|
24
|
+
readonly opacity?: number;
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
const style = computed(() => ({
|
|
28
|
+
'--noise-blend': blend,
|
|
29
|
+
'--noise-opacity': opacity
|
|
30
|
+
}));
|
|
31
|
+
</script>
|