@zakkster/lite-ui-fx 1.0.4 → 1.0.5
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/CHANGELOG.md +100 -0
- package/LICENSE +21 -0
- package/README.md +67 -67
- package/UIFXController.d.ts +2 -0
- package/UIFXController.js +28 -24
- package/llms.txt +13 -11
- package/package.json +16 -5
- package/recipes/UIFX-RECIPE-GUIDE.md +188 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@zakkster/lite-ui-fx` are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows Keep a Changelog; this project adheres to Semantic
|
|
6
|
+
Versioning.
|
|
7
|
+
|
|
8
|
+
## [1.0.5] -- unreleased
|
|
9
|
+
|
|
10
|
+
Truth pass, law pass, and the torture skeleton. No runtime behaviour
|
|
11
|
+
changes: this release makes the package honest, lawful, and provable.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- U-04 (docs sell what does not ship): recipe count corrected to 50
|
|
16
|
+
everywhere (`package.json` description, `llms.txt`); the recipe-guide
|
|
17
|
+
link now points at `recipes/UIFX-RECIPE-GUIDE.md` (was a root-level
|
|
18
|
+
404); the broken import quote in `llms.txt` is repaired.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- U-08 (packaging law): restored `"sideEffects": false` (dropped by
|
|
23
|
+
mistake in 1.0.4 -- see below); added `"engines": { "node": ">=18" }`;
|
|
24
|
+
`UIFX-RECIPE-GUIDE.md` now ships in the package `files[]` so the
|
|
25
|
+
"included in the package" claim is true; `LICENSE`, `README.md`, and
|
|
26
|
+
`CHANGELOG.md` ship as well.
|
|
27
|
+
- U-08 (source law): ASCII-only sweep across the controller, recipe
|
|
28
|
+
volumes, guide, README, and `llms.txt` (em/en dashes, arrows,
|
|
29
|
+
box-drawing, emoji removed). `demo/` is exempt until U6.
|
|
30
|
+
- U-07 (test law): the vitest suite is ported to `node:test` +
|
|
31
|
+
`assert/strict`; the dead `_tickAll`/`_clear` imports are replaced by a
|
|
32
|
+
deterministic RAF stub. `vitest` removed from devDependencies.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `export const VERSION = '1.0.5'` in `UIFXController.js` (three-place
|
|
37
|
+
version sync: `package.json`, this const, and the `llms.txt` VERSION
|
|
38
|
+
line).
|
|
39
|
+
- `test/` node:test suite plus the torture skeleton
|
|
40
|
+
(`node --expose-gc test/torture.mjs`) gated by `@zakkster/lite-leak` and
|
|
41
|
+
`@zakkster/lite-gc-profiler`.
|
|
42
|
+
|
|
43
|
+
### Known issues (see ROADMAP.md)
|
|
44
|
+
|
|
45
|
+
- U-01: Space cannot toggle a toggle; each press fires two spurious
|
|
46
|
+
`onToggle`s. (Fixed in U1.)
|
|
47
|
+
- U-02: one malformed recipe (no `tick`) throws inside the shared ticker
|
|
48
|
+
and permanently freezes every UIFX component on the page. (Fixed in U1.)
|
|
49
|
+
- U-03: "zero-GC in all built-in recipes" is false for 49 of 50 recipes.
|
|
50
|
+
(Fixed in U3.)
|
|
51
|
+
- U-05: recipes misrender at any non-default size (hardcoded geometry).
|
|
52
|
+
(Fixed in U3.)
|
|
53
|
+
- U-06: theming is half-built with three conventions; hardcoded fonts and
|
|
54
|
+
canvas labels. (Fixed in U3.)
|
|
55
|
+
- U-09: every slider mount leaks a `<style>` into `document.head`; never
|
|
56
|
+
removed on destroy. (Fixed in U1.)
|
|
57
|
+
- U-10: fail-open option surface -- unknown option keys silently ignored;
|
|
58
|
+
no initial-value options; DPR read once. (Fixed in U1.)
|
|
59
|
+
- U-11: `getBoundingClientRect()` runs on every pointermove. (Fixed in U1.)
|
|
60
|
+
- U-12: the demos reimplement the library inline instead of consuming it.
|
|
61
|
+
(Fixed in U6.)
|
|
62
|
+
- U-13: recipes are distributed as a GitHub ZIP, not shipped code.
|
|
63
|
+
(Fixed in U2.)
|
|
64
|
+
|
|
65
|
+
## [1.0.4] -- 2026-03-26
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- Metadata patch.
|
|
70
|
+
|
|
71
|
+
### Regression
|
|
72
|
+
|
|
73
|
+
- Dropped `"sideEffects": false` from `package.json` (present in 1.0.3).
|
|
74
|
+
Restored in 1.0.5.
|
|
75
|
+
|
|
76
|
+
## [1.0.3] -- 2026-03-25
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- README-only patch.
|
|
81
|
+
|
|
82
|
+
## [1.0.2] -- 2026-03-25
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
|
|
86
|
+
- README-only patch.
|
|
87
|
+
|
|
88
|
+
## [1.0.1] -- 2026-03-25
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- README-only patch.
|
|
93
|
+
|
|
94
|
+
## [1.0.0] -- 2026-03-25
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- Initial release: the `mountUIFX` controller plus 50 GitHub-hosted
|
|
99
|
+
recipes across toggles, buttons, sliders, knobs, loaders, checkboxes,
|
|
100
|
+
counters, and ratings.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zahary Shinikchiev
|
|
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
CHANGED
|
@@ -8,72 +8,72 @@
|
|
|
8
8
|

|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## What is lite-ui-fx?
|
|
12
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
|
|
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
14
|
|
|
15
|
-
##
|
|
15
|
+
## Live Demo (UI-FX)
|
|
16
16
|
https://cdpn.io/pen/debug/RNGjMjQ
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Live Demo (UI-FX vol.2)
|
|
19
19
|
https://cdpn.io/pen/debug/yyaPKpB
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Live Demo (UI-FX vol3.)
|
|
22
22
|
https://cdpn.io/pen/debug/YPGEaYY
|
|
23
23
|
|
|
24
24
|
**50 recipes** across UI element categories:
|
|
25
25
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
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 to check)
|
|
37
|
+
- **Loaders** -- Orbit planets, DNA helix
|
|
38
|
+
- **Counters** -- Flame heat, Glitch signal
|
|
39
|
+
- **Rating** -- Bubble inflate
|
|
40
40
|
|
|
41
41
|
Every recipe is zero-GC, uses `dt`-based animation, and includes accessibility indicators (focus rings, state labels).
|
|
42
42
|
|
|
43
|
-
`@zakkster/lite-ui-fx` ships only the core controller on npm (zero bloat).
|
|
43
|
+
`@zakkster/lite-ui-fx` ships only the core controller on npm (zero bloat).
|
|
44
44
|
All visual effects live in the GitHub repo as **recipes**.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
- Vol. 1 (10 recipes):
|
|
48
|
-
https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes.js
|
|
49
|
-
- Vol. 2 (20 recipes):
|
|
50
|
-
https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes2.js
|
|
51
|
-
- Vol. 3 (20 recipes):
|
|
52
|
-
https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes3.js
|
|
46
|
+
**Recipe Collections:**
|
|
47
|
+
- Vol. 1 (10 recipes):
|
|
48
|
+
https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes.js
|
|
49
|
+
- Vol. 2 (20 recipes):
|
|
50
|
+
https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes2.js
|
|
51
|
+
- Vol. 3 (20 recipes):
|
|
52
|
+
https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFXRecipes3.js
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/UIFX-RECIPE-GUIDE.md
|
|
54
|
+
**How to write your own:**
|
|
55
|
+
https://github.com/PeshoVurtoleta/lite-ui-fx/blob/main/recipes/UIFX-RECIPE-GUIDE.md
|
|
56
56
|
|
|
57
|
-
Recipes are **optional**, **open-source**, and **not included in the npm package**
|
|
58
|
-
to keep the install size tiny (<2 KB).
|
|
57
|
+
Recipes are **optional**, **open-source**, and **not included in the npm package**
|
|
58
|
+
to keep the install size tiny (<2 KB).
|
|
59
59
|
You can copy/paste any recipe into your project or use them as inspiration.
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Download all recipes as a ZIP
|
|
62
62
|
https://github.com/PeshoVurtoleta/lite-ui-fx/archive/refs/heads/main.zip
|
|
63
63
|
|
|
64
64
|
Part of the [@zakkster/lite-*](https://www.npmjs.com/org/zakkster) ecosystem.
|
|
65
65
|
|
|
66
|
-
##
|
|
66
|
+
## Install
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
69
|
npm i @zakkster/lite-ui-fx
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
> Looking for the visual effects?
|
|
73
|
-
> Recipes live in the GitHub repo
|
|
72
|
+
> Looking for the visual effects?
|
|
73
|
+
> Recipes live in the GitHub repo -- not in the npm package -- to keep the library tiny.
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
##
|
|
76
|
+
## Quick Start
|
|
77
77
|
|
|
78
78
|
```javascript
|
|
79
79
|
import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
|
|
@@ -95,7 +95,7 @@ const instance = mountUIFX(
|
|
|
95
95
|
instance.destroy();
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
##
|
|
98
|
+
## Import Map
|
|
99
99
|
|
|
100
100
|
```javascript
|
|
101
101
|
// Controller (always needed)
|
|
@@ -104,40 +104,40 @@ import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
|
|
|
104
104
|
// Recipes are NOT included in the npm package.
|
|
105
105
|
// Copy them from the GitHub repo into your own ./recipes folder:
|
|
106
106
|
|
|
107
|
-
// Vol. 1
|
|
107
|
+
// Vol. 1 -- 10 recipes (toggles, buttons, sliders)
|
|
108
108
|
import { SwarmToggle, MagneticButton, SparkSlider } from './recipes/UIFXRecipes.js';
|
|
109
109
|
|
|
110
|
-
// Vol. 2
|
|
110
|
+
// Vol. 2 -- 20 recipes (+ loaders, checkboxes, counters, rating)
|
|
111
111
|
import { PendulumToggle, HeartbeatButton, RippleCheck } from './recipes/UIFXRecipes2.js';
|
|
112
112
|
|
|
113
|
-
// Vol. 3
|
|
113
|
+
// Vol. 3 -- 20 recipes (knobs, progress, controls, indicators, mood, feedback, fun)
|
|
114
114
|
import { VolumeKnob, WaterLevel, TimerCountdown } from './recipes/UIFXRecipes3.js';
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
##
|
|
117
|
+
## How It Works
|
|
118
118
|
|
|
119
119
|
```
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
138
|
```
|
|
139
139
|
|
|
140
|
-
##
|
|
140
|
+
## API
|
|
141
141
|
|
|
142
142
|
### `mountUIFX(container, type, recipeFactory, options?)`
|
|
143
143
|
|
|
@@ -159,7 +159,7 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
|
|
|
159
159
|
|------|---------------|-------------|-----------|
|
|
160
160
|
| `UIType.TOGGLE` | `<input type="checkbox" role="switch">` | `onToggle(checked)` | `state.toggled` |
|
|
161
161
|
| `UIType.BUTTON` | `<button>` | `onClick(x, y, state)` | `state.active` |
|
|
162
|
-
| `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0
|
|
162
|
+
| `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0-1) |
|
|
163
163
|
|
|
164
164
|
### State Object (provided to `tick()` every frame)
|
|
165
165
|
|
|
@@ -169,7 +169,7 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
|
|
|
169
169
|
active: boolean; // Pointer pressed
|
|
170
170
|
focused: boolean; // Keyboard focus
|
|
171
171
|
toggled: boolean; // Checkbox state
|
|
172
|
-
val: number; // Slider value (0
|
|
172
|
+
val: number; // Slider value (0-1)
|
|
173
173
|
w: number; // Element width
|
|
174
174
|
h: number; // Element height
|
|
175
175
|
padding: number; // Canvas padding
|
|
@@ -177,16 +177,16 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
|
|
|
177
177
|
}
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
##
|
|
180
|
+
## Comparison
|
|
181
181
|
|
|
182
182
|
| Library | Size | Approach | Recipes | A11y | Install |
|
|
183
183
|
|---------|------|----------|---------|------|---------|
|
|
184
184
|
| Framer Motion | ~45 KB | React HOC | 0 | Via React | `npm i framer-motion` |
|
|
185
185
|
| GSAP | ~25 KB | Timeline | 0 | Manual | `npm i gsap` |
|
|
186
186
|
| Lottie | ~55 KB | JSON animation | After Effects | Manual | `npm i lottie-web` |
|
|
187
|
-
| **lite-ui-fx** | **< 5 KB** | **Canvas hijack** |
|
|
187
|
+
| **lite-ui-fx** | **< 5 KB** | **Canvas hijack** | **50 built-in** | **Native + visual** | **`npm i @zakkster/lite-ui-fx`** |
|
|
188
188
|
|
|
189
|
-
##
|
|
189
|
+
## Writing Custom Recipes
|
|
190
190
|
|
|
191
191
|
See the full [UIFX-RECIPE-GUIDE.md](recipes/UIFX-RECIPE-GUIDE.md) (included in the package).
|
|
192
192
|
|
|
@@ -212,7 +212,7 @@ export function MyButton() {
|
|
|
212
212
|
}
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
-
##
|
|
215
|
+
## TypeScript
|
|
216
216
|
|
|
217
217
|
Full TypeScript declarations are included for:
|
|
218
218
|
|
|
@@ -225,7 +225,7 @@ Full TypeScript declarations are included for:
|
|
|
225
225
|
(Recipes are not part of the npm package, so their types are not included.)
|
|
226
226
|
|
|
227
227
|
|
|
228
|
-
##
|
|
228
|
+
## LLM-Friendly Documentation
|
|
229
229
|
|
|
230
230
|
See `llms.txt` for AI-optimized metadata and the complete recipe catalog.
|
|
231
231
|
|
package/UIFXController.d.ts
CHANGED
package/UIFXController.js
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zakkster/lite-ui-fx
|
|
2
|
+
* @zakkster/lite-ui-fx -- Canvas-Hijacked UI Components
|
|
3
3
|
*
|
|
4
4
|
* Overlays a DPR-aware canvas on top of native HTML elements (buttons,
|
|
5
5
|
* checkboxes, sliders). The native element handles accessibility, focus,
|
|
6
6
|
* and events. The canvas handles visuals via a pluggable recipe system.
|
|
7
7
|
*
|
|
8
8
|
* Architecture:
|
|
9
|
-
* Native element (opacity:0, z-index:2)
|
|
10
|
-
* Canvas overlay (z-index:1)
|
|
11
|
-
* Recipe factory
|
|
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
12
|
*
|
|
13
13
|
* Uses:
|
|
14
|
-
* @zakkster/lite-lerp
|
|
15
|
-
* @zakkster/lite-random
|
|
16
|
-
* @zakkster/lite-color
|
|
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
17
|
*
|
|
18
18
|
* Depends on: @zakkster/lite-ticker (shared RAF loop)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { Ticker } from '@zakkster/lite-ticker';
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Three-place version sync: this constant, package.json "version", and the
|
|
24
|
+
// VERSION line in llms.txt must always match. /release keeps them locked.
|
|
25
|
+
export const VERSION = '1.0.5';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------
|
|
24
28
|
// SHARED TICKER (ref-counted, one RAF for all UI components)
|
|
25
|
-
//
|
|
29
|
+
// ---------------------------------------------------------
|
|
26
30
|
|
|
27
31
|
let _sharedTicker = null;
|
|
28
32
|
let _sharedRefs = 0;
|
|
@@ -46,9 +50,9 @@ function releaseTicker() {
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
|
|
49
|
-
//
|
|
53
|
+
// ---------------------------------------------------------
|
|
50
54
|
// ELEMENT TYPES
|
|
51
|
-
//
|
|
55
|
+
// ---------------------------------------------------------
|
|
52
56
|
|
|
53
57
|
/** @enum {string} */
|
|
54
58
|
export const UIType = Object.freeze({
|
|
@@ -58,9 +62,9 @@ export const UIType = Object.freeze({
|
|
|
58
62
|
});
|
|
59
63
|
|
|
60
64
|
|
|
61
|
-
//
|
|
62
|
-
// UIFXController
|
|
63
|
-
//
|
|
65
|
+
// =========================================================
|
|
66
|
+
// UIFXController -- The Canvas Hijacker
|
|
67
|
+
// =========================================================
|
|
64
68
|
|
|
65
69
|
/**
|
|
66
70
|
* Mount a canvas-rendered recipe onto a native HTML element.
|
|
@@ -81,12 +85,12 @@ export function mountUIFX(container, type, recipeFactory, {
|
|
|
81
85
|
padding = 40,
|
|
82
86
|
label = '',
|
|
83
87
|
} = {}) {
|
|
84
|
-
//
|
|
88
|
+
// -- Resolve dimensions --
|
|
85
89
|
const w = width || (type === UIType.BUTTON ? 160 : type === UIType.SLIDER ? 200 : 64);
|
|
86
90
|
const h = height || (type === UIType.BUTTON ? 48 : type === UIType.SLIDER ? 28 : 36);
|
|
87
91
|
const dpr = window.devicePixelRatio || 1;
|
|
88
92
|
|
|
89
|
-
//
|
|
93
|
+
// -- Create native element (invisible, accessible, receives events) --
|
|
90
94
|
let el;
|
|
91
95
|
if (type === UIType.TOGGLE) {
|
|
92
96
|
el = document.createElement('input');
|
|
@@ -124,7 +128,7 @@ export function mountUIFX(container, type, recipeFactory, {
|
|
|
124
128
|
el.classList.add('uifx-slider');
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
//
|
|
131
|
+
// -- Create canvas overlay (DPR-aware) --
|
|
128
132
|
const canvas = document.createElement('canvas');
|
|
129
133
|
const cw = w + padding * 2;
|
|
130
134
|
const ch = h + padding * 2;
|
|
@@ -140,7 +144,7 @@ export function mountUIFX(container, type, recipeFactory, {
|
|
|
140
144
|
const ctx = canvas.getContext('2d');
|
|
141
145
|
ctx.scale(dpr, dpr);
|
|
142
146
|
|
|
143
|
-
//
|
|
147
|
+
// -- Wrapper --
|
|
144
148
|
const wrapper = document.createElement('div');
|
|
145
149
|
Object.assign(wrapper.style, {
|
|
146
150
|
position: 'relative', display: 'inline-block',
|
|
@@ -150,23 +154,23 @@ export function mountUIFX(container, type, recipeFactory, {
|
|
|
150
154
|
wrapper.appendChild(canvas);
|
|
151
155
|
container.appendChild(wrapper);
|
|
152
156
|
|
|
153
|
-
//
|
|
157
|
+
// -- State --
|
|
154
158
|
const state = {
|
|
155
159
|
hover: false,
|
|
156
160
|
active: false, // pointer is down
|
|
157
161
|
focused: false, // keyboard focus
|
|
158
162
|
toggled: false, // checkbox state
|
|
159
|
-
val: type === UIType.SLIDER ? 0.5 : 0, // slider value 0
|
|
163
|
+
val: type === UIType.SLIDER ? 0.5 : 0, // slider value 0-1
|
|
160
164
|
w, h, padding, dpr,
|
|
161
165
|
};
|
|
162
166
|
|
|
163
167
|
const pointer = { x: -999, y: -999, vx: 0, vy: 0 };
|
|
164
168
|
|
|
165
|
-
//
|
|
169
|
+
// -- Initialize recipe --
|
|
166
170
|
const recipe = recipeFactory();
|
|
167
171
|
if (recipe.init) recipe.init(ctx, w, h, padding);
|
|
168
172
|
|
|
169
|
-
//
|
|
173
|
+
// -- Events (all via AbortController) --
|
|
170
174
|
const ac = new AbortController();
|
|
171
175
|
const signal = ac.signal;
|
|
172
176
|
|
|
@@ -225,7 +229,7 @@ export function mountUIFX(container, type, recipeFactory, {
|
|
|
225
229
|
}, { signal });
|
|
226
230
|
}
|
|
227
231
|
|
|
228
|
-
//
|
|
232
|
+
// -- Render loop (shared ticker) --
|
|
229
233
|
const ticker = acquireTicker();
|
|
230
234
|
let destroyed = false;
|
|
231
235
|
|
|
@@ -242,7 +246,7 @@ export function mountUIFX(container, type, recipeFactory, {
|
|
|
242
246
|
ctx.restore();
|
|
243
247
|
});
|
|
244
248
|
|
|
245
|
-
//
|
|
249
|
+
// -- Public API --
|
|
246
250
|
return {
|
|
247
251
|
/** The native HTML element (for external state reads). */
|
|
248
252
|
el,
|
package/llms.txt
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @zakkster/lite-ui-fx
|
|
2
|
-
> Canvas-hijacked UI components with pluggable recipe system.
|
|
2
|
+
> Canvas-hijacked UI components with pluggable recipe system. 50 built-in recipes.
|
|
3
|
+
|
|
4
|
+
VERSION 1.0.5
|
|
3
5
|
|
|
4
6
|
## Install
|
|
5
7
|
npm i @zakkster/lite-ui-fx
|
|
@@ -9,11 +11,11 @@ Native HTML element (opacity:0, z-index:2) handles events + a11y.
|
|
|
9
11
|
Canvas overlay (z-index:1) renders visuals via a recipe factory function.
|
|
10
12
|
Recipe = { tick(), init?(), onHover?(), onClick?(), onToggle?(), onDrag?(), destroy?() }
|
|
11
13
|
|
|
12
|
-
## Import
|
|
14
|
+
## Import -- Controller
|
|
13
15
|
import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
|
|
14
16
|
|
|
15
|
-
## Import
|
|
16
|
-
import { SwarmToggle, MagneticButton, SparkSlider } from ./recipes/UIFXRecipes.js';
|
|
17
|
+
## Import -- Recipes (3 volumes, 50 total)
|
|
18
|
+
import { SwarmToggle, MagneticButton, SparkSlider } from './recipes/UIFXRecipes.js';
|
|
17
19
|
import { PendulumToggle, HeartbeatButton, AuroraSlider } from './recipes/UIFXRecipes2.js';
|
|
18
20
|
import { VolumeKnob, WaterLevel, TimerCountdown } from './recipes/UIFXRecipes3.js';
|
|
19
21
|
|
|
@@ -22,21 +24,21 @@ const instance = mountUIFX(container, UIType.TOGGLE, SwarmToggle, { label: 'Soun
|
|
|
22
24
|
instance.destroy(); // cleanup
|
|
23
25
|
|
|
24
26
|
## Element Types
|
|
25
|
-
UIType.TOGGLE
|
|
26
|
-
UIType.BUTTON
|
|
27
|
-
UIType.SLIDER
|
|
27
|
+
UIType.TOGGLE -> <input type="checkbox" role="switch"> -> state.toggled, onToggle(checked)
|
|
28
|
+
UIType.BUTTON -> <button> -> state.active, onClick(x, y, state)
|
|
29
|
+
UIType.SLIDER -> <input type="range"> -> state.val (0-1), onDrag(val, velocity)
|
|
28
30
|
|
|
29
31
|
## State Object (provided to tick every frame)
|
|
30
32
|
{ hover, active, focused, toggled, val, w, h, padding, dpr }
|
|
31
33
|
|
|
32
|
-
##
|
|
34
|
+
## 50 Built-in Recipes
|
|
33
35
|
|
|
34
|
-
### Vol. 1 (UIFXRecipes.js)
|
|
36
|
+
### Vol. 1 (UIFXRecipes.js) -- 10 recipes
|
|
35
37
|
Toggles: SwarmToggle, LiquidToggle, NeonPulseToggle
|
|
36
38
|
Buttons: MagneticButton, ShatterButton, ConfettiButton, GlitchButton
|
|
37
39
|
Sliders: SparkSlider, CosmicSlider, LaserSlider
|
|
38
40
|
|
|
39
|
-
### Vol. 2 (UIFXRecipes2.js)
|
|
41
|
+
### Vol. 2 (UIFXRecipes2.js) -- 20 recipes
|
|
40
42
|
Toggles: PendulumToggle, CircuitToggle, LightningToggle, DNAToggle
|
|
41
43
|
Buttons: HeartbeatButton, BreathingButton, InkSplashButton, PixelDissolveButton, FireworkButton
|
|
42
44
|
Sliders: AuroraSlider, WaveSlider, ElasticBandSlider, GravitySlider
|
|
@@ -45,7 +47,7 @@ Checkboxes: RippleCheck, MorphCheck
|
|
|
45
47
|
Counters: FlameCounter, GlitchCounter
|
|
46
48
|
Rating: BubbleRating
|
|
47
49
|
|
|
48
|
-
### Vol. 3 (UIFXRecipes3.js)
|
|
50
|
+
### Vol. 3 (UIFXRecipes3.js) -- 20 recipes
|
|
49
51
|
Knobs: VolumeKnob, CompassKnob
|
|
50
52
|
Progress: RingProgress, BatteryGauge, SignalMeter
|
|
51
53
|
Controls: PillTabs, Stepper, RadioOrbit
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-ui-fx",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Canvas-hijacked UI components with a pluggable recipe system.
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"description": "Canvas-hijacked UI components with a pluggable recipe system. 50 built-in recipes across toggles, buttons, sliders, knobs, loaders, checkboxes, counters, and ratings.",
|
|
5
5
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
8
9
|
"main": "UIFXController.js",
|
|
9
10
|
"types": "UIFXController.d.ts",
|
|
10
11
|
"exports": {
|
|
@@ -16,7 +17,11 @@
|
|
|
16
17
|
"files": [
|
|
17
18
|
"UIFXController.js",
|
|
18
19
|
"UIFXController.d.ts",
|
|
19
|
-
"llms.txt"
|
|
20
|
+
"llms.txt",
|
|
21
|
+
"recipes/UIFX-RECIPE-GUIDE.md",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
20
25
|
],
|
|
21
26
|
"keywords": [
|
|
22
27
|
"ui",
|
|
@@ -32,16 +37,22 @@
|
|
|
32
37
|
"game",
|
|
33
38
|
"a11y"
|
|
34
39
|
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
35
43
|
"dependencies": {
|
|
36
44
|
"@zakkster/lite-ticker": "^1.0.0",
|
|
37
45
|
"@zakkster/lite-lerp": "^1.0.0",
|
|
38
46
|
"@zakkster/lite-random": "^1.0.0"
|
|
39
47
|
},
|
|
40
48
|
"devDependencies": {
|
|
41
|
-
"
|
|
49
|
+
"@zakkster/lite-gc-profiler": "^1.16.0",
|
|
50
|
+
"@zakkster/lite-leak": "^1.10.0",
|
|
51
|
+
"@zakkster/lite-signal": "^1.5.1"
|
|
42
52
|
},
|
|
43
53
|
"scripts": {
|
|
44
|
-
"test": "
|
|
54
|
+
"test": "node --test test/*.test.mjs",
|
|
55
|
+
"torture": "node --expose-gc test/torture.mjs"
|
|
45
56
|
},
|
|
46
57
|
"homepage": "https://github.com/PeshoVurtoleta/lite-ui-fx#readme",
|
|
47
58
|
"repository": {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Writing a UI-FX Recipe
|
|
2
|
+
|
|
3
|
+
A recipe is a plain factory function that returns an object with a `tick()` method and optional lifecycle hooks. The UIFXController handles events, DPR scaling, and the render loop -- the recipe just draws.
|
|
4
|
+
|
|
5
|
+
## Minimal Recipe (copy-paste starter)
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
export function MyToggle() {
|
|
9
|
+
let knobX = 18;
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
tick(ctx, dt, now, state, pointer) {
|
|
13
|
+
const { w, h, toggled } = state;
|
|
14
|
+
|
|
15
|
+
// Animate knob position
|
|
16
|
+
const target = toggled ? w - 18 : 18;
|
|
17
|
+
knobX += (target - knobX) * dt * 12;
|
|
18
|
+
|
|
19
|
+
// Draw track
|
|
20
|
+
ctx.fillStyle = toggled ? 'rgba(110,231,182,.2)' : 'rgba(255,255,255,.06)';
|
|
21
|
+
ctx.beginPath();
|
|
22
|
+
ctx.roundRect(0, 0, w, h, h / 2);
|
|
23
|
+
ctx.fill();
|
|
24
|
+
|
|
25
|
+
// Draw knob
|
|
26
|
+
ctx.fillStyle = toggled ? '#6ee7b6' : '#999';
|
|
27
|
+
ctx.beginPath();
|
|
28
|
+
ctx.arc(knobX, h / 2, 14, 0, Math.PI * 2);
|
|
29
|
+
ctx.fill();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The Recipe Interface
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
interface Recipe {
|
|
39
|
+
// REQUIRED -- called every frame (60fps)
|
|
40
|
+
tick(ctx: CanvasRenderingContext2D, dt: number, now: number, state: State, pointer: Pointer): void;
|
|
41
|
+
|
|
42
|
+
// OPTIONAL -- called once after mount
|
|
43
|
+
init?(ctx: CanvasRenderingContext2D, w: number, h: number, padding: number): void;
|
|
44
|
+
|
|
45
|
+
// OPTIONAL -- event hooks
|
|
46
|
+
onHover?(state: State, pointer: Pointer): void;
|
|
47
|
+
onLeave?(state: State, pointer: Pointer): void;
|
|
48
|
+
onClick?(x: number, y: number, state: State): void;
|
|
49
|
+
onToggle?(checked: boolean, state: State): void; // Toggles only
|
|
50
|
+
onDrag?(value: number, velocity: number, state: State): void; // Sliders only
|
|
51
|
+
|
|
52
|
+
// OPTIONAL -- cleanup
|
|
53
|
+
destroy?(): void;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## The State Object
|
|
58
|
+
|
|
59
|
+
The controller provides this every frame:
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
{
|
|
63
|
+
hover: boolean, // Pointer is inside the element
|
|
64
|
+
active: boolean, // Pointer is pressed down
|
|
65
|
+
focused: boolean, // Element has keyboard focus
|
|
66
|
+
toggled: boolean, // Checkbox checked state (toggles)
|
|
67
|
+
val: number, // 0--1 slider value (sliders)
|
|
68
|
+
w: number, // Element width in CSS pixels
|
|
69
|
+
h: number, // Element height
|
|
70
|
+
padding: number, // Canvas overflow padding
|
|
71
|
+
dpr: number, // Device pixel ratio
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## The Pointer Object
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
{
|
|
79
|
+
x: number, // X position relative to element's top-left
|
|
80
|
+
y: number, // Y position relative to element's top-left
|
|
81
|
+
vx: number, // X velocity (pixels since last move)
|
|
82
|
+
vy: number, // Y velocity
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Coordinate System
|
|
87
|
+
|
|
88
|
+
The canvas is larger than the element (by `padding` on each side) to allow particles to overflow. The controller translates the context so that `(0, 0)` is the element's top-left corner. You draw as if the element starts at origin.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Canvas memory:
|
|
92
|
+
+-------------------------------+
|
|
93
|
+
| padding |
|
|
94
|
+
| +-------------------+ |
|
|
95
|
+
| | (0,0) (w,0)| |
|
|
96
|
+
| | | |
|
|
97
|
+
| | Your drawing space | |
|
|
98
|
+
| | | |
|
|
99
|
+
| | (0,h) (w,h)| |
|
|
100
|
+
| +-------------------+ |
|
|
101
|
+
| padding |
|
|
102
|
+
+-------------------------------+
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Particles that fly outside `(0,0)--(w,h)` are visible because the canvas extends by `padding` in each direction. Default padding is 40px.
|
|
106
|
+
|
|
107
|
+
## Mounting a Recipe
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
import { mountUIFX, UIType } from './UIFXController.js';
|
|
111
|
+
import { MyToggle } from './my-recipe.js';
|
|
112
|
+
|
|
113
|
+
const instance = mountUIFX(
|
|
114
|
+
document.getElementById('container'),
|
|
115
|
+
UIType.TOGGLE,
|
|
116
|
+
MyToggle, // Factory function (NOT called -- the controller calls it)
|
|
117
|
+
{ label: 'Dark mode', width: 64, height: 36 }
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Later:
|
|
121
|
+
instance.destroy();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Three Element Types
|
|
125
|
+
|
|
126
|
+
| Type | Native Element | Recipe Gets | Key State |
|
|
127
|
+
|------|----------------|-------------|-----------|
|
|
128
|
+
| `UIType.TOGGLE` | `<input type="checkbox" role="switch">` | `onToggle(checked)` | `state.toggled` |
|
|
129
|
+
| `UIType.BUTTON` | `<button>` | `onClick(x, y, state)` | `state.active` |
|
|
130
|
+
| `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0--1) |
|
|
131
|
+
|
|
132
|
+
## Using @zakkster Libraries in Recipes
|
|
133
|
+
|
|
134
|
+
Recipes can import any @zakkster library:
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
import { lerp, clamp, easeOut } from '@zakkster/lite-lerp';
|
|
138
|
+
import { Random } from '@zakkster/lite-random';
|
|
139
|
+
import { toCssOklch, lerpOklch } from '@zakkster/lite-color';
|
|
140
|
+
|
|
141
|
+
export function OklchToggle() {
|
|
142
|
+
const rng = new Random(42);
|
|
143
|
+
const off = { l: 0.4, c: 0.05, h: 250 };
|
|
144
|
+
const on = { l: 0.7, c: 0.2, h: 160 };
|
|
145
|
+
let t = 0;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
tick(ctx, dt, now, state) {
|
|
149
|
+
t = lerp(t, state.toggled ? 1 : 0, dt * 8);
|
|
150
|
+
const color = lerpOklch(off, on, easeOut(t));
|
|
151
|
+
|
|
152
|
+
ctx.fillStyle = toCssOklch(color);
|
|
153
|
+
ctx.beginPath();
|
|
154
|
+
ctx.roundRect(0, 0, state.w, state.h, state.h / 2);
|
|
155
|
+
ctx.fill();
|
|
156
|
+
// ...
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Accessibility Checklist
|
|
163
|
+
|
|
164
|
+
The controller makes the native element invisible but fully accessible. Your recipe should add visual feedback:
|
|
165
|
+
|
|
166
|
+
1. **Focus ring** -- draw a dashed outline when `state.focused` is true (keyboard users)
|
|
167
|
+
2. **State label** -- show "ON"/"OFF" or a value percentage so the user sees the state
|
|
168
|
+
3. **Press feedback** -- scale down on `state.active`, spring back on release
|
|
169
|
+
4. **Hover feedback** -- change color/glow when `state.hover` is true
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
// Focus ring helper
|
|
173
|
+
if (state.focused) {
|
|
174
|
+
ctx.strokeStyle = 'rgba(110,231,182,.6)';
|
|
175
|
+
ctx.lineWidth = 2;
|
|
176
|
+
ctx.setLineDash([4, 3]);
|
|
177
|
+
ctx.strokeRect(-2, -2, state.w + 4, state.h + 4);
|
|
178
|
+
ctx.setLineDash([]);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Performance Rules
|
|
183
|
+
|
|
184
|
+
1. **Never allocate in `tick()`** -- pre-allocate TypedArrays in the factory closure or `init()`
|
|
185
|
+
2. **Use `splice()` sparingly** -- for small particle arrays (< 100) it's fine. For larger pools, use a dead-flag pattern
|
|
186
|
+
3. **Reset composite operation** -- if you set `ctx.globalCompositeOperation = 'screen'`, reset to `'source-over'` before returning
|
|
187
|
+
4. **Reset shadow** -- `ctx.shadowBlur = 0` after drawing glowing elements
|
|
188
|
+
5. **Use the `dt` parameter** -- all motion must be `value * dt`, not `value` per frame. This ensures consistent speed regardless of frame rate.
|