Ghost

struxadotcloud/struxa

public
Fork0
CodeIssues0Pull requests0
1branch
struxapackagesuisrccomponentsdither-kitpixel.ts
feat: use dither-kit generative avatars as the default user avatar placeholder15f46c53 months ago109 lines · 3.1 KB
1import { type DitherColor, PALETTE, type Rgb } from "./palette"
2
3// 4×4 ordered (Bayer) matrix, normalized to 0–1 thresholds — the same matrix
4// the charts dither with.
5export const BAYER4 =
6 [0, 8, 2,
7 [12, 4, 14,
8 [3, 11, 1,
9 [15, 7, 13,
10].map((row)
11
12export const clamp01 =
13
14/** 32-bit FNV-1a hash — turns any string seed into a stable uint32. */
15export function fnv1a(str
16 let h =
17 for (let i
18 h ^= str.
19 h = Math.
20 }
21 return h >>>
22}
23
24/** Tiny deterministic PRNG (xorshift32) — returns floats in [0, 1). */
25export function xorshift32(seed
26 let s =
27 return () =>
28 s ^= s
29 s >>>= 0
30 s ^= s
31 s ^= s
32 s >>>= 0
33 return s /
34 }
35}
36
37/** A named palette colour or a raw hue (0–360). */
38export type PixelColor
39
40/** Hue (0–360) → an rgb fill tuned to sit alongside the chart palette. */
41export function hueFill(hue
42 const h =
43 const s =
44 const l =
45 const c =
46 const x =
47 const m =
48 const [r,
49 h < 60
50 ? [c, x
51 : h < 120
52 ? [x, c
53 : h < 180
54 ? [0, c
55 : h < 240
56 ? [0, x
57 : h < 300
58 ? [x, 0
59 : [c, 0
60 return [
61 Math.round((r
62 Math.round((g
63 Math.round((b
64 ]
65}
66
67/** Resolve a {@
68export function fillOf(color
69 return typeof color
70}
71
72// Bloom — same recipe as the charts: a blurred copy of the crisp canvas,
73// composited additively so the glow stays in the dither's own colour.
74export type PixelBloom
75
76const BLOOM_PRESET: Record
77 Exclude<PixelBloom
78 { blur:
79> = {
80 low: { blur: 3
81 high: { blur: 5
82 aura: { blur: 15
83}
84
85export type PixelBloomStyle
86 filter: string
87 opacity: number
88 mixBlendMode: "plus-lighter
89 imageRendering: "auto
90}
91
92/** Style for the bloom layer canvas. null when off. */
93export function pixelBloomStyle(bloom
94 if (bloom ===
95 const cfg =
96 return {
97 filter: `blur(${cfg
98 opacity: cfg.opacity
99 mixBlendMode: "plus-lighter",
100 imageRendering: "auto",
101 }
102}
103
104/** Whether the OS asks for reduced motion (skip entrances). */
105export function pixelPrefersReducedMotion():
106 return (
107 window.matchMedia?.("
108 )
109}
[
10
]
,
6
]
,
9
]
,
5
]
,
=>
row
.
map
(
(
v
)
=>
(
v
+
0.5
)
/
16
))
(
t
:
number
)
=>
(
t
<
0
?
0
:
t
>
1
?
1
:
t
)
:
string
)
:
number
{
0x811c9dc5
=
0
;
i
<
str
.
length
;
i
++
)
{
charCodeAt
(
i
)
imul
(
h
,
0x01000193
)
0
:
number
)
:
()
=>
number
{
seed
||
0x9e3779b9
{
<<
13
>>>
17
<<
5
0x100000000
=
DitherColor
|
number
:
number
)
:
Rgb
{
((
hue
%
360
)
+
360
)
%
360
0.85
0.58
(
1
-
Math
.
abs
(
2
*
l
-
1
))
*
s
c
*
(
1
-
Math
.
abs
(((
h
/
60
)
%
2
)
-
1
))
l
-
c
/
2
g
,
b
]
=
,
0
]
,
0
]
,
x
]
,
c
]
,
c
]
,
x
]
+
m
)
*
255
)
,
+
m
)
*
255
)
,
+
m
)
*
255
)
,
link
PixelColor
}
to its rgb fill.
*/
:
PixelColor
)
:
Rgb
{
===
"
number
"
?
hueFill
(
color
)
:
PALETTE
[
color
]
.
fill
=
"
off
"
|
"
low
"
|
"
high
"
|
"
aura
"
<
,
"
off
"
>
,
number
;
brightness
:
number
;
opacity
:
number
;
saturate
:
number
}
,
brightness
:
1.35
,
opacity
:
0.7
,
saturate
:
1.4
}
,
,
brightness
:
1.5
,
opacity
:
0.78
,
saturate
:
1.5
}
,
,
brightness
:
2.9
,
opacity
:
0.1
,
saturate
:
3
}
,
=
{
"
"
:
PixelBloom
)
:
PixelBloomStyle
|
null
{
"
off
"
)
return
null
BLOOM_PRESET
[
bloom
]
.
blur
}
px) brightness(
${
cfg
.
brightness
}
) saturate(
${
cfg
.
saturate
}
)
`
,
,
boolean
{
(prefers-reduced-motion: reduce)
"
)
?.
matches
??
false