@kongyo2/cards-css 0.1.1 → 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/src/spring.ts CHANGED
@@ -3,43 +3,77 @@ import { Subscribers } from "./subscribers.js";
3
3
 
4
4
  export type SpringValue = number | Record<string, number>;
5
5
 
6
- export interface SpringOpts {
6
+ export interface SpringDynamics {
7
7
  stiffness?: number;
8
8
  damping?: number;
9
9
  precision?: number;
10
10
  }
11
11
 
12
+ export interface SpringOpts extends SpringDynamics {
13
+ /**
14
+ * Per-key dynamics overrides for object springs. Each key (e.g. `x`, `y`, `o`)
15
+ * may carry its own stiffness/damping/precision, giving asymmetric, independent
16
+ * motion per axis. Keys without an entry fall back to the base dynamics.
17
+ */
18
+ axes?: Record<string, SpringDynamics>;
19
+ }
20
+
12
21
  export interface SpringSetOpts {
13
22
  hard?: boolean;
14
23
  soft?: boolean | number;
15
24
  }
16
25
 
17
- interface TickContext {
26
+ interface FrameContext {
18
27
  invMass: number;
19
28
  stiffness: number;
20
29
  damping: number;
21
30
  precision: number;
31
+ axes: Record<string, SpringDynamics> | undefined;
22
32
  settled: boolean;
23
33
  dt: number;
24
34
  }
25
35
 
26
- const tickScalar = (ctx: TickContext, lastValue: number, currentValue: number, targetValue: number): number => {
36
+ interface AxisDynamics {
37
+ stiffness: number;
38
+ damping: number;
39
+ precision: number;
40
+ }
41
+
42
+ const tickScalar = (
43
+ ctx: FrameContext,
44
+ axis: AxisDynamics,
45
+ lastValue: number,
46
+ currentValue: number,
47
+ targetValue: number,
48
+ ): number => {
27
49
  const delta = targetValue - currentValue;
28
50
  const velocity = (currentValue - lastValue) / (ctx.dt || 1 / 60);
29
- const spring = ctx.stiffness * delta;
30
- const damper = ctx.damping * velocity;
51
+ const spring = axis.stiffness * delta;
52
+ const damper = axis.damping * velocity;
31
53
  const acceleration = (spring - damper) * ctx.invMass;
32
54
  const d = (velocity + acceleration) * ctx.dt;
33
- if (Math.abs(d) < ctx.precision && Math.abs(delta) < ctx.precision) {
55
+ if (Math.abs(d) < axis.precision && Math.abs(delta) < axis.precision) {
34
56
  return targetValue;
35
57
  }
36
58
  ctx.settled = false;
37
59
  return currentValue + d;
38
60
  };
39
61
 
40
- const tick = <T extends SpringValue>(ctx: TickContext, last: T, current: T, target: T): T => {
62
+ const resolveAxis = (ctx: FrameContext, key: string): AxisDynamics => {
63
+ const override = ctx.axes?.[key];
64
+ if (!override) {
65
+ return ctx;
66
+ }
67
+ return {
68
+ stiffness: override.stiffness ?? ctx.stiffness,
69
+ damping: override.damping ?? ctx.damping,
70
+ precision: override.precision ?? ctx.precision,
71
+ };
72
+ };
73
+
74
+ const tick = <T extends SpringValue>(ctx: FrameContext, last: T, current: T, target: T): T => {
41
75
  if (typeof current === "number") {
42
- return tickScalar(ctx, last as number, current, target as number) as T;
76
+ return tickScalar(ctx, ctx, last as number, current, target as number) as T;
43
77
  }
44
78
  const cur = current as Record<string, number>;
45
79
  const lst = last as Record<string, number>;
@@ -47,7 +81,7 @@ const tick = <T extends SpringValue>(ctx: TickContext, last: T, current: T, targ
47
81
  const result: Record<string, number> = {};
48
82
  for (const key in cur) {
49
83
  const c = cur[key] ?? 0;
50
- result[key] = tickScalar(ctx, lst[key] ?? c, c, tgt[key] ?? c);
84
+ result[key] = tickScalar(ctx, resolveAxis(ctx, key), lst[key] ?? c, c, tgt[key] ?? c);
51
85
  }
52
86
  return result as T;
53
87
  };
@@ -56,6 +90,7 @@ export class Spring<T extends SpringValue> {
56
90
  stiffness: number;
57
91
  damping: number;
58
92
  precision: number;
93
+ axes: Record<string, SpringDynamics> | undefined;
59
94
 
60
95
  private value: T;
61
96
  private lastValue: T;
@@ -75,6 +110,7 @@ export class Spring<T extends SpringValue> {
75
110
  this.stiffness = opts.stiffness ?? 0.15;
76
111
  this.damping = opts.damping ?? 0.8;
77
112
  this.precision = opts.precision ?? 0.01;
113
+ this.axes = opts.axes;
78
114
  }
79
115
 
80
116
  get current(): T {
@@ -123,11 +159,12 @@ export class Spring<T extends SpringValue> {
123
159
  return false;
124
160
  }
125
161
  this.invMass = Math.min(this.invMass + this.invMassRecoveryRate, 1);
126
- const ctx: TickContext = {
162
+ const ctx: FrameContext = {
127
163
  invMass: this.invMass,
128
164
  stiffness: this.stiffness,
129
165
  damping: this.damping,
130
166
  precision: this.precision,
167
+ axes: this.axes,
131
168
  settled: true,
132
169
  dt: ((time - this.lastTime) * 60) / 1000,
133
170
  };
@@ -32,6 +32,11 @@
32
32
  --pointer-from-center: 0;
33
33
  --pointer-from-top: var(--pointer-from-center);
34
34
  --pointer-from-left: var(--pointer-from-center);
35
+ --pointer-dx: 0;
36
+ --pointer-dy: 0;
37
+ --tilt-x: 0;
38
+ --tilt-y: 0;
39
+ --card-active: 0;
35
40
  }
36
41
 
37
42
  .holo-card {
@@ -43,6 +48,22 @@
43
48
  --angle: 133deg;
44
49
  --imgsize: cover;
45
50
 
51
+ --hc-brightness: 1;
52
+ --hc-contrast: 1;
53
+ --hc-saturate: 1;
54
+ --hc-glare-opacity: 1;
55
+ --hc-shine-opacity: 1;
56
+
57
+ --mask-size: cover;
58
+ --mask-position: center center;
59
+ --mask-repeat: no-repeat;
60
+
61
+ --layer-blend: normal;
62
+ --layer-size: cover;
63
+ --layer-position: center;
64
+ --layer-opacity: 1;
65
+ --layer-parallax: 0;
66
+
46
67
  --red: #f80e35;
47
68
  --yellow: #eedf10;
48
69
  --green: #21e985;
@@ -173,6 +194,11 @@ button.holo-card__rotator {
173
194
  transform: translate3d(0px, 0px, 0.01px);
174
195
  }
175
196
 
197
+ .holo-card__rotator img.holo-card__image {
198
+ height: 100%;
199
+ object-fit: var(--imgsize);
200
+ }
201
+
176
202
  .holo-card__back {
177
203
  background-color: var(--card-back);
178
204
  transform: rotateY(180deg) translateZ(1px);
@@ -212,9 +238,10 @@ button.holo-card__rotator {
212
238
  background: transparent;
213
239
  background-size: cover;
214
240
  background-position: center;
215
- filter: brightness(0.85) contrast(2.75) saturate(0.65);
241
+ filter: brightness(calc(0.85 * var(--hc-brightness))) contrast(calc(2.75 * var(--hc-contrast)))
242
+ saturate(calc(0.65 * var(--hc-saturate)));
216
243
  mix-blend-mode: color-dodge;
217
- opacity: var(--card-opacity);
244
+ opacity: calc(var(--card-opacity) * var(--hc-shine-opacity));
218
245
  }
219
246
 
220
247
  .holo-card__shine::before,
@@ -249,7 +276,7 @@ button.holo-card__rotator {
249
276
  hsla(0, 0%, 100%, 0.65) 20%,
250
277
  hsla(0, 0%, 0%, 0.5) 90%
251
278
  );
252
- opacity: var(--card-opacity);
279
+ opacity: calc(var(--card-opacity) * var(--hc-glare-opacity));
253
280
  mix-blend-mode: overlay;
254
281
  }
255
282
 
@@ -257,6 +284,68 @@ button.holo-card__rotator {
257
284
  .holo-card--masked .holo-card__shine::before,
258
285
  .holo-card--masked .holo-card__shine::after {
259
286
  mask-image: var(--mask);
287
+ mask-size: var(--mask-size);
288
+ mask-position: var(--mask-position);
289
+ mask-repeat: var(--mask-repeat);
290
+ }
291
+
292
+ .holo-card--mask-card .holo-card__front,
293
+ .holo-card--mask-card .holo-card__back {
294
+ mask-image: var(--mask);
295
+ mask-size: var(--mask-size);
296
+ mask-position: var(--mask-position);
297
+ mask-repeat: var(--mask-repeat);
298
+ }
299
+
300
+ .holo-card__layers {
301
+ z-index: 2;
302
+ pointer-events: none;
303
+ }
304
+
305
+ .holo-card__layer {
306
+ grid-area: 1/1;
307
+ background-image: var(--layer-image, none);
308
+ background-size: var(--layer-size);
309
+ background-position: var(--layer-position);
310
+ background-repeat: no-repeat;
311
+ mix-blend-mode: var(--layer-blend);
312
+ opacity: var(--layer-opacity);
313
+ transform: translate3d(
314
+ calc(var(--pointer-dx) * var(--layer-parallax) * 1px),
315
+ calc(var(--pointer-dy) * var(--layer-parallax) * 1px),
316
+ 0.02px
317
+ );
318
+ will-change: transform, opacity;
319
+ }
320
+
321
+ .holo-card__layer--masked {
322
+ mask-image: var(--layer-mask);
260
323
  mask-size: cover;
261
- mask-position: center center;
324
+ mask-position: center;
325
+ }
326
+
327
+ .holo-card__content {
328
+ transform: translate3d(0px, 0px, 0.01px);
329
+ }
330
+
331
+ .holo-card__content *,
332
+ .holo-card__overlay *,
333
+ .holo-card__layer * {
334
+ width: auto;
335
+ display: revert;
336
+ grid-area: auto;
337
+ aspect-ratio: auto;
338
+ overflow: revert;
339
+ border-radius: revert;
340
+ }
341
+
342
+ .holo-card__overlay {
343
+ z-index: 4;
344
+ transform: translateZ(2px);
345
+ pointer-events: none;
346
+ }
347
+
348
+ .holo-card__overlay--interactive,
349
+ .holo-card__overlay--interactive * {
350
+ pointer-events: auto;
262
351
  }
@@ -1,10 +1,13 @@
1
- .holo-card[data-effect="cosmos"] .holo-card__shine {
1
+ .holo-card[data-effect="cosmos"] {
2
2
  --space: 4%;
3
+ --angle: 82deg;
4
+ }
3
5
 
6
+ .holo-card[data-effect="cosmos"] .holo-card__shine {
4
7
  background-image:
5
8
  var(--hc-cosmos-bottom),
6
9
  repeating-linear-gradient(
7
- 82deg,
10
+ var(--angle),
8
11
  hsl(53, 65%, 60%) calc(var(--space) * 1),
9
12
  hsl(93, 56%, 50%) calc(var(--space) * 2),
10
13
  hsl(176, 54%, 49%) calc(var(--space) * 3),
@@ -37,7 +40,7 @@
37
40
  400% 900%,
38
41
  cover;
39
42
 
40
- filter: brightness(1) contrast(1) saturate(0.8);
43
+ filter: brightness(var(--hc-brightness)) contrast(var(--hc-contrast)) saturate(calc(0.8 * var(--hc-saturate)));
41
44
  mix-blend-mode: color-dodge;
42
45
  }
43
46
 
@@ -48,7 +51,7 @@
48
51
  background-image:
49
52
  var(--hc-cosmos-middle),
50
53
  repeating-linear-gradient(
51
- 82deg,
54
+ var(--angle),
52
55
  hsl(53, 65%, 60%) calc(var(--space) * 1),
53
56
  hsl(93, 56%, 50%) calc(var(--space) * 2),
54
57
  hsl(176, 54%, 49%) calc(var(--space) * 3),
@@ -86,7 +89,7 @@
86
89
  background-image:
87
90
  var(--hc-cosmos-top),
88
91
  repeating-linear-gradient(
89
- 82deg,
92
+ var(--angle),
90
93
  hsl(53, 65%, 60%) calc(var(--space) * 1),
91
94
  hsl(93, 56%, 50%) calc(var(--space) * 2),
92
95
  hsl(176, 54%, 49%) calc(var(--space) * 3),
@@ -125,7 +128,7 @@
125
128
  );
126
129
  filter: brightness(0.75) contrast(2) saturate(2);
127
130
  mix-blend-mode: overlay;
128
- opacity: calc(var(--card-opacity) * (0.25 + var(--pointer-from-center)));
131
+ opacity: calc(var(--card-opacity) * (0.25 + var(--pointer-from-center)) * var(--hc-glare-opacity));
129
132
  }
130
133
 
131
134
  .holo-card[data-effect="cosmos"] .holo-card__glare::after {
@@ -17,7 +17,7 @@
17
17
  55% 55%,
18
18
  center center;
19
19
  background-blend-mode: soft-light, color-burn;
20
- filter: brightness(1) contrast(1) saturate(0.9);
20
+ filter: brightness(var(--hc-brightness)) contrast(var(--hc-contrast)) saturate(calc(0.9 * var(--hc-saturate)));
21
21
  }
22
22
 
23
23
  .holo-card[data-effect="glitter"] .holo-card__shine::before {
@@ -88,7 +88,9 @@
88
88
  .holo-card--masked[data-effect="glitter"] .holo-card__glare::after {
89
89
  content: "";
90
90
  mask-image: var(--mask);
91
- mask-size: cover;
91
+ mask-size: var(--mask-size);
92
+ mask-position: var(--mask-position);
93
+ mask-repeat: var(--mask-repeat);
92
94
 
93
95
  background-image: radial-gradient(
94
96
  farthest-corner circle at var(--pointer-x) var(--pointer-y),
@@ -43,7 +43,8 @@
43
43
  cover;
44
44
 
45
45
  background-blend-mode: overlay;
46
- filter: brightness(1.1) contrast(1.1) saturate(1.2);
46
+ filter: brightness(calc(1.1 * var(--hc-brightness))) contrast(calc(1.1 * var(--hc-contrast)))
47
+ saturate(calc(1.2 * var(--hc-saturate)));
47
48
  mix-blend-mode: color-dodge;
48
49
  }
49
50
 
@@ -107,7 +108,7 @@
107
108
  }
108
109
 
109
110
  .holo-card[data-effect="holo"] .holo-card__glare {
110
- opacity: calc(var(--card-opacity) * 0.8);
111
+ opacity: calc(var(--card-opacity) * 0.8 * var(--hc-glare-opacity));
111
112
  filter: brightness(0.8) contrast(1.5);
112
113
  mix-blend-mode: overlay;
113
114
  }
@@ -17,14 +17,15 @@
17
17
  calc(100% * var(--pointer-from-left)) calc(100% * var(--pointer-from-top)),
18
18
  center center;
19
19
 
20
- filter: brightness(var(--foil-brightness)) contrast(1.5) saturate(1);
20
+ filter: brightness(calc(var(--foil-brightness) * var(--hc-brightness))) contrast(calc(1.5 * var(--hc-contrast)))
21
+ saturate(var(--hc-saturate));
21
22
  mix-blend-mode: color-dodge;
22
23
 
23
- opacity: calc((1.5 * var(--card-opacity)) - var(--pointer-from-center));
24
+ opacity: calc(((1.5 * var(--card-opacity)) - var(--pointer-from-center)) * var(--hc-shine-opacity));
24
25
  }
25
26
 
26
27
  .holo-card[data-effect="reverse"] .holo-card__glare {
27
- opacity: var(--card-opacity);
28
+ opacity: calc(var(--card-opacity) * var(--hc-glare-opacity));
28
29
 
29
30
  background-image: radial-gradient(
30
31
  farthest-corner circle at var(--pointer-x) var(--pointer-y),
package/src/types.ts CHANGED
@@ -1,22 +1,170 @@
1
+ import type { SpringOpts } from "./spring.js";
2
+
1
3
  export type HoloEffect = "none" | "holo" | "reverse" | "cosmos" | "glitter";
2
4
 
5
+ /**
6
+ * Arbitrary card content: a DOM node, or a factory that builds one from the
7
+ * owning document (handy for SSR / custom-element setups where `document` is
8
+ * only safe to touch lazily).
9
+ */
10
+ export type HoloContent = Node | ((doc: Document) => Node);
11
+
12
+ /** A map of CSS custom properties to apply to an element; numbers are stringified. */
13
+ export type CssVars = Record<string, string | number>;
14
+
15
+ /**
16
+ * Spring tuning for a single motion target. `axes` carries independent,
17
+ * asymmetric dynamics per component (`x` / `y` / `o`).
18
+ */
19
+ export type SpringTuning = SpringOpts;
20
+
21
+ /** Interaction & physics adjustment surface. */
22
+ export interface PhysicsOptions {
23
+ /** Tilt in degrees reached at the card edge (default ≈ 14.29). */
24
+ maxTilt?: number;
25
+ /** Independent horizontal tilt; overrides `maxTilt` for the X axis. */
26
+ maxTiltX?: number;
27
+ /** Independent vertical tilt; overrides `maxTilt` for the Y axis. */
28
+ maxTiltY?: number;
29
+ /** Multiplier on the foil/background parallax shift (default 1; 0 disables). */
30
+ parallax?: number;
31
+ /** Multiplier on the glare travel away from centre (default 1). */
32
+ glareRange?: number;
33
+ /** ms to wait after the pointer leaves before the relaxed snap-back (default 500). */
34
+ returnDelay?: number;
35
+ /** Tuning shared by the live pointer springs (rotate / glare / background). */
36
+ interactSpring?: SpringTuning;
37
+ /** Tuning shared by the popover springs (scale / translate / flip). */
38
+ popoverSpring?: SpringTuning;
39
+ /** Tuning for the relaxed snap-back applied when the pointer leaves. */
40
+ snapSpring?: SpringTuning;
41
+ /** Per-target overrides layered on top of the group tuning above. */
42
+ springs?: {
43
+ rotate?: SpringTuning;
44
+ glare?: SpringTuning;
45
+ background?: SpringTuning;
46
+ rotateDelta?: SpringTuning;
47
+ translate?: SpringTuning;
48
+ scale?: SpringTuning;
49
+ };
50
+ }
51
+
52
+ /** Showcase (auto-animation) customisation. `true` keeps the legacy defaults. */
53
+ export interface ShowcaseOptions {
54
+ /** ms before the sweep begins (default 2000). */
55
+ delay?: number;
56
+ /** ms the sweep runs before relaxing; ignored when `loop` is set (default 4000). */
57
+ duration?: number;
58
+ /** Keep sweeping until the user interacts instead of running once (default false). */
59
+ loop?: boolean;
60
+ /** Angular step per tick — higher is faster (default 0.05). */
61
+ speed?: number;
62
+ /** Tilt amplitude in degrees (default 25). */
63
+ intensity?: number;
64
+ /** Spring tuning while the showcase runs. */
65
+ spring?: SpringTuning;
66
+ }
67
+
68
+ /** Fine-grained visual / effect control. Numeric fields are multipliers (1 = unchanged). */
69
+ export interface VisualOptions {
70
+ /** Foil brightness multiplier. */
71
+ brightness?: number;
72
+ /** Foil contrast multiplier. */
73
+ contrast?: number;
74
+ /** Foil saturation multiplier. */
75
+ saturate?: number;
76
+ /** Glare layer opacity multiplier. */
77
+ glareOpacity?: number;
78
+ /** Shine/foil layer opacity multiplier. */
79
+ shineOpacity?: number;
80
+ /** Foil line spacing for the glitter/cosmos foils (`--space`); a number is read as a percentage. */
81
+ lineSpace?: string | number;
82
+ /** Foil sweep angle for the glitter/cosmos foils (`--angle`); a number is read as degrees. */
83
+ lineAngle?: string | number;
84
+ /** Glitter cell size (`--glittersize`); a number is read as a percentage. */
85
+ glitterSize?: string | number;
86
+ /** Artwork object-fit (`--imgsize`, default `cover`). */
87
+ imageFit?: string;
88
+ }
89
+
90
+ /** Advanced mask processing. A bare string is shorthand for `{ image }`. */
91
+ export interface MaskOptions {
92
+ /** Mask image URL. */
93
+ image?: string;
94
+ /** `mask-size` (default `cover`). */
95
+ size?: string;
96
+ /** `mask-position` (default `center center`). */
97
+ position?: string;
98
+ /** `mask-repeat` (default `no-repeat`). */
99
+ repeat?: string;
100
+ /**
101
+ * What the mask clips:
102
+ * - `shine` (default) clips only the foil, as before;
103
+ * - `card` clips the whole card into the mask silhouette (artwork included).
104
+ */
105
+ mode?: "shine" | "card";
106
+ }
107
+
108
+ /** An extra stacked layer between the artwork and the foil. */
109
+ export interface HoloLayerOptions {
110
+ /** Image URL painted into the layer. */
111
+ image?: string;
112
+ /** Arbitrary content for the layer (overrides `image`). */
113
+ content?: HoloContent;
114
+ /** `mix-blend-mode` for the layer (default `normal`). */
115
+ blend?: string;
116
+ /** Layer opacity 0–1 (default 1). */
117
+ opacity?: number;
118
+ /** Pointer-driven parallax depth in px; the layer drifts with the tilt (default 0). */
119
+ parallax?: number;
120
+ /** Per-layer mask image URL. */
121
+ mask?: string;
122
+ /** `background-size` when an `image` is used (default `cover`). */
123
+ size?: string;
124
+ /** `background-position` when an `image` is used (default `center`). */
125
+ position?: string;
126
+ /** Extra class names for the layer element. */
127
+ className?: string;
128
+ /** Extra CSS custom properties for the layer element. */
129
+ vars?: CssVars;
130
+ }
131
+
3
132
  export interface HoloCardOptions {
4
133
  effect?: HoloEffect;
5
134
  interactive?: boolean;
6
135
  activateOnClick?: boolean;
7
136
  gyroscope?: boolean;
8
- showcase?: boolean;
137
+ showcase?: boolean | ShowcaseOptions;
9
138
  glow?: string;
10
139
  aspectRatio?: number;
11
140
  textureSeed?: number;
12
- mask?: string;
141
+ mask?: string | MaskOptions;
13
142
  foil?: string;
143
+ /** Interaction & physics adjustments. */
144
+ physics?: PhysicsOptions;
145
+ /** Fine-grained visual control. */
146
+ visual?: VisualOptions;
147
+ /** Extra stacked layers between the artwork and the foil. */
148
+ layers?: HoloLayerOptions[];
149
+ /** Arbitrary CSS custom properties applied to the root (for content linkage). */
150
+ vars?: CssVars;
14
151
  }
15
152
 
16
- export interface CreateHoloCardOptions extends HoloCardOptions {
17
- image: string;
153
+ export interface CreateHoloCardFields extends HoloCardOptions {
18
154
  imageAlt?: string;
19
155
  back?: string;
20
156
  backAlt?: string;
157
+ /** Foreground content above the foil — name plates, badges, live data, etc. */
158
+ overlay?: HoloContent;
159
+ /** Let the overlay receive pointer events (default false: purely decorative). */
160
+ overlayInteractive?: boolean;
21
161
  className?: string;
22
162
  }
163
+
164
+ /**
165
+ * Options for `createHoloCard`. Either `image` (front artwork) or `content`
166
+ * (custom front content) must be supplied — both may be combined — so the card
167
+ * is never built blank.
168
+ */
169
+ export type CreateHoloCardOptions = CreateHoloCardFields &
170
+ ({ image: string; content?: HoloContent } | { image?: string; content: HoloContent });