@fxshelf/glyph-rain 0.1.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.
@@ -0,0 +1,13 @@
1
+ import type { ElementImageContext, PaintableCanvas } from './types'
2
+
3
+ /** True when Chrome's experimental html-in-canvas APIs are available. */
4
+ export function supportsHtmlInCanvas(): boolean {
5
+ if (typeof document === 'undefined') return false
6
+ const probe = document.createElement('canvas') as PaintableCanvas
7
+ const ctx = probe.getContext('2d') as ElementImageContext | null
8
+ return Boolean(
9
+ ctx &&
10
+ typeof ctx.drawElementImage === 'function' &&
11
+ typeof probe.requestPaint === 'function',
12
+ )
13
+ }
@@ -0,0 +1,95 @@
1
+ export type GlyphRainOptions = {
2
+ /** Characters used for the falling glyphs. Deduplicated into a glyph atlas. */
3
+ charset?: string
4
+ /** Size of one glyph cell in CSS pixels (8 to 64). */
5
+ cell?: number
6
+ /** Rain color as [r, g, b] in 0-1 range. */
7
+ color?: [number, number, number]
8
+ /** Color of the bright head glyph as [r, g, b] in 0-1 range. */
9
+ headColor?: [number, number, number]
10
+ /** Fall speed in screen heights per second (0.05 to 3). */
11
+ speed?: number
12
+ /** Per-column speed variation (0 to 1). */
13
+ speedVariance?: number
14
+ /** Fraction of drops that spawn each cycle (0 to 1). */
15
+ density?: number
16
+ /** Length multiplier for the fading trails (0.2 to 3). */
17
+ trail?: number
18
+ /** Brightness of the drop heads and the light they cast (0 to 3). */
19
+ glow?: number
20
+ /** How fast glyphs mutate into other characters (0 to 4). */
21
+ mutate?: number
22
+ /** Random brightness flicker of the streaks (0 to 1). */
23
+ flicker?: number
24
+ /** Parallax rain layers behind the front one (1 to 3). */
25
+ layers?: number
26
+ /** How much the unlit page dims (0 to 1). 0 keeps it fully readable. */
27
+ dim?: number
28
+ /** Strength of the light the drops shine onto the page (0 to 3). */
29
+ light?: number
30
+ /** Radius of each drop's light pool in CSS pixels (20 to 600). */
31
+ lightRadius?: number
32
+ /** How high above the page the lights float, in CSS pixels. Higher is softer. */
33
+ lightHeight?: number
34
+ /** Embossed 3D shading of the page under the lights (0 to 2). */
35
+ relief?: number
36
+ /** How strongly the cursor stirs the rain as it passes (0 to 1). 0 disables it. */
37
+ stir?: number
38
+ /** How far the stirring reaches to either side of the cursor, in CSS pixels. */
39
+ stirRadius?: number
40
+ /** Seconds the stirred wake takes to settle back to its own rhythm. */
41
+ settle?: number
42
+ }
43
+
44
+ export type GlyphRainElements = {
45
+ /** Canvas with layoutsubtree that hosts the HTML content. */
46
+ source: HTMLCanvasElement
47
+ /** The element inside the source canvas that gets captured. */
48
+ content: HTMLElement
49
+ /** Canvas the WebGL effect renders to. */
50
+ output: HTMLCanvasElement
51
+ }
52
+
53
+ export type GlyphRainInstance = {
54
+ /** Update effect options live. */
55
+ setOptions: (options: GlyphRainOptions) => void
56
+ /** Re-read canvas size. Call when the element is resized. */
57
+ resize: () => void
58
+ /** Stop the loop and release all GPU resources. */
59
+ destroy: () => void
60
+ }
61
+
62
+ export const DEFAULT_CHARSET =
63
+ 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン0123456789Z*+-<>¦=:.'
64
+
65
+ export const DEFAULTS: Required<GlyphRainOptions> = {
66
+ charset: DEFAULT_CHARSET,
67
+ cell: 15,
68
+ color: [0.267, 0.455, 1],
69
+ headColor: [0.169, 0.416, 1],
70
+ speed: 0.2,
71
+ speedVariance: 0.5,
72
+ density: 0.15,
73
+ trail: 0.65,
74
+ glow: 1.75,
75
+ mutate: 0,
76
+ flicker: 0,
77
+ layers: 2,
78
+ dim: 0.5,
79
+ light: 2.8,
80
+ lightRadius: 240,
81
+ lightHeight: 172,
82
+ relief: 0.05,
83
+ stir: 0.7,
84
+ stirRadius: 260,
85
+ settle: 0.9,
86
+ }
87
+
88
+ export type PaintableCanvas = HTMLCanvasElement & {
89
+ onpaint?: (() => void) | null
90
+ requestPaint?: () => void
91
+ }
92
+
93
+ export type ElementImageContext = CanvasRenderingContext2D & {
94
+ drawElementImage?: (element: Element, x: number, y: number) => void
95
+ }
@@ -0,0 +1,126 @@
1
+ 'use client'
2
+
3
+ import {
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ useSyncExternalStore,
8
+ type CSSProperties,
9
+ type ReactNode,
10
+ } from 'react'
11
+ import {
12
+ createGlyphRain,
13
+ supportsHtmlInCanvas,
14
+ type GlyphRainInstance,
15
+ type GlyphRainOptions,
16
+ } from '../core/createGlyphRain'
17
+
18
+ export type GlyphRainProps = GlyphRainOptions & {
19
+ children: ReactNode
20
+ className?: string
21
+ style?: CSSProperties
22
+ }
23
+
24
+ const emptySubscribe = () => () => {}
25
+
26
+ export function GlyphRain({
27
+ children,
28
+ className,
29
+ style,
30
+ ...options
31
+ }: GlyphRainProps) {
32
+ const sourceRef = useRef<HTMLCanvasElement>(null)
33
+ const contentRef = useRef<HTMLDivElement>(null)
34
+ const outputRef = useRef<HTMLCanvasElement>(null)
35
+ const instanceRef = useRef<GlyphRainInstance | null>(null)
36
+ const [initialOptions] = useState(options)
37
+ const [failed, setFailed] = useState(false)
38
+
39
+ const supported = useSyncExternalStore(
40
+ emptySubscribe,
41
+ supportsHtmlInCanvas,
42
+ () => false,
43
+ )
44
+ const native = supported && !failed
45
+
46
+ useEffect(() => {
47
+ const source = sourceRef.current
48
+ const content = contentRef.current
49
+ const output = outputRef.current
50
+ if (!source || !content || !output) return
51
+ instanceRef.current = createGlyphRain(
52
+ { source, content, output },
53
+ initialOptions,
54
+ )
55
+ if (native && !instanceRef.current) setFailed(true)
56
+ return () => {
57
+ instanceRef.current?.destroy()
58
+ instanceRef.current = null
59
+ }
60
+ }, [initialOptions, native])
61
+
62
+ useEffect(() => {
63
+ instanceRef.current?.setOptions(options)
64
+ })
65
+
66
+ return (
67
+ <div className={className} style={{ position: 'relative', ...style }}>
68
+ <canvas
69
+ ref={sourceRef}
70
+ // @ts-expect-error experimental html-in-canvas attribute
71
+ layoutsubtree="true"
72
+ suppressHydrationWarning
73
+ style={
74
+ native
75
+ ? {
76
+ position: 'absolute',
77
+ inset: 0,
78
+ width: '100%',
79
+ height: '100%',
80
+ }
81
+ : { display: 'none' }
82
+ }
83
+ >
84
+ {native ? (
85
+ <div
86
+ ref={contentRef}
87
+ style={{
88
+ position: 'relative',
89
+ width: '100%',
90
+ height: '100%',
91
+ overflow: 'auto',
92
+ }}
93
+ >
94
+ {children}
95
+ </div>
96
+ ) : null}
97
+ </canvas>
98
+ {!native ? (
99
+ <div
100
+ ref={contentRef}
101
+ style={{
102
+ position: 'relative',
103
+ width: '100%',
104
+ height: '100%',
105
+ overflow: 'auto',
106
+ }}
107
+ >
108
+ {children}
109
+ </div>
110
+ ) : null}
111
+ <canvas
112
+ ref={outputRef}
113
+ aria-hidden
114
+ style={{
115
+ position: 'absolute',
116
+ inset: 0,
117
+ width: '100%',
118
+ height: '100%',
119
+ pointerEvents: 'none',
120
+ }}
121
+ />
122
+ </div>
123
+ )
124
+ }
125
+
126
+ export default GlyphRain
@@ -0,0 +1,10 @@
1
+ export { GlyphRain, type GlyphRainProps } from './GlyphRain'
2
+ export {
3
+ createGlyphRain,
4
+ supportsHtmlInCanvas,
5
+ DEFAULT_CHARSET,
6
+ DEFAULTS,
7
+ type GlyphRainElements,
8
+ type GlyphRainInstance,
9
+ type GlyphRainOptions,
10
+ } from '../core/createGlyphRain'
@@ -0,0 +1,181 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ nextTick,
5
+ onBeforeUnmount,
6
+ onMounted,
7
+ ref,
8
+ watch,
9
+ type CSSProperties,
10
+ type StyleValue,
11
+ } from 'vue'
12
+ import {
13
+ createGlyphRain,
14
+ supportsHtmlInCanvas,
15
+ type GlyphRainInstance,
16
+ type GlyphRainOptions,
17
+ } from '../core/createGlyphRain'
18
+
19
+ const props = defineProps<{
20
+ charset?: string
21
+ cell?: number
22
+ color?: [number, number, number]
23
+ headColor?: [number, number, number]
24
+ speed?: number
25
+ speedVariance?: number
26
+ density?: number
27
+ trail?: number
28
+ glow?: number
29
+ mutate?: number
30
+ flicker?: number
31
+ layers?: number
32
+ dim?: number
33
+ light?: number
34
+ lightRadius?: number
35
+ lightHeight?: number
36
+ relief?: number
37
+ stir?: number
38
+ stirRadius?: number
39
+ settle?: number
40
+ className?: string
41
+ style?: CSSProperties
42
+ }>()
43
+
44
+ const sourceRef = ref<HTMLCanvasElement | null>(null)
45
+ const contentRef = ref<HTMLDivElement | null>(null)
46
+ const outputRef = ref<HTMLCanvasElement | null>(null)
47
+ const failed = ref(false)
48
+ const supported = ref(false)
49
+
50
+ let instance: GlyphRainInstance | null = null
51
+
52
+ const native = computed(() => supported.value && !failed.value)
53
+
54
+ const rootClass = computed(() => props.className ?? '')
55
+ const rootStyle = computed(
56
+ (): StyleValue => ({
57
+ position: 'relative',
58
+ ...props.style,
59
+ }),
60
+ )
61
+
62
+ function optionPayload(): GlyphRainOptions {
63
+ return {
64
+ charset: props.charset,
65
+ cell: props.cell,
66
+ color: props.color,
67
+ headColor: props.headColor,
68
+ speed: props.speed,
69
+ speedVariance: props.speedVariance,
70
+ density: props.density,
71
+ trail: props.trail,
72
+ glow: props.glow,
73
+ mutate: props.mutate,
74
+ flicker: props.flicker,
75
+ layers: props.layers,
76
+ dim: props.dim,
77
+ light: props.light,
78
+ lightRadius: props.lightRadius,
79
+ lightHeight: props.lightHeight,
80
+ relief: props.relief,
81
+ stir: props.stir,
82
+ stirRadius: props.stirRadius,
83
+ settle: props.settle,
84
+ }
85
+ }
86
+
87
+ function mountEffect() {
88
+ instance?.destroy()
89
+ instance = null
90
+ const source = sourceRef.value
91
+ const content = contentRef.value
92
+ const output = outputRef.value
93
+ if (!source || !content || !output) return
94
+ instance = createGlyphRain({ source, content, output }, optionPayload())
95
+ if (native.value && !instance) failed.value = true
96
+ }
97
+
98
+ onMounted(async () => {
99
+ supported.value = supportsHtmlInCanvas()
100
+ await nextTick()
101
+ mountEffect()
102
+ })
103
+
104
+ watch(
105
+ () => [
106
+ props.charset,
107
+ props.cell,
108
+ props.color,
109
+ props.headColor,
110
+ props.speed,
111
+ props.speedVariance,
112
+ props.density,
113
+ props.trail,
114
+ props.glow,
115
+ props.mutate,
116
+ props.flicker,
117
+ props.layers,
118
+ props.dim,
119
+ props.light,
120
+ props.lightRadius,
121
+ props.lightHeight,
122
+ props.relief,
123
+ props.stir,
124
+ props.stirRadius,
125
+ props.settle,
126
+ ],
127
+ () => {
128
+ instance?.setOptions(optionPayload())
129
+ },
130
+ )
131
+
132
+ onBeforeUnmount(() => {
133
+ instance?.destroy()
134
+ instance = null
135
+ })
136
+ </script>
137
+
138
+ <template>
139
+ <div :class="rootClass" :style="rootStyle">
140
+ <canvas
141
+ ref="sourceRef"
142
+ layoutsubtree="true"
143
+ :style="
144
+ native
145
+ ? {
146
+ position: 'absolute',
147
+ inset: 0,
148
+ width: '100%',
149
+ height: '100%',
150
+ }
151
+ : { display: 'none' }
152
+ "
153
+ >
154
+ <div
155
+ v-if="native"
156
+ ref="contentRef"
157
+ style="position: relative; width: 100%; height: 100%; overflow: auto"
158
+ >
159
+ <slot />
160
+ </div>
161
+ </canvas>
162
+ <div
163
+ v-if="!native"
164
+ ref="contentRef"
165
+ style="position: relative; width: 100%; height: 100%; overflow: auto"
166
+ >
167
+ <slot />
168
+ </div>
169
+ <canvas
170
+ ref="outputRef"
171
+ aria-hidden="true"
172
+ style="
173
+ position: absolute;
174
+ inset: 0;
175
+ width: 100%;
176
+ height: 100%;
177
+ pointer-events: none;
178
+ "
179
+ />
180
+ </div>
181
+ </template>
@@ -0,0 +1 @@
1
+ export { default as GlyphRain } from './GlyphRain.vue'
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<object, object, unknown>
4
+ export default component
5
+ }