UNPKG

ts-hashlife

Version:

Efficient TypeScript implementation of HashLife, an optimized algorithm for simulating Conway's Game of Life with memoization and quadtree-based compression.

314 lines 12.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LifeCanvasDrawer = exports.clamp = void 0; const event_bus_1 = __importDefault(require("./event-bus")); const clamp = (value, min, max) => { return Math.min(max, Math.max(min, value)); }; exports.clamp = clamp; 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 = (0, exports.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; event_bus_1.default.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; event_bus_1.default.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; event_bus_1.default.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), }; } } } exports.LifeCanvasDrawer = LifeCanvasDrawer; //# sourceMappingURL=draw.js.map