mirror of
https://git.sfja.dk/sfja/h6-logicirc.git
synced 2025-07-01 23:44:09 +01:00
Compare commits
3 Commits
22488733fb
...
e17953ae4a
Author | SHA1 | Date | |
---|---|---|---|
|
e17953ae4a | ||
|
c1c6e145a0 | ||
|
bd6c3b7e5c |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
build/
|
||||
|
51
bundle.ts
Normal file
51
bundle.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import * as esbuild from "npm:esbuild@0.20.2";
|
||||
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@^0.11.0";
|
||||
|
||||
async function buildCode() {
|
||||
await esbuild.build({
|
||||
plugins: [...denoPlugins()],
|
||||
entryPoints: ["./src/main.ts"],
|
||||
outfile: "./build/bundle.js",
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
});
|
||||
|
||||
esbuild.stop();
|
||||
}
|
||||
|
||||
async function copyStatic(path: string[] = []) {
|
||||
const dir = path.join("/");
|
||||
await Deno.mkdir("build/" + dir).catch((_) => _);
|
||||
for await (const file of Deno.readDir(`static/${dir}`)) {
|
||||
if (file.isDirectory) {
|
||||
await copyStatic([...path, file.name]);
|
||||
continue;
|
||||
}
|
||||
await Deno.copyFile(
|
||||
`static/${dir}/${file.name}`,
|
||||
`build/${dir}/${file.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type BundleOptions = {
|
||||
quiet?: boolean;
|
||||
};
|
||||
|
||||
export async function bundle(options?: BundleOptions) {
|
||||
if (!options?.quiet) {
|
||||
console.log("info: copying static files");
|
||||
}
|
||||
await copyStatic();
|
||||
if (!options?.quiet) {
|
||||
console.log("info: building code");
|
||||
}
|
||||
await buildCode();
|
||||
if (!options?.quiet) {
|
||||
console.log("success: output in 'build/'");
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
await bundle();
|
||||
}
|
13
deno.jsonc
Normal file
13
deno.jsonc
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"tasks": {
|
||||
"bundle": "deno run --allow-read --allow-write --allow-env --allow-run bundle.ts",
|
||||
"dev": "deno run --allow-net --allow-read --allow-write --allow-env --allow-run dev.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
|
||||
},
|
||||
"fmt": {
|
||||
"indentWidth": 4
|
||||
}
|
||||
}
|
||||
|
128
deno.lock
Normal file
128
deno.lock
Normal file
@ -0,0 +1,128 @@
|
||||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@luca/esbuild-deno-loader@0.11": "0.11.1",
|
||||
"jsr:@std/bytes@^1.0.2": "1.0.4",
|
||||
"jsr:@std/encoding@^1.0.5": "1.0.5",
|
||||
"jsr:@std/path@^1.0.6": "1.0.8",
|
||||
"npm:esbuild@0.20.2": "0.20.2"
|
||||
},
|
||||
"jsr": {
|
||||
"@luca/esbuild-deno-loader@0.11.1": {
|
||||
"integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267",
|
||||
"dependencies": [
|
||||
"jsr:@std/bytes",
|
||||
"jsr:@std/encoding",
|
||||
"jsr:@std/path"
|
||||
]
|
||||
},
|
||||
"@std/bytes@1.0.4": {
|
||||
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
|
||||
},
|
||||
"@std/encoding@1.0.5": {
|
||||
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
|
||||
},
|
||||
"@std/path@1.0.8": {
|
||||
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"@esbuild/aix-ppc64@0.20.2": {
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g=="
|
||||
},
|
||||
"@esbuild/android-arm64@0.20.2": {
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg=="
|
||||
},
|
||||
"@esbuild/android-arm@0.20.2": {
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w=="
|
||||
},
|
||||
"@esbuild/android-x64@0.20.2": {
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg=="
|
||||
},
|
||||
"@esbuild/darwin-arm64@0.20.2": {
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA=="
|
||||
},
|
||||
"@esbuild/darwin-x64@0.20.2": {
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA=="
|
||||
},
|
||||
"@esbuild/freebsd-arm64@0.20.2": {
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw=="
|
||||
},
|
||||
"@esbuild/freebsd-x64@0.20.2": {
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw=="
|
||||
},
|
||||
"@esbuild/linux-arm64@0.20.2": {
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A=="
|
||||
},
|
||||
"@esbuild/linux-arm@0.20.2": {
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg=="
|
||||
},
|
||||
"@esbuild/linux-ia32@0.20.2": {
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig=="
|
||||
},
|
||||
"@esbuild/linux-loong64@0.20.2": {
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ=="
|
||||
},
|
||||
"@esbuild/linux-mips64el@0.20.2": {
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA=="
|
||||
},
|
||||
"@esbuild/linux-ppc64@0.20.2": {
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg=="
|
||||
},
|
||||
"@esbuild/linux-riscv64@0.20.2": {
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg=="
|
||||
},
|
||||
"@esbuild/linux-s390x@0.20.2": {
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ=="
|
||||
},
|
||||
"@esbuild/linux-x64@0.20.2": {
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw=="
|
||||
},
|
||||
"@esbuild/netbsd-x64@0.20.2": {
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ=="
|
||||
},
|
||||
"@esbuild/openbsd-x64@0.20.2": {
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ=="
|
||||
},
|
||||
"@esbuild/sunos-x64@0.20.2": {
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w=="
|
||||
},
|
||||
"@esbuild/win32-arm64@0.20.2": {
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ=="
|
||||
},
|
||||
"@esbuild/win32-ia32@0.20.2": {
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ=="
|
||||
},
|
||||
"@esbuild/win32-x64@0.20.2": {
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ=="
|
||||
},
|
||||
"esbuild@0.20.2": {
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"dependencies": [
|
||||
"@esbuild/aix-ppc64",
|
||||
"@esbuild/android-arm",
|
||||
"@esbuild/android-arm64",
|
||||
"@esbuild/android-x64",
|
||||
"@esbuild/darwin-arm64",
|
||||
"@esbuild/darwin-x64",
|
||||
"@esbuild/freebsd-arm64",
|
||||
"@esbuild/freebsd-x64",
|
||||
"@esbuild/linux-arm",
|
||||
"@esbuild/linux-arm64",
|
||||
"@esbuild/linux-ia32",
|
||||
"@esbuild/linux-loong64",
|
||||
"@esbuild/linux-mips64el",
|
||||
"@esbuild/linux-ppc64",
|
||||
"@esbuild/linux-riscv64",
|
||||
"@esbuild/linux-s390x",
|
||||
"@esbuild/linux-x64",
|
||||
"@esbuild/netbsd-x64",
|
||||
"@esbuild/openbsd-x64",
|
||||
"@esbuild/sunos-x64",
|
||||
"@esbuild/win32-arm64",
|
||||
"@esbuild/win32-ia32",
|
||||
"@esbuild/win32-x64"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
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;
|
||||
}
|
23
src/main.ts
Normal file
23
src/main.ts
Normal file
@ -0,0 +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", { alpha: false })!;
|
||||
|
||||
c.width = document.body.clientWidth;
|
||||
c.height = document.body.clientHeight;
|
||||
c.style.position = "absolute";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
13
static/index.html
Normal file
13
static/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="bundle.js" type="module" defer></script>
|
||||
<title>LogiCirc</title>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="editor"></canvas>
|
||||
</body>
|
||||
</html>
|
11
static/style.css
Normal file
11
static/style.css
Normal file
@ -0,0 +1,11 @@
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user