From 7cec5fa2fbe178591069fc616d786f4c181ec902 Mon Sep 17 00:00:00 2001 From: Dennis Schoepf Date: Mon, 19 Jul 2021 00:07:13 +0200 Subject: [PATCH] Implement companion styling and animations --- assets/companion/companion_0.svg | 3 + assets/companion/companion_1.svg | 3 + index.html | 10 ++- main.ts | 4 + package-lock.json | 24 ++++++ package.json | 2 + src/store.ts | 3 + src/ui/companion.ts | 131 +++++++++++++++++++++++++++++++ styles.scss | 57 ++++++++++++++ 9 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 assets/companion/companion_0.svg create mode 100644 assets/companion/companion_1.svg create mode 100644 src/ui/companion.ts diff --git a/assets/companion/companion_0.svg b/assets/companion/companion_0.svg new file mode 100644 index 0000000..c2e7149 --- /dev/null +++ b/assets/companion/companion_0.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/companion/companion_1.svg b/assets/companion/companion_1.svg new file mode 100644 index 0000000..ab4a0c6 --- /dev/null +++ b/assets/companion/companion_1.svg @@ -0,0 +1,3 @@ + + + diff --git a/index.html b/index.html index 5625b80..1fb60e5 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,15 @@ -
+
+
+ + + +
+
+
+
diff --git a/main.ts b/main.ts index a67ed42..8f5eaa4 100644 --- a/main.ts +++ b/main.ts @@ -4,6 +4,7 @@ import { LegacyScene } from './src/scenes/LegacyScene'; import { OverviewScene } from './src/scenes/OverviewScene'; import { Scenes } from './src/scenes/scenes'; import store from './src/store'; +import { Companion } from './src/ui/companion'; const sketch = (s: p5) => { // Scenes @@ -14,6 +15,8 @@ const sketch = (s: p5) => { s.createCanvas(SCREEN_WIDTH, SCREEN_HEIGHT); s.noCursor(); + new Companion(); + overviewScene = new OverviewScene(); legacyScene = new LegacyScene(); }; @@ -39,4 +42,5 @@ const sketch = (s: p5) => { }; }; +// Setup Sketch export const mp5 = new p5(sketch); diff --git a/package-lock.json b/package-lock.json index 02897e9..5aa1dc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "animejs": "^3.2.1", "p5": "^1.4.0", "zustand": "^3.5.7" }, "devDependencies": { + "@types/animejs": "^3.1.4", "@types/p5": "^1.3.0", "parcel-bundler": "^1.12.5", "prettier": "^2.3.2", @@ -1767,6 +1769,12 @@ "node": ">= 6.0.0" } }, + "node_modules/@types/animejs": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/animejs/-/animejs-3.1.4.tgz", + "integrity": "sha512-WUjeFT2SXd6intfE6cg6eL1jk/JL88JqM2gC4WqO4iHLmbCvHUq6aoLK13lGpDWs4FtS2PHoYraJZ0dEx99Dyg==", + "dev": true + }, "node_modules/@types/p5": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.3.0.tgz", @@ -1850,6 +1858,11 @@ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "dev": true }, + "node_modules/animejs": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.1.tgz", + "integrity": "sha512-sWno3ugFryK5nhiDm/2BKeFCpZv7vzerWUcUPyAZLDhMek3+S/p418ldZJbJXo5ZUOpfm2kP2XRO4NJcULMy9A==" + }, "node_modules/ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -10415,6 +10428,12 @@ "physical-cpu-count": "^2.0.0" } }, + "@types/animejs": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/animejs/-/animejs-3.1.4.tgz", + "integrity": "sha512-WUjeFT2SXd6intfE6cg6eL1jk/JL88JqM2gC4WqO4iHLmbCvHUq6aoLK13lGpDWs4FtS2PHoYraJZ0dEx99Dyg==", + "dev": true + }, "@types/p5": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.3.0.tgz", @@ -10481,6 +10500,11 @@ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "dev": true }, + "animejs": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.1.tgz", + "integrity": "sha512-sWno3ugFryK5nhiDm/2BKeFCpZv7vzerWUcUPyAZLDhMek3+S/p418ldZJbJXo5ZUOpfm2kP2XRO4NJcULMy9A==" + }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", diff --git a/package.json b/package.json index 6aafd5a..b49613a 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "deploy": "./deploy.sh" }, "devDependencies": { + "@types/animejs": "^3.1.4", "@types/p5": "^1.3.0", "parcel-bundler": "^1.12.5", "prettier": "^2.3.2", @@ -18,6 +19,7 @@ "typescript": "^4.3.5" }, "dependencies": { + "animejs": "^3.2.1", "p5": "^1.4.0", "zustand": "^3.5.7" } diff --git a/src/store.ts b/src/store.ts index cb2be05..7d1de96 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,12 +1,15 @@ import create from 'zustand/vanilla'; import { Scenes } from './scenes/scenes'; +import { CompanionState } from './ui/companion'; export interface State { currentScene: Scenes; + companionState: CompanionState; } const store = create(() => ({ currentScene: Scenes.OVERVIEW, + companionState: CompanionState.IDLE, })); export default store; diff --git a/src/ui/companion.ts b/src/ui/companion.ts new file mode 100644 index 0000000..4bd46ee --- /dev/null +++ b/src/ui/companion.ts @@ -0,0 +1,131 @@ +import anime from 'animejs/lib/anime.es'; +import { reject } from 'core-js/fn/promise'; +import store from '../store'; + +export enum CompanionState { + IDLE = 'IDLE', + AWAIT = 'AWAIT', + ACTIVE = 'ACTIVE', + SUCCESS = 'SUCCESS', +} + +export class Companion { + ref: HTMLElement; + hoverAnimation: any; + + constructor() { + this.ref = document.getElementById('companion'); + this.ref.addEventListener('click', () => this.handleClick()); + this.ref.addEventListener('mouseover', () => this.handleMouseEnter()); + this.ref.addEventListener('mouseleave', () => this.handleMouseLeave()); + + store.subscribe( + (companionState) => { + if (companionState === CompanionState.ACTIVE) { + this.showActiveShape(); + this.scaleUpCompanion(); + this.pupilFollowCursor(); + } else if (companionState === CompanionState.IDLE) { + this.scaleDownCompanion(); + this.showIdleShape(); + } else if (companionState === CompanionState.AWAIT) { + this.playAwaitAnimation(); + } else if (companionState === CompanionState.SUCCESS) { + this.playSuccessAnimation(); + this.scaleDownCompanion(); + this.showIdleShape(); + } + }, + (state) => state.companionState + ); + } + + showActiveShape() { + const mouth = document.getElementById('companion-mouth'); + mouth.style.opacity = '100%'; + + anime({ + targets: '#companion-shape polygon', + points: [{ value: '40,0 68,12 80,40 80,75 40,80 0,75 0,40 12,12' }], + easing: 'easeOutQuad', + duration: 500, + loop: false, + }); + } + + showIdleShape() { + const mouth = document.getElementById('companion-mouth'); + mouth.style.opacity = '0%'; + + anime({ + targets: '#companion-shape polygon', + points: [{ value: '40,0 68,12 80,40 68,68 40,80 12,68 0,40 12,12' }], + easing: 'easeOutQuad', + duration: 500, + loop: false, + }); + } + + scaleUpCompanion() { + anime({ + targets: '#companion', + scale: 3, + translateX: -15, + translateY: -12, + loop: false, + duration: 700, + }); + } + + scaleDownCompanion() { + anime({ + targets: '#companion', + scale: 1, + loop: false, + translateX: 0, + translateY: 0, + duration: 800, + }); + } + + playSuccessAnimation() {} + + playAwaitAnimation() {} + + pupilFollowCursor() { + document.addEventListener('mousemove', (e) => { + const pupil = document.getElementById('companion-pupil'); + const pupDim = pupil.getBoundingClientRect(); + + pupil.style.transform = `translate(${-((pupDim.x - e.pageX) * 0.005)}px, ${-( + (pupDim.y - e.pageY) * + 0.015 + )}px)`; + }); + } + + handleClick() { + const { companionState } = store.getState(); + let newCompanionState: CompanionState; + + if (companionState === CompanionState.ACTIVE) { + newCompanionState = CompanionState.IDLE; + } else { + newCompanionState = CompanionState.ACTIVE; + } + + store.setState({ companionState: newCompanionState }); + } + + handleMouseEnter() { + this.showActiveShape(); + } + + handleMouseLeave() { + const { companionState } = store.getState(); + + if (companionState !== CompanionState.ACTIVE) { + this.showIdleShape(); + } + } +} diff --git a/styles.scss b/styles.scss index 9c7eb52..4db5b97 100644 --- a/styles.scss +++ b/styles.scss @@ -4,6 +4,7 @@ body { padding: 0; height: 100%; overflow: hidden; + position: relative; } main { @@ -15,3 +16,59 @@ main { *::after { box-sizing: border-box; } + +.ui { + position: absolute; + bottom: 40px; + left: 40px; + right: 40px; + display: flex; + justify-content: flex-end; + + #companion { + position: relative; + + &:hover { + cursor: pointer; + } + + #companion-shape { + height: 40px; + width: 40px; + } + + #companion-mouth { + position: absolute; + z-index: 2; + bottom: 16%; + left: calc(50% - 15px); + width: 30px; + height: 15px; + border: solid 3px #590c13; + border-color: #590c13 transparent transparent transparent; + border-radius: 70%/15px 15px 0 0; + transform: rotate(180deg); + opacity: 0; + } + + #companion-eye { + height: 20px; + width: 20px; + top: 20%; + left: calc(50% - 10px); + position: absolute; + border-radius: 50%; + background-color: #ebeef2; + + #companion-pupil { + height: 5px; + width: 5px; + top: 50%; + left: calc(50% - 4px); + position: absolute; + border-radius: 50%; + background-color: #0d0d0d; + } + } + } +}