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

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
);
}
}