diff --git a/.gitignore b/.gitignore
index 4d29575..ff7c608 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+
+*.iml
+.idea
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index e0151fd..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/src/App.test.tsx b/src/App.test.tsx
deleted file mode 100644
index 2a68616..0000000
--- a/src/App.test.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/App.tsx b/src/App.tsx
index a53698a..8929a42 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,26 +1,33 @@
-import React from 'react';
-import logo from './logo.svg';
-import './App.css';
+import React, {useEffect, useRef} from 'react';
+import {draw, init, update} from "./draw/MainDraw";
+
+let CANVAS_WIDTH = 1500;
+let CANVAS_HEIGHT = 700;
function App() {
- return (
-
- );
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) {
+ return;
+ }
+ const context = canvasRef.current!.getContext("2d")!;
+ init();
+ const interval = setInterval(() => {
+ update();
+ }, 1000 / 30);
+ draw(context);
+ return () => clearInterval(interval);
+ }, [canvasRef]);
+
+ return (
+
+
Redox
+
+ );
}
+export {CANVAS_WIDTH, CANVAS_HEIGHT};
export default App;
diff --git a/src/draw/MainDraw.ts b/src/draw/MainDraw.ts
new file mode 100644
index 0000000..f36182d
--- /dev/null
+++ b/src/draw/MainDraw.ts
@@ -0,0 +1,23 @@
+import {rect} from "./Shapes";
+import {updateParticles, particlesInit, drawParticles} from "./Particles";
+import {CANVAS_HEIGHT, CANVAS_WIDTH} from "../App";
+
+type FillStyle = string | CanvasGradient | CanvasPattern;
+type Ctx = CanvasRenderingContext2D;
+
+function init() {
+ particlesInit();
+}
+
+function draw(ctx: Ctx) {
+ rect(ctx, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT, "lightgrey");
+ drawParticles(ctx);
+ requestAnimationFrame(() => draw(ctx));
+}
+
+function update() {
+ updateParticles();
+}
+
+export type {Ctx, FillStyle};
+export {update, init, draw};
diff --git a/src/draw/Particles.ts b/src/draw/Particles.ts
new file mode 100644
index 0000000..0a3057d
--- /dev/null
+++ b/src/draw/Particles.ts
@@ -0,0 +1,49 @@
+import {Ctx} from "./MainDraw";
+import Particle from "./classes/Particle";
+import Vector from "./classes/Vector";
+import {CANVAS_HEIGHT, CANVAS_WIDTH} from "../App";
+
+const particles: Particle[] = [];
+
+
+export function particlesInit() {
+ for (let i = 0; i < 200; i++) {
+ particles.push(new Particle(new Vector(
+ Math.random() * (CANVAS_WIDTH - 100) + 50,
+ Math.random() * (CANVAS_HEIGHT - 100) + 50
+ ), Math.random() < 0.3 ? 0 : Math.random() < 0.6 ? -1 : 1));
+ }
+}
+
+export function drawParticles(ctx: Ctx) {
+ particles.forEach(p => p.draw(ctx));
+}
+
+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 < 1000 && dist > 0.30) {
+ const f1 = innerParticle.position.sub(particle.position).scale(0.5 / dist ** 2);
+ if (particle.charge === innerParticle.charge) {
+ particle.applyForce(f1.negated());
+ innerParticle.applyForce(f1);
+ } else {
+ particle.applyForce(f1);
+ innerParticle.applyForce(f1.negated());
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/draw/Shapes.ts b/src/draw/Shapes.ts
new file mode 100644
index 0000000..1277af5
--- /dev/null
+++ b/src/draw/Shapes.ts
@@ -0,0 +1,14 @@
+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();
+}
\ No newline at end of file
diff --git a/src/draw/classes/Drawable.ts b/src/draw/classes/Drawable.ts
new file mode 100644
index 0000000..a46d847
--- /dev/null
+++ b/src/draw/classes/Drawable.ts
@@ -0,0 +1,7 @@
+import {Ctx} from "../MainDraw";
+
+export default interface SimObject {
+ draw(ctx: Ctx): void;
+
+ update(): void;
+}
\ No newline at end of file
diff --git a/src/draw/classes/Particle.ts b/src/draw/classes/Particle.ts
new file mode 100644
index 0000000..16d7c6c
--- /dev/null
+++ b/src/draw/classes/Particle.ts
@@ -0,0 +1,73 @@
+import Vector from "./Vector";
+import SimObject from "./Drawable";
+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.99;
+const RANDOM_ACCELERATION = 2;
+
+export default class Particle implements SimObject {
+ private _position: Vector;
+ private _velocity: Vector;
+ // private _color: FillStyle;
+ private _charge: number;
+
+ constructor(position: Vector, /*color = "black", */charge = 0) {
+ this._position = position;
+ this._velocity = new Vector();
+ //this._color = color;
+ this._charge = charge;
+ }
+
+ public applyForce(force: Vector) {
+ this._velocity = this._velocity.add(force);
+ }
+
+ public draw(ctx: Ctx): void {
+ circle(ctx, this._position.x, this._position.y, PARTICLE_SIZE, colorFromCharge(this._charge));
+ }
+
+ public update(): void {
+ this._position = this._position.add(this._velocity);
+ this._velocity = this._velocity.scale(FRICTION);
+
+ // random movement
+ if (this._velocity.magnitude() < 0.1 && 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));
+ }
+ }
+
+ public get charge() {
+ return this._charge;
+ }
+
+ public get position() {
+ return this._position;
+ }
+}
+
+function colorFromCharge(charge: number): FillStyle {
+ if (charge === 0) {
+ return "black";
+ }
+ if (charge < 0) {
+ return "blue";
+ }
+ return "red";
+}
\ No newline at end of file
diff --git a/src/draw/classes/Vector.ts b/src/draw/classes/Vector.ts
new file mode 100644
index 0000000..516631b
--- /dev/null
+++ b/src/draw/classes/Vector.ts
@@ -0,0 +1,38 @@
+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) {
+ return new Vector(this.x - b.x, this.y - b.y);
+ }
+
+ public scale(number: number) {
+ return new Vector(this.x * number, this.y * number);
+ }
+
+ public magnitude() {
+ 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() {
+ return new Vector(-this.x, -this.y);
+ }
+
+ public normalized() {
+ const factor = this.magnitude();
+ return new Vector(this.x / factor, this.y / factor);
+ }
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index ec2585e..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,13 +0,0 @@
-body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
diff --git a/src/index.tsx b/src/index.tsx
index ef2edf8..7173ce5 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,17 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import './index.css';
import App from './App';
-import reportWebVitals from './reportWebVitals';
ReactDOM.render(
,
document.getElementById('root')
-);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
+);
\ No newline at end of file
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 9dfc1c0..0000000
--- a/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts
deleted file mode 100644
index 49a2a16..0000000
--- a/src/reportWebVitals.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ReportHandler } from 'web-vitals';
-
-const reportWebVitals = (onPerfEntry?: ReportHandler) => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/src/setupTests.ts b/src/setupTests.ts
deleted file mode 100644
index 8f2609b..0000000
--- a/src/setupTests.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';