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.

309 lines 13.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const draw_1 = require("./draw"); const life_universe_1 = require("./life-universe"); const event_bus_1 = __importDefault(require("./event-bus")); const formats_1 = require("./formats"); const macrocell_1 = require("./macrocell"); const drawer = new draw_1.LifeCanvasDrawer(); const life = new life_universe_1.LifeUniverse(); const DEFAULT_BORDER = 2, DEFAULT_CELL = 10, DEFAULT_CELL_COLOR = "#f48c06", DEFAULT_BACKGROUND_COLOR = "#000814", DEFAULT_FPS = 60, /* * path to the folder with all patterns */ PATTERNS_PATH = "/patterns"; class HashLife { constructor(opts = {}) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; this._running = false; this.onStop = null; this.pattern = null; this.handleStart = () => { if (this.running) return; let n = 0, start = Date.now(), frame_time = 1000 / this.max_fps, per_frame = frame_time, last_frame = start - per_frame; this.running = true; if (life.generation === 0) { life.save_rewind_state(); } let interval = null; interval = setInterval(() => { event_bus_1.default.emit("fps", (1000 / frame_time).toFixed(1)); }, 666); const update = () => { var _a; if (!this.running) { if (interval) clearInterval(interval); event_bus_1.default.emit("fps", "00.0"); if (this.onStop) { this.onStop(); this.onStop = null; } return; } const time = Date.now(); if (per_frame * n < time - start) { life.next_generation(true); drawer.redraw(life.root); if (((_a = life.root) === null || _a === void 0 ? void 0 : _a.population) && life.root.population > 0) event_bus_1.default.emit("population", life.root.population); n++; frame_time += (-last_frame - frame_time + (last_frame = time)) / 15; if (frame_time < 0.7 * per_frame) { n = 1; start = Date.now(); } } window.requestAnimationFrame(update); }; update(); }; this.handleStop = (callback) => { if (this.running) { this.running = false; if (callback) this.onStop = callback; } else { if (callback) callback(); } }; this.handleToggle = (callback) => { if (this.running) this.handleStop(callback); else this.handleStart(); }; this.handleReset = () => { if (life.rewind_state) { this.handleStop(() => { life.restore_rewind_state(); this.fit_pattern(); drawer.redraw(life.root); }); } }; this.handleStep = () => { this.step(true); }; this.handleSpeedUp = () => { life.set_step(life.step + 1); }; this.handleSlowDown = () => { if (life.step <= 0) return; life.set_step(life.step - 1); }; this.handleFitPattern = () => { this.fit_pattern(); this.lazy_redraw(life.root); }; this.handleExport = (pattern_name = "Untitled") => { var _a, _b; return formats_1.formats.generate_rle(life, (_b = (_a = this.pattern) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : pattern_name, [ "Generated by ts-hashlife", ]); }; this.handleLoadExampleFromFile = ({ pattern_text, pattern_id, pattern_path, title, }) => { const is_mc = pattern_text.startsWith("[M2]"); let result; if (!is_mc) { const payload = formats_1.formats.parse_pattern(pattern_text.trim()); if (payload.error) { throw new Error(payload.error); } else { result = payload; } } else { result = { comment: "", urls: [], }; } this.handleStop(() => { var _a; life.clear_pattern(); if (!is_mc) { const bounds = life.get_bounds(result.field_x, result.field_y); life.make_center(result.field_x, result.field_y, bounds); life.setup_field(result.field_x, result.field_y, bounds); } else { const mc = (0, macrocell_1.load_macrocell)(life, pattern_text); if (!mc) { throw new Error("Failed to load macrocell"); } result = mc; life.set_step(15); } life.save_rewind_state(); if (result.rule_s && result.rule_b) { life.set_rules(result.rule_s, result.rule_b); } else { life.set_rules((1 << 2) | (1 << 3), 1 << 3); // Default rules } this.fit_pattern(); drawer.redraw(life.root); if ((_a = life.root) === null || _a === void 0 ? void 0 : _a.population) event_bus_1.default.emit("population", life.root.population); if (pattern_id && !result.title) result.title = pattern_id; const pattern = { title: result.title || title || pattern_id || "Untitled", author: result.author, rule: result.rule, description: result.comment, source_url: PATTERNS_PATH + pattern_path, view_url: pattern_path !== null && pattern_path !== void 0 ? pattern_path : "", urls: result.urls, }; this.pattern = pattern; event_bus_1.default.emit("pattern:load", pattern); }); }; this.handleLoadRandomizedPattern = ({ density, width, height, }) => { const normalizedDensity = (0, draw_1.clamp)(density, 0, 1); const normalizedWidth = (0, draw_1.clamp)(width, 100, 1000); const normalizedHeight = (0, draw_1.clamp)(height, 100, 1000); this.handleStop(() => { var _a; life.clear_pattern(); // Calculate number of cells based on density and dimensions const cellCount = Math.round(normalizedWidth * normalizedHeight * normalizedDensity); // Create arrays for cell coordinates const field_x = new Int32Array(cellCount); const field_y = new Int32Array(cellCount); // Generate random positions for (let i = 0; i < cellCount; i++) { field_x[i] = Math.floor(Math.random() * normalizedWidth); field_y[i] = Math.floor(Math.random() * normalizedHeight); } // Set up the universe with the random pattern const bounds = life.get_bounds(field_x, field_y); life.make_center(field_x, field_y, bounds); life.setup_field(field_x, field_y, bounds); // Save initial state for reset functionality life.save_rewind_state(); // Fit pattern to view and redraw this.fit_pattern(); drawer.redraw(life.root); // Update population count in UI if ((_a = life.root) === null || _a === void 0 ? void 0 : _a.population) { event_bus_1.default.emit("population", life.root.population); } // Emit pattern load event with random pattern information const pattern = { title: "Random pattern", author: "", rule: "", description: `Randomly generated pattern with density ${normalizedDensity.toFixed(2)}, width ${normalizedWidth}, height ${normalizedHeight}`, source_url: "", view_url: "", urls: [], }; this.pattern = pattern; event_bus_1.default.emit("pattern:load", pattern); }); }; this.handleWindowResize = () => { drawer.set_size(window.innerWidth, document.body.offsetHeight || window.innerHeight); requestAnimationFrame(() => this.lazy_redraw(life.root)); }; this.handlePan = (dx, dy) => { drawer.pan(dx, dy); this.lazy_redraw(life.root); }; this.handleZoom = (zoomRatio, x, y) => { drawer.zoom_at(zoomRatio, x, y); this.lazy_redraw(life.root); }; this.handleSetUiPadding = (padding = {}) => { this.ui_padding = Object.assign(Object.assign({}, this.ui_padding), padding); this.fit_pattern(); }; // ---------------------------------------- // Helpers // ---------------------------------------- this.fit_pattern = () => { const bounds = life.get_root_bounds(); drawer.fit_bounds({ top: bounds.top, bottom: bounds.bottom, left: bounds.left, right: bounds.right, }, { top: this.ui_padding.top, bottom: this.ui_padding.bottom, left: this.ui_padding.left, right: this.ui_padding.right, }); }; this.step = (is_single = false) => { var _a; const time = Date.now(); if (life.generation === 0) { life.save_rewind_state(); } life.next_generation(is_single); drawer.redraw(life.root); event_bus_1.default.emit("fps", (1000 / (Date.now() - time)).toFixed(1)); if ((_a = life.root) === null || _a === void 0 ? void 0 : _a.population) event_bus_1.default.emit("population", life.root.population); }; this.reset_settings = () => { drawer.background_color = this.background_color; drawer.cell_color = this.cell_color; drawer.default_cell_width = this.cell_width; drawer.cell_width = this.cell_width; drawer.border_width = this.border_width; drawer.center_view(); life.rule_b = 1 << 3; life.rule_s = (1 << 2) | (1 << 3); life.set_step(0); }; this.lazy_redraw = (node) => { if (!this.running || this.max_fps < 15) { drawer.redraw(node); } }; this.max_fps = (_a = opts.max_fps) !== null && _a !== void 0 ? _a : DEFAULT_FPS; this.border_width = (_b = opts.border_width) !== null && _b !== void 0 ? _b : DEFAULT_BORDER; this.cell_width = (_c = opts.cell_width) !== null && _c !== void 0 ? _c : DEFAULT_CELL; this.background_color = (_d = opts.background_color) !== null && _d !== void 0 ? _d : DEFAULT_BACKGROUND_COLOR; this.cell_color = (_e = opts.cell_color) !== null && _e !== void 0 ? _e : DEFAULT_CELL_COLOR; this.ui_padding = { top: (_g = (_f = opts.ui_padding) === null || _f === void 0 ? void 0 : _f.top) !== null && _g !== void 0 ? _g : 0, right: (_j = (_h = opts.ui_padding) === null || _h === void 0 ? void 0 : _h.right) !== null && _j !== void 0 ? _j : 0, bottom: (_l = (_k = opts.ui_padding) === null || _k === void 0 ? void 0 : _k.bottom) !== null && _l !== void 0 ? _l : 0, left: (_o = (_m = opts.ui_padding) === null || _m === void 0 ? void 0 : _m.left) !== null && _o !== void 0 ? _o : 0, }; } get running() { return this._running; } set running(value) { this._running = value; event_bus_1.default.emit(value ? "start" : "stop", value); } // ---------------------------------------- // Event handlers // ---------------------------------------- handleInit(canvas) { const init = drawer.init(canvas); if (!init) return false; this.handleWindowResize(); this.reset_settings(); return true; } } exports.default = HashLife; //# sourceMappingURL=hash-life.js.map