diff --git a/src/canvas.ts b/src/canvas.ts index 47c6684..ddb2cab 100644 --- a/src/canvas.ts +++ b/src/canvas.ts @@ -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); } } diff --git a/src/grid.ts b/src/grid.ts index a9a2852..08d6f2a 100644 --- a/src/grid.ts +++ b/src/grid.ts @@ -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(); 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); + } +} diff --git a/src/main.ts b/src/main.ts index 375806a..1c211ac 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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); diff --git a/src/painter.ts b/src/painter.ts index d58ee11..142065b 100644 --- a/src/painter.ts +++ b/src/painter.ts @@ -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); } } diff --git a/src/renderer.ts b/src/renderer.ts index 97276c9..c571e13 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -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; } diff --git a/src/simulator.ts b/src/simulator.ts index 4b870c2..442e803 100644 --- a/src/simulator.ts +++ b/src/simulator.ts @@ -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); } } diff --git a/static/index.html b/static/index.html index b54b9bc..0f97ed1 100644 --- a/static/index.html +++ b/static/index.html @@ -8,6 +8,7 @@ LogiCirc +