@inglorious/logo 1.0.0 → 2.0.1
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 +9 -0
- package/README.md +71 -0
- package/package.json +52 -42
- package/src/assets/faces/index.js +55 -0
- package/src/handlers.js +99 -0
- package/src/index.js +6 -0
- package/src/render.js +80 -0
- package/src/style.css +44 -0
- package/src/utils.js +42 -0
- package/src/utils.test.js +46 -0
- package/types/index.d.ts +45 -0
- package/.babelrc +0 -6
- package/.eslintrc.cjs +0 -33
- package/.storybook/main.js +0 -18
- package/.storybook/preview.js +0 -14
- package/LICENSE.md +0 -1
- package/dist/faces/index.js +0 -188
- package/dist/index.js +0 -138
- package/dist/logo.js +0 -95
- package/dist/logo.module.css +0 -43
- package/dist/logo.stories.js +0 -56
- package/src/eye.svg +0 -36
- package/src/faces/A.svg +0 -30
- package/src/faces/B.svg +0 -35
- package/src/faces/C.svg +0 -26
- package/src/faces/D.svg +0 -28
- package/src/faces/E.svg +0 -30
- package/src/faces/F.svg +0 -28
- package/src/faces/G.svg +0 -28
- package/src/faces/H.svg +0 -28
- package/src/faces/I.svg +0 -24
- package/src/faces/J.svg +0 -26
- package/src/faces/K.svg +0 -27
- package/src/faces/L.svg +0 -24
- package/src/faces/M.svg +0 -28
- package/src/faces/N.svg +0 -26
- package/src/faces/O.svg +0 -26
- package/src/faces/P.svg +0 -30
- package/src/faces/Q.svg +0 -33
- package/src/faces/R.svg +0 -32
- package/src/faces/S.svg +0 -30
- package/src/faces/T.svg +0 -26
- package/src/faces/U.svg +0 -24
- package/src/faces/V.svg +0 -23
- package/src/faces/W.svg +0 -28
- package/src/faces/X.svg +0 -28
- package/src/faces/Y.svg +0 -25
- package/src/faces/Z.svg +0 -28
- package/src/faces/index.js +0 -55
- package/src/index.jsx +0 -148
- package/src/logo.jsx +0 -101
- package/src/logo.module.css +0 -43
- package/src/logo.stories.jsx +0 -38
- package/vite.config.js +0 -8
- /package/{dist → src/assets}/eye.svg +0 -0
- /package/{dist → src/assets}/faces/A.svg +0 -0
- /package/{dist → src/assets}/faces/B.svg +0 -0
- /package/{dist → src/assets}/faces/C.svg +0 -0
- /package/{dist → src/assets}/faces/D.svg +0 -0
- /package/{dist → src/assets}/faces/E.svg +0 -0
- /package/{dist → src/assets}/faces/F.svg +0 -0
- /package/{dist → src/assets}/faces/G.svg +0 -0
- /package/{dist → src/assets}/faces/H.svg +0 -0
- /package/{dist → src/assets}/faces/I.svg +0 -0
- /package/{dist → src/assets}/faces/J.svg +0 -0
- /package/{dist → src/assets}/faces/K.svg +0 -0
- /package/{dist → src/assets}/faces/L.svg +0 -0
- /package/{dist → src/assets}/faces/M.svg +0 -0
- /package/{dist → src/assets}/faces/N.svg +0 -0
- /package/{dist → src/assets}/faces/O.svg +0 -0
- /package/{dist → src/assets}/faces/P.svg +0 -0
- /package/{dist → src/assets}/faces/Q.svg +0 -0
- /package/{dist → src/assets}/faces/R.svg +0 -0
- /package/{dist → src/assets}/faces/S.svg +0 -0
- /package/{dist → src/assets}/faces/T.svg +0 -0
- /package/{dist → src/assets}/faces/U.svg +0 -0
- /package/{dist → src/assets}/faces/V.svg +0 -0
- /package/{dist → src/assets}/faces/W.svg +0 -0
- /package/{dist → src/assets}/faces/X.svg +0 -0
- /package/{dist → src/assets}/faces/Y.svg +0 -0
- /package/{dist → src/assets}/faces/Z.svg +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2025 Inglorious Coderz Srl.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @inglorious/logo
|
|
2
|
+
|
|
3
|
+
The Inglorious Logo component — a small, dependency-light package that provides the 3D logo render and interactive handlers for Inglorious Web.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Add the package to your project:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @inglorious/logo
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Import the package and its stylesheet, and (optionally) the types for TypeScript.
|
|
20
|
+
|
|
21
|
+
Then, register the `logo` type in your store, create an entity of that type, and use `api.render(entityId)` inside your templates (see the [`web-logo`](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-logo) demo app).
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { createStore } from "@inglorious/web"
|
|
25
|
+
import { logo } from "@inglorious/logo"
|
|
26
|
+
import "@inglorious/logo/style.css"
|
|
27
|
+
|
|
28
|
+
const entities = {
|
|
29
|
+
logo: {
|
|
30
|
+
type: "logo",
|
|
31
|
+
size: 256,
|
|
32
|
+
faces: [
|
|
33
|
+
{ image: "I", reverse: false, eye: true },
|
|
34
|
+
{ image: "W", reverse: false, eye: false },
|
|
35
|
+
],
|
|
36
|
+
isInteractive: false,
|
|
37
|
+
isScrollPrevented: true,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const store = createStore({
|
|
42
|
+
types: { logo },
|
|
43
|
+
entities,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// app render function receives `api` from mount()
|
|
47
|
+
const app = {
|
|
48
|
+
render(api) {
|
|
49
|
+
return api.render("logo")
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// TypeScript: import the entity type if you want to annotate entity shapes
|
|
54
|
+
// import type { LogoEntity } from "@inglorious/logo"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
**MIT License - Free and open source**
|
|
62
|
+
|
|
63
|
+
Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
64
|
+
|
|
65
|
+
You're free to use, modify, and distribute this software. See [LICENSE](./LICENSE) for details.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Contributing
|
|
70
|
+
|
|
71
|
+
Contributions welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
|
package/package.json
CHANGED
|
@@ -1,51 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/logo",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "The Inglorious Coderz logo, remade to be compatible with Inglorious Web.",
|
|
5
|
+
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/IngloriousCoderz/inglorious-forge.git",
|
|
10
|
+
"directory": "packages/logo"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/IngloriousCoderz/inglorious-forge/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"inglorious",
|
|
17
|
+
"inglorious-logo",
|
|
18
|
+
"logo",
|
|
19
|
+
"svg",
|
|
20
|
+
"lit-html",
|
|
21
|
+
"inglorious-web",
|
|
22
|
+
"ui",
|
|
23
|
+
"interactive"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./types/index.d.ts",
|
|
29
|
+
"import": "./src/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./style.css": {
|
|
32
|
+
"import": "./src/style.css"
|
|
33
|
+
}
|
|
13
34
|
},
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
35
|
+
"files": [
|
|
36
|
+
"src",
|
|
37
|
+
"types"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@inglorious/web": "2.4.0"
|
|
17
44
|
},
|
|
18
45
|
"devDependencies": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"@
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"@storybook/blocks": "7.4.6",
|
|
27
|
-
"@storybook/react": "7.4.6",
|
|
28
|
-
"@storybook/react-vite": "7.4.6",
|
|
29
|
-
"@storybook/testing-library": "0.2.2",
|
|
30
|
-
"babel-loader": "^9.1.3",
|
|
31
|
-
"eslint": "^8.51.0",
|
|
32
|
-
"eslint-plugin-react": "^7.33.2",
|
|
33
|
-
"eslint-plugin-react-hooks": "^4.6.0",
|
|
34
|
-
"eslint-plugin-react-refresh": "^0.4.3",
|
|
35
|
-
"eslint-plugin-simple-import-sort": "^10.0.0",
|
|
36
|
-
"prop-types": "15.8.1",
|
|
37
|
-
"rimraf": "^5.0.5",
|
|
38
|
-
"storybook": "7.4.6",
|
|
39
|
-
"vite-plugin-svgr": "^4.1.0",
|
|
40
|
-
"vitest": "^0.34.6"
|
|
46
|
+
"prettier": "^3.6.2",
|
|
47
|
+
"vite": "^7.1.3",
|
|
48
|
+
"vitest": "^4.0.15",
|
|
49
|
+
"@inglorious/eslint-config": "1.1.1"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">= 22"
|
|
41
53
|
},
|
|
54
|
+
"types": "types/index.d.ts",
|
|
42
55
|
"scripts": {
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"build-storybook": "storybook build",
|
|
48
|
-
"build": "babel src -d dist --copy-files",
|
|
49
|
-
"prepublish": "rimraf dist && npm run build"
|
|
56
|
+
"format": "prettier --write '**/*.{js,jsx}'",
|
|
57
|
+
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
|
58
|
+
"test:watch": "vitest",
|
|
59
|
+
"test": "vitest run"
|
|
50
60
|
}
|
|
51
61
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import A from "./A.svg"
|
|
2
|
+
import B from "./B.svg"
|
|
3
|
+
import C from "./C.svg"
|
|
4
|
+
import D from "./D.svg"
|
|
5
|
+
import E from "./E.svg"
|
|
6
|
+
import F from "./F.svg"
|
|
7
|
+
import G from "./G.svg"
|
|
8
|
+
import H from "./H.svg"
|
|
9
|
+
import I from "./I.svg"
|
|
10
|
+
import J from "./J.svg"
|
|
11
|
+
import K from "./K.svg"
|
|
12
|
+
import L from "./L.svg"
|
|
13
|
+
import M from "./M.svg"
|
|
14
|
+
import N from "./N.svg"
|
|
15
|
+
import O from "./O.svg"
|
|
16
|
+
import P from "./P.svg"
|
|
17
|
+
import Q from "./Q.svg"
|
|
18
|
+
import R from "./R.svg"
|
|
19
|
+
import S from "./S.svg"
|
|
20
|
+
import T from "./T.svg"
|
|
21
|
+
import U from "./U.svg"
|
|
22
|
+
import V from "./V.svg"
|
|
23
|
+
import W from "./W.svg"
|
|
24
|
+
import X from "./X.svg"
|
|
25
|
+
import Y from "./Y.svg"
|
|
26
|
+
import Z from "./Z.svg"
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
A,
|
|
30
|
+
B,
|
|
31
|
+
C,
|
|
32
|
+
D,
|
|
33
|
+
E,
|
|
34
|
+
F,
|
|
35
|
+
G,
|
|
36
|
+
H,
|
|
37
|
+
I,
|
|
38
|
+
J,
|
|
39
|
+
K,
|
|
40
|
+
L,
|
|
41
|
+
M,
|
|
42
|
+
N,
|
|
43
|
+
O,
|
|
44
|
+
P,
|
|
45
|
+
Q,
|
|
46
|
+
R,
|
|
47
|
+
S,
|
|
48
|
+
T,
|
|
49
|
+
U,
|
|
50
|
+
V,
|
|
51
|
+
W,
|
|
52
|
+
X,
|
|
53
|
+
Y,
|
|
54
|
+
Z,
|
|
55
|
+
}
|
package/src/handlers.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("./render").LogoEntity} LogoEntity
|
|
3
|
+
*
|
|
4
|
+
* @typedef {Object} Api
|
|
5
|
+
* @property {(id: string) => LogoEntity} getEntity
|
|
6
|
+
* @property {(topic: string, payload: any) => void} notify
|
|
7
|
+
*/
|
|
8
|
+
import { closestAncestor, isTouchDevice, saturate } from "./utils"
|
|
9
|
+
|
|
10
|
+
const MAX_HEAD_TILT_X = 400
|
|
11
|
+
const MAX_HEAD_TILT_Y = 400
|
|
12
|
+
const HALF = 0.5
|
|
13
|
+
const FIRST_ITEM = 0
|
|
14
|
+
|
|
15
|
+
const eventType = isTouchDevice() ? "touchmove" : "mousemove"
|
|
16
|
+
let moveListener = null
|
|
17
|
+
|
|
18
|
+
export const logo = {
|
|
19
|
+
/**
|
|
20
|
+
* Initialize interactivity for the given entity.
|
|
21
|
+
* @param {LogoEntity} entity
|
|
22
|
+
* @param {string} id
|
|
23
|
+
* @param {Api} api
|
|
24
|
+
*/
|
|
25
|
+
create(entity, id, api) {
|
|
26
|
+
if (id !== entity.id) return
|
|
27
|
+
|
|
28
|
+
if (!entity.isInteractive) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
startInteraction(entity, api)
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Update the entity head tilt coordinates.
|
|
37
|
+
* @param {LogoEntity} entity
|
|
38
|
+
* @param {{x:number,y:number}} payload
|
|
39
|
+
*/
|
|
40
|
+
coordsChange(entity, { x, y }) {
|
|
41
|
+
entity.x = x
|
|
42
|
+
entity.y = y
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Cleanup listeners when the entity is destroyed.
|
|
47
|
+
* @param {LogoEntity} entity
|
|
48
|
+
* @param {string} id
|
|
49
|
+
*/
|
|
50
|
+
destroy(entity, id) {
|
|
51
|
+
if (id !== entity.id) return
|
|
52
|
+
|
|
53
|
+
stopInteraction()
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function startInteraction(entity, api) {
|
|
58
|
+
moveListener = createMoveListener(entity.id, api)
|
|
59
|
+
document.addEventListener(eventType, moveListener, {
|
|
60
|
+
passive: !entity.isScrollPrevented,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function stopInteraction() {
|
|
65
|
+
document.removeEventListener(eventType, moveListener)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create the move listener used by `create` / `fieldChange`.
|
|
70
|
+
* The listener calculates the position of the pointer relative to
|
|
71
|
+
* the element center and notifies the entity's `coordsChange`.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} entityId
|
|
74
|
+
* @param {Api} api
|
|
75
|
+
* @returns {(event:MouseEvent|Touch) => void}
|
|
76
|
+
*/
|
|
77
|
+
function createMoveListener(entityId, api) {
|
|
78
|
+
return (event) => {
|
|
79
|
+
const { left, top, width, height } = event.target.getBoundingClientRect()
|
|
80
|
+
|
|
81
|
+
const center = {
|
|
82
|
+
x: window.scrollX + left + width * HALF,
|
|
83
|
+
y: window.scrollY + top + height * HALF,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { target, pageX, pageY } =
|
|
87
|
+
eventType === "touchmove" ? event.touches[FIRST_ITEM] : event
|
|
88
|
+
|
|
89
|
+
const entity = api.getEntity(entityId)
|
|
90
|
+
if (entity.isScrollPrevented && closestAncestor(target, "logo")) {
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
api.notify(`#${entityId}:coordsChange`, {
|
|
95
|
+
x: saturate(pageX - center.x, MAX_HEAD_TILT_X),
|
|
96
|
+
y: saturate(pageY - center.y, MAX_HEAD_TILT_Y),
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/index.js
ADDED
package/src/render.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} FaceSpec
|
|
3
|
+
* @property {string} image - svg key (A..Z)
|
|
4
|
+
* @property {boolean} [reverse] - whether the face image should be flipped horizontally
|
|
5
|
+
* @property {boolean} [eye] - whether to render the small eye asset on the face
|
|
6
|
+
*
|
|
7
|
+
* @typedef {Object} LogoEntity
|
|
8
|
+
* @property {string} id
|
|
9
|
+
* @property {number} size
|
|
10
|
+
* @property {number} x
|
|
11
|
+
* @property {number} y
|
|
12
|
+
* @property {[FaceSpec,FaceSpec]} faces - tuple: [leftFace, rightFace]
|
|
13
|
+
* @property {boolean} [isInteractive]
|
|
14
|
+
* @property {boolean} [isScrollPrevented]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { html, styleMap, when } from "@inglorious/web"
|
|
18
|
+
|
|
19
|
+
import eye from "./assets/eye.svg"
|
|
20
|
+
import * as faces from "./assets/faces"
|
|
21
|
+
|
|
22
|
+
const MINUS_FORTY_DEGREES = -0.6981
|
|
23
|
+
const MINUS_FORTY_FIVE_DEGREES = -0.7854
|
|
24
|
+
const DEFAULT_SIZE = 256
|
|
25
|
+
const DEFAULT_COORDS = 0
|
|
26
|
+
const STEP = 0.001
|
|
27
|
+
const HALF = 0.5
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Render the logo template for a `LogoEntity`.
|
|
31
|
+
* The returned value is a lit-html `TemplateResult` produced by `html`.
|
|
32
|
+
*
|
|
33
|
+
* @param {LogoEntity} entity
|
|
34
|
+
* @returns {import('lit-html').TemplateResult}
|
|
35
|
+
*/
|
|
36
|
+
export function render(entity) {
|
|
37
|
+
const { size = DEFAULT_SIZE, x = DEFAULT_COORDS, y = DEFAULT_COORDS } = entity
|
|
38
|
+
const [left, right] = entity.faces
|
|
39
|
+
|
|
40
|
+
return html`<div
|
|
41
|
+
class="iw-logo"
|
|
42
|
+
style=${styleMap({
|
|
43
|
+
"--size": `${size}px`,
|
|
44
|
+
"--transform": `scaleY(1.2) translateZ(-${size}px) rotateX(${
|
|
45
|
+
MINUS_FORTY_DEGREES - STEP * y
|
|
46
|
+
}rad)
|
|
47
|
+
rotateY(${MINUS_FORTY_FIVE_DEGREES + STEP * x}rad)`,
|
|
48
|
+
"--z-translation": `${size * HALF}px`,
|
|
49
|
+
"--left-face-flip": left.reverse ? "rotateY(180deg)" : "none",
|
|
50
|
+
"--right-face-flip": right.reverse ? "rotateY(180deg)" : "none",
|
|
51
|
+
})}
|
|
52
|
+
>
|
|
53
|
+
<div class="iw-logo-cube">
|
|
54
|
+
<div class="iw-logo-cube-face left">
|
|
55
|
+
<img src=${faces[left.image]} alt=${left.image} />
|
|
56
|
+
${when(
|
|
57
|
+
left.eye,
|
|
58
|
+
() =>
|
|
59
|
+
html`<img
|
|
60
|
+
class="iw-logo-cube-face-eye"
|
|
61
|
+
src=${eye}
|
|
62
|
+
alt="left eye"
|
|
63
|
+
/>`,
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<div class="iw-logo-cube-face right">
|
|
67
|
+
<img src=${faces[right.image]} alt=${right.image} />
|
|
68
|
+
${when(
|
|
69
|
+
right.eye,
|
|
70
|
+
() =>
|
|
71
|
+
html`<img
|
|
72
|
+
class="iw-logo-cube-face-eye"
|
|
73
|
+
src=${eye}
|
|
74
|
+
alt="right eye"
|
|
75
|
+
/>`,
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>`
|
|
80
|
+
}
|
package/src/style.css
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.iw-logo {
|
|
2
|
+
width: var(--size);
|
|
3
|
+
height: var(--size);
|
|
4
|
+
perspective: var(--size);
|
|
5
|
+
user-select: none;
|
|
6
|
+
|
|
7
|
+
> .iw-logo-cube {
|
|
8
|
+
height: var(--size);
|
|
9
|
+
transform-style: preserve-3d;
|
|
10
|
+
transform: var(--transform);
|
|
11
|
+
transition: ease-out 0.2s;
|
|
12
|
+
|
|
13
|
+
> .iw-logo-cube-face {
|
|
14
|
+
position: absolute;
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
transform-origin: bottom center;
|
|
18
|
+
|
|
19
|
+
> img {
|
|
20
|
+
position: absolute;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&.left {
|
|
24
|
+
transform: rotateY(0deg) translateZ(var(--z-translation)) skew(12deg);
|
|
25
|
+
|
|
26
|
+
> img {
|
|
27
|
+
transform: var(--left-face-flip);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&.right {
|
|
32
|
+
transform: rotateY(90deg) translateZ(var(--z-translation)) skew(-12deg);
|
|
33
|
+
|
|
34
|
+
> img {
|
|
35
|
+
transform: var(--right-face-flip);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
> .iw-logo-cube-face-eye {
|
|
39
|
+
transform: rotateY(180deg);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function isTouchDevice() {
|
|
2
|
+
if (
|
|
3
|
+
"ontouchstart" in window ||
|
|
4
|
+
(window.DocumentTouch && document instanceof window.DocumentTouch)
|
|
5
|
+
) {
|
|
6
|
+
return true
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// include the 'heartz' as a way to have a non matching mediaQuery to help terminate the join
|
|
10
|
+
// https://git.io/vznFH
|
|
11
|
+
const prefixes = " -webkit- -moz- -o- -ms- ".split(" ")
|
|
12
|
+
var query = ["(", prefixes.join("touch-enabled),("), "heartz", ")"].join("")
|
|
13
|
+
return window.matchMedia(query).matches
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function saturate(num, limit) {
|
|
17
|
+
if (num < -limit) return -limit
|
|
18
|
+
if (num > limit) return limit
|
|
19
|
+
return num
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function closestAncestor(el, className) {
|
|
23
|
+
const limit = 4
|
|
24
|
+
let i = 0
|
|
25
|
+
let closest = el
|
|
26
|
+
while (closest && i < limit) {
|
|
27
|
+
if (
|
|
28
|
+
closest.className == null ||
|
|
29
|
+
typeof closest.className.split !== "function"
|
|
30
|
+
) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const classes = closest.className.split(" ")
|
|
35
|
+
if (classes.includes(className)) {
|
|
36
|
+
return closest
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
closest = closest.parentNode
|
|
40
|
+
i++
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from "vitest"
|
|
5
|
+
|
|
6
|
+
import { closestAncestor, saturate } from "./utils.js"
|
|
7
|
+
|
|
8
|
+
describe("utils", () => {
|
|
9
|
+
describe("saturate", () => {
|
|
10
|
+
it("clamps positive values to the limit", () => {
|
|
11
|
+
expect(saturate(10, 5)).toBe(5)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("clamps negative values to -limit", () => {
|
|
15
|
+
expect(saturate(-10, 5)).toBe(-5)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("returns value when within limits", () => {
|
|
19
|
+
expect(saturate(3, 5)).toBe(3)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe("closestAncestor", () => {
|
|
24
|
+
it("finds an ancestor by class name up to the search depth", () => {
|
|
25
|
+
const root = document.createElement("div")
|
|
26
|
+
root.className = "logo"
|
|
27
|
+
|
|
28
|
+
const child1 = document.createElement("div")
|
|
29
|
+
const child2 = document.createElement("div")
|
|
30
|
+
const leaf = document.createElement("span")
|
|
31
|
+
|
|
32
|
+
root.appendChild(child1)
|
|
33
|
+
child1.appendChild(child2)
|
|
34
|
+
child2.appendChild(leaf)
|
|
35
|
+
|
|
36
|
+
const found = closestAncestor(leaf, "logo")
|
|
37
|
+
expect(found).toBe(root)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("returns null when ancestor not found or invalid className", () => {
|
|
41
|
+
const el = document.createElement("div")
|
|
42
|
+
const result = closestAncestor(el, "non-existent")
|
|
43
|
+
expect(result).toBeUndefined()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface Face {
|
|
2
|
+
/** name of the svg */
|
|
3
|
+
image:
|
|
4
|
+
| "A"
|
|
5
|
+
| "B"
|
|
6
|
+
| "C"
|
|
7
|
+
| "D"
|
|
8
|
+
| "E"
|
|
9
|
+
| "F"
|
|
10
|
+
| "G"
|
|
11
|
+
| "H"
|
|
12
|
+
| "I"
|
|
13
|
+
| "J"
|
|
14
|
+
| "K"
|
|
15
|
+
| "L"
|
|
16
|
+
| "M"
|
|
17
|
+
| "N"
|
|
18
|
+
| "O"
|
|
19
|
+
| "P"
|
|
20
|
+
| "Q"
|
|
21
|
+
| "R"
|
|
22
|
+
| "S"
|
|
23
|
+
| "T"
|
|
24
|
+
| "U"
|
|
25
|
+
| "V"
|
|
26
|
+
| "W"
|
|
27
|
+
| "X"
|
|
28
|
+
| "Y"
|
|
29
|
+
| "Z"
|
|
30
|
+
/** whether this face should be flipped horizontally */
|
|
31
|
+
reverse?: boolean
|
|
32
|
+
/** whether the small eye asset should be rendered */
|
|
33
|
+
eye?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LogoEntity {
|
|
37
|
+
id?: string
|
|
38
|
+
type: string
|
|
39
|
+
size: number
|
|
40
|
+
x: number
|
|
41
|
+
y: number
|
|
42
|
+
faces: [Face, Face]
|
|
43
|
+
isInteractive?: boolean
|
|
44
|
+
isScrollPrevented?: boolean
|
|
45
|
+
}
|
package/.babelrc
DELETED
package/.eslintrc.cjs
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
root: true,
|
|
3
|
-
env: { browser: true, es2020: true },
|
|
4
|
-
extends: [
|
|
5
|
-
"eslint:recommended",
|
|
6
|
-
"plugin:react/recommended",
|
|
7
|
-
"plugin:react/jsx-runtime",
|
|
8
|
-
"plugin:react-hooks/recommended",
|
|
9
|
-
],
|
|
10
|
-
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
|
11
|
-
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
|
12
|
-
settings: { react: { version: "18.2" } },
|
|
13
|
-
plugins: ["react-refresh", "simple-import-sort"],
|
|
14
|
-
rules: {
|
|
15
|
-
"react-refresh/only-export-components": [
|
|
16
|
-
"warn",
|
|
17
|
-
{ allowConstantExport: true },
|
|
18
|
-
],
|
|
19
|
-
"no-unused-vars": 1,
|
|
20
|
-
"no-console": [1, { allow: ["error"] }],
|
|
21
|
-
"no-magic-numbers": 1,
|
|
22
|
-
"simple-import-sort/imports": "warn",
|
|
23
|
-
"simple-import-sort/exports": "warn",
|
|
24
|
-
},
|
|
25
|
-
overrides: [
|
|
26
|
-
{
|
|
27
|
-
files: "*.test.js",
|
|
28
|
-
rules: {
|
|
29
|
-
"no-magic-numbers": 0,
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
};
|
package/.storybook/main.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
|
2
|
-
const config = {
|
|
3
|
-
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
4
|
-
addons: [
|
|
5
|
-
"@storybook/addon-links",
|
|
6
|
-
"@storybook/addon-essentials",
|
|
7
|
-
"@storybook/addon-onboarding",
|
|
8
|
-
"@storybook/addon-interactions",
|
|
9
|
-
],
|
|
10
|
-
framework: {
|
|
11
|
-
name: "@storybook/react-vite",
|
|
12
|
-
options: {},
|
|
13
|
-
},
|
|
14
|
-
docs: {
|
|
15
|
-
autodocs: "tag",
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
export default config;
|
package/.storybook/preview.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/** @type { import('@storybook/react').Preview } */
|
|
2
|
-
const preview = {
|
|
3
|
-
parameters: {
|
|
4
|
-
actions: { argTypesRegex: "^on[A-Z].*" },
|
|
5
|
-
controls: {
|
|
6
|
-
matchers: {
|
|
7
|
-
color: /(background|color)$/i,
|
|
8
|
-
date: /Date$/,
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export default preview;
|
package/LICENSE.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Copyright © Inglorious Coderz 2023 All rights reserved.
|