# `@hexlife/embed` **Fast, deterministic hexagonal cellular automata for ordinary web pages, browser workers, and Node.** [![npm](https://img.shields.io/npm/v/@hexlife/embed?logo=npm&color=CB3837)](https://www.npmjs.com/package/@hexlife/embed) [![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Sidem/HexLife/blob/main/LICENSE) ![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen) ![types](https://img.shields.io/badge/types-included-3178C6?logo=typescript&logoColor=white) ![engines](https://img.shields.io/badge/Rust-WebAssembly-orange?logo=rust&logoColor=white) ![WebGL2](https://img.shields.io/badge/WebGL2-instanced-990000?logo=webgl&logoColor=white) The package bundles the Rust/Wasm tick engine and WebGL2 renderer used by [HexLife Explorer](https://sidem.github.io/HexLife/), then exposes them at the level a host needs: custom elements for the quickest integration, or DOM-free simulation and renderer-only entrypoints for applications that own more of the stack. For applications that need the hex map but not a cellular automaton, `/hex` is a zero-dependency, DOM-free and Wasm-free pointy-top axial geometry module: ```ts import {axialNeighbor, axialToPixel, pixelToAxial} from '@hexlife/embed/hex' const east = axialNeighbor({q: 0, r: 0}, 0) const center = axialToPixel(east, 24, {x: 320, y: 240}) const picked = pixelToAxial(center, 24, {x: 320, y: 240}) ``` Direction 0 is east; indices continue clockwise in screen coordinates. The contract also includes cube rounding, distance, line traversal, rotation, and negative-coordinate-safe storage chunks. → [Full `/hex` reference](https://github.com/Sidem/HexLife/blob/main/docs/embed/hex.md) The same `(ruleset, seed, density, rows)` produces a byte-identical tick sequence in HexLife Explorer, an embed, and HexLife on Reddit. An embed is therefore a reproducible world, not a visual approximation of one. ## Install ```bash npm install @hexlife/embed ``` ## Quick start ```js import '@hexlife/embed' // registers and ``` ```html ``` The element is `display: block` with a `1 / 1` aspect ratio by default; give it a width and it sizes itself. Everything lives in a shadow root, so the host page's CSS cannot break it and its CSS cannot touch the host. For host-owned computation, use the same engine without mounting a DOM element: ```js import {createDensityState, createSimulation, packCells} from '@hexlife/embed/sim' const initialCells = createDensityState({rows: 64, columns: 74, seed: 12345, density: 0.5}) const sim = await createSimulation({rulesetHex, rows: 64, columns: 74, initialCells}) sim.tick(10) const packed = packCells(sim.snapshotCells()) sim.dispose() ``` ## Printable solids `@hexlife/embed/solid` extrudes a run through time into an object you can print: the hex grid is the cross-section, each tick is a layer, on cells are matter. It **simulates nothing** — it is a layer sink, so the binary, k-state and stochastic engines all feed the same buffer. ```js import {createSolidStack, initSolidEngine} from '@hexlife/embed/solid' await initSolidEngine() const stack = createSolidStack({rows, cols, ticks, interpolate: 'bridge', subLayers: 1, basePlate: 2}) const layer = stack.layerView() // build ONCE, outside the loop for (let tick = 0; tick < ticks; tick++) { layer.set(world.state) // one memcpy per tick stack.pushLayer() world.tick() } const report = stack.finalize({keepComponents: 'plate-connected'}) report.keptComponents // → 1 means it prints as a single piece report.floating // → components that never reach the build surface const bytes = await stack.export({format: '3mf', cellSize: 2, layerHeight: 0.8}) stack.free() // mandatory ``` **Read the report before you export.** A slicer will not join separate bodies — it will happily print forty loose fragments and let you find out on the build plate. **`interpolate: 'bridge'`** is what makes the object hold together. Two prisms on consecutive layers whose cells are diagonal neighbours meet along a single vertical edge — a zero-thickness hinge that reports as connected and prints as two pieces. Bridging inserts exactly the set that turns that into face contact, without the fattening `'union'` causes. For a **vacuum-stable** ruleset it guarantees that nothing floats: every voxel is face-connected down to tick 0. Check with `isVacuumStable()` from `/api` first — the guarantee is precisely as good as its precondition. **Units are millimetres.** `cellSize` (hexagon circumradius) and `layerHeight` are independent, so the object's Z aspect ratio is a print decision rather than an accident of tick count. Formats are `'stl'` (default, universal), `'ply'` (indexed, ~⅓ the bytes) and `'3mf'` (indexed XML in a zip — what slicers prefer, and the only one that carries real units). **`merge: 'greedy'`** is the default and welds runs of coplanar faces into single quads: 586,864 triangles down to 16,796 on a 30×36×100 volume, and the 3MF down to 0.125 MiB. `merge: 'none'` is a supported first-class setting, not a debug flag — merging necessarily leaves T-junctions, slicers do not care, strict manifold validators do. Both meshes bound exactly the same solid. The boundary of the printed object is **open, not toroidal**: the simulation wraps, an object cannot, so features are cut at the grid edge and two pieces touching only across the seam are two pieces. → [Full `/solid` reference](https://github.com/Sidem/HexLife/blob/main/docs/embed/solid.md) ## The same run, in 3D `@hexlife/embed/spacetime` draws those layers instead of meshing them: the grid is the cross-section, time runs up, and the whole history is one ray-marched solid you can turn, zoom and slice. It is the HexLife Explorer's spacetime view, packaged — and it simulates nothing either, so it eats exactly the same per-tick states the extruder does. ```js import {createSpacetimeView} from '@hexlife/embed/spacetime' const view = createSpacetimeView(canvas, {rows, columns, depth: ticks}) for (let tick = 0; tick < ticks; tick++) { view.pushSimulation(sim, tick) // native rule×2+state packing; no JS cell loop sim.tick() } view.draw() // drag to turn, wheel to dolly — controls are on by default view.setCrossSection(30) // slice it: layer 30 as an opaque plane through the solid ``` Cost is **pixels × march steps and is independent of grid resolution** — a 576-row world costs the same per frame as a 96-row one. What a bigger world costs is texture memory: one byte per cell per layer, allocated once. The layer byte *is* the colour-table index, so changing the palette retints every layer of history and re-uploads nothing. → [Full `/spacetime` reference](https://github.com/Sidem/HexLife/blob/main/docs/embed/spacetime.md) ## A close-packed 3D lattice `@hexlife/embed/hcp` is a fourth engine: a general k-state CA on the hexagonal close-packed lattice (12 equidistant neighbours). The v1 rule is a 6-phase tetrahedral block of size `k⁴`. Slot 3 is always the unique lowest site, so gravity lives in the table, not in the tick. Importing it loads **no** bytes into a page that only uses `/ca` or `/sim`. ```js import {initHcpEngine, HexHcp, blockRuleFromTet} from '@hexlife/embed/hcp' await initHcpEngine() const rule = blockRuleFromTet(6, ([a, b, c, apex]) => [a, b, c, apex]) const world = new HexHcp({states: 6, layers: 24, rows: 48, columns: 56, rule}) world.setBlockAlternates(true) world.tick(12) world.paintIf(0, inlets, 0, 1) world.clearStatesInLayer(world.layers - 1, 0b0110) ``` `HXP1` codes freeze the current world — geometry, boundaries, rule, generation, and cells — and resume the next tick identically. `isConservative` / `isIsotropic` report table properties; they never enforce them. → [Full `/hcp` reference](https://github.com/Sidem/HexLife/blob/main/docs/embed/hcp.md) ## Documentation The full reference lives in the repository, one page per surface: | Page | What it covers | | :--- | :--- | | [**Docs index**](https://github.com/Sidem/HexLife/blob/main/docs/embed/README.md) | Everything below, in one table. | | [Getting started](https://github.com/Sidem/HexLife/blob/main/docs/embed/getting-started.md) | Install, first world, sizing, host-owned simulation, requirements. | | [Entry points](https://github.com/Sidem/HexLife/blob/main/docs/embed/entrypoints.md) | Which of the thirteen imports you need and what each requires. | | [`/hex`](https://github.com/Sidem/HexLife/blob/main/docs/embed/hex.md) | Pointy-top axial coordinates, direction protocol, pixels, lines, rounding, and chunks. | | [``](https://github.com/Sidem/HexLife/blob/main/docs/embed/hexlife-world.md) | Attributes, JavaScript API, events, GPU-loss recovery, policies, styling. | | [``](https://github.com/Sidem/HexLife/blob/main/docs/embed/hexlife-grid.md) | Many worlds in one WebGL context. | | [`/sim`](https://github.com/Sidem/HexLife/blob/main/docs/embed/sim.md) | DOM-free simulation, seeded and sparse states, vacuum stability, block skipping. | | [`/render`](https://github.com/Sidem/HexLife/blob/main/docs/embed/renderer.md) | The renderer alone, for hosts that own their simulation. | | [`/spacetime`](https://github.com/Sidem/HexLife/blob/main/docs/embed/spacetime.md) | A run drawn as a 3D solid: one tick per layer, ray-marched, turnable and sliceable. | | [`/ca`](https://github.com/Sidem/HexLife/blob/main/docs/embed/ca.md) | k-state worlds, conservation, isotropy, ``, `HXK1` codes. | | [`/stochastic`](https://github.com/Sidem/HexLife/blob/main/docs/embed/stochastic.md) | Probability and time-in-state, the lattice gas, ``, `HXS1` codes. | | [`/solid`](https://github.com/Sidem/HexLife/blob/main/docs/embed/solid.md) | Extrude a run into a printable solid: welding, components, meshing, STL/PLY/3MF. | | [`/hcp`](https://github.com/Sidem/HexLife/blob/main/docs/embed/hcp.md) | HCP worlds, `k⁴` tetrahedral blocks, ``, `HXP1` codes. | | [`/api`](https://github.com/Sidem/HexLife/blob/main/docs/embed/api.md) | DOM-free metadata, world codecs, palettes, GPU probing. | | [Determinism & versioning](https://github.com/Sidem/HexLife/blob/main/docs/embed/determinism.md) | The reproducibility contract and what a major bump means. | ## Entry points | Import | Needs | Use for | | :--- | :--- | :--- | | `@hexlife/embed` | DOM, WebGL2, Wasm | The browser. Registers `` and ``. | | `@hexlife/embed/api` | Nothing | Node and browsers alike: world codes, ruleset metadata, palettes, GPU probing. | | `@hexlife/embed/hex` | Nothing | Unbounded pointy-top axial coordinates, directions, pixels, lines, and storage chunks. | | `@hexlife/embed/sim` | Wasm | Node and workers: deterministic host-driven simulation, no DOM or rendering. | | `@hexlife/embed/render` | DOM, WebGL2 | Draw externally supplied state without ticking a simulation. | | `@hexlife/embed/spacetime` | DOM, WebGL2 | **A run as a 3D solid.** One retained tick per layer, ray-marched; turn it, slice it, look inside it. No Wasm artifact at all. | | `@hexlife/embed/ca` | Wasm | **k-state** worlds — a second engine, with an optionally mass-conserving backend. | | `@hexlife/embed/ca-element` | DOM, WebGL2, Wasm | Registers ``. | | `@hexlife/embed/stochastic` | Wasm | Probabilistic and time-dependent worlds, plus the conserved lattice gas. A separately loaded artifact. | | `@hexlife/embed/stochastic-element` | DOM, WebGL2, Wasm | Registers ``. The only element entry that reaches the stochastic artifact. | | `@hexlife/embed/solid` | Wasm | **Printable solids.** Extrude a run of any of the engines above through time; export STL, PLY or 3MF. A third separately loaded artifact. | | `@hexlife/embed/hcp` | Wasm | **HCP** worlds — a fourth engine, a 12-neighbour close-packed lattice with `k⁴` tetrahedral blocks. A separately loaded artifact. | | `@hexlife/embed/hcp-element` | DOM, WebGL2, Wasm | Registers ``. The only element entry that reaches the HCP artifact. | A server that validates a pasted world code must import **only** `@hexlife/embed/api` — the root entry evaluates custom-element, Wasm and WebGL code at import time. Importing the package root, `/sim`, `/ca`, `/stochastic` or `/solid` neither downloads nor initializes the HCP artifact. The browser bundle **inlines the Wasm binary** as a data URI rather than fetching a side-car asset, because a strict host CSP (a Reddit webview, for instance) is not something an embed can widen. ## Live examples Every page below is a package consumer with the same presentation shell. The k-state pages resolve the exact published npm version through jsDelivr; they do not reach into Explorer internals. | Demo | What it demonstrates | Package surface | |---|---|---| | [**Interactive demo library**](https://sidem.github.io/HexLife/embed-demos.html) | Nine focused experiments spanning crystal growth, ecology, excitable media, particles, seeded probability, deterministic chaos, sound, and interacting matter. | Every public entrypoint, with each page consuming the published npm package | | [**256 worlds, one rule class**](https://sidem.github.io/HexLife/totalistic-256.html) | All 256 totalistic rules simultaneously, or an equally sized sample of a larger rule class. One shared clock, initial condition, palette, and GPU context make rule-to-rule comparison direct. | ``, ``, `/api` | | [**Coffee extraction lab**](https://sidem.github.io/HexLife/coffee-percolation.html) | Six- and sixteen-state physical models with exact conservation, plus the same extraction on the close-packed 3D lattice. | `/ca`, `/ca-element`, `/hcp`, `/hcp-element` | | [**k-state CA builder**](https://sidem.github.io/HexLife/ca-builder.html) | Edit exact transition tables, paint and run the Wasm world, inspect invariants, and export a standalone npm-package example. | `/ca`, `/ca-element`, `` | The atlas is also a performance demonstration: `` runs hundreds of simulations but draws them through one WebGL2 context, avoiding the browser context limit that makes a wall of independent canvases fail. For sparse or settled worlds, exact uniform-block skipping makes the 383k-cell binary engine about **13× faster** at a fixed point and about **2.4× faster** at 0.2% occupancy on the project benchmark machine. No approximation or separate "fast" result is involved. ## Determinism For the same `(ruleset, seed, density, rows)`, this package and HexLife Explorer run the same Wasm `run_tick` over a grid derived the same way and filled by the same seeded RNG, so they agree tick for tick. The pinned reference, if you want to check a build: ```js const world = document.getElementById('ref') // world.tick(100) world.checksum // → 231200078 ``` ## Requirements WebGL2 and WebAssembly, and Node 20+ for the DOM-free entries. There is no 2D fallback — call `detectGraphicsPath()` from `@hexlife/embed/api` to detect that before you mount anything. ## Versioning The custom-element API is **additive**: attributes, methods and events are only added, never removed or repurposed. A *major* bump is reserved for the things that would break reproducibility — what a ruleset hex decodes to, what an `HXW1.…` code decodes to, or the tick sequence itself. A visual change is not breaking; a world that comes back different is. This package versions independently of the HexLife Explorer application. ## Links - [Documentation](https://github.com/Sidem/HexLife/blob/main/docs/embed/README.md) - [Source and releases](https://github.com/Sidem/HexLife) — `embed-vX.Y.Z` tags - [Changelog](https://github.com/Sidem/HexLife/blob/main/CHANGELOG.md) - [Issues](https://github.com/Sidem/HexLife/issues) - [HexLife Explorer](https://sidem.github.io/HexLife/) — the full lab ## License [MIT](https://github.com/Sidem/HexLife/blob/main/LICENSE) © Sidem