place switches and leds

This commit is contained in:
sfja 2025-05-07 16:26:39 +02:00
parent 0db1c3b90a
commit 505e869e1f
7 changed files with 311 additions and 79 deletions

View File

@ -1,5 +1,5 @@
import { EvHandler, EvHandlerRes, Mouse } from "./input.ts";
import { Renderer } from "./renderer.ts";
import { Renderer, RendererImage } from "./renderer.ts";
export class CanvasRenderer implements Renderer {
constructor(
@ -34,9 +34,15 @@ export class CanvasRenderer implements Renderer {
g.fill();
}
putImageData(data: ImageData, x: number, y: number, _hash = 0n): void {
putImage(
data: RendererImage,
x: number,
y: number,
w = data.width,
h = data.height,
): void {
const { g } = this;
g.putImageData(data, x, y);
g.drawImage(data, x, y, w, h);
}
}

View File

@ -1,14 +1,18 @@
import { Mouse } from "./input.ts";
import { Renderer } from "./renderer.ts";
import { EvHandler, Mouse } from "./input.ts";
import { Renderer, RendererImage } 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);
private _transformingMouse: TransformingMouse;
constructor(
private mouse: Mouse,
) {
this._transformingMouse = new TransformingMouse(this.mouse, this.t);
this.mouse.addOnPress(() => {
this.transformer.startPan();
return "stop";
@ -28,11 +32,14 @@ export class Grid {
} else {
this.transformer.zoomOut();
}
this.tr.clearCache();
return "stop";
});
}
transformingMouse(): Mouse {
return this._transformingMouse;
}
render(r: Renderer, renderTransformed: (r: Renderer) => void) {
this.transformer.updateCanvas(r.width, r.height);
this.drawGrid(r);
@ -138,7 +145,6 @@ class Transformer {
}
class TransformingRenderer implements Renderer {
private imageDataCache = new Map<bigint, ImageData>();
private r!: Renderer;
constructor(
@ -150,10 +156,6 @@ class TransformingRenderer implements Renderer {
renderTransformed(this);
}
clearCache() {
this.imageDataCache.clear();
}
get width(): number {
return this.r.width / this.t.s;
}
@ -171,49 +173,47 @@ class TransformingRenderer implements Renderer {
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 {
putImage(
data: RendererImage,
x: number,
y: number,
w = data.width,
h = data.width,
): 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)!,
r.putImage(
data,
x * s + ox,
y * s + oy,
w * s,
h * s,
);
}
}
class TransformingMouse implements Mouse {
constructor(
private mouse: Mouse,
private t: Transformation,
) {}
get x(): number {
return (this.mouse.x - this.t.ox) / this.t.s;
}
get y(): number {
return (this.mouse.y - this.t.oy) / this.t.s;
}
addOnPress(handler: EvHandler): void {
this.mouse.addOnPress(handler);
}
addOnRelease(handler: EvHandler): void {
this.mouse.addOnRelease(handler);
}
addOnMove(handler: EvHandler): void {
this.mouse.addOnMove(handler);
}
addOnScroll(handler: EvHandler<["up" | "down"]>): void {
this.mouse.addOnScroll(handler);
}
}

View File

@ -7,6 +7,7 @@ const g = c.getContext("2d", { alpha: false })!;
c.width = document.body.clientWidth;
c.height = document.body.clientHeight;
c.style.position = "absolute";
g.imageSmoothingEnabled = false;
const r = new CanvasRenderer(c, g);
const mouse = new CanvasMouse(c);

View File

@ -1,30 +1,22 @@
import { CanvasRenderer } from "./canvas.ts";
import { Renderer } from "./renderer.ts";
import { Renderer, RendererImage } 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.g = this.c.getContext("2d")!;
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);
r.putImage(this.c, x, y, this.c.width, this.c.height);
}
get width(): number {
@ -34,19 +26,21 @@ export class Painter implements Renderer {
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);
putImage(
data: RendererImage,
x: number,
y: number,
w = data.width,
h = data.height,
): void {
this.r.putImage(data, x, y, w, h);
}
}

View File

@ -1,10 +1,15 @@
type N = number;
export type RendererImage =
| HTMLCanvasElement
| OffscreenCanvas
| HTMLImageElement;
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;
putImage(data: RendererImage, x: N, y: N, w?: N, h?: N): void;
}

View File

@ -5,35 +5,260 @@ import { Renderer } from "./renderer.ts";
export class Simulator {
private grid: Grid;
private myComponent = new Component();
private circuit: Circuit;
private tooltip: Tooltip;
private toolbar: Toolbar;
constructor(
private mouse: Mouse,
) {
this.grid = new Grid(this.mouse);
this.circuit = new Circuit(this.grid.transformingMouse());
this.tooltip = new Tooltip(this.circuit, this.grid.transformingMouse());
this.toolbar = new Toolbar(this.mouse, this.tooltip);
}
render(r: Renderer) {
hover: {
if (this.toolbar.hover(this.mouse.x, this.mouse.y) === "break") {
break hover;
}
}
r.clear("black");
this.grid.render(r, (r) => {
this.myComponent.render(r);
this.grid.render(r, (tr) => {
this.circuit.render(tr);
this.toolbar.render(r);
this.tooltip.render(tr);
});
}
}
class Circuit {
private components: Component[] = [];
class Toolbar {
private tools: ComponentFactory[] = [
new SwitchComponent(),
new LedComponent(),
];
private previews: Component[] = [];
private lastWidth = 0;
private lastHeight = 0;
private hoveringComponentIdx?: number;
constructor(
private mouse: Mouse,
private tooltip: Tooltip,
) {
this.fillPreviews();
this.mouse.addOnPress(() => {
const { x, y } = this.mouse;
for (const [i, component] of this.previews.entries()) {
if (
x >= i * 128 + 96 - 48 &&
y >= this.lastHeight - 100 - 48 &&
x < i * 128 + 96 + component.width * 32 + 32 &&
y < this.lastHeight - 100 + component.height * 32 + 32
) {
this.tooltip.select(this.tools[i].newInstance());
return "stop";
}
}
return "bubble";
});
}
private fillPreviews(): void {
// this.previews = this.tools.map((tool) => tool.newInstance());
this.previews = this.tools as unknown as Component[];
}
hover(x: number, y: number): "continue" | "break" {
for (const [i, component] of this.previews.entries()) {
if (
x >= i * 128 + 96 - 48 &&
y >= this.lastHeight - 100 - 48 &&
x < i * 128 + 96 + component.width * 32 + 32 &&
y < this.lastHeight - 100 + component.height * 32 + 32
) {
this.hoveringComponentIdx = i;
return "break";
}
}
this.hoveringComponentIdx = undefined;
return "continue";
}
render(r: Renderer): void {
this.lastWidth = r.width;
this.lastHeight = r.height;
r.fillRect(0, r.height - 200, r.width, 200, "#aaa");
for (const [i, component] of this.previews.entries()) {
if (i === this.hoveringComponentIdx) {
r.fillRect(
i * 128 + 96 - 48,
r.height - 100 - 48,
component.height * 32 + 64,
component.width * 32 + 64,
"#00000088",
);
}
component.render(r, i * 128 + 96, r.height - 100);
}
}
}
class Component {
private p = new Painter(64, 64);
class Tooltip {
private selectedComponent?: Component;
constructor(
private circuit: Circuit,
public mouse: Mouse,
) {
this.mouse.addOnPress(() => {
if (!this.selectedComponent) {
return "bubble";
}
const x = Math.floor(this.mouse.x / 32);
const y = Math.floor(this.mouse.y / 32);
this.circuit.place(this.selectedComponent, x, y);
this.selectedComponent = undefined;
return "stop";
});
}
render(r: Renderer): void {
this.selectedComponent?.render(r, this.mouse.x, this.mouse.y);
}
select(component: Component): void {
this.selectedComponent = component;
}
}
type PlacedComponent = {
component: Component;
x: number;
y: number;
};
class Circuit {
private components: PlacedComponent[] = [];
constructor(
private mouse: Mouse,
) {
this.mouse.addOnPress(() => {
for (const { component, x, y } of this.components) {
if (!component.click) {
continue;
}
if (
this.mouse.x >= x * 32 &&
this.mouse.y >= y * 32 &&
this.mouse.x < x * 32 + component.width * 32 &&
this.mouse.y < y * 32 + component.height * 32
) {
component.click(
this.mouse.x - x * 32,
this.mouse.y - y * 32,
);
return "stop";
}
}
return "bubble";
});
}
place(component: Component, x: number, y: number): void {
this.components.push({ component, x, y });
}
render(r: Renderer): void {
for (const { component, x, y } of this.components) {
component.render(r, x * 32 + 16, y * 32 + 16);
}
}
}
interface Component {
get width(): number;
get height(): number;
render(r: Renderer, x: number, y: number): void;
click?(x: number, y: number): void;
}
interface ComponentFactory {
newInstance(): Component;
}
class SwitchComponent implements Component, ComponentFactory {
private graphicOn = new Painter(96, 64);
private graphicOff = new Painter(96, 64);
private switchOn = false;
public width = 1;
public height = 1;
constructor() {
this.p.clear("black");
this.graphicOff.fillRect(64, 30, 16, 4, "black");
this.graphicOff.fillCirc(48 + 32, 32, 8, "black");
this.graphicOff.fillCirc(48, 32, 16, "black");
this.graphicOn.fillRect(64, 30, 16, 4, "black");
this.graphicOn.fillCirc(48 + 32, 32, 8, "black");
this.graphicOn.fillCirc(48, 32, 16, "red");
}
render(r: Renderer) {
this.p.render(r, 0, 0);
newInstance(): Component {
return new SwitchComponent();
}
render(r: Renderer, x: number, y: number): void {
const graphic = this.switchOn ? this.graphicOn : this.graphicOff;
graphic.render(r, x - 48, y - 32);
}
click(x: number, y: number): void {
const onButton = Math.sqrt(
(x - 16) ** 2 + (y - 16) ** 2,
) <= 16;
if (onButton) {
this.switchOn = !this.switchOn;
}
}
}
class LedComponent implements Component, ComponentFactory {
private graphicOn = new Painter(96, 64);
private graphicOff = new Painter(96, 64);
private switchOn = false;
public width = 1;
public height = 1;
constructor() {
this.graphicOff.fillRect(16, 30, 16, 4, "black");
this.graphicOff.fillCirc(16, 32, 8, "black");
this.graphicOff.fillCirc(48, 32, 16, "black");
this.graphicOn.fillRect(16, 30, 16, 4, "black");
this.graphicOn.fillCirc(16, 32, 8, "black");
this.graphicOn.fillCirc(48, 32, 16, "red");
}
newInstance(): Component {
return new LedComponent();
}
render(r: Renderer, x: number, y: number): void {
const graphic = this.switchOn ? this.graphicOn : this.graphicOff;
graphic.render(r, x - 48, y - 32);
}
}

View File

@ -8,6 +8,7 @@
<title>LogiCirc</title>
</head>
<body>
<script>0</script>
<canvas id="editor"></canvas>
</body>
</html>