@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.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/active-registry.js +10 -0
- package/dist/active-registry.js.map +1 -0
- package/dist/dom.js +66 -0
- package/dist/dom.js.map +1 -0
- package/dist/holo-card.js +450 -0
- package/dist/holo-card.js.map +1 -0
- package/dist/holo-cards.css +601 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/math.js +4 -0
- package/dist/math.js.map +1 -0
- package/dist/orientation.js +64 -0
- package/dist/orientation.js.map +1 -0
- package/dist/spring.js +123 -0
- package/dist/spring.js.map +1 -0
- package/dist/subscribers.js +26 -0
- package/dist/subscribers.js.map +1 -0
- package/dist/textures.js +178 -0
- package/dist/textures.js.map +1 -0
- package/dist/ticker.js +32 -0
- package/dist/ticker.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist-types/active-registry.d.ts +6 -0
- package/dist-types/active-registry.d.ts.map +1 -0
- package/dist-types/dom.d.ts +18 -0
- package/dist-types/dom.d.ts.map +1 -0
- package/dist-types/holo-card.d.ts +58 -0
- package/dist-types/holo-card.d.ts.map +1 -0
- package/dist-types/index.d.ts +13 -0
- package/dist-types/index.d.ts.map +1 -0
- package/dist-types/math.d.ts +4 -0
- package/dist-types/math.d.ts.map +1 -0
- package/dist-types/orientation.d.ts +13 -0
- package/dist-types/orientation.d.ts.map +1 -0
- package/dist-types/spring.d.ts +32 -0
- package/dist-types/spring.d.ts.map +1 -0
- package/dist-types/subscribers.d.ts +10 -0
- package/dist-types/subscribers.d.ts.map +1 -0
- package/dist-types/textures.d.ts +23 -0
- package/dist-types/textures.d.ts.map +1 -0
- package/dist-types/ticker.d.ts +7 -0
- package/dist-types/ticker.d.ts.map +1 -0
- package/dist-types/types.d.ts +21 -0
- package/dist-types/types.d.ts.map +1 -0
- package/package.json +75 -0
- package/src/active-registry.ts +15 -0
- package/src/dom.ts +79 -0
- package/src/holo-card.ts +525 -0
- package/src/index.ts +35 -0
- package/src/math.ts +6 -0
- package/src/orientation.ts +83 -0
- package/src/spring.ts +158 -0
- package/src/styles/base.css +262 -0
- package/src/styles/effects/cosmos.css +143 -0
- package/src/styles/effects/glitter.css +103 -0
- package/src/styles/effects/holo.css +127 -0
- package/src/styles/effects/reverse.css +55 -0
- package/src/styles/index.css +5 -0
- package/src/subscribers.ts +30 -0
- package/src/textures.ts +310 -0
- package/src/ticker.ts +46 -0
- package/src/types.ts +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kongyo2
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# cards-css
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@kongyo2/cards-css)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Framework-agnostic holographic trading-card effect — tilt, shine, glare and four
|
|
7
|
+
foils (`holo` / `reverse` / `cosmos` / `glitter`) that react to pointer and
|
|
8
|
+
gyroscope, with procedurally code-generated textures. No runtime dependencies.
|
|
9
|
+
|
|
10
|
+
[](https://kongyo2.github.io/cards-css/)
|
|
11
|
+
|
|
12
|
+
**[Live demo →](https://kongyo2.github.io/cards-css/)** — move the pointer across a card, or tilt your phone, to see the foil shift.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install @kongyo2/cards-css
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { createHoloCard } from "@kongyo2/cards-css";
|
|
24
|
+
import "@kongyo2/cards-css/styles.css";
|
|
25
|
+
|
|
26
|
+
const card = createHoloCard({
|
|
27
|
+
image: "/cards/phoenix.png",
|
|
28
|
+
imageAlt: "Phoenix",
|
|
29
|
+
effect: "holo",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
document.querySelector("#stage").append(card.element);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`createHoloCard` builds the full element for you. To enhance markup you already
|
|
36
|
+
have on the page (it must contain the `.holo-card__rotator` structure), use
|
|
37
|
+
`attachHoloCard(element, options)` instead.
|
|
38
|
+
|
|
39
|
+
## Effects
|
|
40
|
+
|
|
41
|
+
Set via the `effect` option (or `card.setEffect(...)` at runtime):
|
|
42
|
+
|
|
43
|
+
| Effect | Description |
|
|
44
|
+
| --------- | ---------------------------------------------------------------- |
|
|
45
|
+
| `none` | Tilt, shine and glare only — no foil |
|
|
46
|
+
| `holo` | Rainbow holographic foil |
|
|
47
|
+
| `reverse` | Reverse-line holographic foil |
|
|
48
|
+
| `cosmos` | Galaxy / cosmos foil (procedural — needs `textureSeed`) |
|
|
49
|
+
| `glitter` | Glitter / sparkle foil (procedural — needs `textureSeed`) |
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
| Option | Type | Default | Notes |
|
|
54
|
+
| ----------------- | --------- | -------- | -------------------------------------------------- |
|
|
55
|
+
| `image` | `string` | — | Front image source (required for `createHoloCard`) |
|
|
56
|
+
| `imageAlt` | `string` | `""` | Alt text for the front image |
|
|
57
|
+
| `back` / `backAlt`| `string` | — | Optional card-back image and its alt text |
|
|
58
|
+
| `className` | `string` | — | Extra class names for the root element |
|
|
59
|
+
| `effect` | `HoloEffect` | `"none"` | One of the effects above |
|
|
60
|
+
| `interactive` | `boolean` | `true` | React to pointer move |
|
|
61
|
+
| `activateOnClick` | `boolean` | `false` | Click to pop the card into a centered showcase |
|
|
62
|
+
| `gyroscope` | `boolean` | `true` | Tilt to device orientation while active |
|
|
63
|
+
| `showcase` | `boolean` | `false` | Auto-animate once on mount |
|
|
64
|
+
| `glow` | `string` | — | CSS color for the card glow |
|
|
65
|
+
| `aspectRatio` | `number` | — | Card aspect ratio (width / height) |
|
|
66
|
+
| `textureSeed` | `number` | — | Seed for the generated `cosmos` / `glitter` textures; without it those two foils render without their procedural layers |
|
|
67
|
+
| `mask` / `foil` | `string` | — | URLs for a mask / custom foil overlay |
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
- `createHoloCard(options)` → `HoloCard` — builds the element (`image` required).
|
|
72
|
+
- `attachHoloCard(element, options?)` → `HoloCard` — wraps existing markup.
|
|
73
|
+
- `HoloCard`
|
|
74
|
+
- `element` — the root `HTMLElement` to mount.
|
|
75
|
+
- `active` / `interacting` — state getters.
|
|
76
|
+
- `activate()` / `deactivate()` — pop the card in / out of showcase.
|
|
77
|
+
- `setEffect(effect)` — swap the foil at runtime.
|
|
78
|
+
- `destroy()` — remove listeners and reset the element.
|
|
79
|
+
|
|
80
|
+
On iOS, gyroscope access needs a one-time permission prompt triggered by a user
|
|
81
|
+
gesture — call `requestOrientationPermission()` (exported) from a click/tap
|
|
82
|
+
handler.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
[MIT](./LICENSE) © kongyo2
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Subscribers } from "./subscribers.js";
|
|
2
|
+
let activeCard = null;
|
|
3
|
+
const subscribers = new Subscribers(() => activeCard);
|
|
4
|
+
export const getActiveCard = () => activeCard;
|
|
5
|
+
export const setActiveCard = (card) => {
|
|
6
|
+
activeCard = card;
|
|
7
|
+
subscribers.emit(activeCard);
|
|
8
|
+
};
|
|
9
|
+
export const subscribeActiveCard = (fn) => subscribers.subscribe(fn);
|
|
10
|
+
//# sourceMappingURL=active-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active-registry.js","sourceRoot":"","sources":["../src/active-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAI/C,IAAI,UAAU,GAAgB,IAAI,CAAC;AACnC,MAAM,WAAW,GAAG,IAAI,WAAW,CAAc,GAAG,EAAE,CAAC,UAAU,CAAC,CAAC;AAEnE,MAAM,CAAC,MAAM,aAAa,GAAG,GAAgB,EAAE,CAAC,UAAU,CAAC;AAE3D,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAiB,EAAQ,EAAE;IACvD,UAAU,GAAG,IAAI,CAAC;IAClB,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAC/B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAAiC,EAAgB,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC"}
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const CLASS = {
|
|
2
|
+
root: "holo-card",
|
|
3
|
+
translater: "holo-card__translater",
|
|
4
|
+
rotator: "holo-card__rotator",
|
|
5
|
+
front: "holo-card__front",
|
|
6
|
+
back: "holo-card__back",
|
|
7
|
+
image: "holo-card__image",
|
|
8
|
+
shine: "holo-card__shine",
|
|
9
|
+
glare: "holo-card__glare",
|
|
10
|
+
interactive: "holo-card--interactive",
|
|
11
|
+
active: "holo-card--active",
|
|
12
|
+
interacting: "holo-card--interacting",
|
|
13
|
+
loading: "holo-card--loading",
|
|
14
|
+
masked: "holo-card--masked",
|
|
15
|
+
};
|
|
16
|
+
const requireDocument = () => {
|
|
17
|
+
if (typeof document === "undefined") {
|
|
18
|
+
throw new Error("@kongyo2/cards-css: a DOM document is required to build a holo card element.");
|
|
19
|
+
}
|
|
20
|
+
return document;
|
|
21
|
+
};
|
|
22
|
+
export const buildHoloCardElement = (options) => {
|
|
23
|
+
const doc = requireDocument();
|
|
24
|
+
const root = doc.createElement("div");
|
|
25
|
+
root.className = CLASS.root;
|
|
26
|
+
if (options.className) {
|
|
27
|
+
for (const name of options.className.split(/\s+/).filter(Boolean)) {
|
|
28
|
+
root.classList.add(name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
root.dataset.effect = options.effect ?? "none";
|
|
32
|
+
if (options.mask) {
|
|
33
|
+
root.classList.add(CLASS.masked);
|
|
34
|
+
}
|
|
35
|
+
const translater = doc.createElement("div");
|
|
36
|
+
translater.className = CLASS.translater;
|
|
37
|
+
const rotator = doc.createElement("div");
|
|
38
|
+
rotator.className = CLASS.rotator;
|
|
39
|
+
if (options.back) {
|
|
40
|
+
const back = doc.createElement("img");
|
|
41
|
+
back.className = CLASS.back;
|
|
42
|
+
back.src = options.back;
|
|
43
|
+
back.alt = options.backAlt ?? "";
|
|
44
|
+
back.loading = "lazy";
|
|
45
|
+
rotator.appendChild(back);
|
|
46
|
+
}
|
|
47
|
+
const front = doc.createElement("div");
|
|
48
|
+
front.className = CLASS.front;
|
|
49
|
+
const image = doc.createElement("img");
|
|
50
|
+
image.className = CLASS.image;
|
|
51
|
+
image.src = options.image;
|
|
52
|
+
image.alt = options.imageAlt ?? "";
|
|
53
|
+
image.loading = "lazy";
|
|
54
|
+
front.appendChild(image);
|
|
55
|
+
const shine = doc.createElement("div");
|
|
56
|
+
shine.className = CLASS.shine;
|
|
57
|
+
const glare = doc.createElement("div");
|
|
58
|
+
glare.className = CLASS.glare;
|
|
59
|
+
front.appendChild(shine);
|
|
60
|
+
front.appendChild(glare);
|
|
61
|
+
rotator.appendChild(front);
|
|
62
|
+
translater.appendChild(rotator);
|
|
63
|
+
root.appendChild(translater);
|
|
64
|
+
return root;
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=dom.js.map
|
package/dist/dom.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,IAAI,EAAE,WAAW;IACjB,UAAU,EAAE,uBAAuB;IACnC,OAAO,EAAE,oBAAoB;IAC7B,KAAK,EAAE,kBAAkB;IACzB,IAAI,EAAE,iBAAiB;IACvB,KAAK,EAAE,kBAAkB;IACzB,KAAK,EAAE,kBAAkB;IACzB,KAAK,EAAE,kBAAkB;IACzB,WAAW,EAAE,wBAAwB;IACrC,MAAM,EAAE,mBAAmB;IAC3B,WAAW,EAAE,wBAAwB;IACrC,OAAO,EAAE,oBAAoB;IAC7B,MAAM,EAAE,mBAAmB;CACnB,CAAC;AAEX,MAAM,eAAe,GAAG,GAAa,EAAE;IACrC,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAC;IAClG,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,OAA8B,EAAe,EAAE;IAClF,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;IAE9B,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC;IAC5B,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACtB,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAClE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,MAAM,CAAC;IAC/C,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC5C,UAAU,CAAC,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC;IAExC,MAAM,OAAO,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACzC,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC;IAElC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;QACxB,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;QACjC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACvC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;IAE9B,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACvC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;IAC9B,KAAK,CAAC,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC;IAC1B,KAAK,CAAC,GAAG,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;IACnC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAEzB,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACvC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;IAE9B,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACvC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;IAE9B,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACzB,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACzB,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC3B,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IAE7B,OAAO,IAAI,CAAC;AACd,CAAC,CAAC"}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { adjust, clamp, round } from "./math.js";
|
|
2
|
+
import { Spring } from "./spring.js";
|
|
3
|
+
import { CLASS } from "./dom.js";
|
|
4
|
+
import { getActiveCard, setActiveCard, subscribeActiveCard } from "./active-registry.js";
|
|
5
|
+
import { resetBaseOrientation, subscribeOrientation } from "./orientation.js";
|
|
6
|
+
import { generateTextures, texturesToCssVariables } from "./textures.js";
|
|
7
|
+
const requestFrame = (cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : setTimeout(cb, 16);
|
|
8
|
+
const cancelFrame = (id) => {
|
|
9
|
+
if (typeof cancelAnimationFrame !== "undefined") {
|
|
10
|
+
cancelAnimationFrame(id);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
clearTimeout(id);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const SPRING_INTERACT = { stiffness: 0.066, damping: 0.25 };
|
|
17
|
+
const SPRING_POPOVER = { stiffness: 0.033, damping: 0.45 };
|
|
18
|
+
const SNAP_STIFFNESS = 0.01;
|
|
19
|
+
const SNAP_DAMPING = 0.06;
|
|
20
|
+
export class HoloCard {
|
|
21
|
+
element;
|
|
22
|
+
rotator;
|
|
23
|
+
options;
|
|
24
|
+
springRotate = new Spring({ x: 0, y: 0 }, SPRING_INTERACT);
|
|
25
|
+
springGlare = new Spring({ x: 50, y: 50, o: 0 }, SPRING_INTERACT);
|
|
26
|
+
springBackground = new Spring({ x: 50, y: 50 }, SPRING_INTERACT);
|
|
27
|
+
springRotateDelta = new Spring({ x: 0, y: 0 }, SPRING_POPOVER);
|
|
28
|
+
springTranslate = new Spring({ x: 0, y: 0 }, SPRING_POPOVER);
|
|
29
|
+
springScale = new Spring(1, SPRING_POPOVER);
|
|
30
|
+
isInteracting = false;
|
|
31
|
+
firstPop = true;
|
|
32
|
+
isVisible = typeof document !== "undefined" ? document.visibilityState === "visible" : true;
|
|
33
|
+
destroyed = false;
|
|
34
|
+
renderScheduled = false;
|
|
35
|
+
interactRaf = null;
|
|
36
|
+
pendingUpdate = null;
|
|
37
|
+
repositionTimer = null;
|
|
38
|
+
endTimer = null;
|
|
39
|
+
showcaseStart = null;
|
|
40
|
+
showcaseEnd = null;
|
|
41
|
+
showcaseInterval = null;
|
|
42
|
+
showcaseRunning;
|
|
43
|
+
cleanups = [];
|
|
44
|
+
unsubscribeOrientation = null;
|
|
45
|
+
constructor(element, options = {}) {
|
|
46
|
+
this.element = element;
|
|
47
|
+
const rotator = element.querySelector(`.${CLASS.rotator}`);
|
|
48
|
+
if (!rotator) {
|
|
49
|
+
throw new Error("@kongyo2/cards-css: holo card element is missing its .holo-card__rotator child.");
|
|
50
|
+
}
|
|
51
|
+
this.rotator = rotator;
|
|
52
|
+
this.options = {
|
|
53
|
+
interactive: options.interactive ?? true,
|
|
54
|
+
activateOnClick: options.activateOnClick ?? false,
|
|
55
|
+
gyroscope: options.gyroscope ?? true,
|
|
56
|
+
showcase: options.showcase ?? false,
|
|
57
|
+
};
|
|
58
|
+
this.showcaseRunning = this.options.showcase;
|
|
59
|
+
if (options.effect) {
|
|
60
|
+
element.dataset.effect = options.effect;
|
|
61
|
+
}
|
|
62
|
+
else if (!element.dataset.effect) {
|
|
63
|
+
element.dataset.effect = "none";
|
|
64
|
+
}
|
|
65
|
+
if (options.glow) {
|
|
66
|
+
element.style.setProperty("--card-glow", options.glow);
|
|
67
|
+
}
|
|
68
|
+
if (typeof options.aspectRatio === "number") {
|
|
69
|
+
element.style.setProperty("--card-aspect", String(options.aspectRatio));
|
|
70
|
+
}
|
|
71
|
+
if (options.mask) {
|
|
72
|
+
element.style.setProperty("--mask", `url(${options.mask})`);
|
|
73
|
+
element.classList.add(CLASS.masked);
|
|
74
|
+
}
|
|
75
|
+
if (options.foil) {
|
|
76
|
+
element.style.setProperty("--foil", `url(${options.foil})`);
|
|
77
|
+
}
|
|
78
|
+
this.applyStaticStyles(options.textureSeed);
|
|
79
|
+
for (const spring of [
|
|
80
|
+
this.springRotate,
|
|
81
|
+
this.springGlare,
|
|
82
|
+
this.springBackground,
|
|
83
|
+
this.springRotateDelta,
|
|
84
|
+
this.springTranslate,
|
|
85
|
+
this.springScale,
|
|
86
|
+
]) {
|
|
87
|
+
this.cleanups.push(spring.subscribe(() => this.scheduleRender()));
|
|
88
|
+
}
|
|
89
|
+
this.applyStyles();
|
|
90
|
+
if (this.options.interactive) {
|
|
91
|
+
this.enableInteractive();
|
|
92
|
+
}
|
|
93
|
+
this.cleanups.push(subscribeActiveCard(() => this.onActiveChange()));
|
|
94
|
+
if (typeof document !== "undefined") {
|
|
95
|
+
const onVisibility = () => this.onVisibilityChange();
|
|
96
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
97
|
+
this.cleanups.push(() => document.removeEventListener("visibilitychange", onVisibility));
|
|
98
|
+
}
|
|
99
|
+
if (this.options.showcase) {
|
|
100
|
+
this.startShowcase();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
applyStaticStyles(seed) {
|
|
104
|
+
const seedX = Math.random();
|
|
105
|
+
const seedY = Math.random();
|
|
106
|
+
const cosmosX = Math.floor(seedX * 734);
|
|
107
|
+
const cosmosY = Math.floor(seedY * 1280);
|
|
108
|
+
this.element.style.setProperty("--seedx", String(seedX));
|
|
109
|
+
this.element.style.setProperty("--seedy", String(seedY));
|
|
110
|
+
this.element.style.setProperty("--cosmosbg", `${cosmosX}px ${cosmosY}px`);
|
|
111
|
+
if (typeof seed === "number") {
|
|
112
|
+
const vars = texturesToCssVariables(generateTextures({ seed }));
|
|
113
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
114
|
+
this.element.style.setProperty(name, value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
enableInteractive() {
|
|
119
|
+
this.element.classList.add(CLASS.interactive);
|
|
120
|
+
const onPointerMove = (event) => this.interact(event);
|
|
121
|
+
const onPointerLeave = () => this.interactEnd();
|
|
122
|
+
this.rotator.addEventListener("pointermove", onPointerMove);
|
|
123
|
+
this.rotator.addEventListener("pointerleave", onPointerLeave);
|
|
124
|
+
this.cleanups.push(() => this.rotator.removeEventListener("pointermove", onPointerMove));
|
|
125
|
+
this.cleanups.push(() => this.rotator.removeEventListener("pointerleave", onPointerLeave));
|
|
126
|
+
if (this.options.activateOnClick) {
|
|
127
|
+
const onClick = () => this.toggleActive();
|
|
128
|
+
const onBlur = () => this.deactivate();
|
|
129
|
+
this.rotator.addEventListener("click", onClick);
|
|
130
|
+
this.rotator.addEventListener("blur", onBlur);
|
|
131
|
+
this.rotator.tabIndex = this.rotator.tabIndex >= 0 ? this.rotator.tabIndex : 0;
|
|
132
|
+
this.cleanups.push(() => this.rotator.removeEventListener("click", onClick));
|
|
133
|
+
this.cleanups.push(() => this.rotator.removeEventListener("blur", onBlur));
|
|
134
|
+
const onScroll = () => this.reposition();
|
|
135
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
136
|
+
window.addEventListener("resize", onScroll, { passive: true });
|
|
137
|
+
this.cleanups.push(() => window.removeEventListener("scroll", onScroll));
|
|
138
|
+
this.cleanups.push(() => window.removeEventListener("resize", onScroll));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
interact(event) {
|
|
142
|
+
this.endShowcase();
|
|
143
|
+
if (!this.isVisible) {
|
|
144
|
+
this.setInteracting(false);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const active = getActiveCard();
|
|
148
|
+
if (active && active !== this) {
|
|
149
|
+
this.setInteracting(false);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.setInteracting(true);
|
|
153
|
+
if (this.endTimer) {
|
|
154
|
+
clearTimeout(this.endTimer);
|
|
155
|
+
this.endTimer = null;
|
|
156
|
+
}
|
|
157
|
+
const rect = this.rotator.getBoundingClientRect();
|
|
158
|
+
const absolute = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
159
|
+
const percent = {
|
|
160
|
+
x: clamp(round((100 / rect.width) * absolute.x)),
|
|
161
|
+
y: clamp(round((100 / rect.height) * absolute.y)),
|
|
162
|
+
};
|
|
163
|
+
const center = { x: percent.x - 50, y: percent.y - 50 };
|
|
164
|
+
this.pendingUpdate = {
|
|
165
|
+
background: { x: adjust(percent.x, 0, 100, 37, 63), y: adjust(percent.y, 0, 100, 33, 67) },
|
|
166
|
+
rotate: { x: round(-(center.x / 3.5)), y: round(center.y / 3.5) },
|
|
167
|
+
glare: { x: round(percent.x), y: round(percent.y), o: 1 },
|
|
168
|
+
};
|
|
169
|
+
if (this.interactRaf === null) {
|
|
170
|
+
this.interactRaf = requestFrame(() => {
|
|
171
|
+
if (this.pendingUpdate) {
|
|
172
|
+
this.updateSprings(this.pendingUpdate.background, this.pendingUpdate.rotate, this.pendingUpdate.glare);
|
|
173
|
+
this.pendingUpdate = null;
|
|
174
|
+
}
|
|
175
|
+
this.interactRaf = null;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
interactEnd(delay = 500) {
|
|
180
|
+
if (this.interactRaf !== null) {
|
|
181
|
+
cancelFrame(this.interactRaf);
|
|
182
|
+
this.interactRaf = null;
|
|
183
|
+
}
|
|
184
|
+
this.pendingUpdate = null;
|
|
185
|
+
if (this.endTimer) {
|
|
186
|
+
clearTimeout(this.endTimer);
|
|
187
|
+
}
|
|
188
|
+
this.endTimer = setTimeout(() => {
|
|
189
|
+
this.setInteracting(false);
|
|
190
|
+
this.setSpringDynamics(SNAP_STIFFNESS, SNAP_DAMPING);
|
|
191
|
+
void this.springRotate.set({ x: 0, y: 0 }, { soft: 1 });
|
|
192
|
+
void this.springGlare.set({ x: 50, y: 50, o: 0 }, { soft: 1 });
|
|
193
|
+
void this.springBackground.set({ x: 50, y: 50 }, { soft: 1 });
|
|
194
|
+
}, delay);
|
|
195
|
+
}
|
|
196
|
+
setSpringDynamics(stiffness, damping) {
|
|
197
|
+
for (const spring of [this.springRotate, this.springGlare, this.springBackground]) {
|
|
198
|
+
spring.stiffness = stiffness;
|
|
199
|
+
spring.damping = damping;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
settle(opts) {
|
|
203
|
+
void this.springScale.set(1, opts);
|
|
204
|
+
void this.springTranslate.set({ x: 0, y: 0 }, opts);
|
|
205
|
+
void this.springRotateDelta.set({ x: 0, y: 0 }, opts);
|
|
206
|
+
}
|
|
207
|
+
updateSprings(background, rotate, glare) {
|
|
208
|
+
this.setSpringDynamics(SPRING_INTERACT.stiffness, SPRING_INTERACT.damping);
|
|
209
|
+
void this.springBackground.set(background);
|
|
210
|
+
void this.springRotate.set(rotate);
|
|
211
|
+
void this.springGlare.set(glare);
|
|
212
|
+
}
|
|
213
|
+
setInteracting(value) {
|
|
214
|
+
this.isInteracting = value;
|
|
215
|
+
this.element.classList.toggle(CLASS.interacting, value);
|
|
216
|
+
}
|
|
217
|
+
get interacting() {
|
|
218
|
+
return this.isInteracting;
|
|
219
|
+
}
|
|
220
|
+
scheduleRender() {
|
|
221
|
+
if (this.renderScheduled) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.renderScheduled = true;
|
|
225
|
+
requestFrame(() => {
|
|
226
|
+
this.renderScheduled = false;
|
|
227
|
+
this.applyStyles();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
applyStyles() {
|
|
231
|
+
const glare = this.springGlare.current;
|
|
232
|
+
const rotate = this.springRotate.current;
|
|
233
|
+
const rotateDelta = this.springRotateDelta.current;
|
|
234
|
+
const background = this.springBackground.current;
|
|
235
|
+
const translate = this.springTranslate.current;
|
|
236
|
+
const scale = this.springScale.current;
|
|
237
|
+
const fromCenter = clamp(Math.sqrt((glare.y - 50) * (glare.y - 50) + (glare.x - 50) * (glare.x - 50)) / 50, 0, 1);
|
|
238
|
+
const style = this.element.style;
|
|
239
|
+
style.setProperty("--pointer-x", `${glare.x}%`);
|
|
240
|
+
style.setProperty("--pointer-y", `${glare.y}%`);
|
|
241
|
+
style.setProperty("--pointer-from-center", String(fromCenter));
|
|
242
|
+
style.setProperty("--pointer-from-top", String(glare.y / 100));
|
|
243
|
+
style.setProperty("--pointer-from-left", String(glare.x / 100));
|
|
244
|
+
style.setProperty("--card-opacity", String(glare.o));
|
|
245
|
+
style.setProperty("--rotate-x", `${rotate.x + rotateDelta.x}deg`);
|
|
246
|
+
style.setProperty("--rotate-y", `${rotate.y + rotateDelta.y}deg`);
|
|
247
|
+
style.setProperty("--background-x", `${background.x}%`);
|
|
248
|
+
style.setProperty("--background-y", `${background.y}%`);
|
|
249
|
+
style.setProperty("--card-scale", String(scale));
|
|
250
|
+
style.setProperty("--translate-x", `${translate.x}px`);
|
|
251
|
+
style.setProperty("--translate-y", `${translate.y}px`);
|
|
252
|
+
}
|
|
253
|
+
onActiveChange() {
|
|
254
|
+
if (getActiveCard() === this) {
|
|
255
|
+
this.popover();
|
|
256
|
+
this.element.classList.add(CLASS.active);
|
|
257
|
+
if (this.options.gyroscope) {
|
|
258
|
+
this.startGyroscope();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
this.retreat();
|
|
263
|
+
this.element.classList.remove(CLASS.active);
|
|
264
|
+
this.stopGyroscope();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
popover() {
|
|
268
|
+
const rect = this.element.getBoundingClientRect();
|
|
269
|
+
let delay = 100;
|
|
270
|
+
const scaleW = (window.innerWidth / rect.width) * 0.9;
|
|
271
|
+
const scaleH = (window.innerHeight / rect.height) * 0.9;
|
|
272
|
+
const scaleF = 1.75;
|
|
273
|
+
this.setCenter();
|
|
274
|
+
if (this.firstPop) {
|
|
275
|
+
delay = 1000;
|
|
276
|
+
void this.springRotateDelta.set({ x: 360, y: 0 });
|
|
277
|
+
}
|
|
278
|
+
this.firstPop = false;
|
|
279
|
+
void this.springScale.set(Math.min(scaleW, scaleH, scaleF));
|
|
280
|
+
this.interactEnd(delay);
|
|
281
|
+
}
|
|
282
|
+
retreat() {
|
|
283
|
+
this.settle({ soft: true });
|
|
284
|
+
this.interactEnd(100);
|
|
285
|
+
}
|
|
286
|
+
reset() {
|
|
287
|
+
this.interactEnd(0);
|
|
288
|
+
this.settle({ hard: true });
|
|
289
|
+
void this.springRotate.set({ x: 0, y: 0 }, { hard: true });
|
|
290
|
+
}
|
|
291
|
+
setCenter() {
|
|
292
|
+
const rect = this.element.getBoundingClientRect();
|
|
293
|
+
const view = document.documentElement;
|
|
294
|
+
void this.springTranslate.set({
|
|
295
|
+
x: round(view.clientWidth / 2 - rect.x - rect.width / 2),
|
|
296
|
+
y: round(view.clientHeight / 2 - rect.y - rect.height / 2),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
reposition() {
|
|
300
|
+
if (this.repositionTimer) {
|
|
301
|
+
clearTimeout(this.repositionTimer);
|
|
302
|
+
}
|
|
303
|
+
this.repositionTimer = setTimeout(() => {
|
|
304
|
+
if (getActiveCard() === this) {
|
|
305
|
+
this.setCenter();
|
|
306
|
+
}
|
|
307
|
+
}, 300);
|
|
308
|
+
}
|
|
309
|
+
startGyroscope() {
|
|
310
|
+
if (this.unsubscribeOrientation) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.unsubscribeOrientation = subscribeOrientation((orientation) => this.orientate(orientation));
|
|
314
|
+
}
|
|
315
|
+
stopGyroscope() {
|
|
316
|
+
if (this.unsubscribeOrientation) {
|
|
317
|
+
this.unsubscribeOrientation();
|
|
318
|
+
this.unsubscribeOrientation = null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
orientate(orientation) {
|
|
322
|
+
if (getActiveCard() !== this) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const limit = { x: 16, y: 18 };
|
|
326
|
+
const degrees = {
|
|
327
|
+
x: clamp(orientation.relative.gamma, -limit.x, limit.x),
|
|
328
|
+
y: clamp(orientation.relative.beta, -limit.y, limit.y),
|
|
329
|
+
};
|
|
330
|
+
this.setInteracting(true);
|
|
331
|
+
this.updateSprings({ x: adjust(degrees.x, -limit.x, limit.x, 37, 63), y: adjust(degrees.y, -limit.y, limit.y, 33, 67) }, { x: round(degrees.x * -1), y: round(degrees.y) }, { x: adjust(degrees.x, -limit.x, limit.x, 0, 100), y: adjust(degrees.y, -limit.y, limit.y, 0, 100), o: 1 });
|
|
332
|
+
}
|
|
333
|
+
onVisibilityChange() {
|
|
334
|
+
this.isVisible = document.visibilityState === "visible";
|
|
335
|
+
this.endShowcase();
|
|
336
|
+
this.reset();
|
|
337
|
+
}
|
|
338
|
+
startShowcase() {
|
|
339
|
+
if (!this.isVisible) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const s = 0.02;
|
|
343
|
+
const d = 0.5;
|
|
344
|
+
let r = 0;
|
|
345
|
+
this.showcaseStart = setTimeout(() => {
|
|
346
|
+
this.setInteracting(true);
|
|
347
|
+
this.setSpringDynamics(s, d);
|
|
348
|
+
if (!this.isVisible) {
|
|
349
|
+
this.setInteracting(false);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.showcaseInterval = setInterval(() => {
|
|
353
|
+
r += 0.05;
|
|
354
|
+
void this.springRotate.set({ x: Math.sin(r) * 25, y: Math.cos(r) * 25 });
|
|
355
|
+
void this.springGlare.set({ x: 55 + Math.sin(r) * 55, y: 55 + Math.cos(r) * 55, o: 0.8 });
|
|
356
|
+
void this.springBackground.set({ x: 20 + Math.sin(r) * 20, y: 20 + Math.cos(r) * 20 });
|
|
357
|
+
}, 20);
|
|
358
|
+
this.showcaseEnd = setTimeout(() => {
|
|
359
|
+
if (this.showcaseInterval) {
|
|
360
|
+
clearInterval(this.showcaseInterval);
|
|
361
|
+
this.showcaseInterval = null;
|
|
362
|
+
}
|
|
363
|
+
this.interactEnd(0);
|
|
364
|
+
}, 4000);
|
|
365
|
+
}, 2000);
|
|
366
|
+
}
|
|
367
|
+
endShowcase() {
|
|
368
|
+
if (!this.showcaseRunning) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (this.showcaseEnd) {
|
|
372
|
+
clearTimeout(this.showcaseEnd);
|
|
373
|
+
this.showcaseEnd = null;
|
|
374
|
+
}
|
|
375
|
+
if (this.showcaseStart) {
|
|
376
|
+
clearTimeout(this.showcaseStart);
|
|
377
|
+
this.showcaseStart = null;
|
|
378
|
+
}
|
|
379
|
+
if (this.showcaseInterval) {
|
|
380
|
+
clearInterval(this.showcaseInterval);
|
|
381
|
+
this.showcaseInterval = null;
|
|
382
|
+
}
|
|
383
|
+
this.showcaseRunning = false;
|
|
384
|
+
}
|
|
385
|
+
toggleActive() {
|
|
386
|
+
if (getActiveCard() === this) {
|
|
387
|
+
setActiveCard(null);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
this.endShowcase();
|
|
391
|
+
resetBaseOrientation();
|
|
392
|
+
setActiveCard(this);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
activate() {
|
|
396
|
+
if (getActiveCard() !== this) {
|
|
397
|
+
this.endShowcase();
|
|
398
|
+
resetBaseOrientation();
|
|
399
|
+
setActiveCard(this);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
deactivate() {
|
|
403
|
+
this.interactEnd();
|
|
404
|
+
if (getActiveCard() === this) {
|
|
405
|
+
setActiveCard(null);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
setEffect(effect) {
|
|
409
|
+
this.element.dataset.effect = effect ?? "none";
|
|
410
|
+
}
|
|
411
|
+
get active() {
|
|
412
|
+
return getActiveCard() === this;
|
|
413
|
+
}
|
|
414
|
+
destroy() {
|
|
415
|
+
if (this.destroyed) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this.destroyed = true;
|
|
419
|
+
this.endShowcase();
|
|
420
|
+
this.stopGyroscope();
|
|
421
|
+
if (getActiveCard() === this) {
|
|
422
|
+
setActiveCard(null);
|
|
423
|
+
}
|
|
424
|
+
for (const timer of [this.repositionTimer, this.endTimer]) {
|
|
425
|
+
if (timer) {
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (this.interactRaf !== null) {
|
|
430
|
+
cancelFrame(this.interactRaf);
|
|
431
|
+
this.interactRaf = null;
|
|
432
|
+
}
|
|
433
|
+
for (const cleanup of this.cleanups) {
|
|
434
|
+
cleanup();
|
|
435
|
+
}
|
|
436
|
+
this.cleanups.length = 0;
|
|
437
|
+
for (const spring of [
|
|
438
|
+
this.springRotate,
|
|
439
|
+
this.springGlare,
|
|
440
|
+
this.springBackground,
|
|
441
|
+
this.springRotateDelta,
|
|
442
|
+
this.springTranslate,
|
|
443
|
+
this.springScale,
|
|
444
|
+
]) {
|
|
445
|
+
spring.destroy();
|
|
446
|
+
}
|
|
447
|
+
this.element.classList.remove(CLASS.interactive, CLASS.interacting, CLASS.active);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
//# sourceMappingURL=holo-card.js.map
|