@zakkster/lite-ui-fx 1.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 ADDED
@@ -0,0 +1,232 @@
1
+ # @zakkster/lite-ui-fx
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@zakkster/lite-ui-fx.svg?style=for-the-badge&color=latest)](https://www.npmjs.com/package/@zakkster/lite-ui-fx)
4
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@zakkster/lite-ui-fx?style=for-the-badge)](https://bundlephobia.com/result?p=@zakkster/lite-ui-fx)
5
+ [![npm downloads](https://img.shields.io/npm/dm/@zakkster/lite-ui-fx?style=for-the-badge&color=blue)](https://www.npmjs.com/package/@zakkster/lite-ui-fx)
6
+ [![npm total downloads](https://img.shields.io/npm/dt/@zakkster/lite-ui-fx?style=for-the-badge&color=blue)](https://www.npmjs.com/package/@zakkster/lite-ui-fx)
7
+ ![TypeScript](https://img.shields.io/badge/TypeScript-Types-informational)
8
+ ![Dependencies](https://img.shields.io/badge/dependencies-3-brightgreen)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
10
+
11
+ ## ✨ What is lite-ui-fx?
12
+
13
+ `@zakkster/lite-ui-fx` overlays a DPR-aware canvas on native HTML elements and renders them with pluggable, physics-driven **recipes**. The native element stays invisible but fully accessible — handling focus, keyboard, and pointer events. The canvas handles all visuals.
14
+
15
+ ## đŸŽŦ Live Demo (UI-FX)
16
+ https://cdpn.io/pen/debug/RNGjMjQ
17
+
18
+ ## đŸŽŦ Live Demo (UI-FX vol.2)
19
+ https://cdpn.io/pen/debug/yyaPKpB
20
+
21
+ ## đŸŽŦ Live Demo (UI-FX vol3.)
22
+ https://cdpn.io/pen/debug/YPGEaYY
23
+
24
+ **50 recipes** across UI element categories:
25
+
26
+ - 🔘 **Toggles** — Swarm, Liquid, Neon Pulse, Pendulum, Circuit, Lightning, DNA
27
+ - 🔲 **Buttons** — Magnetic, Shatter, Confetti, Glitch, Heartbeat, Breathing, Ink Splash, Pixel Dissolve, Firework
28
+ - đŸŽšī¸ **Sliders** — Spark, Cosmic Void, Laser, Aurora, Wave, Elastic Band, Gravity
29
+ - đŸŽ›ī¸ **Knobs** — Volume dial, Compass needle
30
+ - 📊 **Progress** — Ring, Battery, Signal meter
31
+ - 🔀 **Controls** — Pill tabs, Stepper, Radio orbit
32
+ - 📈 **Indicators** — Password strength, Water level, Heat map
33
+ - 🌗 **Mood** — Day/night, Reaction picker, Notification bell
34
+ - đŸ’Ŧ **Feedback** — Typewriter, Sound wave, Upload progress
35
+ - 🎮 **Fun** — Scratch reveal, Timer countdown, Pull refresh
36
+ - ✅ **Checkboxes** — Ripple, Morph (X → ✓)
37
+ - 🔄 **Loaders** — Orbit planets, DNA helix
38
+ - đŸ”ĸ **Counters** — Flame heat, Glitch signal
39
+ - ⭐ **Rating** — Bubble inflate
40
+
41
+ Every recipe is zero-GC, uses `dt`-based animation, and includes accessibility indicators (focus rings, state labels).
42
+
43
+ `@zakkster/lite-ui-fx` ships only the core controller on npm (zero bloat).
44
+ All visual effects live in the GitHub repo as **recipes**.
45
+
46
+ 📁 **Recipe Collections:**
47
+ - Vol. 1 (10 recipes):
48
+ https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/UIFXRecipes.js
49
+ - Vol. 2 (20 recipes):
50
+ https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/UIFXRecipes2.js
51
+ - Vol. 3 (20 recipes):
52
+ https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/UIFXRecipes3.js
53
+
54
+ 📘 **How to write your own:**
55
+ https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/UIFX-RECIPE-GUIDE.md
56
+
57
+ Recipes are **optional**, **open-source**, and **not included in the npm package**
58
+ to keep the install size tiny (<2 KB).
59
+ You can copy/paste any recipe into your project or use them as inspiration.
60
+
61
+ đŸ“Ļ Download all recipes as a ZIP
62
+ https://github.com/PeshoVurtoleta/lite-ui-fx/archive/refs/heads/main.zip
63
+
64
+ Part of the [@zakkster/lite-*](https://www.npmjs.com/org/zakkster) ecosystem.
65
+
66
+ ## 🚀 Install
67
+
68
+ ```bash
69
+ npm i @zakkster/lite-ui-fx
70
+ ```
71
+
72
+ > Looking for the visual effects?
73
+ > Recipes live in the GitHub repo — not in the npm package — to keep the library tiny.
74
+
75
+
76
+ ## đŸ•šī¸ Quick Start
77
+
78
+ ```javascript
79
+ import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
80
+ import { SwarmToggle } from './recipes/UIFXRecipes.js';
81
+
82
+ // Mount a canvas-rendered toggle onto a container
83
+ const instance = mountUIFX(
84
+ document.getElementById('my-container'),
85
+ UIType.TOGGLE,
86
+ SwarmToggle,
87
+ { label: 'Sound effects', width: 64, height: 36 }
88
+ );
89
+
90
+ // The native checkbox is invisible but fully accessible.
91
+ // Screen readers see: <input type="checkbox" role="switch" aria-label="Sound effects">
92
+ // Canvas renders: 150 particles forming a knob that explodes on toggle.
93
+
94
+ // Cleanup when done:
95
+ instance.destroy();
96
+ ```
97
+
98
+ ## đŸ“Ļ Import Map
99
+
100
+ ```javascript
101
+ // Controller (always needed)
102
+ import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
103
+
104
+ // Recipes are NOT included in the npm package.
105
+ // Copy them from the GitHub repo into your own ./recipes folder:
106
+
107
+ // Vol. 1 — 10 recipes (toggles, buttons, sliders)
108
+ import { SwarmToggle, MagneticButton, SparkSlider } from './recipes/UIFXRecipes.js';
109
+
110
+ // Vol. 2 — 20 recipes (+ loaders, checkboxes, counters, rating)
111
+ import { PendulumToggle, HeartbeatButton, RippleCheck } from './recipes/UIFXRecipes2.js';
112
+
113
+ // Vol. 3 — 20 recipes (knobs, progress, controls, indicators, mood, feedback, fun)
114
+ import { VolumeKnob, WaterLevel, TimerCountdown } from './recipes/UIFXRecipes3.js';
115
+ ```
116
+
117
+ ## 🧠 How It Works
118
+
119
+ ```
120
+ ┌──────────────────────────────────────────────────┐
121
+ │ mountUIFX(container, type, recipeFactory, opts) │
122
+ │ │
123
+ │ ┌──── Wrapper div ────────────────────────────┐ │
124
+ │ │ │ │
125
+ │ │ Native element (opacity:0, z-index:2) │ │
126
+ │ │ → receives pointer, keyboard, focus events │ │
127
+ │ │ → accessible to screen readers │ │
128
+ │ │ │ │
129
+ │ │ Canvas overlay (z-index:1, DPR-scaled) │ │
130
+ │ │ → recipe.tick() renders every frame │ │
131
+ │ │ → padding allows particle overflow │ │
132
+ │ │ │ │
133
+ │ └──────────────────────────────────────────────┘ │
134
+ │ │
135
+ │ Shared Ticker (ref-counted, one RAF for all) │
136
+ │ AbortController (all events cleaned on destroy) │
137
+ └──────────────────────────────────────────────────┘
138
+ ```
139
+
140
+ ## âš™ī¸ API
141
+
142
+ ### `mountUIFX(container, type, recipeFactory, options?)`
143
+
144
+ | Parameter | Type | Description |
145
+ |-----------|------|-------------|
146
+ | `container` | `HTMLElement` | Parent to mount into |
147
+ | `type` | `'button' \| 'toggle' \| 'slider'` | Determines native element type |
148
+ | `recipeFactory` | `() => Recipe` | Factory function (controller calls it) |
149
+ | `options.width` | `number` | Element width (auto from type if omitted) |
150
+ | `options.height` | `number` | Element height |
151
+ | `options.padding` | `number` | Canvas overflow (default: 40px) |
152
+ | `options.label` | `string` | Accessible label (aria-label) |
153
+
154
+ Returns `{ el, canvas, wrapper, state, destroy() }`.
155
+
156
+ ### Element Types
157
+
158
+ | Type | Native Element | Recipe Hooks | Key State |
159
+ |------|---------------|-------------|-----------|
160
+ | `UIType.TOGGLE` | `<input type="checkbox" role="switch">` | `onToggle(checked)` | `state.toggled` |
161
+ | `UIType.BUTTON` | `<button>` | `onClick(x, y, state)` | `state.active` |
162
+ | `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0–1) |
163
+
164
+ ### State Object (provided to `tick()` every frame)
165
+
166
+ ```typescript
167
+ {
168
+ hover: boolean; // Pointer inside element
169
+ active: boolean; // Pointer pressed
170
+ focused: boolean; // Keyboard focus
171
+ toggled: boolean; // Checkbox state
172
+ val: number; // Slider value (0–1)
173
+ w: number; // Element width
174
+ h: number; // Element height
175
+ padding: number; // Canvas padding
176
+ dpr: number; // Device pixel ratio
177
+ }
178
+ ```
179
+
180
+ ## 📊 Comparison
181
+
182
+ | Library | Size | Approach | Recipes | A11y | Install |
183
+ |---------|------|----------|---------|------|---------|
184
+ | Framer Motion | ~45 KB | React HOC | 0 | Via React | `npm i framer-motion` |
185
+ | GSAP | ~25 KB | Timeline | 0 | Manual | `npm i gsap` |
186
+ | Lottie | ~55 KB | JSON animation | After Effects | Manual | `npm i lottie-web` |
187
+ | **lite-ui-fx** | **< 5 KB** | **Canvas hijack** | **30 built-in** | **Native + visual** | **`npm i @zakkster/lite-ui-fx`** |
188
+
189
+ ## 🎨 Writing Custom Recipes
190
+
191
+ See the full [UIFX-RECIPE-GUIDE.md](recipes/UIFX-RECIPE-GUIDE.md) (included in the package).
192
+
193
+ Minimal recipe:
194
+
195
+ ```javascript
196
+ export function MyButton() {
197
+ let pressScale = 1;
198
+ return {
199
+ onClick() { pressScale = 0.85; },
200
+ tick(ctx, dt, now, state) {
201
+ pressScale += (1 - pressScale) * dt * 10;
202
+ ctx.translate(state.w/2, state.h/2);
203
+ ctx.scale(pressScale, pressScale);
204
+ ctx.translate(-state.w/2, -state.h/2);
205
+ ctx.fillStyle = state.hover ? '#a78bfa' : '#333';
206
+ ctx.beginPath(); ctx.roundRect(0, 0, state.w, state.h, 10); ctx.fill();
207
+ ctx.fillStyle = '#fff'; ctx.font = '600 13px sans-serif';
208
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
209
+ ctx.fillText('PRESS ME', state.w/2, state.h/2);
210
+ },
211
+ };
212
+ }
213
+ ```
214
+
215
+ ## đŸ“Ļ TypeScript
216
+
217
+ Full declarations for controller, all 3 recipe volumes, and shared interfaces:
218
+
219
+ ```
220
+ UIFXController.d.ts — mountUIFX, UIType, UIFXState, UIFXPointer, UIFXRecipe
221
+ UIFXRecipes.d.ts — 10 recipe factories (Vol. 1)
222
+ UIFXRecipes2.d.ts — 20 recipe factories (Vol. 2)
223
+ UIFXRecipes3.d.ts — 20 recipe factories (Vol. 3)
224
+ ```
225
+
226
+ ## 📚 LLM-Friendly Documentation
227
+
228
+ See `llms.txt` for AI-optimized metadata and the complete recipe catalog.
229
+
230
+ ## License
231
+
232
+ MIT
@@ -0,0 +1,71 @@
1
+ export type UITypeValue = 'button' | 'toggle' | 'slider';
2
+
3
+ export declare const UIType: Readonly<{
4
+ BUTTON: 'button';
5
+ TOGGLE: 'toggle';
6
+ SLIDER: 'slider';
7
+ }>;
8
+
9
+ export interface UIFXState {
10
+ hover: boolean;
11
+ active: boolean;
12
+ focused: boolean;
13
+ toggled: boolean;
14
+ val: number;
15
+ w: number;
16
+ h: number;
17
+ padding: number;
18
+ dpr: number;
19
+ }
20
+
21
+ export interface UIFXPointer {
22
+ x: number;
23
+ y: number;
24
+ vx: number;
25
+ vy: number;
26
+ }
27
+
28
+ export interface UIFXRecipe {
29
+ init?(ctx: CanvasRenderingContext2D, w: number, h: number, padding: number): void;
30
+
31
+ tick(ctx: CanvasRenderingContext2D, dt: number, now: number, state: UIFXState, pointer: UIFXPointer): void;
32
+
33
+ onHover?(state: UIFXState, pointer: UIFXPointer): void;
34
+
35
+ onLeave?(state: UIFXState, pointer: UIFXPointer): void;
36
+
37
+ onClick?(x: number, y: number, state: UIFXState): void;
38
+
39
+ onToggle?(checked: boolean, state: UIFXState): void;
40
+
41
+ onDrag?(value: number, velocity: number, state: UIFXState): void;
42
+
43
+ destroy?(): void;
44
+ }
45
+
46
+ export type RecipeFactory = () => UIFXRecipe;
47
+
48
+ export interface MountOptions {
49
+ width?: number;
50
+ height?: number;
51
+ padding?: number;
52
+ label?: string;
53
+ }
54
+
55
+ export interface UIFXInstance {
56
+ el: HTMLElement;
57
+ canvas: HTMLCanvasElement;
58
+ wrapper: HTMLDivElement;
59
+ state: UIFXState;
60
+
61
+ destroy(): void;
62
+ }
63
+
64
+ export declare function mountUIFX(
65
+ container: HTMLElement,
66
+ type: UITypeValue,
67
+ recipeFactory: RecipeFactory,
68
+ options?: MountOptions
69
+ ): UIFXInstance;
70
+
71
+ export default mountUIFX;
@@ -0,0 +1,272 @@
1
+ /**
2
+ * @zakkster/lite-ui-fx — Canvas-Hijacked UI Components
3
+ *
4
+ * Overlays a DPR-aware canvas on top of native HTML elements (buttons,
5
+ * checkboxes, sliders). The native element handles accessibility, focus,
6
+ * and events. The canvas handles visuals via a pluggable recipe system.
7
+ *
8
+ * Architecture:
9
+ * Native element (opacity:0, z-index:2) — receives all pointer/keyboard events
10
+ * Canvas overlay (z-index:1) — renders the visual recipe
11
+ * Recipe factory → { init?, tick, onHover?, onLeave?, onClick?, onToggle?, onDrag?, destroy? }
12
+ *
13
+ * Uses:
14
+ * @zakkster/lite-lerp — interpolation in recipes
15
+ * @zakkster/lite-random — deterministic particle effects
16
+ * @zakkster/lite-color — OKLCH color math (optional per recipe)
17
+ *
18
+ * Depends on: @zakkster/lite-ticker (shared RAF loop)
19
+ */
20
+
21
+ import { Ticker } from '@zakkster/lite-ticker';
22
+
23
+ // ─────────────────────────────────────────────────────────
24
+ // SHARED TICKER (ref-counted, one RAF for all UI components)
25
+ // ─────────────────────────────────────────────────────────
26
+
27
+ let _sharedTicker = null;
28
+ let _sharedRefs = 0;
29
+
30
+ function acquireTicker() {
31
+ if (!_sharedTicker) {
32
+ _sharedTicker = new Ticker();
33
+ _sharedTicker.start();
34
+ }
35
+ _sharedRefs++;
36
+ return _sharedTicker;
37
+ }
38
+
39
+ function releaseTicker() {
40
+ _sharedRefs--;
41
+ if (_sharedRefs <= 0 && _sharedTicker) {
42
+ _sharedTicker.destroy();
43
+ _sharedTicker = null;
44
+ _sharedRefs = 0;
45
+ }
46
+ }
47
+
48
+
49
+ // ─────────────────────────────────────────────────────────
50
+ // ELEMENT TYPES
51
+ // ─────────────────────────────────────────────────────────
52
+
53
+ /** @enum {string} */
54
+ export const UIType = Object.freeze({
55
+ BUTTON: 'button',
56
+ TOGGLE: 'toggle',
57
+ SLIDER: 'slider',
58
+ });
59
+
60
+
61
+ // ═══════════════════════════════════════════════════════════
62
+ // UIFXController — The Canvas Hijacker
63
+ // ═══════════════════════════════════════════════════════════
64
+
65
+ /**
66
+ * Mount a canvas-rendered recipe onto a native HTML element.
67
+ *
68
+ * @param {HTMLElement} container Parent element to mount into
69
+ * @param {string} type 'button' | 'toggle' | 'slider'
70
+ * @param {Function} recipeFactory () => Recipe object
71
+ * @param {Object} [options]
72
+ * @param {number} [options.width] Element width (auto-detected from type if omitted)
73
+ * @param {number} [options.height] Element height
74
+ * @param {number} [options.padding=40] Canvas overflow padding (for particles)
75
+ * @param {string} [options.label] Accessible label for the element
76
+ * @returns {{ el: HTMLElement, destroy: Function }}
77
+ */
78
+ export function mountUIFX(container, type, recipeFactory, {
79
+ width,
80
+ height,
81
+ padding = 40,
82
+ label = '',
83
+ } = {}) {
84
+ // ── Resolve dimensions ──
85
+ const w = width || (type === UIType.BUTTON ? 160 : type === UIType.SLIDER ? 200 : 64);
86
+ const h = height || (type === UIType.BUTTON ? 48 : type === UIType.SLIDER ? 28 : 36);
87
+ const dpr = window.devicePixelRatio || 1;
88
+
89
+ // ── Create native element (invisible, accessible, receives events) ──
90
+ let el;
91
+ if (type === UIType.TOGGLE) {
92
+ el = document.createElement('input');
93
+ el.type = 'checkbox';
94
+ el.setAttribute('role', 'switch');
95
+ if (label) el.setAttribute('aria-label', label);
96
+ } else if (type === UIType.SLIDER) {
97
+ el = document.createElement('input');
98
+ el.type = 'range';
99
+ el.min = '0'; el.max = '100'; el.value = '50';
100
+ if (label) el.setAttribute('aria-label', label);
101
+ } else {
102
+ el = document.createElement('button');
103
+ el.textContent = label || 'Action';
104
+ el.type = 'button';
105
+ }
106
+
107
+ Object.assign(el.style, {
108
+ position: 'relative', zIndex: '2',
109
+ opacity: '0', cursor: 'pointer',
110
+ width: `${w}px`, height: `${h}px`,
111
+ border: 'none', background: 'transparent',
112
+ margin: '0', padding: '0',
113
+ WebkitAppearance: 'none', appearance: 'none',
114
+ });
115
+
116
+ // Slider thumb needs explicit sizing for hit area
117
+ if (type === UIType.SLIDER) {
118
+ const thumbCSS = document.createElement('style');
119
+ thumbCSS.textContent = `
120
+ .uifx-slider::-webkit-slider-thumb { -webkit-appearance:none; width:24px; height:24px; cursor:grab; }
121
+ .uifx-slider::-moz-range-thumb { width:24px; height:24px; cursor:grab; border:none; background:transparent; }
122
+ `;
123
+ document.head.appendChild(thumbCSS);
124
+ el.classList.add('uifx-slider');
125
+ }
126
+
127
+ // ── Create canvas overlay (DPR-aware) ──
128
+ const canvas = document.createElement('canvas');
129
+ const cw = w + padding * 2;
130
+ const ch = h + padding * 2;
131
+ canvas.width = cw * dpr;
132
+ canvas.height = ch * dpr;
133
+ Object.assign(canvas.style, {
134
+ position: 'absolute', top: '0', left: '0',
135
+ width: `${cw}px`, height: `${ch}px`,
136
+ transform: `translate(-${padding}px, -${padding}px)`,
137
+ pointerEvents: 'none', zIndex: '1',
138
+ });
139
+
140
+ const ctx = canvas.getContext('2d');
141
+ ctx.scale(dpr, dpr);
142
+
143
+ // ── Wrapper ──
144
+ const wrapper = document.createElement('div');
145
+ Object.assign(wrapper.style, {
146
+ position: 'relative', display: 'inline-block',
147
+ width: `${w}px`, height: `${h}px`,
148
+ });
149
+ wrapper.appendChild(el);
150
+ wrapper.appendChild(canvas);
151
+ container.appendChild(wrapper);
152
+
153
+ // ── State ──
154
+ const state = {
155
+ hover: false,
156
+ active: false, // pointer is down
157
+ focused: false, // keyboard focus
158
+ toggled: false, // checkbox state
159
+ val: type === UIType.SLIDER ? 0.5 : 0, // slider value 0–1
160
+ w, h, padding, dpr,
161
+ };
162
+
163
+ const pointer = { x: -999, y: -999, vx: 0, vy: 0 };
164
+
165
+ // ── Initialize recipe ──
166
+ const recipe = recipeFactory();
167
+ if (recipe.init) recipe.init(ctx, w, h, padding);
168
+
169
+ // ── Events (all via AbortController) ──
170
+ const ac = new AbortController();
171
+ const signal = ac.signal;
172
+
173
+ function updatePointer(e) {
174
+ const r = el.getBoundingClientRect();
175
+ const nx = e.clientX - r.left;
176
+ const ny = e.clientY - r.top;
177
+ pointer.vx = nx - pointer.x;
178
+ pointer.vy = ny - pointer.y;
179
+ pointer.x = nx;
180
+ pointer.y = ny;
181
+ }
182
+
183
+ el.addEventListener('pointermove', updatePointer, { signal });
184
+ el.addEventListener('pointerenter', (e) => {
185
+ state.hover = true;
186
+ updatePointer(e);
187
+ if (recipe.onHover) recipe.onHover(state, pointer);
188
+ }, { signal });
189
+ el.addEventListener('pointerleave', () => {
190
+ state.hover = false;
191
+ if (recipe.onLeave) recipe.onLeave(state, pointer);
192
+ }, { signal });
193
+ el.addEventListener('pointerdown', (e) => {
194
+ state.active = true;
195
+ updatePointer(e);
196
+ if (recipe.onClick) recipe.onClick(pointer.x, pointer.y, state);
197
+ }, { signal });
198
+ el.addEventListener('pointerup', () => { state.active = false; }, { signal });
199
+
200
+ // Focus tracking (for keyboard accessibility indicators)
201
+ el.addEventListener('focus', () => { state.focused = true; }, { signal });
202
+ el.addEventListener('blur', () => { state.focused = false; }, { signal });
203
+
204
+ // Toggle events
205
+ if (type === UIType.TOGGLE) {
206
+ el.addEventListener('change', () => {
207
+ state.toggled = el.checked;
208
+ if (recipe.onToggle) recipe.onToggle(state.toggled, state);
209
+ }, { signal });
210
+ // Keyboard: Space/Enter toggles checkbox
211
+ el.addEventListener('keydown', (e) => {
212
+ if (e.code === 'Space' || e.code === 'Enter') {
213
+ el.checked = !el.checked;
214
+ state.toggled = el.checked;
215
+ if (recipe.onToggle) recipe.onToggle(state.toggled, state);
216
+ }
217
+ }, { signal });
218
+ }
219
+
220
+ // Slider events
221
+ if (type === UIType.SLIDER) {
222
+ el.addEventListener('input', () => {
223
+ state.val = el.value / 100;
224
+ if (recipe.onDrag) recipe.onDrag(state.val, pointer.vx, state);
225
+ }, { signal });
226
+ }
227
+
228
+ // ── Render loop (shared ticker) ──
229
+ const ticker = acquireTicker();
230
+ let destroyed = false;
231
+
232
+ const removeTick = ticker.add((dtMs) => {
233
+ if (destroyed) return;
234
+ const dt = dtMs / 1000;
235
+ const now = performance.now();
236
+
237
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
238
+ ctx.clearRect(0, 0, cw, ch);
239
+ ctx.save();
240
+ ctx.translate(padding, padding); // Origin = native element's top-left
241
+ recipe.tick(ctx, dt, now, state, pointer);
242
+ ctx.restore();
243
+ });
244
+
245
+ // ── Public API ──
246
+ return {
247
+ /** The native HTML element (for external state reads). */
248
+ el,
249
+
250
+ /** The canvas element (for external styling). */
251
+ canvas,
252
+
253
+ /** The wrapper div (for positioning). */
254
+ wrapper,
255
+
256
+ /** Current state (read-only reference). */
257
+ state,
258
+
259
+ /** Destroy everything. Idempotent. */
260
+ destroy() {
261
+ if (destroyed) return;
262
+ destroyed = true;
263
+ ac.abort();
264
+ removeTick();
265
+ if (recipe.destroy) recipe.destroy();
266
+ releaseTicker();
267
+ wrapper.remove();
268
+ },
269
+ };
270
+ }
271
+
272
+ export default mountUIFX;
package/llms.txt ADDED
@@ -0,0 +1,69 @@
1
+ # @zakkster/lite-ui-fx
2
+ > Canvas-hijacked UI components with pluggable recipe system. 30 built-in recipes.
3
+
4
+ ## Install
5
+ npm i @zakkster/lite-ui-fx
6
+
7
+ ## Core Concept
8
+ Native HTML element (opacity:0, z-index:2) handles events + a11y.
9
+ Canvas overlay (z-index:1) renders visuals via a recipe factory function.
10
+ Recipe = { tick(), init?(), onHover?(), onClick?(), onToggle?(), onDrag?(), destroy?() }
11
+
12
+ ## Import — Controller
13
+ import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
14
+
15
+ ## Import — Recipes (3 volumes, 30 total)
16
+ import { SwarmToggle, MagneticButton, SparkSlider } from ./recipes/UIFXRecipes.js';
17
+ import { PendulumToggle, HeartbeatButton, AuroraSlider } from './recipes/UIFXRecipes2.js';
18
+ import { VolumeKnob, WaterLevel, TimerCountdown } from './recipes/UIFXRecipes3.js';
19
+
20
+ ## Mount
21
+ const instance = mountUIFX(container, UIType.TOGGLE, SwarmToggle, { label: 'Sound' });
22
+ instance.destroy(); // cleanup
23
+
24
+ ## Element Types
25
+ UIType.TOGGLE → <input type="checkbox" role="switch"> → state.toggled, onToggle(checked)
26
+ UIType.BUTTON → <button> → state.active, onClick(x, y, state)
27
+ UIType.SLIDER → <input type="range"> → state.val (0–1), onDrag(val, velocity)
28
+
29
+ ## State Object (provided to tick every frame)
30
+ { hover, active, focused, toggled, val, w, h, padding, dpr }
31
+
32
+ ## 30 Built-in Recipes
33
+
34
+ ### Vol. 1 (UIFXRecipes.js) — 10 recipes
35
+ Toggles: SwarmToggle, LiquidToggle, NeonPulseToggle
36
+ Buttons: MagneticButton, ShatterButton, ConfettiButton, GlitchButton
37
+ Sliders: SparkSlider, CosmicSlider, LaserSlider
38
+
39
+ ### Vol. 2 (UIFXRecipes2.js) — 20 recipes
40
+ Toggles: PendulumToggle, CircuitToggle, LightningToggle, DNAToggle
41
+ Buttons: HeartbeatButton, BreathingButton, InkSplashButton, PixelDissolveButton, FireworkButton
42
+ Sliders: AuroraSlider, WaveSlider, ElasticBandSlider, GravitySlider
43
+ Loaders: OrbitLoader, HelixLoader
44
+ Checkboxes: RippleCheck, MorphCheck
45
+ Counters: FlameCounter, GlitchCounter
46
+ Rating: BubbleRating
47
+
48
+ ### Vol. 3 (UIFXRecipes3.js) — 20 recipes
49
+ Knobs: VolumeKnob, CompassKnob
50
+ Progress: RingProgress, BatteryGauge, SignalMeter
51
+ Controls: PillTabs, Stepper, RadioOrbit
52
+ Indicators: PasswordStrength, WaterLevel, HeatMap
53
+ Mood: DayNightToggle, ReactionPicker, NotificationBell
54
+ Feedback: TypewriterField, SoundWaveBtn, UploadProgress
55
+ Fun: ScratchReveal, TimerCountdown, PullRefresh
56
+
57
+ ## Writing Custom Recipes
58
+ See UIFX-RECIPE-GUIDE.md (included in package).
59
+
60
+ ## Dependencies
61
+ @zakkster/lite-ticker, @zakkster/lite-lerp, @zakkster/lite-random
62
+
63
+ ## Key Facts
64
+ - DPR-aware canvas (retina sharp)
65
+ - AbortController cleanup on destroy
66
+ - Ref-counted shared Ticker (one RAF for all components)
67
+ - Safari-safe roundRect polyfill in recipes
68
+ - Accessibility: role=switch, aria-label, keyboard focus tracking
69
+ - Zero-GC in all built-in recipes (TypedArrays, seeded RNG)
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@zakkster/lite-ui-fx",
3
+ "version": "1.0.0",
4
+ "description": "Canvas-hijacked UI components with a pluggable recipe system. 30 built-in recipes across toggles, buttons, sliders, knobs, loaders, checkboxes, counters, and ratings.",
5
+ "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "UIFXController.js",
9
+ "types": "UIFXController.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./UIFXController.js",
13
+ "types": "./UIFXController.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "UIFXController.js",
18
+ "UIFXController.d.ts",
19
+ "llms.txt"
20
+ ],
21
+ "keywords": [
22
+ "ui",
23
+ "canvas",
24
+ "microinteraction",
25
+ "toggle",
26
+ "button",
27
+ "slider",
28
+ "knob",
29
+ "recipe",
30
+ "animation",
31
+ "zero-gc",
32
+ "game",
33
+ "a11y"
34
+ ],
35
+ "sideEffects": false,
36
+ "dependencies": {
37
+ "@zakkster/lite-ticker": "^1.0.0",
38
+ "@zakkster/lite-lerp": "^1.0.0",
39
+ "@zakkster/lite-random": "^1.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "vitest": "^3.0.0"
43
+ },
44
+ "scripts": {
45
+ "test": "vitest run"
46
+ },
47
+ "homepage": "https://github.com/PeshoVurtoleta/lite-ui-fx#readme",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/PeshoVurtoleta/lite-ui-fx.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/PeshoVurtoleta/lite-ui-fx/issues",
54
+ "email": "shinikchiev@yahoo.com"
55
+ }
56
+ }