add editor
This commit is contained in:
parent
c1c6e145a0
commit
e17953ae4a
105
src/canvas.ts
Normal file
105
src/canvas.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { EvHandler, EvHandlerRes, Mouse } from "./input.ts";
|
||||
import { Renderer } from "./renderer.ts";
|
||||
|
||||
export class CanvasRenderer implements Renderer {
|
||||
constructor(
|
||||
private c: HTMLCanvasElement,
|
||||
private g: CanvasRenderingContext2D,
|
||||
) {}
|
||||
|
||||
get width(): number {
|
||||
return this.c.width;
|
||||
}
|
||||
get height(): number {
|
||||
return this.c.height;
|
||||
}
|
||||
|
||||
clear(color: string): void {
|
||||
const { g } = this;
|
||||
g.fillStyle = color;
|
||||
g.fillRect(0, 0, this.c.width, this.c.height);
|
||||
}
|
||||
|
||||
fillRect(x: number, y: number, w: number, h: number, color: string): void {
|
||||
const { g } = this;
|
||||
g.fillStyle = color;
|
||||
g.fillRect(x, y, w, h);
|
||||
}
|
||||
|
||||
fillCirc(x: number, y: number, radius: number, color: string): void {
|
||||
const { g } = this;
|
||||
g.fillStyle = color;
|
||||
g.beginPath();
|
||||
g.arc(x, y, radius, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
}
|
||||
|
||||
putImageData(data: ImageData, x: number, y: number, _hash = 0n): void {
|
||||
const { g } = this;
|
||||
g.putImageData(data, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
export class CanvasMouse implements Mouse {
|
||||
public x = 0;
|
||||
public y = 0;
|
||||
|
||||
private pressHandlers: EvHandler[] = [];
|
||||
private releaseHandlers: EvHandler[] = [];
|
||||
private moveHandlers: EvHandler[] = [];
|
||||
private scrollHandlers: ((direction: "up" | "down") => EvHandlerRes)[] = [];
|
||||
|
||||
constructor(c: HTMLCanvasElement) {
|
||||
c.onmousemove = (ev) => {
|
||||
this.x = ev.x;
|
||||
this.y = ev.y;
|
||||
};
|
||||
c.onmousedown = (ev) => {
|
||||
if (ev.button === 0) {
|
||||
this.runHandlers(this.pressHandlers);
|
||||
}
|
||||
};
|
||||
c.onmouseup = (ev) => {
|
||||
if (ev.button === 0) {
|
||||
this.runHandlers(this.releaseHandlers);
|
||||
}
|
||||
};
|
||||
c.onmousemove = (ev) => {
|
||||
this.x = ev.x;
|
||||
this.y = ev.y;
|
||||
this.runHandlers(this.moveHandlers);
|
||||
};
|
||||
c.onwheel = (ev) => {
|
||||
if (ev.deltaY !== 0) {
|
||||
this.runHandlers(
|
||||
this.scrollHandlers,
|
||||
ev.deltaY < 0 ? "up" : "down",
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private runHandlers<Args extends unknown[] = []>(
|
||||
handlers: EvHandler<Args>[],
|
||||
...args: Args
|
||||
) {
|
||||
for (const handler of handlers.toReversed()) {
|
||||
if (handler(...args) === "stop") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addOnPress(handler: EvHandler) {
|
||||
this.pressHandlers.push(handler);
|
||||
}
|
||||
addOnRelease(handler: EvHandler) {
|
||||
this.releaseHandlers.push(handler);
|
||||
}
|
||||
addOnMove(handler: EvHandler) {
|
||||
this.moveHandlers.push(handler);
|
||||
}
|
||||
addOnScroll(handler: EvHandler<["up" | "down"]>) {
|
||||
this.scrollHandlers.push(handler);
|
||||
}
|
||||
}
|
219
src/grid.ts
Normal file
219
src/grid.ts
Normal file
@ -0,0 +1,219 @@
|
||||
import { Mouse } from "./input.ts";
|
||||
import { Renderer } from "./renderer.ts";
|
||||
|
||||
export class Grid {
|
||||
private t: Transformation = { s: 2, ox: 0, oy: 0 };
|
||||
private transformer = new Transformer(this.t);
|
||||
private tr = new TransformingRenderer(this.t);
|
||||
|
||||
constructor(
|
||||
private mouse: Mouse,
|
||||
) {
|
||||
this.mouse.addOnPress(() => {
|
||||
this.transformer.startPan();
|
||||
return "stop";
|
||||
});
|
||||
this.mouse.addOnRelease(() => {
|
||||
this.transformer.endPan();
|
||||
return "stop";
|
||||
});
|
||||
this.mouse.addOnMove(() => {
|
||||
const { x, y } = this.mouse;
|
||||
this.transformer.move(x, y);
|
||||
return "stop";
|
||||
});
|
||||
this.mouse.addOnScroll((direction) => {
|
||||
if (direction === "up") {
|
||||
this.transformer.zoomIn();
|
||||
} else {
|
||||
this.transformer.zoomOut();
|
||||
}
|
||||
this.tr.clearCache();
|
||||
return "stop";
|
||||
});
|
||||
}
|
||||
|
||||
render(r: Renderer, renderTransformed: (r: Renderer) => void) {
|
||||
this.transformer.updateCanvas(r.width, r.height);
|
||||
this.drawGrid(r);
|
||||
this.tr.render(r, (tr) => {
|
||||
renderTransformed(tr);
|
||||
});
|
||||
}
|
||||
|
||||
private drawGrid(r: Renderer) {
|
||||
const { t: { s, ox, oy } } = this;
|
||||
r.clear("#ddd");
|
||||
|
||||
const dotSpace = 32 * s;
|
||||
|
||||
const dotOffsetX = ox % dotSpace - dotSpace;
|
||||
const dotOffsetY = oy % dotSpace - dotSpace;
|
||||
|
||||
const dotWidth = r.width / dotSpace + 1;
|
||||
const dotHeight = r.height / dotSpace + 1;
|
||||
|
||||
if (s < 0.2) {
|
||||
return;
|
||||
}
|
||||
for (let y = 0; y < Math.min(dotHeight, 200); ++y) {
|
||||
for (let x = 0; x < Math.min(dotWidth, 200); ++x) {
|
||||
r.fillCirc(
|
||||
x * dotSpace + dotOffsetX + 16 * s,
|
||||
y * dotSpace + dotOffsetY + 16 * s,
|
||||
2 * s,
|
||||
"#bbb",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// offset is screen offset
|
||||
type Transformation = {
|
||||
s: number;
|
||||
ox: number;
|
||||
oy: number;
|
||||
};
|
||||
|
||||
class Transformer {
|
||||
private isPanning = false;
|
||||
|
||||
private lastMouseX = 0;
|
||||
private lastMouseY = 0;
|
||||
private lastCanvasWidth = 0;
|
||||
private lastCanvasHeight = 0;
|
||||
|
||||
constructor(
|
||||
private t: Transformation,
|
||||
) {}
|
||||
|
||||
updateCanvas(width: number, height: number) {
|
||||
this.lastCanvasWidth = width;
|
||||
this.lastCanvasHeight = height;
|
||||
}
|
||||
|
||||
startPan() {
|
||||
this.isPanning = true;
|
||||
}
|
||||
|
||||
endPan() {
|
||||
this.isPanning = false;
|
||||
}
|
||||
|
||||
move(x: number, y: number) {
|
||||
const deltaX = x - this.lastMouseX;
|
||||
const deltaY = y - this.lastMouseY;
|
||||
this.lastMouseX = x;
|
||||
this.lastMouseY = y;
|
||||
if (this.isPanning) {
|
||||
this.t.ox += deltaX;
|
||||
this.t.oy += deltaY;
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn() {
|
||||
this.zoom(1 - 1 / 1.1);
|
||||
}
|
||||
zoomOut() {
|
||||
this.zoom(1 - 1.1);
|
||||
}
|
||||
|
||||
private zoom(factor: number) {
|
||||
this.t.s *= 1 + factor;
|
||||
|
||||
const mouseRatioX = (this.lastMouseX - this.t.ox) /
|
||||
this.lastCanvasWidth;
|
||||
const mouseRatioY = (this.lastMouseY - this.t.oy) /
|
||||
this.lastCanvasHeight;
|
||||
|
||||
const deltaOffsetX = this.lastCanvasWidth * factor *
|
||||
mouseRatioX * -1;
|
||||
const deltaOffsetY = this.lastCanvasHeight * factor *
|
||||
mouseRatioY * -1;
|
||||
|
||||
this.t.ox += deltaOffsetX;
|
||||
this.t.oy += deltaOffsetY;
|
||||
}
|
||||
}
|
||||
|
||||
class TransformingRenderer implements Renderer {
|
||||
private imageDataCache = new Map<bigint, ImageData>();
|
||||
private r!: Renderer;
|
||||
|
||||
constructor(
|
||||
private t: Transformation,
|
||||
) {}
|
||||
|
||||
render(r: Renderer, renderTransformed: (r: Renderer) => void): void {
|
||||
this.r = r;
|
||||
renderTransformed(this);
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.imageDataCache.clear();
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this.r.width / this.t.s;
|
||||
}
|
||||
get height(): number {
|
||||
return this.r.height / this.t.s;
|
||||
}
|
||||
clear(color: string): void {
|
||||
this.r.clear(color);
|
||||
}
|
||||
fillRect(x: number, y: number, w: number, h: number, color: string): void {
|
||||
const { r, t: { s, ox, oy } } = this;
|
||||
r.fillRect(x * s + ox, y * s + oy, w * s, h * s, color);
|
||||
}
|
||||
fillCirc(x: number, y: number, radius: number, color: string): void {
|
||||
const { r, t: { s, ox, oy } } = this;
|
||||
r.fillCirc(x * s + ox, y * s + oy, radius * s, color);
|
||||
}
|
||||
putImageData(data: ImageData, x: number, y: number, hash = 0n): void {
|
||||
const { r, t: { s, ox, oy } } = this;
|
||||
|
||||
if (hash === 0n || !this.imageDataCache.has(hash)) {
|
||||
const scaledWidth = data.width * s;
|
||||
const scaledHeight = data.height * s;
|
||||
|
||||
const canvas = new OffscreenCanvas(
|
||||
Math.max(data.width, scaledWidth),
|
||||
Math.max(data.height, scaledHeight),
|
||||
);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.putImageData(data, 0, 0);
|
||||
ctx.scale(s, s);
|
||||
|
||||
const scaledData = ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
);
|
||||
|
||||
if (hash === 0n) {
|
||||
r.putImageData(
|
||||
scaledData,
|
||||
x * s + ox,
|
||||
y * s + oy,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.imageDataCache.size > 128) {
|
||||
this.imageDataCache.delete(
|
||||
this.imageDataCache.keys().take(1).toArray()[0],
|
||||
);
|
||||
}
|
||||
this.imageDataCache.set(hash, scaledData);
|
||||
console.log(this.imageDataCache);
|
||||
}
|
||||
|
||||
r.putImageData(
|
||||
this.imageDataCache.get(hash)!,
|
||||
x * s + ox,
|
||||
y * s + oy,
|
||||
);
|
||||
}
|
||||
}
|
14
src/input.ts
Normal file
14
src/input.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export type EvHandlerRes = "bubble" | "stop";
|
||||
export type EvHandler<Args extends unknown[] = []> = (
|
||||
...args: Args
|
||||
) => EvHandlerRes;
|
||||
|
||||
export interface Mouse {
|
||||
get x(): number;
|
||||
get y(): number;
|
||||
|
||||
addOnPress(handler: EvHandler): void;
|
||||
addOnRelease(handler: EvHandler): void;
|
||||
addOnMove(handler: EvHandler): void;
|
||||
addOnScroll(handler: EvHandler<["up" | "down"]>): void;
|
||||
}
|
20
src/main.ts
20
src/main.ts
@ -1,9 +1,23 @@
|
||||
import { CanvasMouse, CanvasRenderer } from "./canvas.ts";
|
||||
import { Simulator } from "./simulator.ts";
|
||||
|
||||
const c = document.querySelector<HTMLCanvasElement>("canvas#editor")!;
|
||||
const g = c.getContext("2d")!;
|
||||
const g = c.getContext("2d", { alpha: false })!;
|
||||
|
||||
c.width = document.body.clientWidth;
|
||||
c.height = document.body.clientHeight;
|
||||
c.style.position = "absolute";
|
||||
|
||||
g.fillStyle = "#aa2233";
|
||||
g.fillRect(0, 0, c.width, c.height);
|
||||
const r = new CanvasRenderer(c, g);
|
||||
const mouse = new CanvasMouse(c);
|
||||
|
||||
const simulator = new Simulator(mouse);
|
||||
|
||||
function render() {
|
||||
simulator.render(r);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
render();
|
||||
});
|
||||
}
|
||||
render();
|
||||
|
52
src/painter.ts
Normal file
52
src/painter.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { CanvasRenderer } from "./canvas.ts";
|
||||
import { Renderer } from "./renderer.ts";
|
||||
|
||||
export class Painter implements Renderer {
|
||||
private c: OffscreenCanvas;
|
||||
private g: OffscreenCanvasRenderingContext2D;
|
||||
private r: Renderer;
|
||||
|
||||
private hash: bigint = 0n;
|
||||
|
||||
constructor(width: number, height: number) {
|
||||
this.c = new OffscreenCanvas(width, height);
|
||||
this.g = this.c.getContext("2d", { alpha: false })!;
|
||||
this.r = new CanvasRenderer(
|
||||
this.c as unknown as HTMLCanvasElement,
|
||||
this.g as unknown as CanvasRenderingContext2D,
|
||||
);
|
||||
this.rehash();
|
||||
}
|
||||
|
||||
private rehash() {
|
||||
this.hash = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
|
||||
}
|
||||
|
||||
render(r: Renderer, x: number, y: number) {
|
||||
const data = this.g.getImageData(0, 0, this.c.width, this.c.height);
|
||||
r.putImageData(data, x, y, this.hash);
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this.r.width;
|
||||
}
|
||||
get height(): number {
|
||||
return this.r.height;
|
||||
}
|
||||
clear(color: string): void {
|
||||
this.rehash();
|
||||
this.r.clear(color);
|
||||
}
|
||||
fillRect(x: number, y: number, w: number, h: number, color: string): void {
|
||||
this.rehash();
|
||||
this.r.fillRect(x, y, w, h, color);
|
||||
}
|
||||
fillCirc(x: number, y: number, radius: number, color: string): void {
|
||||
this.rehash();
|
||||
this.r.fillCirc(x, y, radius, color);
|
||||
}
|
||||
putImageData(data: ImageData, x: number, y: number, hash?: bigint): void {
|
||||
this.rehash();
|
||||
this.r.putImageData(data, x, y, hash);
|
||||
}
|
||||
}
|
10
src/renderer.ts
Normal file
10
src/renderer.ts
Normal file
@ -0,0 +1,10 @@
|
||||
type N = number;
|
||||
|
||||
export interface Renderer {
|
||||
get width(): N;
|
||||
get height(): N;
|
||||
clear(color: string): void;
|
||||
fillRect(x: N, y: N, w: N, h: N, color: string): void;
|
||||
fillCirc(x: N, y: N, radius: N, color: string): void;
|
||||
putImageData(data: ImageData, x: N, y: N, hash?: bigint): void;
|
||||
}
|
39
src/simulator.ts
Normal file
39
src/simulator.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Grid } from "./grid.ts";
|
||||
import { Mouse } from "./input.ts";
|
||||
import { Painter } from "./painter.ts";
|
||||
import { Renderer } from "./renderer.ts";
|
||||
|
||||
export class Simulator {
|
||||
private grid: Grid;
|
||||
|
||||
private myComponent = new Component();
|
||||
|
||||
constructor(
|
||||
private mouse: Mouse,
|
||||
) {
|
||||
this.grid = new Grid(this.mouse);
|
||||
}
|
||||
|
||||
render(r: Renderer) {
|
||||
r.clear("black");
|
||||
this.grid.render(r, (r) => {
|
||||
this.myComponent.render(r);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Circuit {
|
||||
private components: Component[] = [];
|
||||
}
|
||||
|
||||
class Component {
|
||||
private p = new Painter(64, 64);
|
||||
|
||||
constructor() {
|
||||
this.p.clear("black");
|
||||
}
|
||||
|
||||
render(r: Renderer) {
|
||||
this.p.render(r, 0, 0);
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="bundle.js" defer></script>
|
||||
<script src="bundle.js" type="module" defer></script>
|
||||
<title>LogiCirc</title>
|
||||
</head>
|
||||
<body>
|
||||
|
Loading…
x
Reference in New Issue
Block a user