@mhmo91/schmancy 0.10.31 → 0.10.33
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/dist/agent/schmancy.agent.js +2217 -2140
- package/dist/agent/schmancy.agent.js.map +1 -1
- package/dist/avatar.cjs +1 -1
- package/dist/avatar.js +1 -1
- package/dist/badge.cjs +1 -1
- package/dist/badge.js +1 -1
- package/dist/{button-DlqCWuk-.cjs → button-nDZQe1ES.cjs} +1 -1
- package/dist/{button-DlqCWuk-.cjs.map → button-nDZQe1ES.cjs.map} +1 -1
- package/dist/{button-BnGN6SsV.js → button-qARUurjf.js} +1 -1
- package/dist/{button-BnGN6SsV.js.map → button-qARUurjf.js.map} +1 -1
- package/dist/button.cjs +1 -1
- package/dist/button.js +2 -2
- package/dist/{chips-lipKBK9P.cjs → chips-DIZFWnDZ.cjs} +1 -1
- package/dist/{chips-lipKBK9P.cjs.map → chips-DIZFWnDZ.cjs.map} +1 -1
- package/dist/{chips-B8HM25xv.js → chips-xaoSmwBK.js} +1 -1
- package/dist/{chips-B8HM25xv.js.map → chips-xaoSmwBK.js.map} +1 -1
- package/dist/chips.cjs +1 -1
- package/dist/chips.js +1 -1
- package/dist/content-drawer.cjs +1 -1
- package/dist/content-drawer.js +1 -1
- package/dist/{date-range-CObvXmZ4.js → date-range-8OkCahnR.js} +1 -1
- package/dist/{date-range-CObvXmZ4.js.map → date-range-8OkCahnR.js.map} +1 -1
- package/dist/{date-range-DXct0_Jg.cjs → date-range-Bbzg9aym.cjs} +1 -1
- package/dist/{date-range-DXct0_Jg.cjs.map → date-range-Bbzg9aym.cjs.map} +1 -1
- package/dist/date-range.cjs +1 -1
- package/dist/date-range.js +1 -1
- package/dist/{details-DSGEewvZ.cjs → details-3X9YKpuP.cjs} +1 -1
- package/dist/{details-DSGEewvZ.cjs.map → details-3X9YKpuP.cjs.map} +1 -1
- package/dist/{details-nRdT8J-W.js → details-BO_3CCNn.js} +1 -1
- package/dist/{details-nRdT8J-W.js.map → details-BO_3CCNn.js.map} +1 -1
- package/dist/details.cjs +1 -1
- package/dist/details.js +1 -1
- package/dist/{directives-DJbBHfID.cjs → directives-BOsvcH83.cjs} +11 -11
- package/dist/directives-BOsvcH83.cjs.map +1 -0
- package/dist/{directives-DZzxV3Hh.js → directives-D7AoVfPK.js} +231 -158
- package/dist/directives-D7AoVfPK.js.map +1 -0
- package/dist/directives.cjs +1 -1
- package/dist/directives.js +2 -2
- package/dist/form.cjs +1 -1
- package/dist/form.js +2 -2
- package/dist/handover/agent-runtime-followups.md +1 -1
- package/dist/handover/agent-runtime-v1.md +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.js +11 -11
- package/dist/{magnetic-MQ3HMHJi.cjs → magnetic-DKtc4umC.cjs} +1 -1
- package/dist/magnetic-DKtc4umC.cjs.map +1 -0
- package/dist/{magnetic-B2VKNfDu.js → magnetic-DaOOv5Dz.js} +13 -9
- package/dist/magnetic-DaOOv5Dz.js.map +1 -0
- package/dist/{menu-BNPbrAmd.cjs → menu-B5EKUeeD.cjs} +1 -1
- package/dist/{menu-BNPbrAmd.cjs.map → menu-B5EKUeeD.cjs.map} +1 -1
- package/dist/{menu-D2cZSp74.js → menu-CgdXrzir.js} +1 -1
- package/dist/{menu-D2cZSp74.js.map → menu-CgdXrzir.js.map} +1 -1
- package/dist/menu.cjs +1 -1
- package/dist/menu.js +1 -1
- package/dist/nav-drawer.cjs +1 -1
- package/dist/nav-drawer.js +1 -1
- package/dist/navigation-bar.cjs +1 -1
- package/dist/navigation-bar.js +1 -1
- package/dist/{overlay-U5jr3OYG.cjs → overlay-5PMZ75PO.cjs} +1 -1
- package/dist/{overlay-U5jr3OYG.cjs.map → overlay-5PMZ75PO.cjs.map} +1 -1
- package/dist/{overlay-BVdgWkIj.js → overlay-BWcB2pRx.js} +2 -2
- package/dist/{overlay-BVdgWkIj.js.map → overlay-BWcB2pRx.js.map} +1 -1
- package/dist/overlay.cjs +1 -1
- package/dist/{overlay.confirm-body-BYPEKZtR.js → overlay.confirm-body-B7W0DOGS.js} +1 -1
- package/dist/{overlay.confirm-body-BYPEKZtR.js.map → overlay.confirm-body-B7W0DOGS.js.map} +1 -1
- package/dist/{overlay.confirm-body-BCWt92R7.cjs → overlay.confirm-body-CsvwcBvG.cjs} +1 -1
- package/dist/{overlay.confirm-body-BCWt92R7.cjs.map → overlay.confirm-body-CsvwcBvG.cjs.map} +1 -1
- package/dist/overlay.js +3 -3
- package/dist/{overlay.service-D4_SgGuT.cjs → overlay.service-CC4zckoV.cjs} +1 -1
- package/dist/{overlay.service-D4_SgGuT.cjs.map → overlay.service-CC4zckoV.cjs.map} +1 -1
- package/dist/{overlay.service-Dq2X6ibl.js → overlay.service-zx465FI8.js} +2 -2
- package/dist/{overlay.service-Dq2X6ibl.js.map → overlay.service-zx465FI8.js.map} +1 -1
- package/dist/{select-gRJb1TEf.js → select-C_ljy5k4.js} +1 -1
- package/dist/{select-gRJb1TEf.js.map → select-C_ljy5k4.js.map} +1 -1
- package/dist/{select-Gb9fTA4M.cjs → select-D61MbhmA.cjs} +1 -1
- package/dist/{select-Gb9fTA4M.cjs.map → select-D61MbhmA.cjs.map} +1 -1
- package/dist/select.cjs +1 -1
- package/dist/select.js +1 -1
- package/dist/{src-DavVEUeO.js → src-CdX0NekF.js} +8 -8
- package/dist/{src-DavVEUeO.js.map → src-CdX0NekF.js.map} +1 -1
- package/dist/{src-C_7k7YhE.cjs → src-DEgL_xJv.cjs} +1 -1
- package/dist/{src-C_7k7YhE.cjs.map → src-DEgL_xJv.cjs.map} +1 -1
- package/dist/teleport.cjs +1 -1
- package/dist/teleport.js +1 -1
- package/package.json +21 -21
- package/src/directives/art/art.directive.ts +4 -2
- package/src/directives/art/effects/starfield.ts +222 -77
- package/src/directives/art/types.ts +41 -6
- package/src/directives/magnetic.ts +8 -1
- package/types/src/directives/art/effects/starfield.d.ts +11 -4
- package/types/src/directives/art/types.d.ts +41 -6
- package/dist/directives-DJbBHfID.cjs.map +0 -1
- package/dist/directives-DZzxV3Hh.js.map +0 -1
- package/dist/magnetic-B2VKNfDu.js.map +0 -1
- package/dist/magnetic-MQ3HMHJi.cjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mhmo91/schmancy",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.33",
|
|
4
4
|
"description": "UI library build with web components",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"customElements": "custom-elements.json",
|
|
@@ -89,42 +89,42 @@
|
|
|
89
89
|
"dependencies": {
|
|
90
90
|
"@floating-ui/dom": "^1.7.6",
|
|
91
91
|
"@lit-labs/motion": "^1.1.0",
|
|
92
|
-
"@lit-labs/signals": "^0.
|
|
92
|
+
"@lit-labs/signals": "^0.3.0",
|
|
93
93
|
"@lit-labs/virtualizer": "^2.1.1",
|
|
94
94
|
"@lit/context": "^1.1.6",
|
|
95
95
|
"@material/material-color-utilities": "^0.4.0",
|
|
96
96
|
"@material/web": "^2.4.1",
|
|
97
97
|
"dayjs": "^1.11.20",
|
|
98
|
-
"immer": "^11.1.
|
|
99
|
-
"lit": "^3.3.
|
|
98
|
+
"immer": "^11.1.8",
|
|
99
|
+
"lit": "^3.3.3",
|
|
100
100
|
"rxjs": "^7.8.2",
|
|
101
101
|
"ts-is-present": "^1.2.2"
|
|
102
102
|
},
|
|
103
103
|
"devDependencies": {
|
|
104
104
|
"@rollup/plugin-strip": "^3.0.4",
|
|
105
105
|
"@rollup/plugin-terser": "^1.0.0",
|
|
106
|
-
"@tailwindcss/postcss": "^4.
|
|
107
|
-
"@tailwindcss/vite": "^4.
|
|
108
|
-
"@types/node": "^25.
|
|
109
|
-
"@vitest/browser-playwright": "^4.1.
|
|
110
|
-
"@vitest/coverage-v8": "^4.1.
|
|
111
|
-
"@vitest/ui": "^4.1.
|
|
112
|
-
"axe-core": "^4.11.
|
|
113
|
-
"happy-dom": "^20.
|
|
114
|
-
"knip": "^6.
|
|
106
|
+
"@tailwindcss/postcss": "^4.3.0",
|
|
107
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
108
|
+
"@types/node": "^25.8.0",
|
|
109
|
+
"@vitest/browser-playwright": "^4.1.6",
|
|
110
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
111
|
+
"@vitest/ui": "^4.1.6",
|
|
112
|
+
"axe-core": "^4.11.4",
|
|
113
|
+
"happy-dom": "^20.9.0",
|
|
114
|
+
"knip": "^6.13.1",
|
|
115
115
|
"lit-analyzer": "^2.0.3",
|
|
116
|
-
"oxlint": "^1.
|
|
117
|
-
"playwright": "^1.
|
|
118
|
-
"postcss": "^8.5.
|
|
119
|
-
"prettier": "^3.8.
|
|
116
|
+
"oxlint": "^1.64.0",
|
|
117
|
+
"playwright": "^1.60.0",
|
|
118
|
+
"postcss": "^8.5.14",
|
|
119
|
+
"prettier": "^3.8.3",
|
|
120
120
|
"rollup-plugin-copy": "^3.5.0",
|
|
121
|
-
"sass": "^1.
|
|
122
|
-
"tailwindcss": "^4.
|
|
121
|
+
"sass": "^1.99.0",
|
|
122
|
+
"tailwindcss": "^4.3.0",
|
|
123
123
|
"ts-lit-plugin": "^2.0.2",
|
|
124
124
|
"ts-morph": "^28.0.0",
|
|
125
125
|
"typescript": "^5.9.3",
|
|
126
|
-
"vite": "^8.0.
|
|
127
|
-
"vitest": "^4.1.
|
|
126
|
+
"vite": "^8.0.13",
|
|
127
|
+
"vitest": "^4.1.6",
|
|
128
128
|
"web-component-analyzer": "^2.0.0"
|
|
129
129
|
},
|
|
130
130
|
"peerDependencies": {
|
|
@@ -29,14 +29,15 @@ class ArtDirective extends AsyncDirective {
|
|
|
29
29
|
|
|
30
30
|
override update(part: ElementPart, [options]: [ArtOptions]) {
|
|
31
31
|
const element = part.element as HTMLElement
|
|
32
|
-
const { name, color, intensity = 1, speed = 1 } = options
|
|
32
|
+
const { name, color, intensity = 1, speed = 1, density = 1 } = options
|
|
33
33
|
|
|
34
34
|
if (
|
|
35
35
|
this.state &&
|
|
36
36
|
(this.state.effect !== name ||
|
|
37
37
|
this.state.color !== color ||
|
|
38
38
|
this.state.intensity !== intensity ||
|
|
39
|
-
this.state.speed !== speed
|
|
39
|
+
this.state.speed !== speed ||
|
|
40
|
+
this.state.density !== density)
|
|
40
41
|
) {
|
|
41
42
|
this.cleanup()
|
|
42
43
|
}
|
|
@@ -49,6 +50,7 @@ class ArtDirective extends AsyncDirective {
|
|
|
49
50
|
color,
|
|
50
51
|
intensity,
|
|
51
52
|
speed,
|
|
53
|
+
density,
|
|
52
54
|
element,
|
|
53
55
|
isVisible: true,
|
|
54
56
|
initialized: false,
|
|
@@ -1,107 +1,252 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Starfield Effect —
|
|
2
|
+
* Starfield Effect — surreal deep-space drift with rare comets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A recessive page backdrop. One <canvas>; stars live in
|
|
5
|
+
* structure-of-arrays Float32Array tables (zero per-frame allocation) and
|
|
6
|
+
* render as pre-rendered glow sprites via drawImage — no gradient is built
|
|
7
|
+
* inside the RAF loop. Three parallax depth layers drift on slow orbit paths;
|
|
8
|
+
* colour temperature runs blue-white → warm; entrance reveals far→near in
|
|
9
|
+
* waves behind a faint corner nebula; a pooled comet streaks by rarely.
|
|
10
|
+
* Sparse and dim by design — tune reach with the `density` ArtOption.
|
|
11
|
+
*
|
|
12
|
+
* Performance budget: ≤ MAX_STAR_COUNT drawImage calls + one nebula blit per
|
|
13
|
+
* frame, dpr capped at 2, the whole field idle under prefers-reduced-motion.
|
|
7
14
|
*/
|
|
8
15
|
|
|
9
16
|
import type { ArtState } from '../types'
|
|
10
17
|
import { createOverlayContainer } from '../utils'
|
|
11
18
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
const appearDelay = g * 0.6
|
|
28
|
-
return {
|
|
29
|
-
bg: stops.join(','),
|
|
30
|
-
appearDelay,
|
|
31
|
-
twinkleDuration: 5 + g * 0.7,
|
|
32
|
-
twinkleDelay: appearDelay + 2.5,
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
}
|
|
19
|
+
const BASE_STAR_COUNT = 90
|
|
20
|
+
// Hard ceiling: a wide monitor must never carpet, whatever the density prop asks.
|
|
21
|
+
const MAX_STAR_COUNT = 140
|
|
22
|
+
const COMET_POOL = 3
|
|
23
|
+
const APPEAR_DURATION = 2.5
|
|
24
|
+
const SPRITE_PX = 64
|
|
25
|
+
// Cool → warm. Index 0 is the dominant blue-white; the tail warms toward amber.
|
|
26
|
+
const TEMPERATURE_RGB = [
|
|
27
|
+
[205, 222, 255],
|
|
28
|
+
[224, 233, 255],
|
|
29
|
+
[255, 255, 255],
|
|
30
|
+
[255, 240, 214],
|
|
31
|
+
[255, 222, 184],
|
|
32
|
+
]
|
|
36
33
|
|
|
37
34
|
export function createStarfieldOverlay(state: ArtState): void {
|
|
38
35
|
const { element } = state
|
|
39
36
|
const overlay = createOverlayContainer('starfield-overlay')
|
|
40
37
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
overlay.appendChild(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
38
|
+
const canvas = document.createElement('canvas')
|
|
39
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;'
|
|
40
|
+
overlay.appendChild(canvas)
|
|
41
|
+
element.appendChild(overlay)
|
|
42
|
+
|
|
43
|
+
const ctx = canvas.getContext('2d')
|
|
44
|
+
if (!ctx) return
|
|
45
|
+
|
|
46
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
|
47
|
+
const rect = element.getBoundingClientRect()
|
|
48
|
+
const deviceWidth = Math.max(1, Math.round(rect.width * dpr))
|
|
49
|
+
const deviceHeight = Math.max(1, Math.round(rect.height * dpr))
|
|
50
|
+
canvas.width = deviceWidth
|
|
51
|
+
canvas.height = deviceHeight
|
|
52
|
+
|
|
53
|
+
const sprites = TEMPERATURE_RGB.map(([r, g, b]) => {
|
|
54
|
+
const s = document.createElement('canvas')
|
|
55
|
+
s.width = SPRITE_PX
|
|
56
|
+
s.height = SPRITE_PX
|
|
57
|
+
const sc = s.getContext('2d')!
|
|
58
|
+
const grad = sc.createRadialGradient(SPRITE_PX / 2, SPRITE_PX / 2, 0, SPRITE_PX / 2, SPRITE_PX / 2, SPRITE_PX / 2)
|
|
59
|
+
grad.addColorStop(0, `rgba(${r},${g},${b},1)`)
|
|
60
|
+
grad.addColorStop(0.18, `rgba(${r},${g},${b},0.35)`)
|
|
61
|
+
grad.addColorStop(0.5, `rgba(${r},${g},${b},0)`)
|
|
62
|
+
sc.fillStyle = grad
|
|
63
|
+
sc.fillRect(0, 0, SPRITE_PX, SPRITE_PX)
|
|
64
|
+
return s
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const nebula = document.createElement('canvas')
|
|
68
|
+
nebula.width = 512
|
|
69
|
+
nebula.height = 512
|
|
70
|
+
const nc = nebula.getContext('2d')!
|
|
71
|
+
const ng = nc.createRadialGradient(256, 256, 0, 256, 256, 256)
|
|
72
|
+
ng.addColorStop(0, 'rgba(150,170,255,0.16)')
|
|
73
|
+
ng.addColorStop(0.4, 'rgba(120,110,210,0.07)')
|
|
74
|
+
ng.addColorStop(1, 'rgba(0,0,0,0)')
|
|
75
|
+
nc.fillStyle = ng
|
|
76
|
+
nc.fillRect(0, 0, 512, 512)
|
|
77
|
+
|
|
78
|
+
const starCount = Math.min(
|
|
79
|
+
MAX_STAR_COUNT,
|
|
80
|
+
Math.round(BASE_STAR_COUNT * Math.min(1, Math.max(0.5, (rect.width * rect.height) / (1280 * 720))) * state.density),
|
|
81
|
+
)
|
|
82
|
+
const sx = new Float32Array(starCount)
|
|
83
|
+
const sy = new Float32Array(starCount)
|
|
84
|
+
const sr = new Float32Array(starCount)
|
|
85
|
+
const sphase = new Float32Array(starCount)
|
|
86
|
+
const stwinkle = new Float32Array(starCount)
|
|
87
|
+
const sdepth = new Float32Array(starCount)
|
|
88
|
+
const sbucket = new Uint8Array(starCount)
|
|
89
|
+
const sappear = new Float32Array(starCount)
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < starCount; i++) {
|
|
92
|
+
const depthBand = i % 3 // 0 far, 1 mid, 2 near — even split across layers
|
|
93
|
+
const depth = depthBand === 0 ? 0.18 : depthBand === 1 ? 0.5 : 1
|
|
94
|
+
sx[i] = Math.random()
|
|
95
|
+
sy[i] = Math.random()
|
|
96
|
+
sr[i] = (0.7 + Math.random() * 1.6) * (0.6 + 0.7 * depth)
|
|
97
|
+
sphase[i] = Math.random() * Math.PI * 2
|
|
98
|
+
stwinkle[i] = 0.6 + Math.random() * 1.8
|
|
99
|
+
sdepth[i] = depth
|
|
100
|
+
// Field skews blue-white; only a minority warms — that asymmetry reads as authored.
|
|
101
|
+
sbucket[i] = Math.random() < 0.7 ? (Math.random() < 0.5 ? 0 : 1) : 2 + Math.floor(Math.random() * 3)
|
|
102
|
+
// Far layer arrives first, near layer last → the depth reveals as waves.
|
|
103
|
+
sappear[i] = (1 - depth) * 1.6 + Math.random() * 0.8
|
|
58
104
|
}
|
|
59
105
|
|
|
60
|
-
element.appendChild(overlay)
|
|
61
106
|
state.overlayElement = overlay
|
|
62
|
-
state.starfield = {
|
|
107
|
+
state.starfield = {
|
|
108
|
+
canvas,
|
|
109
|
+
ctx,
|
|
110
|
+
sprites,
|
|
111
|
+
nebula,
|
|
112
|
+
starCount,
|
|
113
|
+
sx,
|
|
114
|
+
sy,
|
|
115
|
+
sr,
|
|
116
|
+
sphase,
|
|
117
|
+
stwinkle,
|
|
118
|
+
sdepth,
|
|
119
|
+
sbucket,
|
|
120
|
+
sappear,
|
|
121
|
+
cx: new Float32Array(COMET_POOL),
|
|
122
|
+
cy: new Float32Array(COMET_POOL),
|
|
123
|
+
cvx: new Float32Array(COMET_POOL),
|
|
124
|
+
cvy: new Float32Array(COMET_POOL),
|
|
125
|
+
clife: new Float32Array(COMET_POOL),
|
|
126
|
+
cometCount: COMET_POOL,
|
|
127
|
+
nextCometAt: performance.now() + 14000 + Math.random() * 16000,
|
|
128
|
+
dpr,
|
|
129
|
+
deviceWidth,
|
|
130
|
+
deviceHeight,
|
|
131
|
+
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
132
|
+
drawnStatic: false,
|
|
133
|
+
lastFrame: performance.now(),
|
|
134
|
+
startTime: performance.now(),
|
|
135
|
+
}
|
|
63
136
|
}
|
|
64
137
|
|
|
65
138
|
export function animateStarfield(state: ArtState, currentTime: number): void {
|
|
66
|
-
|
|
139
|
+
const sf = state.starfield
|
|
140
|
+
if (!sf) return
|
|
67
141
|
|
|
68
142
|
const { intensity = 1, speed = 1 } = state
|
|
69
|
-
const {
|
|
70
|
-
|
|
143
|
+
const { ctx, canvas } = sf
|
|
144
|
+
|
|
145
|
+
if (intensity <= 0) {
|
|
146
|
+
ctx.clearRect(0, 0, sf.deviceWidth, sf.deviceHeight)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
71
149
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
150
|
+
// One layout read per frame; resize only when the backing store must change.
|
|
151
|
+
const cssW = canvas.clientWidth
|
|
152
|
+
const cssH = canvas.clientHeight
|
|
153
|
+
const wantW = Math.max(1, Math.round(cssW * sf.dpr))
|
|
154
|
+
const wantH = Math.max(1, Math.round(cssH * sf.dpr))
|
|
155
|
+
if (wantW !== sf.deviceWidth || wantH !== sf.deviceHeight) {
|
|
156
|
+
canvas.width = wantW
|
|
157
|
+
canvas.height = wantH
|
|
158
|
+
sf.deviceWidth = wantW
|
|
159
|
+
sf.deviceHeight = wantH
|
|
160
|
+
sf.drawnStatic = false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const W = sf.deviceWidth
|
|
164
|
+
const H = sf.deviceHeight
|
|
165
|
+
|
|
166
|
+
if (sf.reducedMotion && sf.drawnStatic) return
|
|
167
|
+
|
|
168
|
+
const elapsed = ((currentTime - sf.startTime) / 1000) * speed
|
|
169
|
+
const dt = Math.min((currentTime - sf.lastFrame) / 1000, 0.05)
|
|
170
|
+
sf.lastFrame = currentTime
|
|
171
|
+
|
|
172
|
+
ctx.clearRect(0, 0, W, H)
|
|
173
|
+
|
|
174
|
+
// Entrance nebula: blooms in over the first ~4s, then holds at a faint floor.
|
|
175
|
+
const nebulaAlpha = (Math.min(1, elapsed / 4) * 0.7 + 0.3) * 0.14 * intensity
|
|
176
|
+
if (nebulaAlpha > 0.001) {
|
|
177
|
+
const nSize = Math.max(W, H) * 1.3
|
|
178
|
+
ctx.globalAlpha = nebulaAlpha
|
|
179
|
+
ctx.drawImage(sf.nebula, W * 0.78 - nSize / 2, H * 0.28 - nSize / 2, nSize, nSize)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Normal compositing — additive blend is what turned overlap into a glowing carpet.
|
|
183
|
+
for (let i = 0; i < sf.starCount; i++) {
|
|
184
|
+
const appear = sf.sappear[i]
|
|
185
|
+
let reveal: number
|
|
186
|
+
if (sf.reducedMotion) {
|
|
187
|
+
reveal = 1
|
|
188
|
+
} else if (elapsed < appear) {
|
|
75
189
|
continue
|
|
190
|
+
} else if (elapsed < appear + APPEAR_DURATION) {
|
|
191
|
+
const t = (elapsed - appear) / APPEAR_DURATION
|
|
192
|
+
reveal = 1 - (1 - t) * (1 - t) * (1 - t)
|
|
193
|
+
} else {
|
|
194
|
+
reveal = 1
|
|
76
195
|
}
|
|
77
196
|
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
let opacity: number
|
|
197
|
+
const depth = sf.sdepth[i]
|
|
198
|
+
const twinkle = sf.reducedMotion ? 0.9 : 0.82 + 0.18 * Math.sin(elapsed * sf.stwinkle[i] + sf.sphase[i])
|
|
81
199
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
200
|
+
// Slow orbit drift, amplitude scaled by depth — the parallax is the surreality.
|
|
201
|
+
const driftAmp = sf.reducedMotion ? 0 : depth * 14 * sf.dpr
|
|
202
|
+
const px = sf.sx[i] * W + Math.sin(elapsed * 0.05 + sf.sphase[i]) * driftAmp
|
|
203
|
+
const py = sf.sy[i] * H + Math.cos(elapsed * 0.04 + sf.sphase[i]) * driftAmp * 0.6
|
|
204
|
+
|
|
205
|
+
const size = sf.sr[i] * sf.dpr * 2.3
|
|
206
|
+
ctx.globalAlpha = reveal * twinkle * (0.3 + 0.5 * depth) * intensity
|
|
207
|
+
ctx.drawImage(sf.sprites[sf.sbucket[i]], px - size, py - size, size * 2, size * 2)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!sf.reducedMotion) {
|
|
211
|
+
if (currentTime >= sf.nextCometAt) {
|
|
212
|
+
for (let c = 0; c < sf.cometCount; c++) {
|
|
213
|
+
if (sf.clife[c] > 0) continue
|
|
214
|
+
const fromLeft = Math.random() < 0.5
|
|
215
|
+
sf.cx[c] = (fromLeft ? -0.05 : 1.05) * W
|
|
216
|
+
sf.cy[c] = Math.random() * 0.45 * H
|
|
217
|
+
const sp = (520 + Math.random() * 420) * sf.dpr
|
|
218
|
+
sf.cvx[c] = (fromLeft ? 1 : -1) * sp
|
|
219
|
+
sf.cvy[c] = (0.35 + Math.random() * 0.35) * sp
|
|
220
|
+
sf.clife[c] = 1
|
|
221
|
+
break
|
|
102
222
|
}
|
|
223
|
+
sf.nextCometAt = currentTime + (20000 + Math.random() * 25000) / speed
|
|
103
224
|
}
|
|
104
225
|
|
|
105
|
-
|
|
226
|
+
for (let c = 0; c < sf.cometCount; c++) {
|
|
227
|
+
if (sf.clife[c] <= 0) continue
|
|
228
|
+
sf.cx[c] += sf.cvx[c] * dt * speed
|
|
229
|
+
sf.cy[c] += sf.cvy[c] * dt * speed
|
|
230
|
+
sf.clife[c] -= dt / 1.6
|
|
231
|
+
|
|
232
|
+
const tailX = sf.cx[c] - sf.cvx[c] * 0.16
|
|
233
|
+
const tailY = sf.cy[c] - sf.cvy[c] * 0.16
|
|
234
|
+
const grad = ctx.createLinearGradient(sf.cx[c], sf.cy[c], tailX, tailY)
|
|
235
|
+
const headA = Math.max(0, Math.min(1, sf.clife[c])) * 0.55 * intensity
|
|
236
|
+
grad.addColorStop(0, `rgba(255,255,255,${headA})`)
|
|
237
|
+
grad.addColorStop(0.4, `rgba(190,210,255,${headA * 0.4})`)
|
|
238
|
+
grad.addColorStop(1, 'rgba(190,210,255,0)')
|
|
239
|
+
ctx.globalAlpha = 1
|
|
240
|
+
ctx.strokeStyle = grad
|
|
241
|
+
ctx.lineWidth = 2 * sf.dpr
|
|
242
|
+
ctx.lineCap = 'round'
|
|
243
|
+
ctx.beginPath()
|
|
244
|
+
ctx.moveTo(sf.cx[c], sf.cy[c])
|
|
245
|
+
ctx.lineTo(tailX, tailY)
|
|
246
|
+
ctx.stroke()
|
|
247
|
+
}
|
|
106
248
|
}
|
|
249
|
+
|
|
250
|
+
ctx.globalAlpha = 1
|
|
251
|
+
sf.drawnStatic = true
|
|
107
252
|
}
|
|
@@ -15,6 +15,8 @@ export interface ArtOptions {
|
|
|
15
15
|
intensity?: number
|
|
16
16
|
/** Animation speed multiplier: 0.5 = half speed, 1 = normal, 2 = double (default: 1) */
|
|
17
17
|
speed?: number
|
|
18
|
+
/** Particle density multiplier: 0.5 = sparse, 1 = baseline, 2 = dense (default: 1; starfield only) */
|
|
19
|
+
density?: number
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export interface Particle<T extends SVGElement = SVGElement> {
|
|
@@ -52,6 +54,7 @@ export interface ArtState {
|
|
|
52
54
|
color: string
|
|
53
55
|
intensity: number
|
|
54
56
|
speed: number
|
|
57
|
+
density: number
|
|
55
58
|
element: HTMLElement
|
|
56
59
|
overlayElement?: HTMLElement
|
|
57
60
|
animationId?: number
|
|
@@ -118,12 +121,44 @@ export interface ArtState {
|
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
starfield?: {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
canvas: HTMLCanvasElement
|
|
125
|
+
ctx: CanvasRenderingContext2D
|
|
126
|
+
/** Pre-rendered glow sprites, cool→warm. Drawn per star so no per-frame gradient is built. */
|
|
127
|
+
sprites: HTMLCanvasElement[]
|
|
128
|
+
/** Pre-rendered entrance nebula bloom, drawn once per frame at a computed alpha. */
|
|
129
|
+
nebula: HTMLCanvasElement
|
|
130
|
+
starCount: number
|
|
131
|
+
/** Structure-of-arrays star table — cache-friendly, zero allocation per frame. */
|
|
132
|
+
sx: Float32Array
|
|
133
|
+
sy: Float32Array
|
|
134
|
+
sr: Float32Array
|
|
135
|
+
sphase: Float32Array
|
|
136
|
+
stwinkle: Float32Array
|
|
137
|
+
/** 0 = far (drifts least, dimmest) → 1 = near (drifts most, brightest). */
|
|
138
|
+
sdepth: Float32Array
|
|
139
|
+
/** Index into `sprites` — fixes each star's colour temperature for its life. */
|
|
140
|
+
sbucket: Uint8Array
|
|
141
|
+
/** Per-star entrance delay (s) so the field reveals far→near in waves. */
|
|
142
|
+
sappear: Float32Array
|
|
143
|
+
/** Fixed comet pool. `life <= 0` marks a free slot; no array churn. */
|
|
144
|
+
cx: Float32Array
|
|
145
|
+
cy: Float32Array
|
|
146
|
+
cvx: Float32Array
|
|
147
|
+
cvy: Float32Array
|
|
148
|
+
clife: Float32Array
|
|
149
|
+
cometCount: number
|
|
150
|
+
/** ms timestamp of the next comet spawn — gates the rare streak. */
|
|
151
|
+
nextCometAt: number
|
|
152
|
+
/** Capped device-pixel-ratio; bounds backing-store fill cost on retina. */
|
|
153
|
+
dpr: number
|
|
154
|
+
/** Cached device-pixel canvas size; a change is the only resize trigger. */
|
|
155
|
+
deviceWidth: number
|
|
156
|
+
deviceHeight: number
|
|
157
|
+
/** Static-render once, then idle the RAF loop. */
|
|
158
|
+
reducedMotion: boolean
|
|
159
|
+
drawnStatic: boolean
|
|
160
|
+
/** Previous frame ms — frame-rate-independent comet motion + jump clamp. */
|
|
161
|
+
lastFrame: number
|
|
127
162
|
startTime: number
|
|
128
163
|
}
|
|
129
164
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { directive, type ElementPart, PartType } from 'lit/directive.js'
|
|
2
2
|
import { AsyncDirective } from 'lit/async-directive.js'
|
|
3
3
|
import { animationFrameScheduler, fromEvent, merge, Subject } from 'rxjs'
|
|
4
|
-
import { auditTime, map, takeUntil } from 'rxjs/operators'
|
|
4
|
+
import { auditTime, filter, map, takeUntil } from 'rxjs/operators'
|
|
5
5
|
import { SPRING_SNAPPY } from '../utils/animation'
|
|
6
6
|
import { reducedMotion$ } from './reduced-motion'
|
|
7
7
|
|
|
@@ -77,6 +77,13 @@ class MagneticDirective extends AsyncDirective {
|
|
|
77
77
|
|
|
78
78
|
const move$ = fromEvent<MouseEvent>(target, 'mousemove').pipe(
|
|
79
79
|
auditTime(0, animationFrameScheduler),
|
|
80
|
+
filter(() =>
|
|
81
|
+
this.element.checkVisibility?.({
|
|
82
|
+
contentVisibilityAuto: true,
|
|
83
|
+
checkOpacity: true,
|
|
84
|
+
checkVisibilityCSS: true,
|
|
85
|
+
} as CheckVisibilityOptions) ?? true,
|
|
86
|
+
),
|
|
80
87
|
map(e => {
|
|
81
88
|
const rect = this.cachedRect ?? this.element.getBoundingClientRect()
|
|
82
89
|
const centerX = rect.left + rect.width / 2
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Starfield Effect —
|
|
2
|
+
* Starfield Effect — surreal deep-space drift with rare comets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A recessive page backdrop. One <canvas>; stars live in
|
|
5
|
+
* structure-of-arrays Float32Array tables (zero per-frame allocation) and
|
|
6
|
+
* render as pre-rendered glow sprites via drawImage — no gradient is built
|
|
7
|
+
* inside the RAF loop. Three parallax depth layers drift on slow orbit paths;
|
|
8
|
+
* colour temperature runs blue-white → warm; entrance reveals far→near in
|
|
9
|
+
* waves behind a faint corner nebula; a pooled comet streaks by rarely.
|
|
10
|
+
* Sparse and dim by design — tune reach with the `density` ArtOption.
|
|
11
|
+
*
|
|
12
|
+
* Performance budget: ≤ MAX_STAR_COUNT drawImage calls + one nebula blit per
|
|
13
|
+
* frame, dpr capped at 2, the whole field idle under prefers-reduced-motion.
|
|
7
14
|
*/
|
|
8
15
|
import type { ArtState } from '../types';
|
|
9
16
|
export declare function createStarfieldOverlay(state: ArtState): void;
|
|
@@ -12,6 +12,8 @@ export interface ArtOptions {
|
|
|
12
12
|
intensity?: number;
|
|
13
13
|
/** Animation speed multiplier: 0.5 = half speed, 1 = normal, 2 = double (default: 1) */
|
|
14
14
|
speed?: number;
|
|
15
|
+
/** Particle density multiplier: 0.5 = sparse, 1 = baseline, 2 = dense (default: 1; starfield only) */
|
|
16
|
+
density?: number;
|
|
15
17
|
}
|
|
16
18
|
export interface Particle<T extends SVGElement = SVGElement> {
|
|
17
19
|
element: T;
|
|
@@ -46,6 +48,7 @@ export interface ArtState {
|
|
|
46
48
|
color: string;
|
|
47
49
|
intensity: number;
|
|
48
50
|
speed: number;
|
|
51
|
+
density: number;
|
|
49
52
|
element: HTMLElement;
|
|
50
53
|
overlayElement?: HTMLElement;
|
|
51
54
|
animationId?: number;
|
|
@@ -115,12 +118,44 @@ export interface ArtState {
|
|
|
115
118
|
startTime: number;
|
|
116
119
|
};
|
|
117
120
|
starfield?: {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
canvas: HTMLCanvasElement;
|
|
122
|
+
ctx: CanvasRenderingContext2D;
|
|
123
|
+
/** Pre-rendered glow sprites, cool→warm. Drawn per star so no per-frame gradient is built. */
|
|
124
|
+
sprites: HTMLCanvasElement[];
|
|
125
|
+
/** Pre-rendered entrance nebula bloom, drawn once per frame at a computed alpha. */
|
|
126
|
+
nebula: HTMLCanvasElement;
|
|
127
|
+
starCount: number;
|
|
128
|
+
/** Structure-of-arrays star table — cache-friendly, zero allocation per frame. */
|
|
129
|
+
sx: Float32Array;
|
|
130
|
+
sy: Float32Array;
|
|
131
|
+
sr: Float32Array;
|
|
132
|
+
sphase: Float32Array;
|
|
133
|
+
stwinkle: Float32Array;
|
|
134
|
+
/** 0 = far (drifts least, dimmest) → 1 = near (drifts most, brightest). */
|
|
135
|
+
sdepth: Float32Array;
|
|
136
|
+
/** Index into `sprites` — fixes each star's colour temperature for its life. */
|
|
137
|
+
sbucket: Uint8Array;
|
|
138
|
+
/** Per-star entrance delay (s) so the field reveals far→near in waves. */
|
|
139
|
+
sappear: Float32Array;
|
|
140
|
+
/** Fixed comet pool. `life <= 0` marks a free slot; no array churn. */
|
|
141
|
+
cx: Float32Array;
|
|
142
|
+
cy: Float32Array;
|
|
143
|
+
cvx: Float32Array;
|
|
144
|
+
cvy: Float32Array;
|
|
145
|
+
clife: Float32Array;
|
|
146
|
+
cometCount: number;
|
|
147
|
+
/** ms timestamp of the next comet spawn — gates the rare streak. */
|
|
148
|
+
nextCometAt: number;
|
|
149
|
+
/** Capped device-pixel-ratio; bounds backing-store fill cost on retina. */
|
|
150
|
+
dpr: number;
|
|
151
|
+
/** Cached device-pixel canvas size; a change is the only resize trigger. */
|
|
152
|
+
deviceWidth: number;
|
|
153
|
+
deviceHeight: number;
|
|
154
|
+
/** Static-render once, then idle the RAF loop. */
|
|
155
|
+
reducedMotion: boolean;
|
|
156
|
+
drawnStatic: boolean;
|
|
157
|
+
/** Previous frame ms — frame-rate-independent comet motion + jump clamp. */
|
|
158
|
+
lastFrame: number;
|
|
124
159
|
startTime: number;
|
|
125
160
|
};
|
|
126
161
|
}
|