Merge pull request #8 from dennisschoepf/project-vis

This commit is contained in:
Dennis Schoepf 2021-08-02 13:33:19 +02:00 committed by GitHub
commit 1e04b3a826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 908 additions and 9287 deletions

3
.htmlnanorc.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
minifySvg: false,
};

View file

@ -22,7 +22,24 @@
<textarea id="message-input" placeholder="Bitte gib einen Text ein ..." />
<button type="button" id="message-confirm">Weiter</button>
</div>
<div id="info-message">
<div id="info-message-header">
<h2 id="info-message-headline">Test</h2>
<img id="info-message-img" src="" />
</div>
<div id="info-message-contents">Test</div>
<a id="info-message-link" href="" target="_blank">Check this out on Github</a>
<div id="info-message-close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path
fill="#9CA1A6"
d="M56.5 47.5L78.9 25c1.2-1.2 1.2-3.1 0-4.2-1.2-1.2-3.1-1.2-4.2 0L52.3 43.3 29.8 20.8c-1.2-1.2-3.1-1.2-4.2 0-1.2 1.2-1.2 3.1 0 4.2L48 47.5 25.6 70c-1.2 1.2-1.2 3.1 0 4.2.6.6 1.4.9 2.1.9s1.5-.3 2.1-.9l22.5-22.5 22.5 22.5c.6.6 1.4.9 2.1.9s1.5-.3 2.1-.9c1.2-1.2 1.2-3.1 0-4.2L56.5 47.5z"
/>
</svg>
</div>
</div>
</div>
<div id="backdrop"></div>
<script src="./main.ts"></script>
</body>
</html>

26
main.ts
View file

@ -1,24 +1,26 @@
import p5 from 'p5';
import { SCREEN_WIDTH, SCREEN_HEIGHT } from './src/constants/screen';
import { LegacyScene } from './src/scenes/LegacyScene';
import { DetailScene } from './src/scenes/DetailScene';
import { OverviewScene } from './src/scenes/OverviewScene';
import { Scenes } from './src/scenes/scenes';
import store from './src/store';
import { Companion, CompanionState } from './src/ui/companion';
import { InfoMessage } from './src/ui/info';
const sketch = (s: p5) => {
// Scenes
let overviewScene: OverviewScene;
let legacyScene: LegacyScene;
let detailScene: DetailScene;
s.setup = () => {
s.createCanvas(SCREEN_WIDTH, SCREEN_HEIGHT);
s.noCursor();
new Companion();
new InfoMessage();
overviewScene = new OverviewScene();
legacyScene = new LegacyScene();
detailScene = new DetailScene();
};
s.draw = () => {
@ -26,20 +28,20 @@ const sketch = (s: p5) => {
if (currentScene === Scenes.OVERVIEW) {
overviewScene.draw();
} else if (currentScene === Scenes.LEGACY) {
legacyScene.draw();
} else if (currentScene === Scenes.DETAIL) {
detailScene.draw();
}
};
s.mousePressed = () => {
const { currentScene, companionState } = store.getState();
const { currentScene, companionState, infoMessageShown } = store.getState();
if (companionState === CompanionState.ACTIVE) return;
if (currentScene === Scenes.OVERVIEW) {
overviewScene.onSceneClick();
} else if (currentScene === Scenes.LEGACY) {
legacyScene.onSceneClick();
if (companionState !== CompanionState.ACTIVE || !infoMessageShown) {
if (currentScene === Scenes.OVERVIEW) {
overviewScene.onSceneClick();
} else if (currentScene === Scenes.DETAIL) {
detailScene.onSceneClick();
}
}
};
};

38
metadata/project.json Normal file
View file

@ -0,0 +1,38 @@
{
"subprojects": [
{
"name": "block",
"path": "packages/block",
"size": 2450,
"revealables": [
{
"type": "LEGACY",
"name": "trieNode",
"path": "src/trieNode.ts",
"size": 85,
"contents": "This file is a bit long, you might want to take a clear look and refactor it. The contributors of this subpackage could help here. Try to reveal them to see their contact info.",
"url": "https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/trie/src/trieNode.ts",
"imageUrl": null
},
{
"type": "CONTRIBUTOR",
"name": "Holger Drewes (holgerd77)",
"path": null,
"size": 40,
"contents": "This contributor has a lot of commits in this sub package, below you can find the contact information and a few commits from this repo.",
"url": "https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/trie/src/trieNode.ts",
"imageUrl": "https://avatars.githubusercontent.com/u/931137?v=4"
},
{
"type": "PACKAGE",
"name": "chalk",
"path": "https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/client/package.json",
"size": 65,
"contents": "This package is used in this part of the project. Take a look at the documentation in order to make yourself familiar with it",
"url": "https://github.com/chalk/chalk",
"imageUrl": null
}
]
}
]
}

9260
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@
},
"devDependencies": {
"@types/animejs": "^3.1.4",
"@types/lodash": "^4.14.171",
"@types/p5": "^1.3.0",
"parcel-bundler": "^1.12.5",
"prettier": "^2.3.2",
@ -20,7 +21,9 @@
},
"dependencies": {
"animejs": "^3.2.1",
"lodash": "^4.17.21",
"p5": "^1.4.0",
"rxjs": "^7.2.0",
"zustand": "^3.5.7"
}
}

View file

@ -1 +0,0 @@
export class Api {}

18
src/area.ts Normal file
View file

@ -0,0 +1,18 @@
import { BehaviorSubject, Subject } from 'rxjs';
import { mp5 } from '../main';
import { Area } from './types';
export const playerHead$ = new Subject<Area>();
export const revealedArea$ = new BehaviorSubject<Area>({
x: 0,
y: 0,
w: 0,
});
export function areasColliding(areaOne: Area, areaTwo: Area): boolean {
const distanceBetweenPoints = mp5.dist(areaOne.x, areaOne.y, areaTwo.x, areaTwo.y);
const shapeArea = areaTwo.w / 2 + areaOne.w / 2;
return distanceBetweenPoints < shapeArea;
}

56
src/constants/messages.ts Normal file
View file

@ -0,0 +1,56 @@
import { CompanionMessage } from '../ui/companion';
export const introMessage = {
text: "Hello there 👋 Let's start exploring the <a href='https://github.com/ethereumjs/ethereumjs-monorepo'>ethereumjs<a/> open source project",
inputWanted: false,
};
export const feedbackQuestions: CompanionMessage[] = [
{
text: 'Would you say, that you have learned something about the underlying project from going through this interactive probe? If yes, what have you learned? If no, what was missing from the probe in your opinion?',
inputWanted: true,
},
{
text: 'What was your overall experience going through this probe? What did you like or did not like? Is there anything that stood out for you?',
inputWanted: true,
},
{
text: 'How did you experience the companion (Lower right)? Was it helpful or rather annoying?',
inputWanted: true,
},
{
text: 'Could you imageine yourself using the probe on different projects to learn about them? If so, on which projects would you want to try it out?',
inputWanted: true,
},
{
text: 'Have you felt like any information was missing on the things that were shown within the probe?',
inputWanted: true,
},
{
text: 'Would you have liked to see additional information on the underlying project? If so, what kind of information and how would you have liked its presentation?',
inputWanted: true,
},
{
text: 'Do you have any additional ideas on how playful elements or game mechanics could be used within the onboarding phase of software development projects?',
inputWanted: true,
},
{
text: 'Anything else you want to mention?',
inputWanted: true,
},
];
export const knowledgeQuestions: CompanionMessage[] = [
{
text: 'You would like to ask something on X within the block subproject, who could you ask about that?',
inputWanted: true,
},
{
text: 'What packages that are used in this project can you think of?',
inputWanted: true,
},
{
text: 'Legacy',
inputWanted: true,
},
];

106
src/helpers.ts Normal file
View file

@ -0,0 +1,106 @@
import { mp5 } from '../main';
import { SCREEN_HEIGHT, SCREEN_WIDTH } from './constants/screen';
import { Edge } from './sketchObjects/Edge';
import { RevealableInterface, RevealableTypes } from './sketchObjects/Revealable';
import { Coordinates, JSONSubproject, SubProject } from './types';
export function getEdgeDimensions({ size }: JSONSubproject): number {
const radius = size * 0.05;
return radius > 150 ? 150 : radius;
}
export function generateRandomEdgeCoordinates(): Coordinates {
return {
x: mp5.random(150, SCREEN_WIDTH - 150),
y: mp5.random(150, SCREEN_HEIGHT - 150),
};
}
export function isColliding(
coordinatesToTest: Coordinates,
existingCoordinates: Coordinates[]
): boolean {
return existingCoordinates.some(
(existingCoordinate) =>
mp5.dist(
existingCoordinate.x,
existingCoordinate.y,
coordinatesToTest.x,
coordinatesToTest.y
) < 300
);
}
export function generateEdgeCoords(existingEdges: Edge[]): Coordinates {
let newCoords: Coordinates;
const existingCoordinates = existingEdges.map(({ x, y }) => ({ x, y }));
do {
newCoords = generateRandomEdgeCoordinates();
} while (isColliding(newCoords, existingCoordinates));
return newCoords;
}
export function generateEdges(subprojects: JSONSubproject[]): Edge[] {
let edges = [];
subprojects.forEach((subproject) => {
const coordinates = generateEdgeCoords(edges);
edges.push({
x: coordinates.x,
y: coordinates.y,
r: getEdgeDimensions(subproject),
name: subproject.name,
});
});
return edges.map(
(edge) =>
new Edge({
x: edge.x,
y: edge.y,
r: edge.r,
name: edge.name,
})
);
}
export function getTypedSubproject(name: string, projects: JSONSubproject[]): SubProject {
return projects
.filter((project) => project.name === name)
.map((project) => ({
...project,
revealables: project.revealables.map((revealable) => ({
...revealable,
type: RevealableTypes[revealable.type],
})),
}))[0];
}
export function getRevealablesforSubproject(
subProjectName: string,
subProjects: JSONSubproject[]
): RevealableInterface[] {
return subProjects
.filter((subproject) => subproject.name === subProjectName)[0]
.revealables.map((revealable) => ({
...revealable,
type: RevealableTypes[revealable.type],
}));
}
export function generateRevealableCoords(): Coordinates[] {
const areaWidth = mp5.width / 3;
const rowHeight = mp5.height / 2;
// Max. 6 revealables one in each area
return [
{ x: mp5.random(25, areaWidth), y: mp5.random(25, rowHeight) },
{ x: mp5.random(areaWidth, areaWidth * 2), y: mp5.random(25, rowHeight) },
{ x: mp5.random(areaWidth * 2, areaWidth * 3), y: mp5.random(25, rowHeight) },
{ x: mp5.random(25, areaWidth), y: mp5.random(rowHeight, rowHeight * 2) },
{ x: mp5.random(areaWidth, areaWidth * 2), y: mp5.random(rowHeight, rowHeight * 2) },
{ x: mp5.random(areaWidth * 2, areaWidth * 3), y: mp5.random(rowHeight, rowHeight * 2) },
];
}

75
src/scenes/DetailScene.ts Normal file
View file

@ -0,0 +1,75 @@
import _ from 'lodash';
import { mp5 } from '../../main';
import { colors } from '../constants/colors';
import { generateRevealableCoords } from '../helpers';
import { Player } from '../sketchObjects/Player';
import { Revealable, RevealableInterface } from '../sketchObjects/Revealable';
import store from '../store';
import { Coordinates } from '../types';
import { CompanionState } from '../ui/companion';
import { Scenes } from './scenes';
export class DetailScene {
player: Player;
revealables: RevealableInterface[];
revealableCoords: Coordinates[];
revealableObjects: Revealable[];
constructor() {
this.player = new Player();
store.subscribe((state, prevState) => {
if (!_.isEqual(state.revealables, prevState.revealables)) {
this.revealables = state.revealables;
this.revealableCoords = generateRevealableCoords();
this.revealableObjects = this.revealables.map(
(revealable, i) =>
new Revealable(revealable, {
x: this.revealableCoords[i].x,
y: this.revealableCoords[i].y,
w: this.revealables[i].size,
})
);
}
});
}
draw() {
mp5.background(mp5.color(colors.greyLighter));
this.player.drawOnReveal();
this.player.follow();
this.revealableObjects.forEach((revObj) => {
revObj.draw();
});
this.player.move();
if (
this.revealableObjects.every((revObj) => revObj.wasInteractedWith) &&
!(store.getState().companionState === CompanionState.ACTIVE)
) {
store.setState((state) => ({
finishedSubProjects: [...state.finishedSubProjects, state.currentSubproject],
}));
store.getState().addUserMessage({
text: "Yaay! You've found all of the important parts of this part of the repository. You will be returned to the subproject overview now. Pick the next subproject you want to take a look at there.",
inputWanted: false,
onNext: () => store.setState({ currentScene: Scenes.OVERVIEW }),
showIdle: false,
});
}
}
onSceneClick() {
this.revealableObjects.forEach((revObj) => {
if (revObj.isHovered) {
revObj.onClick();
} else {
this.player.reveal();
}
});
}
}

View file

@ -1,15 +0,0 @@
import { mp5 } from '../../main';
import store from '../store';
import { Scenes } from './scenes';
export class LegacyScene {
constructor() {}
draw() {
mp5.background(100);
}
onSceneClick() {
console.log('Click on legacy scene');
store.setState({ currentScene: Scenes.OVERVIEW });
}
}

View file

@ -2,21 +2,20 @@ import { mp5 } from '../../main';
import { Player } from '../sketchObjects/Player';
import { colors } from '../constants/colors';
import { Edge } from '../sketchObjects/Edge';
import { Scenes } from './scenes';
import store from '../store';
import { generateEdges } from '../helpers';
import { Scenes } from './scenes';
import projectMetadata from '../../metadata/project.json';
import { playerHead$ } from '../area';
import { Area } from '../types';
export class OverviewScene {
player: Player;
edgeData: Array<{ x: number; y: number; r: number; scene: Scenes }>;
playerHead: Area;
edges: Edge[];
constructor() {
this.edgeData = [
{ x: 100, y: 100, r: 50, scene: Scenes.LEGACY },
{ x: 900, y: 400, r: 100, scene: Scenes.LEGACY },
{ x: 300, y: 600, r: 75, scene: Scenes.LEGACY },
];
this.edges = this.edgeData.map((edge) => new Edge(edge.x, edge.y, edge.r));
this.edges = generateEdges(projectMetadata.subprojects);
this.player = new Player();
}
@ -29,15 +28,22 @@ export class OverviewScene {
}
public onSceneClick() {
this.edgeData.forEach((edge, i) => {
this.edges.forEach((edge, i) => {
const dist = mp5.dist(mp5.mouseX, mp5.mouseY, edge.x, edge.y);
if (dist < edge.r) {
store.setState({ currentScene: edge.scene });
store.getState().setProjectMetadata(edge.name);
store.setState({ currentSubproject: edge.name, currentScene: Scenes.DETAIL });
}
});
}
private drawLocations() {
this.edges.forEach((edgeShape) => edgeShape.draw());
this.edges.forEach((edgeShape) => {
if (store.getState().finishedSubProjects.some((fsp) => fsp === edgeShape.name)) {
edgeShape.finished = true;
}
edgeShape.draw();
});
}
}

View file

@ -1,6 +1,4 @@
export enum Scenes {
OVERVIEW = 'OVERVIEW',
LEGACY = 'LEGACY',
PACKAGES = 'PACKAGES',
CONTRIBUTORS = 'CONTRIBUTORS',
DETAIL = 'DETAIL',
}

View file

@ -1,19 +1,65 @@
import { mp5 } from '../../main';
import { areasColliding, playerHead$ } from '../area';
import { colors } from '../constants/colors';
export class Edge {
x: number;
y: number;
r: number;
name: string;
constructor(x: number, y: number, r: number) {
currentSize: number;
maxSize: number;
isHovered: boolean;
hoverColor: any;
finished: boolean;
constructor({ x, y, r, name }: { x: number; y: number; r: number; name: string }) {
this.x = x;
this.y = y;
this.r = r;
this.name = name;
this.maxSize = r * 2 + 20;
this.currentSize = r * 2;
this.hoverColor = mp5.color(colors.red);
this.hoverColor.setAlpha(200);
playerHead$.subscribe((playerHead) => {
this.isHovered = areasColliding(playerHead, { x: this.x, y: this.y, w: this.r * 2 });
});
}
draw() {
mp5.fill(mp5.color(colors.grey));
mp5.ellipse(this.x, this.y, this.r * 2);
if (this.finished) {
mp5.fill(mp5.color(colors.greyLight));
mp5.ellipse(this.x, this.y, this.r * 2);
mp5.textSize(20);
mp5.fill(mp5.color(colors.grey));
mp5.text(`packages/${this.name}`, this.x - this.r / 2, this.y);
} else {
mp5.fill(mp5.color(colors.grey));
if (this.isHovered) {
mp5.fill(mp5.color(this.hoverColor));
if (this.currentSize < this.maxSize) {
this.currentSize++;
} else {
this.currentSize = this.maxSize;
}
} else {
if (this.currentSize > this.r * 2) {
this.currentSize--;
} else {
this.currentSize = this.r * 2;
}
}
mp5.ellipse(this.x, this.y, this.currentSize);
mp5.textSize(20);
mp5.fill(mp5.color(colors.black));
mp5.text(`packages/${this.name}`, this.x - this.r / 2, this.y);
}
}
}

View file

@ -1,4 +1,5 @@
import { mp5 } from '../../main';
import { playerHead$, revealedArea$ } from '../area';
import { colors } from '../constants/colors';
export class Player {
@ -7,6 +8,11 @@ export class Player {
r: number;
easing: number;
history: Array<{ x: number; y: number }> = [];
lastTrailEl: { x: number; y: number };
cursorOnRevealClick: { x: number; y: number };
showRevealEl: boolean;
revealElCoordinates: { x: number; y: number };
lastRevealClickTime: number;
constructor() {
this.x = mp5.height / 2;
@ -28,12 +34,47 @@ export class Player {
this.drawPlayerTrail();
this.drawPlayerShape(this.x, this.y);
playerHead$.next({ x: this.x, y: this.y, w: this.r });
}
public move() {
this.drawCursorIndicator(mp5.mouseX, mp5.mouseY, 4);
}
public reveal() {
const { x, y } = this.lastTrailEl;
if (x && y) {
this.showRevealEl = true;
this.revealElCoordinates = { x, y };
this.cursorOnRevealClick = { x: mp5.mouseX, y: mp5.mouseY };
this.lastRevealClickTime = mp5.millis();
}
}
public drawOnReveal() {
const timeElapsedSinceRevealClick = mp5.millis() - this.lastRevealClickTime;
if (this.showRevealEl) {
const x = this.cursorOnRevealClick.x;
const y = this.cursorOnRevealClick.y;
const w = timeElapsedSinceRevealClick * 0.5;
mp5.fill(mp5.color(colors.greyLighter));
mp5.strokeWeight(5);
mp5.stroke(mp5.color(255));
mp5.ellipse(x, y, w);
revealedArea$.next({ x, y, w });
if (timeElapsedSinceRevealClick > 2000) {
this.showRevealEl = false;
revealedArea$.next({ x: 0, y: 0, w: 0 });
}
}
}
private drawPlayerShape(x: number, y: number) {
mp5.fill(mp5.color(colors.grey));
mp5.noStroke();
@ -48,6 +89,11 @@ export class Player {
private drawPlayerTrail() {
const immediateHistory = this.history.slice(1).slice(-30);
const lastTrailElPosition = immediateHistory[0];
if (lastTrailElPosition) {
this.lastTrailEl = lastTrailElPosition;
}
immediateHistory.forEach((pointInHistory, i) => {
if (i % 5 === 0) {

View file

@ -0,0 +1,174 @@
import { combineLatest } from 'rxjs';
import { mp5 } from '../../main';
import { areasColliding, playerHead$, revealedArea$ } from '../area';
import { colors } from '../constants/colors';
import store from '../store';
import { Area } from '../types';
export enum RevealableTypes {
LEGACY = 'LEGACY',
CONTRIBUTOR = 'CONTRIBUTOR',
PACKAGE = 'PACKAGE',
}
export interface RevealableInterface {
type: RevealableTypes;
name: string;
contents: string;
url: string;
size: number;
path?: string;
imageUrl?: string;
}
enum RevealableStates {
HIDDEN = 'HIDDEN',
REVEALED = 'REVEALED',
FOUND = 'FOUND',
INACTIVE = 'INACTIVE',
}
export class Revealable {
state: RevealableStates = RevealableStates.HIDDEN;
area: Area;
type: RevealableTypes;
name: string;
path: string;
contents: string;
url: string;
imageUrl: string;
isHovered: boolean;
isRevealed: boolean;
wasInteractedWith: boolean;
minSize: number = 5;
currentSize: number;
maxSize: number;
pulseCurrentSize: number;
pulseOpacity: number = 255;
pulseCountUp: boolean;
constructor({ type, name, path, contents, url, imageUrl }: RevealableInterface, area: Area) {
this.type = type;
this.name = name;
this.path = path;
this.contents = contents;
this.url = url;
this.imageUrl = imageUrl;
this.area = area;
this.currentSize = this.minSize;
this.pulseCurrentSize = area.w;
this.maxSize = area.w;
combineLatest([revealedArea$, playerHead$]).subscribe(([revealedArea, playerHead]) => {
const isRevealed = areasColliding(revealedArea, {
x: this.area.x,
y: this.area.y,
w: this.currentSize,
});
const isHovered = areasColliding(playerHead, {
x: this.area.x,
y: this.area.y,
w: this.currentSize,
});
if (this.wasInteractedWith) {
this.state = RevealableStates.INACTIVE;
} else {
if (
((isRevealed && isHovered) || (!isRevealed && isHovered)) &&
(this.state === RevealableStates.REVEALED || this.state === RevealableStates.FOUND)
) {
this.state = RevealableStates.FOUND;
} else if (isRevealed && !isHovered) {
this.state = RevealableStates.REVEALED;
} else {
this.state = RevealableStates.HIDDEN;
}
}
this.isHovered = this.state === RevealableStates.FOUND ? isHovered : false;
this.isRevealed = isRevealed;
});
}
public draw() {
if (this.state === RevealableStates.HIDDEN) {
this.reduceSize();
} else if (this.state === RevealableStates.REVEALED) {
this.increaseSize();
mp5.fill(mp5.color(colors.greyLight));
mp5.ellipse(this.area.x, this.area.y, this.currentSize);
} else if (this.state === RevealableStates.FOUND) {
this.pulsate();
mp5.fill(mp5.color(colors.red));
mp5.ellipse(this.area.x, this.area.y, this.currentSize);
} else if (this.state === RevealableStates.INACTIVE) {
this.reduceSize();
mp5.fill(mp5.color(colors.greyDark));
mp5.ellipse(this.area.x, this.area.y, this.currentSize);
}
}
public onClick() {
if (this.isHovered && !this.wasInteractedWith) {
this.wasInteractedWith = true;
store.getState().addInfoMessage({
headline: this.name,
innerHTML: this.contents,
imgUrl: this.imageUrl,
url: this.url,
});
}
}
private reduceSize() {
if (this.currentSize > this.minSize) {
this.currentSize -= 8;
} else {
this.currentSize = this.minSize;
}
}
private increaseSize() {
if (this.currentSize < this.maxSize) {
this.currentSize += 8;
} else {
this.currentSize = this.maxSize;
}
}
private pulsate() {
const minPulse = this.currentSize;
const maxPulse = this.currentSize + 40;
let color: any = mp5.color(colors.red);
if (this.pulseCountUp) {
this.pulseCurrentSize += 1;
this.pulseOpacity = this.pulseOpacity > 255 ? 255 : this.pulseOpacity - 6;
if (this.pulseCurrentSize > maxPulse) {
this.pulseCountUp = false;
}
} else {
this.pulseCurrentSize -= 6;
if (this.pulseCurrentSize < minPulse) {
this.pulseCountUp = true;
this.pulseOpacity = 255;
}
}
color.setAlpha(this.pulseOpacity);
mp5.fill(mp5.color(color));
mp5.ellipse(this.area.x, this.area.y, this.pulseCurrentSize);
}
}

View file

@ -2,21 +2,47 @@ import create from 'zustand/vanilla';
import { devtools } from 'zustand/middleware';
import { Scenes } from './scenes/scenes';
import { CompanionMessage, CompanionState } from './ui/companion';
import project from '../metadata/project.json';
import { InfoMessageType } from './ui/info';
import { RevealableInterface, RevealableTypes } from './sketchObjects/Revealable';
import { getRevealablesforSubproject } from './helpers';
import { SubProject } from './types';
export interface State {
currentScene: Scenes;
currentSubproject?: string;
companionState: CompanionState;
infoMessageShown: boolean;
infoMessages: InfoMessageType[];
addInfoMessage: (newMessage: InfoMessageType) => void;
userMessages: CompanionMessage[];
addUserMessage: (newMessage: CompanionMessage) => void;
revealables: RevealableInterface[];
finishedSubProjects: string[];
setProjectMetadata: (projectName: string) => void;
}
const store = create<State>(
devtools((set) => ({
currentScene: Scenes.OVERVIEW,
currentSubproject: null,
companionState: CompanionState.IDLE,
infoMessageShown: false,
infoMessages: [],
addInfoMessage: (newMessage) =>
set((state) => ({ ...state, infoMessages: [...state.infoMessages, newMessage] })),
userMessages: [],
addUserMessage: (newMessage) =>
set((state) => ({ userMessages: [...state.userMessages, newMessage] })),
set((state) => ({
userMessages: [...state.userMessages, newMessage],
})),
revealables: [],
finishedSubProjects: [],
setProjectMetadata: (projectName) =>
set((state) => ({
...state,
revealables: getRevealablesforSubproject(projectName, project.subprojects),
})),
}))
);

33
src/types.ts Normal file
View file

@ -0,0 +1,33 @@
import { RevealableInterface } from './sketchObjects/Revealable';
export interface SubProject {
name: string;
path: string;
size: number;
revealables: RevealableInterface[];
}
export interface JSONSubproject {
name: string;
path: string;
size: number;
revealables: Array<{
type: string;
name: string;
contents: string;
url: string;
size: number;
path?: string;
imageUrl?: string;
}>;
}
export interface Coordinates {
x: number;
y: number;
}
export interface Area {
x: number;
y: number;
w: number;
}

View file

@ -11,7 +11,9 @@ export enum CompanionState {
export interface CompanionMessage {
text: string;
inputWanted: boolean;
timestamp: number;
timestamp?: number;
onNext?: () => void;
showIdle?: boolean;
}
export class Companion {
@ -41,9 +43,9 @@ export class Companion {
store.subscribe(
(companionState) => {
if (companionState === CompanionState.ACTIVE) {
this.stopAwaitAnimation();
this.showActiveShape();
this.scaleUpCompanion();
this.stopAwaitAnimation();
this.showMessage(this.message);
} else if (companionState === CompanionState.IDLE) {
this.scaleDownCompanion();
@ -61,21 +63,18 @@ export class Companion {
(state) => state.companionState
);
store.subscribe(
(messages: CompanionMessage[], prevMessages: CompanionMessage[]) => {
if (prevMessages.length !== messages.length) {
const newMessage = messages[messages.length - 1];
this.message = newMessage;
store.subscribe((state, prevState) => {
if (prevState.userMessages.length < state.userMessages.length) {
const newMessage = state.userMessages[state.userMessages.length - 1];
this.message = newMessage;
store.setState({ companionState: CompanionState.AWAIT });
}
},
(state) => state.userMessages
);
store.setState({ companionState: CompanionState.ACTIVE });
}
});
}
showMessage(message: CompanionMessage) {
this.messageTextRef.innerText = message.text;
this.messageTextRef.innerHTML = message.text;
this.messageRef.style.display = 'flex';
if (message.inputWanted) {
@ -86,16 +85,18 @@ export class Companion {
}
confirmMessage() {
console.log(this.message);
if (this.message.inputWanted) {
// Get text from textarea
// Send via API
// TODO: Get text from textarea
// TODO: Send via API
}
// Hide Message
store.setState({ companionState: CompanionState.IDLE });
this.messageRef.style.display = 'none';
store.setState({ companionState: CompanionState.IDLE });
if (this.message.onNext) {
this.message.onNext();
}
}
showActiveShape() {
@ -163,12 +164,12 @@ export class Companion {
}
stopAwaitAnimation() {
anime.remove('#companion-await-indicator');
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() {

91
src/ui/info.ts Normal file
View file

@ -0,0 +1,91 @@
import store from '../store';
export interface InfoMessageType {
headline: string;
innerHTML: string;
imgUrl?: string;
url?: string;
}
export class InfoMessage {
infoMessage: HTMLElement;
infoMessageHeadline: HTMLElement;
infoMessageContents: HTMLElement;
infoMessageClose: HTMLElement;
infoMessageImgRef: HTMLImageElement;
infoMessageLinkRef: HTMLAnchorElement;
backdrop: HTMLElement;
constructor() {
this.infoMessage = document.getElementById('info-message');
this.infoMessageHeadline = document.getElementById('info-message-headline');
this.infoMessageContents = document.getElementById('info-message-contents');
this.infoMessageClose = document.getElementById('info-message-close');
this.infoMessageImgRef = document.getElementById('info-message-img') as HTMLImageElement;
this.infoMessageLinkRef = document.getElementById('info-message-link') as HTMLAnchorElement;
this.backdrop = document.getElementById('backdrop');
this.backdrop.addEventListener('click', this.onBackdropClick);
this.infoMessageClose.addEventListener('click', this.onCloseClick);
store.subscribe((state, prevState) => {
if (state.infoMessageShown) {
this.show();
} else {
this.hide();
}
if (state.infoMessages.length > prevState.infoMessages.length) {
const newMessage = state.infoMessages[state.infoMessages.length - 1];
this.setContents(newMessage.headline, newMessage.innerHTML);
if (newMessage.imgUrl) {
this.setImg(newMessage.imgUrl);
} else {
this.infoMessageImgRef.style.display = 'none';
}
if (newMessage.url) {
this.setLink(newMessage.url);
} else {
this.infoMessageLinkRef.style.display = 'none';
}
store.setState({ infoMessageShown: true });
}
});
}
private setContents(headline: string, innerHTML: string) {
this.infoMessageHeadline.innerText = headline;
this.infoMessageContents.innerHTML = innerHTML;
}
private setImg(imgUrl: string) {
this.infoMessageImgRef.src = imgUrl;
this.infoMessageImgRef.style.display = 'block';
}
private setLink(url: string) {
this.infoMessageLinkRef.href = url;
this.infoMessageLinkRef.style.display = 'block';
}
private show() {
this.infoMessage.style.display = 'block';
this.backdrop.style.display = 'block';
}
private hide() {
this.infoMessage.style.display = 'none';
this.backdrop.style.display = 'none';
}
private onBackdropClick() {
store.setState({ infoMessageShown: false });
}
private onCloseClick() {
store.setState({ infoMessageShown: false });
}
}

View file

@ -142,4 +142,84 @@ button {
border-left: 14px solid white;
}
}
#info-message {
display: none;
position: fixed;
z-index: 10;
top: 50%;
left: 50%;
min-width: 60%;
max-width: 90%;
transform: translate(-50%, -50%);
background-color: #fff;
padding: 2.5rem 4rem;
border-radius: 15px;
--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
#info-message-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 25px;
#info-message-img {
display: none;
height: 100px;
width: 100px;
border-radius: 50%;
}
#info-message-headline {
margin: 0;
text-align: center;
}
}
#info-message-link {
display: none;
min-width: 20%;
padding: 6px 15px;
background-color: transparent;
border: 2px solid black;
border-radius: 5px;
font-weight: bold;
text-decoration: none;
color: black;
text-align: center;
margin-top: 50px;
float: right;
&:hover {
cursor: pointer;
background-color: black;
color: white;
}
}
#info-message-close {
position: absolute;
top: 10px;
right: 10px;
height: 25px;
width: 25px;
&:hover {
cursor: pointer;
}
}
}
}
#backdrop {
display: none;
position: fixed;
z-index: 3;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
}