glass-pulse-fx 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jerry Kou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # glass-pulse-fx
2
+
3
+ > Frosted-glass UI surfaces lit by animated, original WebGL shaders.
4
+
5
+ Wrap any element — button, chip, card — and `glass-pulse-fx` paints a frosted-glass
6
+ material **on top of it**: a shared animated shader, a `backdrop-filter` frost, an opaque
7
+ blurred core, an optional lit rim, and two bloom layers that spill glow past the
8
+ silhouette. It never replaces your element — it reads your element's own background and
9
+ frosts *that*, so the glass sits on your surface.
10
+
11
+ One shared WebGL context renders the effect for the whole page; every instance is a handful
12
+ of cheap canvas copies plus CSS layers. The base shader is original (no third-party
13
+ attribution required) and the shader layer is pluggable.
14
+
15
+ ## Requirements
16
+
17
+ - **React 18+** for the `<GlassFx>` component — or skip React entirely with the
18
+ framework-free `glass-pulse-fx/core` (`createGlass`).
19
+ - **WebGL** for the animated light. Without it the effect degrades to a flat fill + border;
20
+ without `backdrop-filter`, to a flat translucent tint.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install glass-pulse-fx
26
+ ```
27
+
28
+ ## Quick start (React)
29
+
30
+ ```tsx
31
+ import { GlassFx } from 'glass-pulse-fx';
32
+ import { bloom } from 'glass-pulse-fx/presets';
33
+
34
+ export default function Cta() {
35
+ return (
36
+ <GlassFx preset={bloom} radius={12}>
37
+ <button className="your-button">Upgrade to Pro</button>
38
+ </GlassFx>
39
+ );
40
+ }
41
+ ```
42
+
43
+ `<GlassFx>` wraps your component and overlays the glass. Style `.your-button` however you
44
+ like — its background becomes the frosted surface. `preset` is one ready-made look;
45
+ `radius` matches the glass corners to your component.
46
+
47
+ ## Quick start (vanilla)
48
+
49
+ ```ts
50
+ import { createGlass } from 'glass-pulse-fx/core';
51
+ import { comet } from 'glass-pulse-fx/presets';
52
+
53
+ const glass = createGlass(document.querySelector('#cta')!, {
54
+ preset: comet,
55
+ theme: 'dark',
56
+ });
57
+
58
+ // later — tweak live, then clean up
59
+ glass.update({ bgBlur: 10, frost: 0.5 });
60
+ glass.destroy();
61
+ ```
62
+
63
+ ## The overlay model
64
+
65
+ `glass-pulse-fx` paints **on top of** the element you wrap — it never touches its
66
+ background, border, radius, or shadow. The frost and core take their tint from your
67
+ element's own background (flattened to an opaque color over whatever sits behind it), so the
68
+ glass reads as *your* component's surface, frosted and lit — not a fill the library owns.
69
+
70
+ In short: style your component normally; the library reads it. `fill` and `border` (below)
71
+ are **optional overrides** — by default the surface tint comes from your element's
72
+ background and your element's own CSS border is the border.
73
+
74
+ ## Presets
75
+
76
+ A `GlassPreset` is one shareable look — shader + params + glass material. It applies
77
+ identically in dark and light mode; anything it does **not** pin (palette, frost, …) still
78
+ adapts to the theme defaults. It carries **no component styling** (`fill` / `border` /
79
+ `radius`) — those belong to the component you wrap. Eleven presets ship — `bloom`, `halo`,
80
+ `rush`, `comet`, `cinder`, `plasma`, `kaleido`, `nimbus`, `emerald`, `glow`, `tide` (plus
81
+ `LIBRARY_PRESETS`, the full array):
82
+
83
+ ```tsx
84
+ import { GlassFx } from 'glass-pulse-fx';
85
+ import { bloom } from 'glass-pulse-fx/presets';
86
+
87
+ <GlassFx preset={bloom}>
88
+ <button className="your-button">Ping</button>
89
+ </GlassFx>
90
+ ```
91
+
92
+ Want a *different* look per mode? One ternary:
93
+
94
+ ```tsx
95
+ import { rush, tide } from 'glass-pulse-fx/presets';
96
+
97
+ <GlassFx preset={isDark ? tide : rush} theme={isDark ? 'dark' : 'light'}>
98
+ ```
99
+
100
+ Vanilla — `preset` is a `createGlass` option:
101
+
102
+ ```ts
103
+ import { comet } from 'glass-pulse-fx/presets';
104
+ const glass = createGlass(el, { preset: comet });
105
+ ```
106
+
107
+ ### Custom presets
108
+
109
+ A preset is just an object — the [demo playground](#demo--playground) serializes whatever
110
+ you tune into a ready-to-paste literal (it updates live as you drag the controls; copying it
111
+ **freezes that look** into your code — it's a snapshot, not a live link):
112
+
113
+ ```ts
114
+ import type { GlassPreset } from 'glass-pulse-fx';
115
+
116
+ const mine: GlassPreset = {
117
+ name: 'Mine',
118
+ version: 1,
119
+ effectParams: { speed: 0.4, colors: ['#ff2d9b', '#19c3ff', '#15e6a4'] },
120
+ settings: { bgBlur: 10, frost: 0.5 },
121
+ };
122
+
123
+ <GlassFx preset={mine}>…</GlassFx>
124
+ ```
125
+
126
+ ## Props (React) / options (vanilla)
127
+
128
+ | Prop / option | Type | Default | Notes |
129
+ |---|---|---|---|
130
+ | `preset` | `GlassPreset` | — | a shareable look; explicit props below win over it |
131
+ | `effect` | `'panes'` | `panes` | base shader |
132
+ | `effectParams` | `Partial<EffectParams>` | — | merged onto the preset's params + the effect's theme defaults |
133
+ | `theme` | `'dark' \| 'light' \| 'auto'` | `auto` (React) / `dark` (vanilla) | `auto` follows `prefers-color-scheme` |
134
+ | `fill` | CSS color | your element's background | override the frosted surface tint |
135
+ | `border` | `Partial<BorderConfig>` | off | optional lit rim `{ width, opacity, color }` — off by default, your element's own border shows |
136
+ | `radius` | `number \| string` | inferred | sets the glass clip + your element's `border-radius` |
137
+ | `kind` | `'pill' \| 'circle' \| 'rect' \| 'tag' \| 'card' \| 'icon'` | inferred | crop scale + default corner radius |
138
+ | `fps` | `15 \| 30 \| 60` | `30` | animation paint rate |
139
+ | `paused` | `boolean` | `false` | also auto-pauses on reduced-motion / offscreen / hidden tab |
140
+ | `bloomClip` | `boolean` | `false` | clip the bloom to the rounded box instead of letting it spill |
141
+ | `settings` | `GlassSettingsPatch` | — | glass material, merged onto theme defaults |
142
+ | `settingsByTheme` | `Partial<Record<Theme, GlassSettingsPatch>>` | — | per-theme glass overrides (React) |
143
+
144
+ ### `GlassSettings` (the glass material — effect-independent)
145
+
146
+ | Field | Range | Default (dark) | Meaning |
147
+ |---|---|---|---|
148
+ | `bgBlur` | 0–20 px | 6 | frost background-blur |
149
+ | `frost` | 0.3–1 | 0.66 | frost tint opacity over the shader |
150
+ | `frostInset` | 0–12 px | 0 | insets the frost veil, exposing a raw full-brightness shader rim at the edge |
151
+ | `shaderInset` | 0–28 px | 0 | insets the animated shader source/bloom from the outer edge |
152
+ | `coreInset` | 0–28 px | 8 | opaque core inset from edge |
153
+ | `coreBlur` | 0–32 px | 8 | layer blur softening core → rim |
154
+ | `coreOpacity` | 0–1 | 1 | core opacity (lower = shader shows through the center) |
155
+ | `coreProportional` | bool | `false` | scale `coreInset` + `coreBlur` with element size (ref 52px) |
156
+ | `saturate` | 1–2× | 1.3 | chroma boost in the frost (counters the veil) |
157
+ | `innerBloom` | `{ size 0–24, level 0–1 }` | `{2, 1}` | tight, full-bright edge bleed |
158
+ | `outerBloom` | `{ size 0–64, level 0–0.9 }` | `{16, 0.45}` | wide, soft ambient glow |
159
+
160
+ ## How it works
161
+
162
+ Per instance, bottom → top: **outer bloom** · **inner bloom** · a shared rounded
163
+ **surface mask** containing the insettable **shader** crop, **frost** (`backdrop-filter:
164
+ saturate() blur()` over a tint taken from your element's background), and **core** (an
165
+ inset, layer-blurred opaque fill) · an optional **lit border** · then your content. The
166
+ wrapper is `position:relative; isolation:isolate` and is **not** `overflow:hidden` (the
167
+ blooms must spill, unless you set `bloomClip`). All effect layers are `pointer-events:none`,
168
+ and your element's own background, border, radius, and shadow are left untouched.
169
+
170
+ ## Shaders
171
+
172
+ One built-in base shader (the layer is pluggable — bring your own):
173
+
174
+ - **Panes** — discrete colored bands moving along a 1D coordinate: each fades in, holds,
175
+ fades out (via alpha, so the glass shows through), then a transparent interval before the
176
+ next. Knobs: `motion` (**Linear** sweep, **Center** — mirrored, bands emanate from the
177
+ middle, **Radial** — concentric rings ripple outward, **Orbit** — spokes sweep around
178
+ like a radar; `scale` rounds to the spoke count and the colour gradient snaps to whole
179
+ cycles around the ring so it wraps seamlessly),
180
+ `speed` (sign sets direction), a **velocity preset** (`velocity` — how band speed varies
181
+ across the axis: uniform, ease in/out, slow/fast middle), `scale` (band density), `interval`
182
+ (transparent spacing between bands), `rampIn` / `rampOut` (independent leading/trailing
183
+ fade), `angle`, and a **colour field decoupled from the bands** — 1–5 palette stops
184
+ sampled by `colorSpread` (along the motion), `colorSkew` (perpendicular → mesh) and
185
+ `colorDrift` (over time), so colour can vary *within* a band, not just band-to-band.
186
+
187
+ ### Writing your own shader
188
+
189
+ Effects are isolated to [`src/engine/effects/`](src/engine/effects/). To add one:
190
+
191
+ 1. Write an `EffectDef` (GLSL `frag`, `uniforms`, per-theme `defaults`, an `upload`, and
192
+ demo `controls`) — see [`panes.ts`](src/engine/effects/panes.ts). Reuse the shared
193
+ `paneColor` helper + uniform plumbing in [`common.ts`](src/engine/effects/common.ts).
194
+ 2. List it in [`effects/index.ts`](src/engine/effects/index.ts) and add its id to the
195
+ `EffectId` union + any new fields to `EffectParams` in [`types.ts`](src/types.ts).
196
+
197
+ The renderer and compositor never reference uniform names — they only call
198
+ `renderEffect(id, params, time)`.
199
+
200
+ ## Performance & accessibility
201
+
202
+ - One WebGL context, one program per effect, one RAF loop (~30 fps), torn down on last
203
+ unmount. Cost is one shader render per distinct (effect, params) group + N cheap copies.
204
+ - Offscreen instances are skipped (`IntersectionObserver`); the loop pauses when the tab is
205
+ hidden and freezes on `prefers-reduced-motion: reduce`.
206
+ - `backdrop-filter` is the heaviest part — set `bgBlur: 0` to drop the frost blur.
207
+ - No WebGL → flat `fill` + border. No `backdrop-filter` → flat translucent tint.
208
+
209
+ ## Demo / playground
210
+
211
+ ```bash
212
+ npm run dev # http://localhost:5173 — sidebar playground, save full-look presets
213
+ npm run build:demo # static site -> dist-demo/
214
+ ```
215
+
216
+ The playground docks all controls in a left sidebar, shows live mockups, and keeps your
217
+ presets in `localStorage` (library presets from [`src/presets/`](src/presets/) appear
218
+ read-only — Duplicate to riff on one). The **Install** tab generates ready-to-paste React or
219
+ vanilla code from whatever you've tuned. See [SEPARATING-THE-DEMO.md](SEPARATING-THE-DEMO.md)
220
+ for deploying it standalone.
221
+
222
+ ## License
223
+
224
+ MIT © 2026 Jerry Kou. All shaders are original work.