import type { Neko as DesktopWindow } from "https://deno.land/x/neko/core/Neko.ts"; import type * as skia from "https://deno.land/x/skia_canvas/mod.ts"; export default class Canvas { #canvas?: skia.Canvas | HTMLCanvasElement; #window?: DesktopWindow; #img?: typeof skia.Image; #ctx?: Context2D; static async new( { title, width, height } = {} as { title?: string; width: number; height: number; }, ) { if (!("window" in globalThis)) throw "Canvas must be in Main thread"; const self = new Canvas(Canvas._guard); if ("Deno" in globalThis) { const [{ Neko }, { createCanvas }, { Image }] = await Promise.all( [ "neko/core/Neko.ts", "skia_canvas/src/canvas.ts", "skia_canvas/src/image.ts", ].map((mod) => import(`https://deno.land/x/${mod}`)), ); self.#img = Image; self.#canvas = createCanvas(width, height); self.#window = new Neko({ title, width, height, resize: true }); } else { document.body.append( self.#canvas = (document.querySelector("canvas:not(#memviz)") ?? document.createElement("canvas")) as HTMLCanvasElement, ); if (!self.#canvas.hasAttribute("width")) self.#canvas.width = width; if (!self.#canvas.hasAttribute("height")) self.#canvas.height = height; } return self; } async loadImage(path: string | URL) { if ("Deno" in globalThis) return this.#img!.load(path); return createImageBitmap(await fetch(path).then((as) => as.blob())); } #cursor = { x: NaN, y: NaN }; #onmousemove?: (e: MouseEvent) => void; get mouse() { const p = this.#cursor; if ("Deno" in globalThis) [p.x, p.y] = this.#window!.mousePosition; else if (!this.#onmousemove) { addEventListener( "mousemove", this.#onmousemove = (e) => (p.x = e.x, p.y = e.y), ); } return p; } reset() { this.#ctx?.clearRect(0, 0, this.#canvas!.width, this.#canvas!.height); this.#ctx?.resetTransform(); } draw(fn: (ctx: Context2D) => void) { this.reset(); fn(this.#ctx ??= this.getContext("2d")); if ("Deno" in globalThis) { this.#window!.setFrameBuffer( (this.#canvas as skia.Canvas).pixels, this.#canvas!.width, this.#canvas!.height, ); } } getContext(type: "2d") { return this.#ctx ??= this.#canvas!.getContext(type, { desynchronized: true, })!; } private static readonly _guard = Symbol(); // to bad typesccrypt can't do typeof MemViz.#guard constructor(guard: typeof Canvas._guard) { if (guard !== Canvas._guard) throw null; } } type Context2D = CanvasRenderingContext2D | skia.CanvasRenderingContext2D;