Merge pull request #8 from dennisschoepf/project-vis
This commit is contained in:
commit
1e04b3a826
22 changed files with 908 additions and 9287 deletions
3
.htmlnanorc.js
Normal file
3
.htmlnanorc.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
minifySvg: false,
|
||||
};
|
||||
17
index.html
17
index.html
|
|
@ -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
26
main.ts
|
|
@ -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
38
metadata/project.json
Normal 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
9260
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export class Api {}
|
||||
18
src/area.ts
Normal file
18
src/area.ts
Normal 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
56
src/constants/messages.ts
Normal 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
106
src/helpers.ts
Normal 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
75
src/scenes/DetailScene.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
export enum Scenes {
|
||||
OVERVIEW = 'OVERVIEW',
|
||||
LEGACY = 'LEGACY',
|
||||
PACKAGES = 'PACKAGES',
|
||||
CONTRIBUTORS = 'CONTRIBUTORS',
|
||||
DETAIL = 'DETAIL',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
174
src/sketchObjects/Revealable.ts
Normal file
174
src/sketchObjects/Revealable.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
28
src/store.ts
28
src/store.ts
|
|
@ -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
33
src/types.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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
91
src/ui/info.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
80
styles.scss
80
styles.scss
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue