@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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/glyph-rain.iife.js +865 -0
- package/dist/glyph-rain.iife.js.map +1 -0
- package/dist/index.cjs +825 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +77 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +819 -0
- package/dist/index.js.map +1 -0
- package/docs//344/275/277/347/224/250/350/257/264/346/230/216.md +45 -0
- package/docs//345/212/250/346/225/210/350/257/264/346/230/216-/344/270/223/344/270/232.md +18 -0
- package/docs//345/212/250/346/225/210/350/257/264/346/230/216-/345/244/247/347/231/275/350/257/235.md +7 -0
- package/package.json +85 -0
- package/src/core/atlas.ts +28 -0
- package/src/core/createGlyphRain.ts +514 -0
- package/src/core/shaders.ts +208 -0
- package/src/core/supports.ts +13 -0
- package/src/core/types.ts +95 -0
- package/src/react/GlyphRain.tsx +126 -0
- package/src/react/index.ts +10 -0
- package/src/vue/GlyphRain.vue +181 -0
- package/src/vue/index.ts +1 -0
- package/src/vue-shim.d.ts +5 -0
|
@@ -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,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>
|
package/src/vue/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as GlyphRain } from './GlyphRain.vue'
|