@kongyo2/cards-css 0.1.0 → 0.2.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 +37 -3
- package/dist/dom.js +105 -12
- package/dist/dom.js.map +1 -1
- package/dist/holo-card.js +265 -42
- package/dist/holo-card.js.map +1 -1
- package/dist/holo-cards.css +94 -17
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/spring.js +20 -6
- package/dist/spring.js.map +1 -1
- package/dist-types/dom.d.ts +17 -1
- package/dist-types/dom.d.ts.map +1 -1
- package/dist-types/holo-card.d.ts +33 -2
- package/dist-types/holo-card.d.ts.map +1 -1
- package/dist-types/index.d.ts +3 -3
- package/dist-types/index.d.ts.map +1 -1
- package/dist-types/spring.d.ts +10 -1
- package/dist-types/spring.d.ts.map +1 -1
- package/dist-types/types.d.ts +147 -4
- package/dist-types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/dom.ts +127 -13
- package/src/holo-card.ts +326 -47
- package/src/index.ts +15 -3
- package/src/spring.ts +47 -10
- package/src/styles/base.css +93 -4
- package/src/styles/effects/cosmos.css +9 -6
- package/src/styles/effects/glitter.css +4 -2
- package/src/styles/effects/holo.css +3 -2
- package/src/styles/effects/reverse.css +4 -3
- package/src/types.ts +152 -4
package/src/holo-card.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { adjust, clamp, round } from "./math.js";
|
|
2
|
-
import { Spring, type SpringSetOpts } from "./spring.js";
|
|
3
|
-
import { CLASS } from "./dom.js";
|
|
2
|
+
import { Spring, type SpringSetOpts, type SpringOpts, type SpringDynamics } from "./spring.js";
|
|
3
|
+
import { CLASS, applyVars, buildLayerElement, normalizeMask } from "./dom.js";
|
|
4
4
|
import { getActiveCard, setActiveCard, subscribeActiveCard } from "./active-registry.js";
|
|
5
5
|
import { resetBaseOrientation, subscribeOrientation, type RelativeOrientation } from "./orientation.js";
|
|
6
6
|
import { generateTextures, texturesToCssVariables } from "./textures.js";
|
|
7
|
-
import type { HoloCardOptions } from "./types.js";
|
|
7
|
+
import type { CssVars, HoloCardOptions, HoloLayerOptions, ShowcaseOptions, VisualOptions } from "./types.js";
|
|
8
8
|
|
|
9
9
|
const requestFrame = (cb: () => void): number =>
|
|
10
10
|
typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : setTimeout(cb, 16);
|
|
@@ -21,6 +21,86 @@ const SPRING_INTERACT = { stiffness: 0.066, damping: 0.25 };
|
|
|
21
21
|
const SPRING_POPOVER = { stiffness: 0.033, damping: 0.45 };
|
|
22
22
|
const SNAP_STIFFNESS = 0.01;
|
|
23
23
|
const SNAP_DAMPING = 0.06;
|
|
24
|
+
const DEFAULT_MAX_TILT = 50 / 3.5;
|
|
25
|
+
const DEFAULT_PRECISION = 0.01;
|
|
26
|
+
|
|
27
|
+
interface BaseDynamics {
|
|
28
|
+
stiffness: number;
|
|
29
|
+
damping: number;
|
|
30
|
+
precision?: number;
|
|
31
|
+
axes?: Record<string, SpringDynamics>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface MutableDynamics {
|
|
35
|
+
stiffness: number;
|
|
36
|
+
damping: number;
|
|
37
|
+
precision: number;
|
|
38
|
+
axes: Record<string, SpringDynamics> | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const mergeAxes = (
|
|
42
|
+
base: Record<string, SpringDynamics> | undefined,
|
|
43
|
+
override: Record<string, SpringDynamics> | undefined,
|
|
44
|
+
): Record<string, SpringDynamics> | undefined => {
|
|
45
|
+
if (!base) {
|
|
46
|
+
return override;
|
|
47
|
+
}
|
|
48
|
+
if (!override) {
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
const out: Record<string, SpringDynamics> = { ...base };
|
|
52
|
+
for (const key of Object.keys(override)) {
|
|
53
|
+
out[key] = { ...base[key], ...override[key] };
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const resolveDynamics = (base: BaseDynamics, override?: SpringOpts): BaseDynamics => {
|
|
59
|
+
const out: BaseDynamics = {
|
|
60
|
+
stiffness: override?.stiffness ?? base.stiffness,
|
|
61
|
+
damping: override?.damping ?? base.damping,
|
|
62
|
+
};
|
|
63
|
+
const precision = override?.precision ?? base.precision;
|
|
64
|
+
if (precision !== undefined) {
|
|
65
|
+
out.precision = precision;
|
|
66
|
+
}
|
|
67
|
+
const axes = mergeAxes(base.axes, override?.axes);
|
|
68
|
+
if (axes !== undefined) {
|
|
69
|
+
out.axes = axes;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const assignDynamics = (spring: MutableDynamics, dyn: BaseDynamics): void => {
|
|
75
|
+
spring.stiffness = dyn.stiffness;
|
|
76
|
+
spring.damping = dyn.damping;
|
|
77
|
+
spring.precision = dyn.precision ?? DEFAULT_PRECISION;
|
|
78
|
+
spring.axes = dyn.axes;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const cssDimension = (value: string | number, unit: string): string =>
|
|
82
|
+
typeof value === "number" ? `${value}${unit}` : value;
|
|
83
|
+
|
|
84
|
+
interface ResolvedShowcase {
|
|
85
|
+
delay: number;
|
|
86
|
+
duration: number;
|
|
87
|
+
loop: boolean;
|
|
88
|
+
speed: number;
|
|
89
|
+
intensity: number;
|
|
90
|
+
dynamics: BaseDynamics;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const resolveShowcase = (showcase: boolean | ShowcaseOptions | undefined): ResolvedShowcase => {
|
|
94
|
+
const opts: ShowcaseOptions = typeof showcase === "object" ? showcase : {};
|
|
95
|
+
return {
|
|
96
|
+
delay: opts.delay ?? 2000,
|
|
97
|
+
duration: opts.duration ?? 4000,
|
|
98
|
+
loop: opts.loop ?? false,
|
|
99
|
+
speed: opts.speed ?? 0.05,
|
|
100
|
+
intensity: opts.intensity ?? 25,
|
|
101
|
+
dynamics: resolveDynamics({ stiffness: 0.02, damping: 0.5 }, opts.spring),
|
|
102
|
+
};
|
|
103
|
+
};
|
|
24
104
|
|
|
25
105
|
interface Vec2 {
|
|
26
106
|
x: number;
|
|
@@ -36,16 +116,34 @@ export class HoloCard {
|
|
|
36
116
|
readonly element: HTMLElement;
|
|
37
117
|
|
|
38
118
|
private readonly rotator: HTMLElement;
|
|
119
|
+
private frontElement: HTMLElement | null;
|
|
120
|
+
private layersElement: HTMLElement | null = null;
|
|
39
121
|
private readonly options: Required<
|
|
40
122
|
Pick<HoloCardOptions, "interactive" | "activateOnClick" | "gyroscope" | "showcase">
|
|
41
123
|
>;
|
|
42
124
|
|
|
43
|
-
private readonly springRotate
|
|
44
|
-
private readonly springGlare
|
|
45
|
-
private readonly springBackground
|
|
46
|
-
private readonly
|
|
47
|
-
private readonly
|
|
48
|
-
private readonly
|
|
125
|
+
private readonly springRotate: Spring<Vec2>;
|
|
126
|
+
private readonly springGlare: Spring<Glare>;
|
|
127
|
+
private readonly springBackground: Spring<Vec2>;
|
|
128
|
+
private readonly springPointer: Spring<Vec2>;
|
|
129
|
+
private readonly springRotateDelta: Spring<Vec2>;
|
|
130
|
+
private readonly springTranslate: Spring<Vec2>;
|
|
131
|
+
private readonly springScale: Spring<number>;
|
|
132
|
+
|
|
133
|
+
private readonly liveRotate: BaseDynamics;
|
|
134
|
+
private readonly liveGlare: BaseDynamics;
|
|
135
|
+
private readonly liveBackground: BaseDynamics;
|
|
136
|
+
private readonly livePointer: BaseDynamics;
|
|
137
|
+
private readonly snapDynamics: BaseDynamics;
|
|
138
|
+
|
|
139
|
+
private readonly tiltFactorX: number;
|
|
140
|
+
private readonly tiltFactorY: number;
|
|
141
|
+
private readonly tiltScaleX: number;
|
|
142
|
+
private readonly tiltScaleY: number;
|
|
143
|
+
private readonly parallax: number;
|
|
144
|
+
private readonly glareRange: number;
|
|
145
|
+
private readonly returnDelay: number;
|
|
146
|
+
private readonly showcaseConfig: ResolvedShowcase;
|
|
49
147
|
|
|
50
148
|
private isInteracting = false;
|
|
51
149
|
private firstPop = true;
|
|
@@ -54,7 +152,7 @@ export class HoloCard {
|
|
|
54
152
|
|
|
55
153
|
private renderScheduled = false;
|
|
56
154
|
private interactRaf: number | null = null;
|
|
57
|
-
private pendingUpdate: { background: Vec2; rotate: Vec2; glare: Glare } | null = null;
|
|
155
|
+
private pendingUpdate: { background: Vec2; rotate: Vec2; glare: Glare; pointer: Vec2 } | null = null;
|
|
58
156
|
|
|
59
157
|
private repositionTimer: ReturnType<typeof setTimeout> | null = null;
|
|
60
158
|
private endTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -73,6 +171,8 @@ export class HoloCard {
|
|
|
73
171
|
throw new Error("@kongyo2/cards-css: holo card element is missing its .holo-card__rotator child.");
|
|
74
172
|
}
|
|
75
173
|
this.rotator = rotator;
|
|
174
|
+
this.frontElement = element.querySelector<HTMLElement>(`.${CLASS.front}`);
|
|
175
|
+
this.layersElement = element.querySelector<HTMLElement>(`.${CLASS.layers}`);
|
|
76
176
|
|
|
77
177
|
this.options = {
|
|
78
178
|
interactive: options.interactive ?? true,
|
|
@@ -80,7 +180,37 @@ export class HoloCard {
|
|
|
80
180
|
gyroscope: options.gyroscope ?? true,
|
|
81
181
|
showcase: options.showcase ?? false,
|
|
82
182
|
};
|
|
83
|
-
this.showcaseRunning =
|
|
183
|
+
this.showcaseRunning = Boolean(options.showcase);
|
|
184
|
+
this.showcaseConfig = resolveShowcase(options.showcase);
|
|
185
|
+
|
|
186
|
+
const physics = options.physics ?? {};
|
|
187
|
+
this.tiltFactorX = (physics.maxTiltX ?? physics.maxTilt ?? DEFAULT_MAX_TILT) / 50;
|
|
188
|
+
this.tiltFactorY = (physics.maxTiltY ?? physics.maxTilt ?? DEFAULT_MAX_TILT) / 50;
|
|
189
|
+
this.tiltScaleX = (physics.maxTiltX ?? physics.maxTilt ?? DEFAULT_MAX_TILT) / DEFAULT_MAX_TILT;
|
|
190
|
+
this.tiltScaleY = (physics.maxTiltY ?? physics.maxTilt ?? DEFAULT_MAX_TILT) / DEFAULT_MAX_TILT;
|
|
191
|
+
this.parallax = physics.parallax ?? 1;
|
|
192
|
+
this.glareRange = physics.glareRange ?? 1;
|
|
193
|
+
this.returnDelay = physics.returnDelay ?? 500;
|
|
194
|
+
|
|
195
|
+
const interactBase = resolveDynamics(SPRING_INTERACT, physics.interactSpring);
|
|
196
|
+
const popoverBase = resolveDynamics(SPRING_POPOVER, physics.popoverSpring);
|
|
197
|
+
this.snapDynamics = resolveDynamics({ stiffness: SNAP_STIFFNESS, damping: SNAP_DAMPING }, physics.snapSpring);
|
|
198
|
+
|
|
199
|
+
this.liveRotate = resolveDynamics(interactBase, physics.springs?.rotate);
|
|
200
|
+
this.liveGlare = resolveDynamics(interactBase, physics.springs?.glare);
|
|
201
|
+
this.liveBackground = resolveDynamics(interactBase, physics.springs?.background);
|
|
202
|
+
this.livePointer = interactBase;
|
|
203
|
+
|
|
204
|
+
this.springRotate = new Spring<Vec2>({ x: 0, y: 0 }, this.liveRotate);
|
|
205
|
+
this.springGlare = new Spring<Glare>({ x: 50, y: 50, o: 0 }, this.liveGlare);
|
|
206
|
+
this.springBackground = new Spring<Vec2>({ x: 50, y: 50 }, this.liveBackground);
|
|
207
|
+
this.springPointer = new Spring<Vec2>({ x: 50, y: 50 }, this.livePointer);
|
|
208
|
+
this.springRotateDelta = new Spring<Vec2>(
|
|
209
|
+
{ x: 0, y: 0 },
|
|
210
|
+
resolveDynamics(popoverBase, physics.springs?.rotateDelta),
|
|
211
|
+
);
|
|
212
|
+
this.springTranslate = new Spring<Vec2>({ x: 0, y: 0 }, resolveDynamics(popoverBase, physics.springs?.translate));
|
|
213
|
+
this.springScale = new Spring<number>(1, resolveDynamics(popoverBase, physics.springs?.scale));
|
|
84
214
|
|
|
85
215
|
if (options.effect) {
|
|
86
216
|
element.dataset.effect = options.effect;
|
|
@@ -93,13 +223,35 @@ export class HoloCard {
|
|
|
93
223
|
if (typeof options.aspectRatio === "number") {
|
|
94
224
|
element.style.setProperty("--card-aspect", String(options.aspectRatio));
|
|
95
225
|
}
|
|
96
|
-
|
|
97
|
-
|
|
226
|
+
|
|
227
|
+
const mask = normalizeMask(options.mask);
|
|
228
|
+
if (mask?.image) {
|
|
229
|
+
element.style.setProperty("--mask", `url(${mask.image})`);
|
|
230
|
+
if (mask.size) {
|
|
231
|
+
element.style.setProperty("--mask-size", mask.size);
|
|
232
|
+
}
|
|
233
|
+
if (mask.position) {
|
|
234
|
+
element.style.setProperty("--mask-position", mask.position);
|
|
235
|
+
}
|
|
236
|
+
if (mask.repeat) {
|
|
237
|
+
element.style.setProperty("--mask-repeat", mask.repeat);
|
|
238
|
+
}
|
|
98
239
|
element.classList.add(CLASS.masked);
|
|
240
|
+
if (mask.mode === "card") {
|
|
241
|
+
element.classList.add(CLASS.maskCard);
|
|
242
|
+
}
|
|
99
243
|
}
|
|
100
244
|
if (options.foil) {
|
|
101
245
|
element.style.setProperty("--foil", `url(${options.foil})`);
|
|
102
246
|
}
|
|
247
|
+
this.applyVisual(options.visual);
|
|
248
|
+
applyVars(element, options.vars);
|
|
249
|
+
|
|
250
|
+
if (!this.layersElement && options.layers?.length && this.frontElement) {
|
|
251
|
+
for (const layer of options.layers) {
|
|
252
|
+
this.addLayer(layer);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
103
255
|
|
|
104
256
|
this.applyStaticStyles(options.textureSeed);
|
|
105
257
|
|
|
@@ -107,6 +259,7 @@ export class HoloCard {
|
|
|
107
259
|
this.springRotate,
|
|
108
260
|
this.springGlare,
|
|
109
261
|
this.springBackground,
|
|
262
|
+
this.springPointer,
|
|
110
263
|
this.springRotateDelta,
|
|
111
264
|
this.springTranslate,
|
|
112
265
|
this.springScale,
|
|
@@ -133,6 +286,35 @@ export class HoloCard {
|
|
|
133
286
|
}
|
|
134
287
|
}
|
|
135
288
|
|
|
289
|
+
private applyVisual(visual: VisualOptions | undefined): void {
|
|
290
|
+
if (!visual) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const style = this.element.style;
|
|
294
|
+
const setNumber = (property: string, value: number | undefined): void => {
|
|
295
|
+
if (typeof value === "number") {
|
|
296
|
+
style.setProperty(property, String(value));
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
setNumber("--hc-brightness", visual.brightness);
|
|
300
|
+
setNumber("--hc-contrast", visual.contrast);
|
|
301
|
+
setNumber("--hc-saturate", visual.saturate);
|
|
302
|
+
setNumber("--hc-glare-opacity", visual.glareOpacity);
|
|
303
|
+
setNumber("--hc-shine-opacity", visual.shineOpacity);
|
|
304
|
+
if (visual.lineSpace !== undefined) {
|
|
305
|
+
style.setProperty("--space", cssDimension(visual.lineSpace, "%"));
|
|
306
|
+
}
|
|
307
|
+
if (visual.lineAngle !== undefined) {
|
|
308
|
+
style.setProperty("--angle", cssDimension(visual.lineAngle, "deg"));
|
|
309
|
+
}
|
|
310
|
+
if (visual.glitterSize !== undefined) {
|
|
311
|
+
style.setProperty("--glittersize", cssDimension(visual.glitterSize, "%"));
|
|
312
|
+
}
|
|
313
|
+
if (visual.imageFit !== undefined) {
|
|
314
|
+
style.setProperty("--imgsize", visual.imageFit);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
136
318
|
private applyStaticStyles(seed: number | undefined): void {
|
|
137
319
|
const seedX = Math.random();
|
|
138
320
|
const seedY = Math.random();
|
|
@@ -162,12 +344,25 @@ export class HoloCard {
|
|
|
162
344
|
|
|
163
345
|
if (this.options.activateOnClick) {
|
|
164
346
|
const onClick = (): void => this.toggleActive();
|
|
165
|
-
const
|
|
347
|
+
const onFocusOut = (event: FocusEvent): void => {
|
|
348
|
+
const next = event.relatedTarget;
|
|
349
|
+
if (next instanceof Node && this.element.contains(next)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.deactivate();
|
|
353
|
+
};
|
|
166
354
|
this.rotator.addEventListener("click", onClick);
|
|
167
|
-
this.rotator.addEventListener("
|
|
355
|
+
this.rotator.addEventListener("focusout", onFocusOut);
|
|
168
356
|
this.rotator.tabIndex = this.rotator.tabIndex >= 0 ? this.rotator.tabIndex : 0;
|
|
169
357
|
this.cleanups.push(() => this.rotator.removeEventListener("click", onClick));
|
|
170
|
-
this.cleanups.push(() => this.rotator.removeEventListener("
|
|
358
|
+
this.cleanups.push(() => this.rotator.removeEventListener("focusout", onFocusOut));
|
|
359
|
+
|
|
360
|
+
const interactiveOverlay = this.element.querySelector<HTMLElement>(`.${CLASS.overlayInteractive}`);
|
|
361
|
+
if (interactiveOverlay) {
|
|
362
|
+
const stopClick = (event: Event): void => event.stopPropagation();
|
|
363
|
+
interactiveOverlay.addEventListener("click", stopClick);
|
|
364
|
+
this.cleanups.push(() => interactiveOverlay.removeEventListener("click", stopClick));
|
|
365
|
+
}
|
|
171
366
|
|
|
172
367
|
const onScroll = (): void => this.reposition();
|
|
173
368
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
@@ -177,6 +372,14 @@ export class HoloCard {
|
|
|
177
372
|
}
|
|
178
373
|
}
|
|
179
374
|
|
|
375
|
+
private parallaxBackground(x: number, y: number): Vec2 {
|
|
376
|
+
return { x: round(50 + (x - 50) * this.parallax), y: round(50 + (y - 50) * this.parallax) };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private rangeGlare(x: number, y: number, o: number): Glare {
|
|
380
|
+
return { x: round(50 + (x - 50) * this.glareRange), y: round(50 + (y - 50) * this.glareRange), o };
|
|
381
|
+
}
|
|
382
|
+
|
|
180
383
|
private interact(event: PointerEvent): void {
|
|
181
384
|
this.endShowcase();
|
|
182
385
|
|
|
@@ -206,15 +409,17 @@ export class HoloCard {
|
|
|
206
409
|
const center = { x: percent.x - 50, y: percent.y - 50 };
|
|
207
410
|
|
|
208
411
|
this.pendingUpdate = {
|
|
209
|
-
background:
|
|
210
|
-
rotate: { x: round(-(center.x
|
|
211
|
-
glare:
|
|
412
|
+
background: this.parallaxBackground(adjust(percent.x, 0, 100, 37, 63), adjust(percent.y, 0, 100, 33, 67)),
|
|
413
|
+
rotate: { x: round(-(center.x * this.tiltFactorX)), y: round(center.y * this.tiltFactorY) },
|
|
414
|
+
glare: this.rangeGlare(round(percent.x), round(percent.y), 1),
|
|
415
|
+
pointer: { x: round(percent.x), y: round(percent.y) },
|
|
212
416
|
};
|
|
213
417
|
|
|
214
418
|
if (this.interactRaf === null) {
|
|
215
419
|
this.interactRaf = requestFrame(() => {
|
|
216
420
|
if (this.pendingUpdate) {
|
|
217
|
-
|
|
421
|
+
const update = this.pendingUpdate;
|
|
422
|
+
this.updateSprings(update.background, update.rotate, update.glare, update.pointer);
|
|
218
423
|
this.pendingUpdate = null;
|
|
219
424
|
}
|
|
220
425
|
this.interactRaf = null;
|
|
@@ -222,7 +427,7 @@ export class HoloCard {
|
|
|
222
427
|
}
|
|
223
428
|
}
|
|
224
429
|
|
|
225
|
-
private interactEnd(delay =
|
|
430
|
+
private interactEnd(delay = this.returnDelay): void {
|
|
226
431
|
if (this.interactRaf !== null) {
|
|
227
432
|
cancelFrame(this.interactRaf);
|
|
228
433
|
this.interactRaf = null;
|
|
@@ -234,18 +439,26 @@ export class HoloCard {
|
|
|
234
439
|
}
|
|
235
440
|
this.endTimer = setTimeout(() => {
|
|
236
441
|
this.setInteracting(false);
|
|
237
|
-
this.
|
|
442
|
+
this.setGroupDynamics(this.snapDynamics);
|
|
238
443
|
void this.springRotate.set({ x: 0, y: 0 }, { soft: 1 });
|
|
239
444
|
void this.springGlare.set({ x: 50, y: 50, o: 0 }, { soft: 1 });
|
|
240
445
|
void this.springBackground.set({ x: 50, y: 50 }, { soft: 1 });
|
|
446
|
+
void this.springPointer.set({ x: 50, y: 50 }, { soft: 1 });
|
|
241
447
|
}, delay);
|
|
242
448
|
}
|
|
243
449
|
|
|
244
|
-
private
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
450
|
+
private setGroupDynamics(dyn: BaseDynamics): void {
|
|
451
|
+
assignDynamics(this.springRotate, dyn);
|
|
452
|
+
assignDynamics(this.springGlare, dyn);
|
|
453
|
+
assignDynamics(this.springBackground, dyn);
|
|
454
|
+
assignDynamics(this.springPointer, dyn);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private applyLiveDynamics(): void {
|
|
458
|
+
assignDynamics(this.springRotate, this.liveRotate);
|
|
459
|
+
assignDynamics(this.springGlare, this.liveGlare);
|
|
460
|
+
assignDynamics(this.springBackground, this.liveBackground);
|
|
461
|
+
assignDynamics(this.springPointer, this.livePointer);
|
|
249
462
|
}
|
|
250
463
|
|
|
251
464
|
private settle(opts: SpringSetOpts): void {
|
|
@@ -254,11 +467,12 @@ export class HoloCard {
|
|
|
254
467
|
void this.springRotateDelta.set({ x: 0, y: 0 }, opts);
|
|
255
468
|
}
|
|
256
469
|
|
|
257
|
-
private updateSprings(background: Vec2, rotate: Vec2, glare: Glare): void {
|
|
258
|
-
this.
|
|
470
|
+
private updateSprings(background: Vec2, rotate: Vec2, glare: Glare, pointer: Vec2): void {
|
|
471
|
+
this.applyLiveDynamics();
|
|
259
472
|
void this.springBackground.set(background);
|
|
260
473
|
void this.springRotate.set(rotate);
|
|
261
474
|
void this.springGlare.set(glare);
|
|
475
|
+
void this.springPointer.set(pointer);
|
|
262
476
|
}
|
|
263
477
|
|
|
264
478
|
private setInteracting(value: boolean): void {
|
|
@@ -286,6 +500,7 @@ export class HoloCard {
|
|
|
286
500
|
const rotate = this.springRotate.current;
|
|
287
501
|
const rotateDelta = this.springRotateDelta.current;
|
|
288
502
|
const background = this.springBackground.current;
|
|
503
|
+
const pointer = this.springPointer.current;
|
|
289
504
|
const translate = this.springTranslate.current;
|
|
290
505
|
const scale = this.springScale.current;
|
|
291
506
|
|
|
@@ -297,9 +512,13 @@ export class HoloCard {
|
|
|
297
512
|
style.setProperty("--pointer-from-center", String(fromCenter));
|
|
298
513
|
style.setProperty("--pointer-from-top", String(glare.y / 100));
|
|
299
514
|
style.setProperty("--pointer-from-left", String(glare.x / 100));
|
|
515
|
+
style.setProperty("--pointer-dx", String(round((pointer.x - 50) / 50)));
|
|
516
|
+
style.setProperty("--pointer-dy", String(round((pointer.y - 50) / 50)));
|
|
300
517
|
style.setProperty("--card-opacity", String(glare.o));
|
|
301
518
|
style.setProperty("--rotate-x", `${rotate.x + rotateDelta.x}deg`);
|
|
302
519
|
style.setProperty("--rotate-y", `${rotate.y + rotateDelta.y}deg`);
|
|
520
|
+
style.setProperty("--tilt-x", String(round(rotate.x + rotateDelta.x)));
|
|
521
|
+
style.setProperty("--tilt-y", String(round(rotate.y + rotateDelta.y)));
|
|
303
522
|
style.setProperty("--background-x", `${background.x}%`);
|
|
304
523
|
style.setProperty("--background-y", `${background.y}%`);
|
|
305
524
|
style.setProperty("--card-scale", String(scale));
|
|
@@ -311,12 +530,14 @@ export class HoloCard {
|
|
|
311
530
|
if (getActiveCard() === this) {
|
|
312
531
|
this.popover();
|
|
313
532
|
this.element.classList.add(CLASS.active);
|
|
533
|
+
this.element.style.setProperty("--card-active", "1");
|
|
314
534
|
if (this.options.gyroscope) {
|
|
315
535
|
this.startGyroscope();
|
|
316
536
|
}
|
|
317
537
|
} else {
|
|
318
538
|
this.retreat();
|
|
319
539
|
this.element.classList.remove(CLASS.active);
|
|
540
|
+
this.element.style.setProperty("--card-active", "0");
|
|
320
541
|
this.stopGyroscope();
|
|
321
542
|
}
|
|
322
543
|
}
|
|
@@ -391,11 +612,17 @@ export class HoloCard {
|
|
|
391
612
|
x: clamp(orientation.relative.gamma, -limit.x, limit.x),
|
|
392
613
|
y: clamp(orientation.relative.beta, -limit.y, limit.y),
|
|
393
614
|
};
|
|
615
|
+
const gx = adjust(degrees.x, -limit.x, limit.x, 0, 100);
|
|
616
|
+
const gy = adjust(degrees.y, -limit.y, limit.y, 0, 100);
|
|
394
617
|
this.setInteracting(true);
|
|
395
618
|
this.updateSprings(
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
619
|
+
this.parallaxBackground(
|
|
620
|
+
adjust(degrees.x, -limit.x, limit.x, 37, 63),
|
|
621
|
+
adjust(degrees.y, -limit.y, limit.y, 33, 67),
|
|
622
|
+
),
|
|
623
|
+
{ x: round(degrees.x * -1 * this.tiltScaleX), y: round(degrees.y * this.tiltScaleY) },
|
|
624
|
+
this.rangeGlare(gx, gy, 1),
|
|
625
|
+
{ x: gx, y: gy },
|
|
399
626
|
);
|
|
400
627
|
}
|
|
401
628
|
|
|
@@ -409,30 +636,44 @@ export class HoloCard {
|
|
|
409
636
|
if (!this.isVisible) {
|
|
410
637
|
return;
|
|
411
638
|
}
|
|
412
|
-
const
|
|
413
|
-
const
|
|
639
|
+
const config = this.showcaseConfig;
|
|
640
|
+
const amp = config.intensity;
|
|
414
641
|
let r = 0;
|
|
415
642
|
this.showcaseStart = setTimeout(() => {
|
|
643
|
+
if (this.endTimer) {
|
|
644
|
+
clearTimeout(this.endTimer);
|
|
645
|
+
this.endTimer = null;
|
|
646
|
+
}
|
|
416
647
|
this.setInteracting(true);
|
|
417
|
-
this.
|
|
648
|
+
this.setGroupDynamics(config.dynamics);
|
|
418
649
|
if (!this.isVisible) {
|
|
419
650
|
this.setInteracting(false);
|
|
420
651
|
return;
|
|
421
652
|
}
|
|
422
653
|
this.showcaseInterval = setInterval(() => {
|
|
423
|
-
r +=
|
|
424
|
-
void this.springRotate.set({ x: Math.sin(r) *
|
|
425
|
-
void this.springGlare.set({
|
|
426
|
-
|
|
654
|
+
r += config.speed;
|
|
655
|
+
void this.springRotate.set({ x: Math.sin(r) * amp, y: Math.cos(r) * amp });
|
|
656
|
+
void this.springGlare.set({
|
|
657
|
+
x: 50 + Math.sin(r) * amp * 2.2,
|
|
658
|
+
y: 50 + Math.cos(r) * amp * 2.2,
|
|
659
|
+
o: 0.8,
|
|
660
|
+
});
|
|
661
|
+
void this.springBackground.set({
|
|
662
|
+
x: 50 + Math.sin(r) * amp * 0.8,
|
|
663
|
+
y: 50 + Math.cos(r) * amp * 0.8,
|
|
664
|
+
});
|
|
665
|
+
void this.springPointer.set({ x: 50 + Math.sin(r) * amp * 1.6, y: 50 + Math.cos(r) * amp * 1.6 });
|
|
427
666
|
}, 20);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
667
|
+
if (!config.loop) {
|
|
668
|
+
this.showcaseEnd = setTimeout(() => {
|
|
669
|
+
if (this.showcaseInterval) {
|
|
670
|
+
clearInterval(this.showcaseInterval);
|
|
671
|
+
this.showcaseInterval = null;
|
|
672
|
+
}
|
|
673
|
+
this.interactEnd(0);
|
|
674
|
+
}, config.duration);
|
|
675
|
+
}
|
|
676
|
+
}, config.delay);
|
|
436
677
|
}
|
|
437
678
|
|
|
438
679
|
private endShowcase(): void {
|
|
@@ -483,6 +724,43 @@ export class HoloCard {
|
|
|
483
724
|
this.element.dataset.effect = effect ?? "none";
|
|
484
725
|
}
|
|
485
726
|
|
|
727
|
+
/** The `.holo-card__front` element, for appending custom content at runtime. */
|
|
728
|
+
get front(): HTMLElement | null {
|
|
729
|
+
return this.frontElement;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** Apply CSS custom properties to the root element (for content linkage). */
|
|
733
|
+
setVars(vars: CssVars): void {
|
|
734
|
+
applyVars(this.element, vars);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Update fine-grained visual controls at runtime. */
|
|
738
|
+
setVisual(visual: VisualOptions): void {
|
|
739
|
+
this.applyVisual(visual);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Insert an extra layer between the artwork and the foil at runtime, returning
|
|
744
|
+
* the created element. Requires the card to have a `.holo-card__front`.
|
|
745
|
+
*/
|
|
746
|
+
addLayer(layer: HoloLayerOptions): HTMLElement {
|
|
747
|
+
const front = this.frontElement;
|
|
748
|
+
if (!front) {
|
|
749
|
+
throw new Error("@kongyo2/cards-css: cannot add a layer — the card has no .holo-card__front element.");
|
|
750
|
+
}
|
|
751
|
+
const doc = front.ownerDocument;
|
|
752
|
+
const element = buildLayerElement(doc, layer);
|
|
753
|
+
if (!this.layersElement) {
|
|
754
|
+
const container = doc.createElement("div");
|
|
755
|
+
container.className = CLASS.layers;
|
|
756
|
+
const shine = front.querySelector(`.${CLASS.shine}`);
|
|
757
|
+
front.insertBefore(container, shine);
|
|
758
|
+
this.layersElement = container;
|
|
759
|
+
}
|
|
760
|
+
this.layersElement.appendChild(element);
|
|
761
|
+
return element;
|
|
762
|
+
}
|
|
763
|
+
|
|
486
764
|
get active(): boolean {
|
|
487
765
|
return getActiveCard() === this;
|
|
488
766
|
}
|
|
@@ -514,6 +792,7 @@ export class HoloCard {
|
|
|
514
792
|
this.springRotate,
|
|
515
793
|
this.springGlare,
|
|
516
794
|
this.springBackground,
|
|
795
|
+
this.springPointer,
|
|
517
796
|
this.springRotateDelta,
|
|
518
797
|
this.springTranslate,
|
|
519
798
|
this.springScale,
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { buildHoloCardElement } from "./dom.js";
|
|
|
3
3
|
import type { CreateHoloCardOptions, HoloCardOptions } from "./types.js";
|
|
4
4
|
|
|
5
5
|
export { HoloCard } from "./holo-card.js";
|
|
6
|
-
export { buildHoloCardElement, CLASS } from "./dom.js";
|
|
6
|
+
export { buildHoloCardElement, buildLayerElement, applyVars, normalizeMask, CLASS, type ResolvedMask } from "./dom.js";
|
|
7
7
|
export {
|
|
8
8
|
generateTextures,
|
|
9
9
|
texturesToCssVariables,
|
|
@@ -22,9 +22,21 @@ export {
|
|
|
22
22
|
type RelativeOrientation,
|
|
23
23
|
} from "./orientation.js";
|
|
24
24
|
export { getActiveCard, setActiveCard, subscribeActiveCard } from "./active-registry.js";
|
|
25
|
-
export { Spring, type SpringValue, type SpringOpts, type SpringSetOpts } from "./spring.js";
|
|
25
|
+
export { Spring, type SpringValue, type SpringOpts, type SpringSetOpts, type SpringDynamics } from "./spring.js";
|
|
26
26
|
export { round, clamp, adjust } from "./math.js";
|
|
27
|
-
export type {
|
|
27
|
+
export type {
|
|
28
|
+
HoloEffect,
|
|
29
|
+
HoloCardOptions,
|
|
30
|
+
CreateHoloCardOptions,
|
|
31
|
+
HoloContent,
|
|
32
|
+
CssVars,
|
|
33
|
+
SpringTuning,
|
|
34
|
+
PhysicsOptions,
|
|
35
|
+
ShowcaseOptions,
|
|
36
|
+
VisualOptions,
|
|
37
|
+
MaskOptions,
|
|
38
|
+
HoloLayerOptions,
|
|
39
|
+
} from "./types.js";
|
|
28
40
|
|
|
29
41
|
export const createHoloCard = (options: CreateHoloCardOptions): HoloCard => {
|
|
30
42
|
const element = buildHoloCardElement(options);
|