@ev3-art/epoch-one
v0.2.2
Published
Embeddable WebGL2 generative-art engine for displaying Epoch One pieces. Zero-dependency core with vanilla and React entry points.
Maintainers
Readme
@ev3-art/epoch-one
Embeddable WebGL2 generative-art engine for displaying Epoch One pieces. Zero-dependency core with vanilla and React entry points.
Install
bun add @ev3-art/epoch-one
# or
npm i @ev3-art/epoch-oneReact and react-dom ≥ 19 are peer dependencies — required only when using the /react entry point.
Vanilla usage
import { mount } from '@ev3-art/epoch-one/vanilla';
import type { EpochOneNft } from '@ev3-art/epoch-one/vanilla';
const nft: EpochOneNft = {
seed: 'abc123',
jsonUri: 'https://example.com/metadata/1.json',
thumbnailUrl: 'https://example.com/images/1.png',
frameCount: 300,
};
// The host element must have explicit dimensions — it becomes the centering and
// clipping root for the canvas. An unsized element collapses (give it width/height).
const el = document.getElementById('art-container')!;
const { engine, destroy } = mount(el, {
nft,
isAdmin: false, // optional — enables admin-only controls (reseed key)
onReady(engine) {
console.log('engine started', engine);
},
onFirstFrame(engine) {
console.log('first frame painted');
},
// fetch: customFetch, // optional — custom fetch for auth / gateway rewrite
// container: menuRoot, // optional — separate element to attach menu + brush into
});
// Later: clean up
destroy();The host element receives the ev3-epoch-one-root class (flex centering, overflow hidden, CSS container). When container is omitted the same element hosts the menu and brush overlay. When container is provided it must also be explicitly sized (both elements receive the root class).
mount returns { engine, destroy }. Call destroy() when done — it tears down the controller, menu, brush overlay, and canvas.
React usage
import { EpochOneEngine } from '@ev3-art/epoch-one/react';
import type { EpochOneNft } from '@ev3-art/epoch-one/react';
const nft: EpochOneNft = {
seed: 'abc123',
jsonUri: 'https://example.com/metadata/1.json',
thumbnailUrl: 'https://example.com/images/1.png',
frameCount: 300,
};
function ArtViewer() {
return (
<div style={{ width: 800, height: 600 }}>
{/* EpochOneEngine fills its parent (width/height 100%) — size the parent */}
<EpochOneEngine
nft={nft}
isAdmin={false}
onReady={(engine) => console.log('ready', engine)}
onFirstFrame={(engine) => console.log('first frame')}
className="my-art-class"
/>
</div>
);
}EpochOneEngine props (EpochOneEngineProps):
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| nft | EpochOneNft | yes | NFT descriptor (see below) |
| isAdmin | boolean | no | Enable admin-only controls (reseed key). Default false. |
| onReady | (engine: Engine) => void | no | Called after NFT loads and engine starts. |
| onFirstFrame | (engine: Engine) => void | no | Called on the first painted animation frame after start. |
| fetch | typeof fetch | no | Custom fetch for NFT assets (auth headers / gateway rewrite). |
| className | string | no | Extra CSS class on the root div. |
Changing nft or isAdmin remounts the engine. Changing onReady, onFirstFrame, or fetch does not (they are captured in refs).
Requires React ≥ 19 and react-dom ≥ 19.
The EpochOneNft contract
interface EpochOneNft {
/** Deterministic seed string — drives all shader parameters. */
seed: string;
/** Off-chain metadata JSON URL. The hidden `engine` block is fetched from here. */
jsonUri: string;
/** Base image URL (the genesis frame / thumbnail). */
thumbnailUrl: string;
/** Fallback total frame count when the `engine` block is absent. */
frameCount: number;
/** Movement-buffer URL. Present on evolved NFTs. */
movementBufferUrl?: string;
/** Paint-buffer URL. Present on evolved NFTs. */
paintBufferUrl?: string;
/**
* Post-processed ImageIn background URL (hand-crafted ImageIn NFTs only).
* Immutable reset target — separate from the evolving thumbnail/ping-pong buffer.
*/
imageInUrl?: string;
}The consuming app derives this from its own chain or DAS result. The package is chain-agnostic (zero Solana dependencies). When both movementBufferUrl and paintBufferUrl are present, the engine fetches and restores the evolved state. Buffer fetch failures propagate as errors (they are load-bearing); engine block fetch failures fall back silently to descriptor values.
The engine block (authoring overrides)
jsonUri may carry an optional top-level engine block. loadNft fetches and parses it automatically — consumers never construct or pass it. Malformed or unknown fields are dropped, and any absent field falls back to its seed-derived value. A failed fetch or parse is non-fatal: the piece renders from its descriptor and seed.
{
"engine": {
"engineFrame": 300, // resume/target frame; defaults to the descriptor frameCount
"manualMode": false, // load paused for manual stepping
"globalFreeze": false, // freeze the simulation on load
"overrides": { "aspect": "16:9", "orientation": "portrait" }
}
}Every overrides field is optional (type EngineBlockOverrides, exported from the main barrel):
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| aspect | '4:3' \| '16:9' | '4:3' | Render aspect ratio. |
| orientation | 'landscape' \| 'portrait' | 'landscape' | Pair '16:9' + 'portrait' for a 9:16 vertical piece. |
| palette | string | seed | A premade palette name. Mutually exclusive with paletteColors. |
| paletteColors | 3 × [number, number, number] | seed | Inline image-derived palette (three RGB rows). |
| useColorCycle | boolean | false | Only paired with paletteColors. |
| bloomIntensity | number | seed | Bloom strength. |
| lumaWeight | number | seed | Luma weighting. |
| baseChunkSize | number | seed | Unscaled units. |
| hueSpeed | number | seed | Absolute cycle speed (turns/frame @60fps). |
| moveSpeed | number | seed | Movement speed. |
import type { EngineBlockOverrides } from '@ev3-art/epoch-one';From a Solana DAS asset (recommended)
If you already have a Metaplex DAS asset — e.g. from a Helius getAsset / getAssetsByGroup call — skip the manual construction above and use the converter:
import { epochOneNftFromDasAsset, type EpochOneNft } from '@ev3-art/epoch-one';
const nft: EpochOneNft | null = epochOneNftFromDasAsset(dasAsset);
if (!nft) {
// Not an Epoch One asset — the converter already logged the reason via console.error.
return;
}
// pass `nft` to mount() / loadNft() / <EpochOneEngine nft={nft} />The converter is synchronous and chain-agnostic — it reads the DAS JSON only (no network, no Solana dependencies). It returns null (after a console.error) when the asset is not an Epoch One piece, detected from the Seed trait or a custom image/EpochOne-* file. The same epochOneNftFromDasAsset and DasAsset type are re-exported from @ev3-art/epoch-one/vanilla and @ev3-art/epoch-one/react.
Advanced: EngineController and /ui
For custom or non-React hosts that need direct control over the menu lifecycle.
EngineController
Exported from @ev3-art/epoch-one (main barrel).
import { EngineController } from '@ev3-art/epoch-one';
import type { EngineControllerOptions } from '@ev3-art/epoch-one';EngineController is framework-neutral: it handles scoped-CSS injection, the tools menu, engine dispatch, recording-status sync, pointer routing, and the brush overlay. Both mount() and <EpochOneEngine> construct exactly this internally.
EngineControllerOptions:
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| canvas | HTMLCanvasElement | yes | The canvas the engine renders to. |
| engine | Engine | yes | The engine instance. |
| menuHtml | string | yes | Raw menu markup string. Pass menuHtml from /ui. |
| menuCss | string | yes | Menu stylesheet string. Pass menuCss from /ui. |
| isAdmin | boolean | no | Enable admin-only controls. Default false. |
| brushSizeOptions | number[] | no | Custom brush size ladder. |
| onAppMenu | () => void | no | Called when the user triggers the system / app menu action. |
| root | HTMLElement | no | Element to attach menu + brush into. Defaults to document.body. |
Call controller.destroy() to remove event listeners, the menu container, and the brush overlay.
/ui — raw menu assets
import { menuHtml, menuCss, setupMenu } from '@ev3-art/epoch-one/ui';
import type { MenuController, MenuOptions, MenuState } from '@ev3-art/epoch-one/ui';menuHtml(string) — build-inlined menu markup. Inject into the DOM before callingsetupMenu. Required byEngineController.menuCss(string) — build-inlined menu stylesheet. Inject once into a<style>tag per page. Required byEngineController.setupMenu(opts?: MenuOptions): MenuController | null— wires keyboard shortcuts, click delegation, and brush drag against the injected HTML. Returnsnullif#tt-menu-containeris not found in the DOM. Most hosts useEngineControllerrather than callingsetupMenudirectly.
MenuController methods: getState(), setState(partial), open(), close(), show(), hide(), updateActiveStates(), updateBrushDisplay(), setRecordingStatus(status), setRotateAvailable(available), setRotateActive(active), setToolsEnabled(enabled), setDrawingEnabled(enabled), destroy().
Limitations
- Single instance per page. The engine uses a global instance guard. Mounting a second engine before destroying the first logs a console warning — the tools menu and keyboard shortcuts resolve DOM by fixed ids, so concurrent instances collide. Destroy the active instance first.
- ESM-only. Use
<script type="module">in HTML or a bundler (Vite, esbuild, Rollup). This package is not an IIFE and cannot be used as a plain<script>drop-in.
License
MIT © 2026 EV3
