Merge pull request #7 from dennisschoepf/companion
This commit is contained in:
commit
4053961357
10 changed files with 410 additions and 5 deletions
3
assets/companion/companion_0.svg
Normal file
3
assets/companion/companion_0.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="342" height="296" viewBox="0 0 342 296" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M171 0L341.607 295.5H0.39299L171 0Z" fill="#590C13"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 170 B |
3
assets/companion/companion_1.svg
Normal file
3
assets/companion/companion_1.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="300" height="275" viewBox="0 0 300 275" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M224.013 25L150 0L75.987 25L26.6449 100L4.9344 175L0 225L16.7763 250L75.9868 266.5L150 275L224.013 266.5L283.224 250L300 225L295.066 175L273.355 100L224.013 25Z" fill="#590C13"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 295 B |
18
index.html
18
index.html
|
|
@ -6,7 +6,23 @@
|
|||
<link rel="stylesheet" href="./styles.scss" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="ui"></div>
|
||||
<div class="ui">
|
||||
<div id="companion">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="companion-shape" viewBox="0 0 80 80">
|
||||
<polygon fill="#A60303" points="40,0 68,12 80,40 68,68 40,80 12,68 0,40 12,12"></polygon>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="companion-await-indicator" viewBox="0 0 80 80">
|
||||
<polygon fill="#A60303" points="40,0 68,12 80,40 68,68 40,80 12,68 0,40 12,12"></polygon>
|
||||
</svg>
|
||||
<div id="companion-eye"><div id="companion-pupil"></div></div>
|
||||
<div id="companion-mouth"></div>
|
||||
</div>
|
||||
<div id="message">
|
||||
<div id="message-text"></div>
|
||||
<textarea id="message-input" placeholder="Bitte gib einen Text ein ..." />
|
||||
<button type="button" id="message-confirm">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
8
main.ts
8
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, CompanionState } 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();
|
||||
};
|
||||
|
|
@ -29,7 +32,9 @@ const sketch = (s: p5) => {
|
|||
};
|
||||
|
||||
s.mousePressed = () => {
|
||||
const { currentScene } = store.getState();
|
||||
const { currentScene, companionState } = store.getState();
|
||||
|
||||
if (companionState === CompanionState.ACTIVE) return;
|
||||
|
||||
if (currentScene === Scenes.OVERVIEW) {
|
||||
overviewScene.onSceneClick();
|
||||
|
|
@ -39,4 +44,5 @@ const sketch = (s: p5) => {
|
|||
};
|
||||
};
|
||||
|
||||
// Setup Sketch
|
||||
export const mp5 = new p5(sketch);
|
||||
|
|
|
|||
24
package-lock.json
generated
24
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
export enum Scenes {
|
||||
OVERVIEW = 'OVERVIEW',
|
||||
LEGACY = 'LEGACY',
|
||||
PACKAGES = 'PACKAGES',
|
||||
CONTRIBUTORS = 'CONTRIBUTORS',
|
||||
}
|
||||
|
|
|
|||
15
src/store.ts
15
src/store.ts
|
|
@ -1,12 +1,23 @@
|
|||
import create from 'zustand/vanilla';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { Scenes } from './scenes/scenes';
|
||||
import { CompanionMessage, CompanionState } from './ui/companion';
|
||||
|
||||
export interface State {
|
||||
currentScene: Scenes;
|
||||
companionState: CompanionState;
|
||||
userMessages: CompanionMessage[];
|
||||
addUserMessage: (newMessage: CompanionMessage) => void;
|
||||
}
|
||||
|
||||
const store = create<State>(() => ({
|
||||
const store = create<State>(
|
||||
devtools((set) => ({
|
||||
currentScene: Scenes.OVERVIEW,
|
||||
}));
|
||||
companionState: CompanionState.IDLE,
|
||||
userMessages: [],
|
||||
addUserMessage: (newMessage) =>
|
||||
set((state) => ({ userMessages: [...state.userMessages, newMessage] })),
|
||||
}))
|
||||
);
|
||||
|
||||
export default store;
|
||||
|
|
|
|||
210
src/ui/companion.ts
Normal file
210
src/ui/companion.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import anime from 'animejs/lib/anime.es';
|
||||
import store from '../store';
|
||||
|
||||
export enum CompanionState {
|
||||
IDLE = 'IDLE',
|
||||
AWAIT = 'AWAIT',
|
||||
ACTIVE = 'ACTIVE',
|
||||
SUCCESS = 'SUCCESS',
|
||||
}
|
||||
|
||||
export interface CompanionMessage {
|
||||
text: string;
|
||||
inputWanted: boolean;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export class Companion {
|
||||
ref: HTMLElement;
|
||||
messageRef: HTMLElement;
|
||||
messageTextRef: HTMLElement;
|
||||
messageInputRef: HTMLElement;
|
||||
messageButtonRef: HTMLElement;
|
||||
hoverAnimation: any;
|
||||
message: CompanionMessage;
|
||||
|
||||
constructor() {
|
||||
this.ref = document.getElementById('companion');
|
||||
this.messageRef = document.getElementById('message');
|
||||
this.messageTextRef = document.getElementById('message-text');
|
||||
this.messageInputRef = document.getElementById('message-input');
|
||||
this.messageButtonRef = document.getElementById('message-confirm');
|
||||
|
||||
this.ref.addEventListener('click', () => this.handleClick());
|
||||
this.ref.addEventListener('mouseover', () => this.handleMouseEnter());
|
||||
this.ref.addEventListener('mouseleave', () => this.handleMouseLeave());
|
||||
|
||||
this.messageButtonRef.addEventListener('click', () => this.confirmMessage());
|
||||
|
||||
this.pupilFollowCursor();
|
||||
|
||||
store.subscribe(
|
||||
(companionState) => {
|
||||
if (companionState === CompanionState.ACTIVE) {
|
||||
this.showActiveShape();
|
||||
this.scaleUpCompanion();
|
||||
this.stopAwaitAnimation();
|
||||
this.showMessage(this.message);
|
||||
} else if (companionState === CompanionState.IDLE) {
|
||||
this.scaleDownCompanion();
|
||||
this.showIdleShape();
|
||||
this.stopAwaitAnimation();
|
||||
} else if (companionState === CompanionState.AWAIT) {
|
||||
this.playAwaitAnimation();
|
||||
} else if (companionState === CompanionState.SUCCESS) {
|
||||
this.stopAwaitAnimation();
|
||||
this.playSuccessAnimation();
|
||||
this.scaleDownCompanion();
|
||||
this.showIdleShape();
|
||||
}
|
||||
},
|
||||
(state) => state.companionState
|
||||
);
|
||||
|
||||
store.subscribe(
|
||||
(messages: CompanionMessage[], prevMessages: CompanionMessage[]) => {
|
||||
if (prevMessages.length !== messages.length) {
|
||||
const newMessage = messages[messages.length - 1];
|
||||
this.message = newMessage;
|
||||
|
||||
store.setState({ companionState: CompanionState.AWAIT });
|
||||
}
|
||||
},
|
||||
(state) => state.userMessages
|
||||
);
|
||||
}
|
||||
|
||||
showMessage(message: CompanionMessage) {
|
||||
this.messageTextRef.innerText = message.text;
|
||||
this.messageRef.style.display = 'flex';
|
||||
|
||||
if (message.inputWanted) {
|
||||
this.messageInputRef.style.display = 'block';
|
||||
} else {
|
||||
this.messageInputRef.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
confirmMessage() {
|
||||
console.log(this.message);
|
||||
|
||||
if (this.message.inputWanted) {
|
||||
// Get text from textarea
|
||||
// Send via API
|
||||
}
|
||||
|
||||
// Hide Message
|
||||
store.setState({ companionState: CompanionState.IDLE });
|
||||
this.messageRef.style.display = 'none';
|
||||
}
|
||||
|
||||
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() {
|
||||
document.getElementById('companion-await-indicator').style.display = 'inline';
|
||||
anime({
|
||||
targets: '#companion-await-indicator',
|
||||
keyframes: [
|
||||
{ scale: 4, opacity: 0, rotate: '45deg', duration: 900 },
|
||||
{ scale: 5, duration: 200 },
|
||||
],
|
||||
easing: 'easeOutQuad',
|
||||
duration: 900,
|
||||
loop: true,
|
||||
});
|
||||
}
|
||||
|
||||
stopAwaitAnimation() {
|
||||
const indicator = document.getElementById('companion-await-indicator');
|
||||
indicator.style.display = 'none';
|
||||
indicator.style.opacity = '1';
|
||||
indicator.style.transform = 'scale(1) rotate(0)';
|
||||
|
||||
anime.remove('#companion-await-indicator');
|
||||
}
|
||||
|
||||
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.012
|
||||
)}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();
|
||||
}
|
||||
}
|
||||
}
|
||||
128
styles.scss
128
styles.scss
|
|
@ -4,6 +4,10 @@ body {
|
|||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
main {
|
||||
|
|
@ -15,3 +19,127 @@ main {
|
|||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.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-await-indicator {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#message {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
bottom: 50px;
|
||||
margin-right: 100px;
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
background-color: white;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.15);
|
||||
flex-direction: column;
|
||||
min-width: 70%;
|
||||
|
||||
#message-text {
|
||||
margin-bottom: 25px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#message-input {
|
||||
margin-bottom: 25px;
|
||||
font-size: 16px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 5px;
|
||||
border: 2px solid #b0b7bf;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
#message-confirm {
|
||||
align-self: flex-end;
|
||||
padding: 6px 15px;
|
||||
background-color: transparent;
|
||||
border: 2px solid black;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
max-width: 180px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
bottom: 10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 14px solid transparent;
|
||||
border-bottom: 14px solid transparent;
|
||||
border-left: 14px solid white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue