@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 +232 -0
- package/UIFXController.d.ts +71 -0
- package/UIFXController.js +272 -0
- package/llms.txt +69 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# @zakkster/lite-ui-fx
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@zakkster/lite-ui-fx)
|
|
4
|
+
[](https://bundlephobia.com/result?p=@zakkster/lite-ui-fx)
|
|
5
|
+
[](https://www.npmjs.com/package/@zakkster/lite-ui-fx)
|
|
6
|
+
[](https://www.npmjs.com/package/@zakkster/lite-ui-fx)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
[](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
|
+
}
|