ts-hashlife
Version:
Efficient TypeScript implementation of HashLife, an optimized algorithm for simulating Conway's Game of Life with memoization and quadtree-based compression.
306 lines • 12.3 kB
JavaScript
import EventBus from "./event-bus";
export const clamp = (value, min, max) => {
return Math.min(max, Math.max(min, value));
};
export class LifeCanvasDrawer {
constructor() {
this.pixel_ratio = 1;
this.canvas_width = 0;
this.canvas_height = 0;
this._canvas_offset_x = 0;
this._canvas_offset_y = 0;
this._cell_width = 0;
this._default_cell_width = 0;
this.border_width = 0;
this.background_color = null;
this.cell_color = null;
this.set_cell_width = (cell_width) => {
const clampedWidth = clamp(cell_width, 0.01, 500);
if (clampedWidth === this.cell_width)
return false;
this.cell_width = clampedWidth;
return true;
};
}
// ----------------------------------------
// Getters and setters
// ----------------------------------------
get default_cell_width() {
return this._default_cell_width;
}
set default_cell_width(value) {
this._default_cell_width = value;
}
get canvas_offset_x() {
return this._canvas_offset_x;
}
set canvas_offset_x(value) {
this._canvas_offset_x = value;
EventBus.emit("pan:x", value.toFixed(0));
}
get canvas_offset_y() {
return this._canvas_offset_y;
}
set canvas_offset_y(value) {
this._canvas_offset_y = value;
EventBus.emit("pan:y", value.toFixed(0));
}
get cell_width() {
return this._cell_width;
}
set cell_width(value) {
this._cell_width = value;
this.border_width = Math.floor((value - 5) / 5) + 1;
const ratio = value / this._default_cell_width;
EventBus.emit("zoom", ratio >= 1 ? `1:${Math.round(ratio)}` : `${Math.round(1 / ratio)}:1`);
}
// ----------------------------------------
//
// ----------------------------------------
init(canvas) {
this.canvas = canvas;
if (!(canvas === null || canvas === void 0 ? void 0 : canvas.getContext))
return false;
this.context = this.canvas.getContext("2d");
document.body.style.overscrollBehavior = "none";
return true;
}
set_size(width, height) {
if (width !== this.canvas_width || height !== this.canvas_height) {
const factor = window.devicePixelRatio || 1;
this.pixel_ratio = factor;
this.canvas.width = Math.round(width * factor);
this.canvas.height = Math.round(height * factor);
this.canvas.style.width = `${width}px`;
this.canvas.style.height = `${height}px`;
this.canvas_width = this.canvas.width;
this.canvas_height = this.canvas.height;
this.image_data = this.context.createImageData(this.canvas_width, this.canvas_height);
this.image_data_data = new Int32Array(this.image_data.data.buffer);
for (let i = 0; i < width * height; i++) {
this.image_data_data[i] = 0xff << 24;
}
}
}
draw_node(node, size, left, top) {
if (node.population === 0) {
return;
}
const adjustedLeft = left + this.canvas_offset_x;
const adjustedTop = top + this.canvas_offset_y;
const adjustedSize = size;
if (adjustedLeft + adjustedSize < 0 ||
adjustedTop + adjustedSize < 0 ||
adjustedLeft >= this.canvas_width ||
adjustedTop >= this.canvas_height) {
return;
}
if (node.level === 0 || adjustedSize <= 1) {
if (node.population) {
this.fill_square(adjustedLeft, adjustedTop, adjustedSize);
}
}
else {
size /= 2;
this.draw_node(node.nw, size, left, top);
this.draw_node(node.ne, size, left + size, top);
this.draw_node(node.sw, size, left, top + size);
this.draw_node(node.se, size, left + size, top + size);
}
}
fill_square(x, y, size) {
x = Math.round(x);
y = Math.round(y);
size = Math.round(size);
let width = size - this.border_width;
let height = width;
if (x < 0) {
width += x;
x = 0;
}
if (x + width > this.canvas_width) {
width = this.canvas_width - x;
}
if (y < 0) {
height += y;
y = 0;
}
if (y + height > this.canvas_height) {
height = this.canvas_height - y;
}
if (width <= 0 || height <= 0) {
return;
}
let pointer = x + y * this.canvas_width;
const row_width = this.canvas_width - width;
const color = this.cell_color_rgb.r |
(this.cell_color_rgb.g << 8) |
(this.cell_color_rgb.b << 16) |
(0xff << 24);
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
this.image_data_data[pointer] = color;
pointer++;
}
pointer += row_width;
}
}
redraw(node) {
const bg_color_rgb = this.color2rgb(this.background_color || "#000");
const bg_color_int = bg_color_rgb.r |
(bg_color_rgb.g << 8) |
(bg_color_rgb.b << 16) |
(0xff << 24);
this.cell_color_rgb = this.color2rgb(this.cell_color || "#000");
const count = this.canvas_width * this.canvas_height;
for (let i = 0; i < count; i++) {
this.image_data_data[i] = bg_color_int;
}
const size = Math.pow(2, node.level - 1) * this.cell_width;
this.draw_node(node, 2 * size, -size, -size);
this.context.putImageData(this.image_data, 0, 0);
if (this.cell_width > 10)
this.draw_grid_lines();
}
draw_grid_lines() {
const ctx = this.context;
ctx.strokeStyle = `rgba(180, 180, 180, ${this.cell_width > 15 ? 0.2 : 0.4})`;
ctx.lineWidth = Math.min((Math.floor((this.cell_width - 10) / 10) + 1) / 4, 1);
const startX = this.canvas_offset_x % this.cell_width;
const startY = this.canvas_offset_y % this.cell_width;
// Vertical lines
for (let x = startX; x < this.canvas_width; x += this.cell_width) {
ctx.beginPath();
ctx.moveTo(x - this.border_width / 2, 0);
ctx.lineTo(x - this.border_width / 2, this.canvas_height);
ctx.stroke();
}
// Horizontal lines
for (let y = startY; y < this.canvas_height; y += this.cell_width) {
ctx.beginPath();
ctx.moveTo(0, y - this.border_width / 2);
ctx.lineTo(this.canvas_width, y - this.border_width / 2);
ctx.stroke();
}
}
pan(dx, dy) {
this.canvas_offset_x -= dx * this.pixel_ratio;
this.canvas_offset_y -= dy * this.pixel_ratio;
}
center_view() {
this.canvas_offset_x = this.canvas_width >> 1;
this.canvas_offset_y = this.canvas_height >> 1;
}
zoom(out, center_x, center_y) {
if (out) {
const didUpdate = this.set_cell_width(this.cell_width / 2);
if (!didUpdate)
return;
this.canvas_offset_x -= Math.round((this.canvas_offset_x - center_x) / 2);
this.canvas_offset_y -= Math.round((this.canvas_offset_y - center_y) / 2);
}
else {
const didUpdate = this.set_cell_width(this.cell_width * 2);
if (!didUpdate)
return;
this.canvas_offset_x += Math.round(this.canvas_offset_x - center_x);
this.canvas_offset_y += Math.round(this.canvas_offset_y - center_y);
}
}
zoom_at(zoom_factor, pinch_origin_x, pinch_origin_y) {
// Store the old cell width before updating
const old_cell_width = this.cell_width;
// Update the cell width with the new zoom factor
const didUpdate = this.set_cell_width(this.cell_width * zoom_factor);
if (!didUpdate)
return;
// Calculate the new scale factor
const new_cell_width = this.cell_width;
const scale_factor = new_cell_width / old_cell_width;
// Adjust the canvas offsets to zoom relative to the pinch origin
this.canvas_offset_x +=
(1 - scale_factor) *
(pinch_origin_x * this.pixel_ratio - this.canvas_offset_x);
this.canvas_offset_y +=
(1 - scale_factor) *
(pinch_origin_y * this.pixel_ratio - this.canvas_offset_y);
}
zoom_centered(out) {
this.zoom(out, this.canvas_width >> 1, this.canvas_height >> 1);
}
zoom_to(level) {
while (this.cell_width > level) {
this.zoom_centered(true);
}
while (this.cell_width * 2 < level) {
this.zoom_centered(false);
}
}
fit_bounds(bounds, padding) {
const width = bounds.right - bounds.left;
const height = bounds.bottom - bounds.top;
const pad = {
right: ((padding === null || padding === void 0 ? void 0 : padding.right) || 0) * this.pixel_ratio,
left: ((padding === null || padding === void 0 ? void 0 : padding.left) || 0) * this.pixel_ratio,
bottom: ((padding === null || padding === void 0 ? void 0 : padding.bottom) || 0) * this.pixel_ratio,
top: ((padding === null || padding === void 0 ? void 0 : padding.top) || 0) * this.pixel_ratio,
};
if (isFinite(width) && isFinite(height)) {
const availableWidth = this.canvas_width - (pad.left + pad.right);
const availableHeight = this.canvas_height - (pad.top + pad.bottom);
const relative_size = Math.min(16, // maximum cell size
availableWidth / width, availableHeight / height);
this.zoom_to(relative_size);
// Calculate center position accounting for padding
// This shifts the pattern to account for the padding on all sides
const horizontalCenter = pad.left + availableWidth / 2;
const verticalCenter = pad.top + availableHeight / 2;
this.canvas_offset_x = Math.round(horizontalCenter - (bounds.left + width / 2) * this.cell_width);
this.canvas_offset_y = Math.round(verticalCenter - (bounds.top + height / 2) * this.cell_width);
}
else {
this.zoom_to(16);
// Center within available space
const availableWidth = this.canvas_width - (pad.left + pad.right);
const availableHeight = this.canvas_height - (pad.top + pad.bottom);
this.canvas_offset_x = pad.left + (availableWidth >> 1);
this.canvas_offset_y = pad.top + (availableHeight >> 1);
}
}
// draw_cell(x: number, y: number, set: boolean): void {
// const size = this.cell_width + this.cell_width * this.zoom_factor;
// const cell_x = x * size + this.canvas_offset_x;
// const cell_y = y * size + this.canvas_offset_y;
// const width = Math.ceil(size) - size * this.border_width;
// console.log(size, cell_x, cell_y, width)
// this.context.fillStyle = set
// ? this.cell_color || "#000"
// : this.background_color || "#000";
// this.context.fillRect(cell_x, cell_y, width, width);
// }
pixel2cell(x, y) {
return {
x: Math.floor((x * this.pixel_ratio - this.canvas_offset_x + this.border_width / 2) /
this.cell_width),
y: Math.floor((y * this.pixel_ratio - this.canvas_offset_y + this.border_width / 2) /
this.cell_width),
};
}
color2rgb(color) {
if (color.length === 4) {
return {
r: parseInt(color[1] + color[1], 16),
g: parseInt(color[2] + color[2], 16),
b: parseInt(color[3] + color[3], 16),
};
}
else {
return {
r: parseInt(color.slice(1, 3), 16),
g: parseInt(color.slice(3, 5), 16),
b: parseInt(color.slice(5, 7), 16),
};
}
}
}
//# sourceMappingURL=draw.js.map