rkgk/static/canvas-renderer.js

170 lines
5.9 KiB
JavaScript
Raw Normal View History

2024-08-10 23:13:20 +02:00
import { listen } from "./framework.js";
import { Viewport } from "./viewport.js";
class CanvasRenderer extends HTMLElement {
viewport = new Viewport();
constructor() {
super();
}
connectedCallback() {
this.canvas = this.appendChild(document.createElement("canvas"));
this.ctx = this.canvas.getContext("2d");
let resizeObserver = new ResizeObserver(() => this.#updateSize());
resizeObserver.observe(this);
this.#cursorReportingBehaviour();
this.#draggingBehaviour();
this.#zoomingBehaviour();
this.#paintingBehaviour();
}
initialize(wall, painter) {
this.wall = wall;
this.painter = painter;
requestAnimationFrame(() => this.#render());
}
#updateSize() {
this.canvas.width = this.clientWidth;
this.canvas.height = this.clientHeight;
// Rerender immediately after the canvas is resized, as its contents have now been invalidated.
this.#render();
}
2024-08-15 20:01:23 +02:00
getWindowSize() {
return {
width: this.clientWidth,
height: this.clientHeight,
};
}
2024-08-10 23:13:20 +02:00
#render() {
// NOTE: We should probably render on-demand only when it's needed.
requestAnimationFrame(() => this.#render());
this.#renderWall();
}
2024-08-15 20:01:23 +02:00
getVisibleRect() {
return this.viewport.getVisibleRect(this.getWindowSize());
}
getVisibleChunkRect() {
let visibleRect = this.viewport.getVisibleRect(this.getWindowSize());
let left = Math.floor(visibleRect.x / this.wall.chunkSize);
let top = Math.floor(visibleRect.y / this.wall.chunkSize);
let right = Math.ceil((visibleRect.x + visibleRect.width) / this.wall.chunkSize);
let bottom = Math.ceil((visibleRect.y + visibleRect.height) / this.wall.chunkSize);
return { left, top, right, bottom };
}
2024-08-10 23:13:20 +02:00
#renderWall() {
if (this.wall == null) {
console.debug("wall is not available, skipping rendering");
return;
}
this.ctx.fillStyle = "white";
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.save();
this.ctx.translate(Math.floor(this.clientWidth / 2), Math.floor(this.clientHeight / 2));
this.ctx.scale(this.viewport.zoom, this.viewport.zoom);
this.ctx.translate(-this.viewport.panX, -this.viewport.panY);
2024-08-15 20:01:23 +02:00
let visibleRect = this.viewport.getVisibleRect(this.getWindowSize());
2024-08-10 23:13:20 +02:00
let left = Math.floor(visibleRect.x / this.wall.chunkSize);
let top = Math.floor(visibleRect.y / this.wall.chunkSize);
let right = Math.ceil((visibleRect.x + visibleRect.width) / this.wall.chunkSize);
let bottom = Math.ceil((visibleRect.y + visibleRect.height) / this.wall.chunkSize);
for (let chunkY = top; chunkY < bottom; ++chunkY) {
for (let chunkX = left; chunkX < right; ++chunkX) {
let x = chunkX * this.wall.chunkSize;
let y = chunkY * this.wall.chunkSize;
let chunk = this.wall.getChunk(chunkX, chunkY);
if (chunk != null) {
this.ctx.globalCompositeOperation = "source-over";
this.ctx.imageSmoothingEnabled = false;
2024-08-10 23:13:20 +02:00
this.ctx.drawImage(chunk.canvas, x, y);
}
}
}
this.ctx.restore();
if (this.ctx.brushPreview != null) {
this.ctx.drawImage(this.ctx.brushPreview, 0, 0);
}
}
async #cursorReportingBehaviour() {
while (true) {
let event = await listen([this, "mousemove"]);
let [x, y] = this.viewport.toViewportSpace(
event.clientX - this.clientLeft,
event.offsetY - this.clientTop,
2024-08-15 20:01:23 +02:00
this.getWindowSize(),
2024-08-10 23:13:20 +02:00
);
this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y }));
}
}
async #draggingBehaviour() {
while (true) {
let mouseDown = await listen([this, "mousedown"]);
if (mouseDown.button == 1) {
mouseDown.preventDefault();
while (true) {
let event = await listen([window, "mousemove"], [window, "mouseup"]);
if (event.type == "mousemove") {
this.viewport.panAround(event.movementX, event.movementY);
this.dispatchEvent(new Event(".viewportUpdate"));
} else if (event.type == "mouseup") {
this.dispatchEvent(new Event(".viewportUpdateEnd"));
2024-08-10 23:13:20 +02:00
break;
}
}
}
}
}
async #zoomingBehaviour() {
while (true) {
let event = await listen([this, "wheel"]);
// TODO: Touchpad zoom
this.viewport.zoomIn(event.deltaY > 0 ? -1 : 1);
this.dispatchEvent(new Event(".viewportUpdate"));
this.dispatchEvent(new Event(".viewportUpdateEnd"));
2024-08-10 23:13:20 +02:00
}
}
async #paintingBehaviour() {
const paint = (x, y) => {
2024-08-15 20:01:23 +02:00
let [wallX, wallY] = this.viewport.toViewportSpace(x, y, this.getWindowSize());
2024-08-10 23:13:20 +02:00
this.dispatchEvent(Object.assign(new Event(".paint"), { x: wallX, y: wallY }));
};
while (true) {
let mouseDown = await listen([this, "mousedown"]);
if (mouseDown.button == 0) {
paint(mouseDown.offsetX, mouseDown.offsetY);
while (true) {
let event = await listen([window, "mousemove"], [window, "mouseup"]);
if (event.type == "mousemove") {
paint(event.clientX - this.clientLeft, event.offsetY - this.clientTop);
} else if (event.type == "mouseup") {
break;
}
}
}
}
}
}
customElements.define("rkgk-canvas-renderer", CanvasRenderer);