@kongyo2/cards-css 0.1.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/dist/active-registry.js +10 -0
  4. package/dist/active-registry.js.map +1 -0
  5. package/dist/dom.js +66 -0
  6. package/dist/dom.js.map +1 -0
  7. package/dist/holo-card.js +450 -0
  8. package/dist/holo-card.js.map +1 -0
  9. package/dist/holo-cards.css +601 -0
  10. package/dist/index.js +15 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/math.js +4 -0
  13. package/dist/math.js.map +1 -0
  14. package/dist/orientation.js +64 -0
  15. package/dist/orientation.js.map +1 -0
  16. package/dist/spring.js +123 -0
  17. package/dist/spring.js.map +1 -0
  18. package/dist/subscribers.js +26 -0
  19. package/dist/subscribers.js.map +1 -0
  20. package/dist/textures.js +178 -0
  21. package/dist/textures.js.map +1 -0
  22. package/dist/ticker.js +32 -0
  23. package/dist/ticker.js.map +1 -0
  24. package/dist/types.js +2 -0
  25. package/dist/types.js.map +1 -0
  26. package/dist-types/active-registry.d.ts +6 -0
  27. package/dist-types/active-registry.d.ts.map +1 -0
  28. package/dist-types/dom.d.ts +18 -0
  29. package/dist-types/dom.d.ts.map +1 -0
  30. package/dist-types/holo-card.d.ts +58 -0
  31. package/dist-types/holo-card.d.ts.map +1 -0
  32. package/dist-types/index.d.ts +13 -0
  33. package/dist-types/index.d.ts.map +1 -0
  34. package/dist-types/math.d.ts +4 -0
  35. package/dist-types/math.d.ts.map +1 -0
  36. package/dist-types/orientation.d.ts +13 -0
  37. package/dist-types/orientation.d.ts.map +1 -0
  38. package/dist-types/spring.d.ts +32 -0
  39. package/dist-types/spring.d.ts.map +1 -0
  40. package/dist-types/subscribers.d.ts +10 -0
  41. package/dist-types/subscribers.d.ts.map +1 -0
  42. package/dist-types/textures.d.ts +23 -0
  43. package/dist-types/textures.d.ts.map +1 -0
  44. package/dist-types/ticker.d.ts +7 -0
  45. package/dist-types/ticker.d.ts.map +1 -0
  46. package/dist-types/types.d.ts +21 -0
  47. package/dist-types/types.d.ts.map +1 -0
  48. package/package.json +75 -0
  49. package/src/active-registry.ts +15 -0
  50. package/src/dom.ts +79 -0
  51. package/src/holo-card.ts +525 -0
  52. package/src/index.ts +35 -0
  53. package/src/math.ts +6 -0
  54. package/src/orientation.ts +83 -0
  55. package/src/spring.ts +158 -0
  56. package/src/styles/base.css +262 -0
  57. package/src/styles/effects/cosmos.css +143 -0
  58. package/src/styles/effects/glitter.css +103 -0
  59. package/src/styles/effects/holo.css +127 -0
  60. package/src/styles/effects/reverse.css +55 -0
  61. package/src/styles/index.css +5 -0
  62. package/src/subscribers.ts +30 -0
  63. package/src/textures.ts +310 -0
  64. package/src/ticker.ts +46 -0
  65. package/src/types.ts +22 -0
@@ -0,0 +1,127 @@
1
+ .holo-card[data-effect="holo"] .holo-card__shine {
2
+ --scanlines-space: 1px;
3
+ --scanlines-light: #666;
4
+ --scanlines-dark: black;
5
+
6
+ --bars: 3%;
7
+ --bar-color: hsla(0, 0%, 70%, 1);
8
+ --bar-bg: hsla(0, 0%, 0%, 1);
9
+
10
+ background-image:
11
+ repeating-linear-gradient(
12
+ 110deg,
13
+ var(--violet),
14
+ var(--blue),
15
+ var(--green),
16
+ var(--yellow),
17
+ var(--red),
18
+ var(--violet),
19
+ var(--blue),
20
+ var(--green),
21
+ var(--yellow),
22
+ var(--red),
23
+ var(--violet),
24
+ var(--blue),
25
+ var(--green),
26
+ var(--yellow),
27
+ var(--red)
28
+ ),
29
+ repeating-linear-gradient(
30
+ 90deg,
31
+ var(--scanlines-dark) calc(var(--scanlines-space) * 0),
32
+ var(--scanlines-dark) calc(var(--scanlines-space) * 2),
33
+ var(--scanlines-light) calc(var(--scanlines-space) * 2),
34
+ var(--scanlines-light) calc(var(--scanlines-space) * 4)
35
+ );
36
+
37
+ background-position:
38
+ calc(((50% - var(--background-x)) * 2.6) + 50%) calc(((50% - var(--background-y)) * 3.5) + 50%),
39
+ center center;
40
+
41
+ background-size:
42
+ 400% 400%,
43
+ cover;
44
+
45
+ background-blend-mode: overlay;
46
+ filter: brightness(1.1) contrast(1.1) saturate(1.2);
47
+ mix-blend-mode: color-dodge;
48
+ }
49
+
50
+ .holo-card[data-effect="holo"] .holo-card__shine::before {
51
+ content: "";
52
+
53
+ background-image:
54
+ repeating-linear-gradient(
55
+ 90deg,
56
+ var(--bar-bg) calc(var(--bars) * 2),
57
+ var(--bar-color) calc(var(--bars) * 3),
58
+ var(--bar-bg) calc(var(--bars) * 3.5),
59
+ var(--bar-color) calc(var(--bars) * 4),
60
+ var(--bar-bg) calc(var(--bars) * 5),
61
+ var(--bar-bg) calc(var(--bars) * 14)
62
+ ),
63
+ repeating-linear-gradient(
64
+ 90deg,
65
+ var(--bar-bg) calc(var(--bars) * 2),
66
+ var(--bar-color) calc(var(--bars) * 3),
67
+ var(--bar-bg) calc(var(--bars) * 3.5),
68
+ var(--bar-color) calc(var(--bars) * 4),
69
+ var(--bar-bg) calc(var(--bars) * 5),
70
+ var(--bar-bg) calc(var(--bars) * 10)
71
+ );
72
+
73
+ background-position:
74
+ calc((((50% - var(--background-x)) * 1.65) + 50%) + (var(--background-y) * 0.5)) var(--background-x),
75
+ calc((((50% - var(--background-x)) * -0.9) + 50%) - (var(--background-y) * 0.75)) var(--background-y);
76
+
77
+ background-size:
78
+ 200% 200%,
79
+ 200% 200%;
80
+
81
+ background-blend-mode: screen;
82
+ filter: brightness(1.15) contrast(1.1);
83
+ mix-blend-mode: hard-light;
84
+ }
85
+
86
+ .holo-card[data-effect="holo"] .holo-card__shine::after {
87
+ content: "";
88
+
89
+ background-image: radial-gradient(
90
+ farthest-corner circle at var(--pointer-x) var(--pointer-y),
91
+ hsla(0, 0%, 90%, 0.8) 0%,
92
+ hsla(0, 0%, 78%, 0.1) 25%,
93
+ hsl(0, 0%, 0%) 90%
94
+ );
95
+
96
+ background-position: center center;
97
+ background-size: cover;
98
+
99
+ mix-blend-mode: luminosity;
100
+ filter: brightness(0.6) contrast(4);
101
+ }
102
+
103
+ @media screen and (max-width: 900px) {
104
+ .holo-card[data-effect="holo"] .holo-card__shine {
105
+ --scanlines-space: 0.5px;
106
+ }
107
+ }
108
+
109
+ .holo-card[data-effect="holo"] .holo-card__glare {
110
+ opacity: calc(var(--card-opacity) * 0.8);
111
+ filter: brightness(0.8) contrast(1.5);
112
+ mix-blend-mode: overlay;
113
+ }
114
+
115
+ .holo-card[data-effect="holo"] .holo-card__glare::after {
116
+ content: "";
117
+
118
+ background-image: radial-gradient(
119
+ farthest-corner circle at var(--pointer-x) var(--pointer-y),
120
+ hsl(180, 100%, 95%) 5%,
121
+ hsla(0, 0%, 39%, 0.25) 55%,
122
+ hsla(0, 0%, 0%, 0.36) 110%
123
+ );
124
+
125
+ mix-blend-mode: overlay;
126
+ filter: brightness(0.6) contrast(3);
127
+ }
@@ -0,0 +1,55 @@
1
+ .holo-card[data-effect="reverse"] {
2
+ --foil-brightness: 0.55;
3
+ }
4
+
5
+ .holo-card[data-effect="reverse"] .holo-card__shine {
6
+ background-image:
7
+ radial-gradient(circle at var(--pointer-x) var(--pointer-y), #fff 5%, #000 50%, #fff 80%),
8
+ linear-gradient(-45deg, #000 15%, #fff, #000 85%), var(--foil);
9
+
10
+ background-blend-mode: soft-light, difference;
11
+ background-size:
12
+ 120% 120%,
13
+ 200% 200%,
14
+ cover;
15
+ background-position:
16
+ center center,
17
+ calc(100% * var(--pointer-from-left)) calc(100% * var(--pointer-from-top)),
18
+ center center;
19
+
20
+ filter: brightness(var(--foil-brightness)) contrast(1.5) saturate(1);
21
+ mix-blend-mode: color-dodge;
22
+
23
+ opacity: calc((1.5 * var(--card-opacity)) - var(--pointer-from-center));
24
+ }
25
+
26
+ .holo-card[data-effect="reverse"] .holo-card__glare {
27
+ opacity: var(--card-opacity);
28
+
29
+ background-image: radial-gradient(
30
+ farthest-corner circle at var(--pointer-x) var(--pointer-y),
31
+ hsla(0, 0%, 100%, 0.8) 10%,
32
+ hsla(0, 0%, 100%, 0.5) 20%,
33
+ hsla(0, 0%, 0%, 0.75) 90%
34
+ );
35
+
36
+ filter: brightness(0.7) contrast(1.5);
37
+ }
38
+
39
+ .holo-card[data-effect="reverse"] .holo-card__glare::after {
40
+ content: "";
41
+ opacity: var(--card-opacity);
42
+
43
+ background-image: radial-gradient(
44
+ farthest-corner circle at var(--pointer-x) var(--pointer-y),
45
+ hsl(0, 0%, 100%) 10%,
46
+ hsla(0, 0%, 100%, 0.5) 20%,
47
+ hsla(0, 0%, 0%, 0.5) 120%
48
+ );
49
+
50
+ filter: brightness(1) contrast(1.5);
51
+ }
52
+
53
+ .holo-card[data-effect="reverse"]:not(.holo-card--masked) .holo-card__shine {
54
+ --foil: none;
55
+ }
@@ -0,0 +1,5 @@
1
+ @import "./base.css";
2
+ @import "./effects/holo.css";
3
+ @import "./effects/reverse.css";
4
+ @import "./effects/cosmos.css";
5
+ @import "./effects/glitter.css";
@@ -0,0 +1,30 @@
1
+ export class Subscribers<T> {
2
+ private readonly fns = new Set<(value: T) => void>();
3
+ private readonly getCurrent: () => T;
4
+
5
+ constructor(getCurrent: () => T) {
6
+ this.getCurrent = getCurrent;
7
+ }
8
+
9
+ subscribe(fn: (value: T) => void): () => void {
10
+ this.fns.add(fn);
11
+ fn(this.getCurrent());
12
+ return () => {
13
+ this.fns.delete(fn);
14
+ };
15
+ }
16
+
17
+ emit(value: T): void {
18
+ for (const fn of this.fns) {
19
+ fn(value);
20
+ }
21
+ }
22
+
23
+ get size(): number {
24
+ return this.fns.size;
25
+ }
26
+
27
+ clear(): void {
28
+ this.fns.clear();
29
+ }
30
+ }
@@ -0,0 +1,310 @@
1
+ export interface TextureOptions {
2
+ seed?: number;
3
+ }
4
+
5
+ export interface Textures {
6
+ grain: string;
7
+ glitter: string;
8
+ cosmosBottom: string;
9
+ cosmosMiddle: string;
10
+ cosmosTop: string;
11
+ }
12
+
13
+ export const DEFAULT_TEXTURE_SEED = 0x9e3779b9;
14
+
15
+ const mulberry32 = (seed: number): (() => number) => {
16
+ let a = seed >>> 0;
17
+ return () => {
18
+ a = (a + 0x6d2b79f5) | 0;
19
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
20
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
21
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
22
+ };
23
+ };
24
+
25
+ const svgToDataUri = (svg: string): string => {
26
+ const cleaned = svg
27
+ .replace(/>\s+</g, "><")
28
+ .replace(/\s{2,}/g, " ")
29
+ .trim();
30
+ const encoded = cleaned
31
+ .replace(/%/g, "%25")
32
+ .replace(/</g, "%3C")
33
+ .replace(/>/g, "%3E")
34
+ .replace(/#/g, "%23")
35
+ .replace(/&/g, "%26")
36
+ .replace(/"/g, "'")
37
+ .replace(/\n/g, "%0A");
38
+ return `data:image/svg+xml,${encoded}`;
39
+ };
40
+
41
+ const svgDocument = (width: number, height: number, defs: string, body: string): string =>
42
+ svgToDataUri(
43
+ `<svg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}' viewBox='0 0 ${width} ${height}'>` +
44
+ (defs ? `<defs>${defs}</defs>` : "") +
45
+ body +
46
+ `</svg>`,
47
+ );
48
+
49
+ const pick = <T>(rng: () => number, items: readonly T[]): T => {
50
+ const value = items[Math.floor(rng() * items.length)];
51
+ return value ?? (items[0] as T);
52
+ };
53
+
54
+ const rand = (rng: () => number, min: number, max: number): number => min + rng() * (max - min);
55
+
56
+ const circle = (
57
+ x: number,
58
+ y: number,
59
+ r: number,
60
+ fill: string,
61
+ opacity: number,
62
+ opts: { rDigits?: number; filter?: string } = {},
63
+ ): string => {
64
+ const filter = opts.filter ? ` filter='url(${opts.filter})'` : "";
65
+ return `<circle cx='${x.toFixed(1)}' cy='${y.toFixed(1)}' r='${r.toFixed(opts.rDigits ?? 2)}' fill='${fill}' opacity='${opacity.toFixed(2)}'${filter}/>`;
66
+ };
67
+
68
+ const gaussianBlurFilter = (id: string, std: number, margin: string, size: string): string =>
69
+ `<filter id='${id}' x='${margin}' y='${margin}' width='${size}' height='${size}'><feGaussianBlur stdDeviation='${std}'/></filter>`;
70
+
71
+ const discreteTable = (keep: number, steps = 32): string => {
72
+ const ones = Math.min(steps, Math.max(1, Math.round(steps * keep)));
73
+ const cells: string[] = [];
74
+ for (let i = 0; i < steps - ones; i += 1) {
75
+ cells.push("0");
76
+ }
77
+ for (let i = 0; i < ones; i += 1) {
78
+ cells.push("1");
79
+ }
80
+ return cells.join(" ");
81
+ };
82
+
83
+ interface SpeckleLayer {
84
+ freq: number;
85
+ keep: number;
86
+ color: string;
87
+ opacity: number;
88
+ }
89
+
90
+ const speckleField = (
91
+ idBase: string,
92
+ seed: number,
93
+ layers: readonly SpeckleLayer[],
94
+ stitch = true,
95
+ ): { defs: string; body: string } => {
96
+ let defs = "";
97
+ let body = "";
98
+ const stitchTiles = stitch ? "stitch" : "noStitch";
99
+ layers.forEach((layer, index) => {
100
+ const id = `${idBase}${index}`;
101
+ defs +=
102
+ `<filter id='${id}' x='0%' y='0%' width='100%' height='100%'>` +
103
+ `<feTurbulence type='fractalNoise' baseFrequency='${layer.freq}' numOctaves='1' seed='${(seed + index * 37) % 9973}' stitchTiles='${stitchTiles}' result='n'/>` +
104
+ `<feColorMatrix in='n' type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0' result='a'/>` +
105
+ `<feComponentTransfer in='a' result='m'><feFuncA type='discrete' tableValues='${discreteTable(layer.keep)}'/></feComponentTransfer>` +
106
+ `<feComposite in='SourceGraphic' in2='m' operator='in'/>` +
107
+ `</filter>`;
108
+ body += `<rect width='100%' height='100%' fill='${layer.color}' filter='url(#${id})' opacity='${layer.opacity}'/>`;
109
+ });
110
+ return { defs, body };
111
+ };
112
+
113
+ const clusterBlobs = (
114
+ rng: () => number,
115
+ width: number,
116
+ height: number,
117
+ count: number,
118
+ palette: readonly string[],
119
+ blurId: string,
120
+ radius: [number, number],
121
+ opacity: [number, number],
122
+ ): string => {
123
+ let blobs = "";
124
+ for (let i = 0; i < count; i += 1) {
125
+ blobs += circle(
126
+ rng() * width,
127
+ rng() * height,
128
+ rand(rng, radius[0], radius[1]),
129
+ pick(rng, palette),
130
+ rand(rng, opacity[0], opacity[1]),
131
+ { rDigits: 1 },
132
+ );
133
+ }
134
+ return `<g filter='url(${blurId})'>${blobs}</g>`;
135
+ };
136
+
137
+ const brightStars = (rng: () => number, width: number, height: number, count: number, glowId: string): string => {
138
+ let stars = "";
139
+ for (let i = 0; i < count; i += 1) {
140
+ const x = rng() * width;
141
+ const y = rng() * height;
142
+ const r = rand(rng, 1.1, 2.6);
143
+ stars +=
144
+ circle(x, y, r * 2.6, "#ffffff", 0.14, { rDigits: 1, filter: glowId }) +
145
+ circle(x, y, r, "#ffffff", rand(rng, 0.75, 1));
146
+ }
147
+ return stars;
148
+ };
149
+
150
+ const ringClusters = (rng: () => number, width: number, height: number, count: number, color: string): string => {
151
+ let out = "";
152
+ for (let i = 0; i < count; i += 1) {
153
+ const cx = rng() * width;
154
+ const cy = rng() * height;
155
+ const radius = rand(rng, 14, 30);
156
+ const dots = 26 + Math.floor(rng() * 22);
157
+ for (let k = 0; k < dots; k += 1) {
158
+ const angle = rng() * Math.PI * 2;
159
+ const rr = radius * rand(rng, 0.78, 1.28);
160
+ out += circle(
161
+ cx + Math.cos(angle) * rr,
162
+ cy + Math.sin(angle) * rr * 1.05,
163
+ rand(rng, 0.5, 1.5),
164
+ color,
165
+ rand(rng, 0.4, 0.9),
166
+ );
167
+ }
168
+ }
169
+ return out;
170
+ };
171
+
172
+ const COSMOS_W = 512;
173
+ const COSMOS_H = 716;
174
+
175
+ const cosmosDefs = (extraDefs: string): string =>
176
+ gaussianBlurFilter("blur", 2.6, "-30%", "160%") + gaussianBlurFilter("glow", 2.4, "-200%", "500%") + extraDefs;
177
+
178
+ export const grainTexture = (seed: number): string =>
179
+ svgDocument(
180
+ 200,
181
+ 200,
182
+ `<filter id='grain' x='0%' y='0%' width='100%' height='100%'>` +
183
+ `<feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' seed='${seed % 9973}' stitchTiles='stitch' result='n'/>` +
184
+ `<feColorMatrix in='n' type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0' result='a'/>` +
185
+ `<feComponentTransfer in='a' result='m'><feFuncA type='discrete' tableValues='${discreteTable(0.28)}'/></feComponentTransfer>` +
186
+ `<feFlood flood-color='#ffffff' result='w'/>` +
187
+ `<feComposite in='w' in2='m' operator='in'/>` +
188
+ `</filter>`,
189
+ `<rect width='100%' height='100%' fill='#000000'/>` +
190
+ `<rect width='100%' height='100%' filter='url(#grain)' opacity='0.4'/>`,
191
+ );
192
+
193
+ export const glitterTexture = (seed: number): string => {
194
+ const { defs, body } = speckleField("gl", seed, [
195
+ { freq: 0.82, keep: 0.42, color: "#8f8f8f", opacity: 0.85 },
196
+ { freq: 0.7, keep: 0.22, color: "#dcdcdc", opacity: 1 },
197
+ { freq: 0.6, keep: 0.08, color: "#ffffff", opacity: 1 },
198
+ ]);
199
+ const rng = mulberry32(seed + 99);
200
+ const flares = brightStars(rng, 240, 240, 16, "#glow");
201
+ return svgDocument(
202
+ 240,
203
+ 240,
204
+ `${gaussianBlurFilter("glow", 1.5, "-200%", "500%")}${defs}`,
205
+ `<rect width='100%' height='100%' fill='#050505'/>${body}${flares}`,
206
+ );
207
+ };
208
+
209
+ const cosmosLayer = (
210
+ idBase: string,
211
+ seed: number,
212
+ layers: readonly SpeckleLayer[],
213
+ paint: (rng: () => number, body: string) => string,
214
+ ): string => {
215
+ const rng = mulberry32(seed);
216
+ const { defs, body } = speckleField(idBase, seed, layers, false);
217
+ return svgDocument(COSMOS_W, COSMOS_H, cosmosDefs(defs), paint(rng, body));
218
+ };
219
+
220
+ const cosmosBottom = (seed: number): string =>
221
+ cosmosLayer(
222
+ "csb",
223
+ seed,
224
+ [
225
+ { freq: 0.85, keep: 0.32, color: "#97a3c8", opacity: 0.8 },
226
+ { freq: 0.72, keep: 0.13, color: "#ffffff", opacity: 0.95 },
227
+ { freq: 0.76, keep: 0.05, color: "#9fb6ff", opacity: 0.85 },
228
+ { freq: 0.78, keep: 0.04, color: "#ffc2d8", opacity: 0.8 },
229
+ ],
230
+ (rng, body) => {
231
+ const clusters = clusterBlobs(
232
+ rng,
233
+ COSMOS_W,
234
+ COSMOS_H,
235
+ 26,
236
+ ["#465777", "#6d6088", "#9a7790", "#aab3cc", "#566f9e"],
237
+ "#blur",
238
+ [5, 14],
239
+ [0.18, 0.4],
240
+ );
241
+ const stars = brightStars(rng, COSMOS_W, COSMOS_H, 24, "#glow");
242
+ return `<rect width='100%' height='100%' fill='#04030c'/>${clusters}${body}${stars}`;
243
+ },
244
+ );
245
+
246
+ const cosmosMiddle = (seed: number): string =>
247
+ cosmosLayer(
248
+ "csm",
249
+ seed,
250
+ [
251
+ { freq: 0.66, keep: 0.2, color: "#241a4e", opacity: 0.95 },
252
+ { freq: 0.58, keep: 0.11, color: "#4a2168", opacity: 0.9 },
253
+ { freq: 0.52, keep: 0.055, color: "#7a1f6b", opacity: 0.85 },
254
+ { freq: 0.5, keep: 0.03, color: "#c87a3a", opacity: 0.85 },
255
+ ],
256
+ (rng, body) => {
257
+ const clusters = clusterBlobs(
258
+ rng,
259
+ COSMOS_W,
260
+ COSMOS_H,
261
+ 16,
262
+ ["#241a4e", "#3a2168", "#1a1430"],
263
+ "#blur",
264
+ [7, 18],
265
+ [0.45, 0.8],
266
+ );
267
+ return `${clusters}${body}`;
268
+ },
269
+ );
270
+
271
+ const cosmosTop = (seed: number): string =>
272
+ cosmosLayer(
273
+ "cst",
274
+ seed,
275
+ [
276
+ { freq: 0.62, keep: 0.06, color: "#6a6a76", opacity: 0.85 },
277
+ { freq: 0.52, keep: 0.035, color: "#42424c", opacity: 0.9 },
278
+ ],
279
+ (rng, body) => {
280
+ const rings = ringClusters(rng, COSMOS_W, COSMOS_H, 7, "#54545f");
281
+ return `${body}${rings}`;
282
+ },
283
+ );
284
+
285
+ export const generateTextures = (options: TextureOptions = {}): Textures => {
286
+ const seed = options.seed ?? DEFAULT_TEXTURE_SEED;
287
+ return {
288
+ grain: grainTexture(seed),
289
+ glitter: glitterTexture(seed + 1),
290
+ cosmosBottom: cosmosBottom(seed + 2),
291
+ cosmosMiddle: cosmosMiddle(seed + 3),
292
+ cosmosTop: cosmosTop(seed + 4),
293
+ };
294
+ };
295
+
296
+ export const TEXTURE_VARIABLES = {
297
+ grain: "--hc-grain",
298
+ glitter: "--hc-glitter",
299
+ cosmosBottom: "--hc-cosmos-bottom",
300
+ cosmosMiddle: "--hc-cosmos-middle",
301
+ cosmosTop: "--hc-cosmos-top",
302
+ } as const satisfies Record<keyof Textures, string>;
303
+
304
+ export const texturesToCssVariables = (textures: Textures): Record<string, string> => {
305
+ const vars: Record<string, string> = {};
306
+ for (const key of Object.keys(TEXTURE_VARIABLES) as (keyof Textures)[]) {
307
+ vars[TEXTURE_VARIABLES[key]] = `url("${textures[key]}")`;
308
+ }
309
+ return vars;
310
+ };
package/src/ticker.ts ADDED
@@ -0,0 +1,46 @@
1
+ export const now = (): number => (typeof performance !== "undefined" ? performance.now() : Date.now());
2
+
3
+ const raf: (cb: (time: number) => void) => void =
4
+ typeof requestAnimationFrame !== "undefined"
5
+ ? (cb) => requestAnimationFrame(cb)
6
+ : (cb) => setTimeout(() => cb(now()), 1000 / 60);
7
+
8
+ interface Task {
9
+ c: (time: number) => boolean;
10
+ f: () => void;
11
+ }
12
+
13
+ export interface TaskHandle {
14
+ promise: Promise<void>;
15
+ abort: () => void;
16
+ }
17
+
18
+ const tasks = new Set<Task>();
19
+
20
+ const runTasks = (time: number): void => {
21
+ tasks.forEach((task) => {
22
+ if (!task.c(time)) {
23
+ tasks.delete(task);
24
+ task.f();
25
+ }
26
+ });
27
+ if (tasks.size !== 0) {
28
+ raf(runTasks);
29
+ }
30
+ };
31
+
32
+ export const loop = (callback: (time: number) => boolean): TaskHandle => {
33
+ let task: Task;
34
+ if (tasks.size === 0) {
35
+ raf(runTasks);
36
+ }
37
+ return {
38
+ promise: new Promise<void>((fulfil) => {
39
+ task = { c: callback, f: fulfil };
40
+ tasks.add(task);
41
+ }),
42
+ abort: () => {
43
+ tasks.delete(task);
44
+ },
45
+ };
46
+ };
package/src/types.ts ADDED
@@ -0,0 +1,22 @@
1
+ export type HoloEffect = "none" | "holo" | "reverse" | "cosmos" | "glitter";
2
+
3
+ export interface HoloCardOptions {
4
+ effect?: HoloEffect;
5
+ interactive?: boolean;
6
+ activateOnClick?: boolean;
7
+ gyroscope?: boolean;
8
+ showcase?: boolean;
9
+ glow?: string;
10
+ aspectRatio?: number;
11
+ textureSeed?: number;
12
+ mask?: string;
13
+ foil?: string;
14
+ }
15
+
16
+ export interface CreateHoloCardOptions extends HoloCardOptions {
17
+ image: string;
18
+ imageAlt?: string;
19
+ back?: string;
20
+ backAlt?: string;
21
+ className?: string;
22
+ }