ts-hashlife
Version:
Efficient TypeScript implementation of HashLife, an optimized algorithm for simulating Conway's Game of Life with memoization and quadtree-based compression.
304 lines • 12.9 kB
JavaScript
import { clamp, LifeCanvasDrawer } from "./draw";
import { LifeUniverse } from "./life-universe";
import eventBus from "./event-bus";
import { formats } from "./formats";
import { load_macrocell } from "./macrocell";
const drawer = new LifeCanvasDrawer();
const life = new 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(() => {
eventBus.emit("fps", (1000 / frame_time).toFixed(1));
}, 666);
const update = () => {
var _a;
if (!this.running) {
if (interval)
clearInterval(interval);
eventBus.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)
eventBus.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.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.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 = 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)
eventBus.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;
eventBus.emit("pattern:load", pattern);
});
};
this.handleLoadRandomizedPattern = ({ density, width, height, }) => {
const normalizedDensity = clamp(density, 0, 1);
const normalizedWidth = clamp(width, 100, 1000);
const normalizedHeight = 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) {
eventBus.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;
eventBus.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);
eventBus.emit("fps", (1000 / (Date.now() - time)).toFixed(1));
if ((_a = life.root) === null || _a === void 0 ? void 0 : _a.population)
eventBus.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;
eventBus.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;
}
}
export default HashLife;
//# sourceMappingURL=hash-life.js.map