get rid of react

This commit is contained in:
nora 2023-02-08 21:52:51 +01:00
parent 6029115be5
commit 6d85b2e5dd
39 changed files with 806 additions and 11491 deletions

1
.gitignore vendored
View file

@ -10,6 +10,7 @@
# production # production
/build /build
/out
# misc # misc
.DS_Store .DS_Store

19
index.html Normal file
View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Redox</title>
</head>
<body>
<canvas id="main-canvas" width="1500" height="700"> </canvas>
<script type="module">
import { start } from "./out/MainDraw.js";
start();
</script>
</body>
</html>

View file

@ -2,42 +2,14 @@
"name": "redox", "name": "redox",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.16.5", "prettier": "^2.8.4",
"@testing-library/react": "^13.4.0", "typescript": "^4.9.5"
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.4.0",
"@types/node": "^18.13.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^3.1.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View file

@ -1,31 +0,0 @@
import React, {useEffect, useRef} from 'react';
import {canvasClick, draw, init, update, canvasMouseMove} from "./draw/MainDraw";
let CANVAS_WIDTH = 1500;
let CANVAS_HEIGHT = 900;
function App() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const context = canvasRef.current!.getContext("2d")!;
init();
const interval = setInterval(() => {
update();
}, 1000 / 60);
draw(context);
return () => clearInterval(interval);
}, [canvasRef]);
return (
<canvas onMouseMove={canvasMouseMove} onClick={canvasClick} ref={canvasRef} height={CANVAS_HEIGHT}
width={CANVAS_WIDTH}/>
);
}
export {CANVAS_WIDTH, CANVAS_HEIGHT};
export default App;

75
src/MainDraw.ts Normal file
View file

@ -0,0 +1,75 @@
import { rect } from "./Shapes.js";
import {
changeMouseProperties,
drawParticles,
initParticles,
updateParticles,
} from "./Particles.js";
import Vector from "./classes/Vector.js";
import {
drawUI,
handleUIClick,
handleUIMouseMove,
initUI,
} from "./ui/main/UI.js";
type FillStyle = string | CanvasGradient | CanvasPattern;
type Ctx = CanvasRenderingContext2D;
let CANVAS_WIDTH = 0;
let CANVAS_HEIGHT = 0;
function start() {
const c = document.getElementById("main-canvas") as HTMLCanvasElement;
CANVAS_WIDTH = c.width;
CANVAS_HEIGHT = c.height;
init();
const context = c.getContext("2d")!;
setInterval(() => {
update();
}, 1000 / 60);
draw(context);
c.addEventListener("mousemove", canvasMouseMove);
c.addEventListener("click", canvasClick);
}
function init() {
initParticles();
initUI();
}
function draw(ctx: Ctx) {
console.log(CANVAS_HEIGHT, CANVAS_WIDTH);
rect(ctx, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, "lightgrey");
drawParticles(ctx);
drawUI(ctx);
requestAnimationFrame(() => draw(ctx));
}
function update() {
updateParticles();
}
function canvasMouseMove(e: MouseEvent) {
changeMouseProperties((old) => ({ ...old, pos: getMousePos(e) }));
handleUIMouseMove(getMousePos(e));
}
function canvasClick(e: MouseEvent) {
handleUIClick(getMousePos(e));
}
function getMousePos(evt: any): Vector {
const rect = evt.currentTarget.getBoundingClientRect();
return new Vector(evt.clientX - rect.left, evt.clientY - rect.top);
}
export type { Ctx, FillStyle };
export { start, CANVAS_HEIGHT, CANVAS_WIDTH };

116
src/Particles.ts Normal file
View file

@ -0,0 +1,116 @@
import { Ctx, CANVAS_WIDTH, CANVAS_HEIGHT } from "./MainDraw.js";
import Particle, { colorFromCharge } from "./classes/Particle.js";
import Vector from "./classes/Vector.js";
import { circle } from "./Shapes.js";
import { LeftClickAction } from "./ui/main/UI.js";
let particles: Particle[] = [];
let mouseProperties: MouseProperties = {
charge: 0,
strength: 1,
pos: new Vector(),
};
export function initParticles() {
particles = [];
const chargeToMass = [1, 1, 1];
for (let i = 0; i < 200; i++) {
const charge = Math.random() < 0.3 ? 0 : Math.random() < 0.5 ? 1 : -1;
particles.push(
new Particle(
new Vector(
Math.random() * (CANVAS_WIDTH - 100) + 50,
Math.random() * (CANVAS_HEIGHT - 100) + 50
),
charge,
chargeToMass[charge + 1]
)
);
}
}
interface MouseProperties {
charge: number;
strength: number;
pos: Vector;
}
export function getMousePosition(): Vector {
return mouseProperties.pos;
}
export function invokeDefaultLeftClickAction(
action: LeftClickAction,
mousePos: Vector
) {
particles = action(mousePos, particles);
}
export function changeMouseProperties(
transformer: (old: MouseProperties) => MouseProperties
) {
mouseProperties = transformer(mouseProperties);
}
export function drawParticles(ctx: Ctx) {
particles.forEach((p) => p.draw(ctx));
circle(
ctx,
mouseProperties.pos.x,
mouseProperties.pos.y,
5,
colorFromCharge(mouseProperties.charge)
);
}
export function updateParticles() {
calculateChargedForces();
particles.forEach((p) => p.update());
}
function calculateChargedForces() {
for (let i = 0; i < particles.length; i++) {
const particle = particles[i];
if (particle.charge !== 0) {
for (let j = i + 1; j < particles.length; j++) {
const innerParticle = particles[j];
if (innerParticle.charge !== 0) {
const dist = particle.position.distance(innerParticle.position);
if (dist < 300 && dist > 0) {
const f1 = innerParticle.position
.sub(particle.position)
.scale(0.5 / dist ** 2);
/*
that does not actually work because the world does not work like that
but there is probably something missing here if the charges aren't equal
.scale(Math.max(Math.abs(particle.charge), Math.abs(innerParticle.charge))
/ Math.min(Math.abs(particle.charge), Math.abs(innerParticle.charge)));
*/
if (particle.charge === innerParticle.charge) {
particle.applyForce(f1.negated());
innerParticle.applyForce(f1);
} else {
particle.applyForce(f1);
innerParticle.applyForce(f1.negated());
}
}
}
}
// mouse
const dist = particle.position.distance(mouseProperties.pos);
if (mouseProperties.charge !== 0 && dist < 10000 && dist > 0.3) {
const f1 = mouseProperties.pos
.sub(particle.position)
.scale(0.5 / dist ** 2)
.scale(mouseProperties.strength * 5);
if (particle.charge === mouseProperties.charge) {
particle.applyForce(f1.negated());
} else {
particle.applyForce(f1);
}
}
}
}
}

39
src/Shapes.ts Normal file
View file

@ -0,0 +1,39 @@
import { Ctx, FillStyle } from "./MainDraw.js";
export function rect(
ctx: Ctx,
x: number,
y: number,
w: number,
h: number,
color: FillStyle = "black"
): void {
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
}
export function circle(
ctx: Ctx,
x: number,
y: number,
r: number,
color: FillStyle = "black"
): void {
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(x, y, r, r, 0, 0, 50);
ctx.fill();
}
export function text(
ctx: Ctx,
x: number,
y: number,
text: string,
fontSize: number = 15,
color: FillStyle = "black"
) {
ctx.fillStyle = color;
ctx.font = `${fontSize}px Arial`;
ctx.fillText(text, x, y);
}

100
src/classes/Particle.ts Normal file
View file

@ -0,0 +1,100 @@
import Vector from "./Vector.js";
import SimObject from "./SimObject.js";
import { CANVAS_HEIGHT, CANVAS_WIDTH, Ctx, FillStyle } from "../MainDraw.js";
import { circle } from "../Shapes.js";
const PARTICLE_SIZE = 5;
const PARTICLE_EDGE_REPULSION_FORCE = 0.1;
const FRICTION = -0.01;
const RANDOM_ACCELERATION = 0.5;
export default class Particle implements SimObject {
private _position: Vector;
private _velocity: Vector;
private _acceleration: Vector;
private readonly _mass: number;
private readonly _charge: number;
constructor(position: Vector, charge = 0, mass = 1) {
this._position = position;
this._velocity = new Vector();
this._acceleration = new Vector();
this._charge = charge;
this._mass = mass;
}
public applyForce(force: Vector) {
this._acceleration = this._acceleration.add(force);
}
public draw(ctx: Ctx): void {
circle(
ctx,
this._position.x,
this._position.y,
PARTICLE_SIZE,
colorFromCharge(this._charge)
);
}
public update(): void {
// random movement
if (
this._acceleration.magnitude() < 1 &&
this._velocity.magnitude() < 0.2 &&
Math.random() > 0.4
) {
this.applyForce(
new Vector(
(Math.random() - 0.5) * RANDOM_ACCELERATION,
(Math.random() - 0.5) * RANDOM_ACCELERATION
)
);
}
if (this._position.x < 50) {
this.applyForce(new Vector(PARTICLE_EDGE_REPULSION_FORCE, 0));
}
if (this._position.x > CANVAS_WIDTH - 50) {
this.applyForce(new Vector(-PARTICLE_EDGE_REPULSION_FORCE, 0));
}
if (this._position.y > CANVAS_HEIGHT - 50) {
this.applyForce(new Vector(0, -PARTICLE_EDGE_REPULSION_FORCE));
}
if (this._position.y < 50) {
this.applyForce(new Vector(0, PARTICLE_EDGE_REPULSION_FORCE));
}
this._velocity = this._velocity.add(
this._acceleration
.add(
new Vector(this._velocity.x * FRICTION, this._velocity.y * FRICTION)
)
.scaleInverse(this._mass)
);
this._position = this._position.add(this._velocity);
this._acceleration = new Vector();
}
public get charge() {
return this._charge;
}
public get position() {
return this._position;
}
set position(value: Vector) {
this._position = value;
}
}
export function colorFromCharge(charge: number): FillStyle {
if (charge === 0) {
return "black";
}
if (charge < 0) {
return "blue";
}
return "red";
}

7
src/classes/SimObject.ts Normal file
View file

@ -0,0 +1,7 @@
import { Ctx } from "../MainDraw.js";
export default interface SimObject {
draw(ctx: Ctx): void;
update(): void;
}

42
src/classes/Vector.ts Normal file
View file

@ -0,0 +1,42 @@
export default class Vector {
public x: number;
public y: number;
constructor(x: number = 0, y: number = 0) {
this.x = x;
this.y = y;
}
public add(b: Vector): Vector {
return new Vector(this.x + b.x, this.y + b.y);
}
public sub(b: Vector): Vector {
return new Vector(this.x - b.x, this.y - b.y);
}
public scale(n: number): Vector {
return new Vector(this.x * n, this.y * n);
}
public scaleInverse(n: number): Vector {
return new Vector(this.x / n, this.y / n);
}
public magnitude(): number {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
public distance(b: Vector) {
return Math.sqrt((this.x - b.x) ** 2 + (this.y - b.y) ** 2);
}
public negated(): Vector {
return new Vector(-this.x, -this.y);
}
public normalized(): Vector {
const factor = this.magnitude();
return new Vector(this.x / factor, this.y / factor);
}
}

View file

@ -1,46 +0,0 @@
import {rect} from "./Shapes";
import {changeMouseProperties, drawParticles, initParticles, updateParticles} from "./Particles";
import {CANVAS_HEIGHT, CANVAS_WIDTH} from "../App";
import {MouseEvent} from "react";
import Vector from "./classes/Vector";
import {drawUI, handleUIClick, handleUIMouseMove, initUI} from "./ui/main/UI";
type FillStyle = string | CanvasGradient | CanvasPattern;
type Ctx = CanvasRenderingContext2D;
type MouseEvt = MouseEvent<HTMLCanvasElement>;
function init() {
initParticles();
initUI();
}
function draw(ctx: Ctx) {
rect(ctx, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, "lightgrey");
drawParticles(ctx);
drawUI(ctx);
requestAnimationFrame(() => draw(ctx));
}
function update() {
updateParticles();
}
function canvasMouseMove(e: MouseEvt) {
changeMouseProperties(old => ({...old, pos: getMousePos(e)}));
handleUIMouseMove(getMousePos(e));
}
function canvasClick(e: MouseEvt) {
handleUIClick(getMousePos(e));
}
function getMousePos(evt: MouseEvt): Vector {
const rect = evt.currentTarget.getBoundingClientRect();
return new Vector(
evt.clientX - rect.left,
evt.clientY - rect.top
);
}
export type {Ctx, FillStyle, MouseEvt};
export {update, init, draw, canvasClick, canvasMouseMove};

View file

@ -1,94 +0,0 @@
import {Ctx} from "./MainDraw";
import Particle, {colorFromCharge} from "./classes/Particle";
import Vector from "./classes/Vector";
import {CANVAS_HEIGHT, CANVAS_WIDTH} from "../App";
import {circle} from "./Shapes";
import {LeftClickAction} from "./ui/main/UI";
let particles: Particle[] = [];
let mouseProperties: MouseProperties = {charge: 0, strength: 1, pos: new Vector()};
export function initParticles() {
particles = [];
const chargeToMass = [1, 1, 1];
for (let i = 0; i < 200; i++) {
const charge = Math.random() < 0.3 ? 0 : Math.random() < 0.5 ? 1 : -1;
particles.push(new Particle(new Vector(
Math.random() * (CANVAS_WIDTH - 100) + 50,
Math.random() * (CANVAS_HEIGHT - 100) + 50
), charge, chargeToMass[charge + 1]));
}
}
interface MouseProperties {
charge: number,
strength: number,
pos: Vector
}
export function getMousePosition(): Vector {
return mouseProperties.pos;
}
export function invokeDefaultLeftClickAction(action: LeftClickAction, mousePos: Vector) {
particles = action(mousePos, particles);
}
export function changeMouseProperties(transformer: (old: MouseProperties) => MouseProperties) {
mouseProperties = transformer(mouseProperties);
}
export function drawParticles(ctx: Ctx) {
particles.forEach(p => p.draw(ctx));
circle(ctx, mouseProperties.pos.x, mouseProperties.pos.y, 5, colorFromCharge(mouseProperties.charge));
}
export function updateParticles() {
calculateChargedForces();
particles.forEach(p => p.update());
}
function calculateChargedForces() {
for (let i = 0; i < particles.length; i++) {
const particle = particles[i];
if (particle.charge !== 0) {
for (let j = i + 1; j < particles.length; j++) {
const innerParticle = particles[j];
if (innerParticle.charge !== 0) {
const dist = particle.position.distance(innerParticle.position);
if (dist < 300 && dist > 0) {
const f1 = innerParticle.position.sub(particle.position).scale(0.5 / dist ** 2)
/*
that does not actually work because the world does not work like that
but there is probably something missing here if the charges aren't equal
.scale(Math.max(Math.abs(particle.charge), Math.abs(innerParticle.charge))
/ Math.min(Math.abs(particle.charge), Math.abs(innerParticle.charge)));
*/
if (particle.charge === innerParticle.charge) {
particle.applyForce(f1.negated());
innerParticle.applyForce(f1);
} else {
particle.applyForce(f1);
innerParticle.applyForce(f1.negated());
}
}
}
}
// mouse
const dist = particle.position.distance(mouseProperties.pos);
if (mouseProperties.charge !== 0 && dist < 10000 && dist > 0.30) {
const f1 = mouseProperties.pos
.sub(particle.position)
.scale(0.5 / dist ** 2)
.scale(mouseProperties.strength * 5);
if (particle.charge === mouseProperties.charge) {
particle.applyForce(f1.negated());
} else {
particle.applyForce(f1);
}
}
}
}
}

View file

@ -1,20 +0,0 @@
import {Ctx, FillStyle} from "./MainDraw";
export function rect(ctx: Ctx, x: number, y: number, w: number, h: number, color: FillStyle = "black"): void {
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
}
export function circle(ctx: Ctx, x: number, y: number, r: number, color: FillStyle = "black"): void {
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(x, y, r, r, 0, 0, 50);
ctx.fill();
}
export function text(ctx: Ctx, x: number, y: number, text: string, fontSize: number = 15, color: FillStyle = "black") {
ctx.fillStyle = color;
ctx.font = `${fontSize}px Arial`;
ctx.fillText(text, x, y);
}

View file

@ -1,85 +0,0 @@
import Vector from "./Vector";
import SimObject from "./SimObject";
import {Ctx, FillStyle} from "../MainDraw";
import {circle} from "../Shapes";
import {CANVAS_HEIGHT, CANVAS_WIDTH} from "../../App";
const PARTICLE_SIZE = 5;
const PARTICLE_EDGE_REPULSION_FORCE = 0.1;
const FRICTION = -0.01;
const RANDOM_ACCELERATION = 0.5;
export default class Particle implements SimObject {
private _position: Vector;
private _velocity: Vector;
private _acceleration: Vector;
private readonly _mass: number;
private readonly _charge: number;
constructor(position: Vector, charge = 0, mass = 1) {
this._position = position;
this._velocity = new Vector();
this._acceleration = new Vector();
this._charge = charge;
this._mass = mass;
}
public applyForce(force: Vector) {
this._acceleration = this._acceleration.add(force);
}
public draw(ctx: Ctx): void {
circle(ctx, this._position.x, this._position.y, PARTICLE_SIZE, colorFromCharge(this._charge));
}
public update(): void {
// random movement
if (this._acceleration.magnitude() < 1 && this._velocity.magnitude() < 0.2 && Math.random() > 0.4) {
this.applyForce(new Vector((Math.random() - 0.5) * RANDOM_ACCELERATION, (Math.random() - 0.5) * RANDOM_ACCELERATION));
}
if (this._position.x < 50) {
this.applyForce(new Vector(PARTICLE_EDGE_REPULSION_FORCE, 0));
}
if (this._position.x > CANVAS_WIDTH - 50) {
this.applyForce(new Vector(-PARTICLE_EDGE_REPULSION_FORCE, 0));
}
if (this._position.y > CANVAS_HEIGHT - 50) {
this.applyForce(new Vector(0, -PARTICLE_EDGE_REPULSION_FORCE));
}
if (this._position.y < 50) {
this.applyForce(new Vector(0, PARTICLE_EDGE_REPULSION_FORCE));
}
this._velocity = this._velocity
.add(this._acceleration
.add(new Vector(this._velocity.x * FRICTION, this._velocity.y * FRICTION))
.scaleInverse(this._mass));
this._position = this._position.add(this._velocity);
this._acceleration = new Vector();
}
public get charge() {
return this._charge;
}
public get position() {
return this._position;
}
set position(value: Vector) {
this._position = value;
}
}
export function colorFromCharge(charge: number): FillStyle {
if (charge === 0) {
return "black";
}
if (charge < 0) {
return "blue";
}
return "red";
}

View file

@ -1,7 +0,0 @@
import {Ctx} from "../MainDraw";
export default interface SimObject {
draw(ctx: Ctx): void;
update(): void;
}

View file

@ -1,42 +0,0 @@
export default class Vector {
public x: number;
public y: number;
constructor(x: number = 0, y: number = 0) {
this.x = x;
this.y = y;
}
public add(b: Vector): Vector {
return new Vector(this.x + b.x, this.y + b.y);
}
public sub(b: Vector): Vector {
return new Vector(this.x - b.x, this.y - b.y);
}
public scale(n: number): Vector {
return new Vector(this.x * n, this.y * n);
}
public scaleInverse(n: number): Vector {
return new Vector(this.x / n, this.y / n);
}
public magnitude(): number {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
public distance(b: Vector) {
return Math.sqrt((this.x - b.x) ** 2 + (this.y - b.y) ** 2)
}
public negated(): Vector {
return new Vector(-this.x, -this.y);
}
public normalized(): Vector {
const factor = this.magnitude();
return new Vector(this.x / factor, this.y / factor);
}
}

View file

@ -1,76 +0,0 @@
import Button from "./main/Button";
import Vector from "../classes/Vector";
import {Ctx} from "../MainDraw";
import {rect} from "../Shapes";
import {changeMouseProperties} from "../Particles";
enum ChargeState {
NEGATIVE = -1,
NEUTRAL = 0,
POSITIVE = 1,
}
export default class MouseChargeButton extends Button {
private _state: ChargeState;
constructor(pos: Vector, size: Vector) {
super(pos, size);
this._state = ChargeState.NEUTRAL;
}
public draw(ctx: Ctx) {
let color;
switch (this._state) {
case ChargeState.NEGATIVE:
color = this._isHovered ? "rgb(78,78,255)" : "blue";
break;
case ChargeState.NEUTRAL:
color = this._isHovered ? "rgb(147,147,147)" : "grey";
break;
case ChargeState.POSITIVE:
color = this._isHovered ? "rgb(255,82,82)" : "red";
}
rect(ctx, this._position.x, this._position.y, this._size.x, this._size.y, color);
}
public click() {
switch (this._state) {
case ChargeState.NEGATIVE:
this._state = ChargeState.NEUTRAL;
break;
case ChargeState.NEUTRAL:
this._state = ChargeState.POSITIVE;
break;
case ChargeState.POSITIVE:
this._state = ChargeState.NEGATIVE;
}
changeMouseProperties(old => (
{...old, charge: this._state}
));
}
}
export class MouseChargeStrengthButton extends Button {
private _state: number;
constructor(pos: Vector, size: Vector) {
super(pos, size);
this._state = 0;
}
public draw(ctx: Ctx) {
rect(ctx, this._position.x, this._position.y, this._size.x, this._size.y, "grey");
rect(ctx, this._position.x, this._position.y, this._size.x, (this._size.y / (10 / this._state)), "green");
}
public click() {
this._state++;
if (this._state > 10) {
this._state = 1;
}
changeMouseProperties(old => ({
...old,
strength: this._state
}));
}
}

View file

@ -1,5 +0,0 @@
import Button from "./main/Button";
export default class PauseButton extends Button {
}

View file

@ -1,24 +0,0 @@
import {Ctx} from "../../MainDraw";
import Vector from "../../classes/Vector";
import {rect} from "../../Shapes";
import UIComponent from "./UIComponent";
class Button extends UIComponent {
private _clicked: boolean;
constructor(pos: Vector, size: Vector) {
super(pos, size);
this._clicked = false;
}
public draw(ctx: Ctx) {
const color = this._isHovered ? "red" : "grey";
rect(ctx, this._position.x, this._position.y, this._size.x, this._size.y, color);
}
public click() {
this._clicked = true;
}
}
export default Button;

View file

@ -1,57 +0,0 @@
import {Ctx} from "../../MainDraw";
import Vector from "../../classes/Vector";
import {CANVAS_WIDTH} from "../../../App";
import UIComponent from "./UIComponent";
import MouseChargeButton, {MouseChargeStrengthButton} from "../MouseChargeButton";
import initToolbar from "../toolbar/Toolbar";
import Particle from "../../classes/Particle";
import {invokeDefaultLeftClickAction} from "../../Particles";
export type LeftClickAction = (pos: Vector, particles: Particle[]) => Particle[];
export const leftClickNoOp: LeftClickAction = (_, p) => p;
const uiComponents: UIComponent[] = [];
let defaultLeftClickAction: LeftClickAction = leftClickNoOp;
export function initUI() {
uiComponents.push(new MouseChargeButton(
new Vector(CANVAS_WIDTH - 60, 10),
new Vector(50, 50),
));
uiComponents.push(new MouseChargeStrengthButton(
new Vector(CANVAS_WIDTH - 60, 70),
new Vector(50, 50),
));
uiComponents.push(initToolbar());
}
export function handleUIMouseMove(coords: Vector) {
for (let component of uiComponents) {
const isInside = component.isInside(coords);
if (isInside && !component.wasHovered) {
component.onHoverEnter();
} else if (!isInside && component.wasHovered) {
component.onHoverLeave();
}
component.wasHovered = isInside;
}
}
export function setDefaultLeftClickAction(action: LeftClickAction) {
defaultLeftClickAction = action;
}
export function handleUIClick(coords: Vector) {
for (let component of uiComponents) {
if (component.isInside(coords)) {
component.click(coords);
return;
}
}
invokeDefaultLeftClickAction(defaultLeftClickAction, coords);
}
export function drawUI(ctx: Ctx) {
uiComponents.forEach(uic => uic.draw(ctx))
}

View file

@ -1,53 +0,0 @@
import {Ctx} from "../../MainDraw";
import Vector from "../../classes/Vector";
export default abstract class UIComponent {
protected _size: Vector;
protected _position: Vector;
protected _isHovered: boolean;
private _wasHovered: boolean;
protected constructor(pos: Vector, size: Vector) {
this._position = pos;
this._size = size;
this._wasHovered = false;
this._isHovered = false;
}
public abstract draw(ctx: Ctx): void;
public abstract click(mousePos: Vector): void;
public onHoverEnter(): void {
this._isHovered = true;
}
public onHoverLeave(): void {
this._isHovered = false;
}
get wasHovered(): boolean {
return this._wasHovered;
}
set wasHovered(wasHovered) {
this._wasHovered = wasHovered;
}
get size(): Vector {
return this._size;
}
get position(): Vector {
return this._position;
}
public isInside(coords: Vector): boolean {
return coords.x > this._position.x
&& coords.x < this._position.x + this._size.x
&& coords.y > this._position.y
&& coords.y < this._position.y + this._size.y;
}
}

View file

@ -1,130 +0,0 @@
import UIComponent from "../main/UIComponent";
import {Ctx, FillStyle} from "../../MainDraw";
import Vector from "../../classes/Vector";
import {rect, text} from "../../Shapes";
import {CANVAS_HEIGHT} from "../../../App";
import {LeftClickAction, leftClickNoOp, setDefaultLeftClickAction} from "../main/UI";
import Particle from "../../classes/Particle";
import {getMousePosition} from "../../Particles";
export default function initToolbar(): Toolbar {
const toolbar = new Toolbar(
new Vector(50, CANVAS_HEIGHT - 100),
50
);
toolbar.pushTool(new ToolbarTool(
"rgb(255,134,134)",
"rgb(255,0,0)",
(mousePos, particles) => [...particles, new Particle(mousePos, 1)],
"Create a new + particle"))
toolbar.pushTool(new ToolbarTool(
"rgb(156,187,255)",
"rgb(0,84,255)",
(mousePos, particles) => [...particles, new Particle(mousePos, -1)],
"Create a new - particle"
))
toolbar.pushTool(new ToolbarTool(
"rgb(255,203,145)",
"rgb(255,169,0)",
(pos, particles) => particles.filter(p => p.position.distance(pos) > 50),
"Delete all particles near the mouse"
));
toolbar.pushTool(new ToolbarTool(
"rgb(152,255,185)",
"rgb(58,141,0)",
() => [],
"Delete all particles"));
return toolbar;
}
class Toolbar extends UIComponent {
private _tools: ToolbarTool[];
private readonly _scale: number;
private _activeIndex: number;
constructor(pos: Vector, size: number) {
super(pos, new Vector(0, size));
this._tools = [];
this._scale = size;
this._activeIndex = -1;
}
private recalculateSize(): void {
this._size = new Vector(this._scale * this._tools.length, this._scale);
}
pushTool(tool: ToolbarTool): void {
this._tools.push(tool);
this.recalculateSize();
}
click(mousePos: Vector): void {
const index = Math.floor((mousePos.x - this._position.x) / this._scale);
if (this._activeIndex === index) {
this._activeIndex = -1;
setDefaultLeftClickAction(leftClickNoOp)
return;
}
this._activeIndex = index;
setDefaultLeftClickAction(this._tools[index].leftClickAction);
}
draw(ctx: Ctx): void {
let description = undefined;
const mousePos = getMousePosition();
this._tools.forEach((tool, i) => {
tool.drawTool(ctx,
new Vector(
this.position.x + this.scale * i,
this.position.y
),
this._scale,
this._activeIndex === i,
);
const index = Math.floor((mousePos.x - this._position.x) / this._scale);
if (index === i) {
description = tool.description;
}
})
if (description && this._isHovered) {
text(ctx, mousePos.x, mousePos.y, description);
}
}
get scale(): number {
return this._scale;
}
}
class ToolbarTool {
private readonly _leftClickAction: LeftClickAction;
private readonly _color: FillStyle;
private readonly _activeColor: FillStyle;
private readonly _description: string;
constructor(color: FillStyle, activeColor: FillStyle, leftClick: LeftClickAction, description: string) {
this._color = color;
this._activeColor = activeColor;
this._leftClickAction = leftClick;
this._description = description;
}
get description(): string {
return this._description;
}
get leftClickAction(): LeftClickAction {
return this._leftClickAction;
}
drawTool(ctx: Ctx, pos: Vector, size: number, active: boolean): void {
rect(ctx, pos.x, pos.y, size, size, active ? this._activeColor : this._color);
}
}

View file

@ -1,10 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

View file

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View file

@ -0,0 +1,95 @@
import Button from "./main/Button.js";
import Vector from "../classes/Vector.js";
import { Ctx } from "../MainDraw.js";
import { rect } from "../Shapes.js";
import { changeMouseProperties } from "../Particles.js";
enum ChargeState {
NEGATIVE = -1,
NEUTRAL = 0,
POSITIVE = 1,
}
export default class MouseChargeButton extends Button {
private _state: ChargeState;
constructor(pos: Vector, size: Vector) {
super(pos, size);
this._state = ChargeState.NEUTRAL;
}
public draw(ctx: Ctx) {
let color;
switch (this._state) {
case ChargeState.NEGATIVE:
color = this._isHovered ? "rgb(78,78,255)" : "blue";
break;
case ChargeState.NEUTRAL:
color = this._isHovered ? "rgb(147,147,147)" : "grey";
break;
case ChargeState.POSITIVE:
color = this._isHovered ? "rgb(255,82,82)" : "red";
}
rect(
ctx,
this._position.x,
this._position.y,
this._size.x,
this._size.y,
color
);
}
public click() {
switch (this._state) {
case ChargeState.NEGATIVE:
this._state = ChargeState.NEUTRAL;
break;
case ChargeState.NEUTRAL:
this._state = ChargeState.POSITIVE;
break;
case ChargeState.POSITIVE:
this._state = ChargeState.NEGATIVE;
}
changeMouseProperties((old) => ({ ...old, charge: this._state }));
}
}
export class MouseChargeStrengthButton extends Button {
private _state: number;
constructor(pos: Vector, size: Vector) {
super(pos, size);
this._state = 0;
}
public draw(ctx: Ctx) {
rect(
ctx,
this._position.x,
this._position.y,
this._size.x,
this._size.y,
"grey"
);
rect(
ctx,
this._position.x,
this._position.y,
this._size.x,
this._size.y / (10 / this._state),
"green"
);
}
public click() {
this._state++;
if (this._state > 10) {
this._state = 1;
}
changeMouseProperties((old) => ({
...old,
strength: this._state,
}));
}
}

3
src/ui/PauseButton.ts Normal file
View file

@ -0,0 +1,3 @@
import Button from "./main/Button.js";
export default class PauseButton extends Button {}

31
src/ui/main/Button.ts Normal file
View file

@ -0,0 +1,31 @@
import { Ctx } from "../../MainDraw.js";
import Vector from "../../classes/Vector.js";
import { rect } from "../../Shapes.js";
import UIComponent from "./UIComponent.js";
class Button extends UIComponent {
private _clicked: boolean;
constructor(pos: Vector, size: Vector) {
super(pos, size);
this._clicked = false;
}
public draw(ctx: Ctx) {
const color = this._isHovered ? "red" : "grey";
rect(
ctx,
this._position.x,
this._position.y,
this._size.x,
this._size.y,
color
);
}
public click() {
this._clicked = true;
}
}
export default Button;

62
src/ui/main/UI.ts Normal file
View file

@ -0,0 +1,62 @@
import { CANVAS_WIDTH, Ctx } from "../../MainDraw.js";
import Vector from "../../classes/Vector.js";
import UIComponent from "./UIComponent.js";
import MouseChargeButton, {
MouseChargeStrengthButton,
} from "../MouseChargeButton.js";
import initToolbar from "../toolbar/Toolbar.js";
import Particle from "../../classes/Particle.js";
import { invokeDefaultLeftClickAction } from "../../Particles.js";
export type LeftClickAction = (
pos: Vector,
particles: Particle[]
) => Particle[];
export const leftClickNoOp: LeftClickAction = (_, p) => p;
const uiComponents: UIComponent[] = [];
let defaultLeftClickAction: LeftClickAction = leftClickNoOp;
export function initUI() {
uiComponents.push(
new MouseChargeButton(new Vector(CANVAS_WIDTH - 60, 10), new Vector(50, 50))
);
uiComponents.push(
new MouseChargeStrengthButton(
new Vector(CANVAS_WIDTH - 60, 70),
new Vector(50, 50)
)
);
uiComponents.push(initToolbar());
}
export function handleUIMouseMove(coords: Vector) {
for (let component of uiComponents) {
const isInside = component.isInside(coords);
if (isInside && !component.wasHovered) {
component.onHoverEnter();
} else if (!isInside && component.wasHovered) {
component.onHoverLeave();
}
component.wasHovered = isInside;
}
}
export function setDefaultLeftClickAction(action: LeftClickAction) {
defaultLeftClickAction = action;
}
export function handleUIClick(coords: Vector) {
for (let component of uiComponents) {
if (component.isInside(coords)) {
component.click(coords);
return;
}
}
invokeDefaultLeftClickAction(defaultLeftClickAction, coords);
}
export function drawUI(ctx: Ctx) {
uiComponents.forEach((uic) => uic.draw(ctx));
}

View file

@ -0,0 +1,54 @@
import { Ctx } from "../../MainDraw.js";
import Vector from "../../classes/Vector.js";
export default abstract class UIComponent {
protected _size: Vector;
protected _position: Vector;
protected _isHovered: boolean;
private _wasHovered: boolean;
protected constructor(pos: Vector, size: Vector) {
this._position = pos;
this._size = size;
this._wasHovered = false;
this._isHovered = false;
}
public abstract draw(ctx: Ctx): void;
public abstract click(mousePos: Vector): void;
public onHoverEnter(): void {
this._isHovered = true;
}
public onHoverLeave(): void {
this._isHovered = false;
}
get wasHovered(): boolean {
return this._wasHovered;
}
set wasHovered(wasHovered) {
this._wasHovered = wasHovered;
}
get size(): Vector {
return this._size;
}
get position(): Vector {
return this._position;
}
public isInside(coords: Vector): boolean {
return (
coords.x > this._position.x &&
coords.x < this._position.x + this._size.x &&
coords.y > this._position.y &&
coords.y < this._position.y + this._size.y
);
}
}

151
src/ui/toolbar/Toolbar.ts Normal file
View file

@ -0,0 +1,151 @@
import UIComponent from "../main/UIComponent.js";
import { CANVAS_HEIGHT, Ctx, FillStyle } from "../../MainDraw.js";
import Vector from "../../classes/Vector.js";
import { rect, text } from "../../Shapes.js";
import {
LeftClickAction,
leftClickNoOp,
setDefaultLeftClickAction,
} from "../main/UI.js";
import Particle from "../../classes/Particle.js";
import { getMousePosition } from "../../Particles.js";
export default function initToolbar(): Toolbar {
const toolbar = new Toolbar(new Vector(50, CANVAS_HEIGHT - 100), 50);
toolbar.pushTool(
new ToolbarTool(
"rgb(255,134,134)",
"rgb(255,0,0)",
(mousePos, particles) => [...particles, new Particle(mousePos, 1)],
"Create a new + particle"
)
);
toolbar.pushTool(
new ToolbarTool(
"rgb(156,187,255)",
"rgb(0,84,255)",
(mousePos, particles) => [...particles, new Particle(mousePos, -1)],
"Create a new - particle"
)
);
toolbar.pushTool(
new ToolbarTool(
"rgb(255,203,145)",
"rgb(255,169,0)",
(pos, particles) =>
particles.filter((p) => p.position.distance(pos) > 50),
"Delete all particles near the mouse"
)
);
toolbar.pushTool(
new ToolbarTool(
"rgb(152,255,185)",
"rgb(58,141,0)",
() => [],
"Delete all particles"
)
);
return toolbar;
}
class Toolbar extends UIComponent {
private _tools: ToolbarTool[];
private readonly _scale: number;
private _activeIndex: number;
constructor(pos: Vector, size: number) {
super(pos, new Vector(0, size));
this._tools = [];
this._scale = size;
this._activeIndex = -1;
}
private recalculateSize(): void {
this._size = new Vector(this._scale * this._tools.length, this._scale);
}
pushTool(tool: ToolbarTool): void {
this._tools.push(tool);
this.recalculateSize();
}
click(mousePos: Vector): void {
const index = Math.floor((mousePos.x - this._position.x) / this._scale);
if (this._activeIndex === index) {
this._activeIndex = -1;
setDefaultLeftClickAction(leftClickNoOp);
return;
}
this._activeIndex = index;
setDefaultLeftClickAction(this._tools[index].leftClickAction);
}
draw(ctx: Ctx): void {
let description = undefined;
const mousePos = getMousePosition();
this._tools.forEach((tool, i) => {
tool.drawTool(
ctx,
new Vector(this.position.x + this.scale * i, this.position.y),
this._scale,
this._activeIndex === i
);
const index = Math.floor((mousePos.x - this._position.x) / this._scale);
if (index === i) {
description = tool.description;
}
});
if (description && this._isHovered) {
text(ctx, mousePos.x, mousePos.y, description);
}
}
get scale(): number {
return this._scale;
}
}
class ToolbarTool {
private readonly _leftClickAction: LeftClickAction;
private readonly _color: FillStyle;
private readonly _activeColor: FillStyle;
private readonly _description: string;
constructor(
color: FillStyle,
activeColor: FillStyle,
leftClick: LeftClickAction,
description: string
) {
this._color = color;
this._activeColor = activeColor;
this._leftClickAction = leftClick;
this._description = description;
}
get description(): string {
return this._description;
}
get leftClickAction(): LeftClickAction {
return this._leftClickAction;
}
drawTool(ctx: Ctx, pos: Vector, size: number, active: boolean): void {
rect(
ctx,
pos.x,
pos.y,
size,
size,
active ? this._activeColor : this._color
);
}
}

View file

@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -17,10 +13,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "outDir": "out"
"jsx": "react-jsx"
}, },
"include": [ "include": ["src"]
"src"
]
} }

10701
yarn.lock

File diff suppressed because it is too large Load diff