@hypertools/sdk
v0.4.6
Published
Vanilla-first SDK for embedding interactive creative coding experiences. Supports p5.js, Three.js, WebGL, and more.
Maintainers
Readme
@hypertools/sdk
Vanilla-first SDK for embedding interactive creative coding experiences. Works with p5.js, Three.js, WebGL, Canvas, and any other rendering library.
Features
- Framework-agnostic - Works with p5.js, Three.js, vanilla Canvas, WebGL, or any rendering library
- Reactive parameters - Define control parameters that automatically trigger updates
- Video & image capture - Built-in recording with MediaRecorder and canvas capture
- Timeline animations - Keyframe-based animation system with easing functions
- React integration - Optional React hooks and components
- TypeScript first - Full type definitions included
- Zero dependencies - Core SDK has no runtime dependencies
Installation
# npm
npm install @hypertools/sdk
# yarn
yarn add @hypertools/sdk
# pnpm
pnpm add @hypertools/sdk
# bun
bun add @hypertools/sdkQuick Start
Basic Usage
import { Experience } from '@hypertools/sdk';
const experience = new Experience({
mount: document.getElementById('canvas-container'),
paramDefs: {
color: { type: 'color', value: '#ff0000', label: 'Color' },
speed: { type: 'number', value: 5, min: 1, max: 10, label: 'Speed' },
showGrid: { type: 'boolean', value: true, label: 'Show Grid' },
},
setup(context) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
context.mount.appendChild(canvas);
// Access reactive parameters
console.log(context.params.color); // '#ff0000'
console.log(context.params.speed); // 5
console.log(context.params.showGrid); // true
// Draw loop
function draw() {
ctx.fillStyle = context.params.color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Listen to frame events
context.experience.on('frame', draw);
// Return cleanup function
return () => {
canvas.remove();
};
},
});
// Control playback
experience.play();
experience.pause();
// Update parameters programmatically
experience.setParam('color', '#00ff00');
experience.setParams({ speed: 8, showGrid: false });
// Capture images
const blob = await experience.captureImage('png');
// Clean up when done
experience.destroy();With p5.js
import { Experience } from '@hypertools/sdk';
import p5 from 'p5';
new Experience({
mount: document.getElementById('sketch'),
paramDefs: {
backgroundColor: { type: 'color', value: '#1a1a2e' },
circleCount: { type: 'number', value: 10, min: 1, max: 50 },
animate: { type: 'boolean', value: true },
},
setup(context) {
let sketch;
new p5((p) => {
sketch = p;
p.setup = () => {
p.createCanvas(800, 600);
};
p.draw = () => {
if (!context.params.animate) return;
p.background(context.params.backgroundColor);
for (let i = 0; i < context.params.circleCount; i++) {
p.circle(
p.random(p.width),
p.random(p.height),
p.random(20, 50)
);
}
};
}, context.mount);
return () => sketch?.remove();
},
});With Three.js
import { Experience } from '@hypertools/sdk';
import * as THREE from 'three';
new Experience({
mount: document.getElementById('scene'),
paramDefs: {
rotationSpeed: { type: 'number', value: 0.01, min: 0, max: 0.1, step: 0.001 },
cubeColor: { type: 'color', value: '#00ff88' },
wireframe: { type: 'boolean', value: false },
},
setup(context) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(800, 600);
context.mount.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({
color: context.params.cubeColor,
wireframe: context.params.wireframe,
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
// Register object for external access
context.registerObject('mainCube', cube);
// Animation loop
context.experience.on('frame', () => {
cube.rotation.x += context.params.rotationSpeed;
cube.rotation.y += context.params.rotationSpeed;
material.color.set(context.params.cubeColor);
material.wireframe = context.params.wireframe;
renderer.render(scene, camera);
});
return () => {
renderer.dispose();
geometry.dispose();
material.dispose();
};
},
});Entry Points
The SDK is modular - import only what you need:
| Entry | Purpose |
|-------|---------|
| @hypertools/sdk | Core Experience class + utilities |
| @hypertools/sdk/controls | Tweakpane-based control panel (requires tweakpane peer dep) |
| @hypertools/sdk/recording | Video/image capture & timeline |
| @hypertools/sdk/react | React hooks & components (requires react peer dep) |
| @hypertools/sdk/capture | Low-level capture utilities |
API Reference
Experience
The main class for creating interactive experiences.
interface ExperienceConfig {
mount: HTMLElement; // Container element
paramDefs?: ParamDefinitions; // Parameter schema
initialParams?: Record<string, unknown>; // Override defaults
setup: SetupFunction; // Your code
autoplay?: boolean; // Start immediately (default: true)
frameRate?: number; // Target FPS (default: 60)
background?: string; // CSS background
}Methods
// Playback
experience.play();
experience.pause();
experience.toggle();
// Parameters
experience.setParam('key', value);
experience.setParams({ key1: value1, key2: value2 });
experience.getParams(); // Get all current values
experience.resetParams(); // Reset to defaults
// Object registry (Spline-like API)
experience.registerObject('name', object, metadata);
experience.findObjectByName('name');
experience.findObjectById('id');
experience.getAllObjects();
// Events
experience.on('ready', () => {});
experience.on('frame', ({ frame, deltaTime }) => {});
experience.on('paramChange', ({ key, value, previousValue }) => {});
experience.on('resize', ({ width, height }) => {});
experience.on('error', ({ error }) => {});
experience.once('ready', () => {});
experience.off('ready', handler);
// Capture
const blob = await experience.captureImage('png');
// Cleanup
experience.destroy();Properties
experience.isReady; // boolean
experience.isPlaying; // boolean
experience.isDestroyed; // boolean
experience.currentFrame; // number
experience.params; // Reactive proxy
experience.mount; // HTMLElementParameter Types
// Number
{ type: 'number', value: 5, min: 0, max: 10, step: 0.1, label: 'Speed' }
// Color
{ type: 'color', value: '#ff0000', label: 'Background' }
// Boolean
{ type: 'boolean', value: true, label: 'Show Grid' }
// String
{ type: 'string', value: 'hello', label: 'Text' }
// Select
{ type: 'select', value: 'option1', options: [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
], label: 'Mode' }Recording Module
import { VideoRecorder, ImageCapture, Timeline } from '@hypertools/sdk/recording';
// Video recording
const recorder = VideoRecorder.start(canvas, {
format: 'webm', // 'webm' | 'mp4'
quality: 0.9, // 0-1
frameRate: 60, // FPS
});
// Later...
const videoBlob = await recorder.stop();
// Image capture
const imageBlob = await ImageCapture.capture(canvas, 'png');
// Timeline animation
const timeline = new Timeline({
loop: true,
duration: 5000, // 5 seconds
});
timeline
.addKeyframe(0, { opacity: 0, scale: 0.5 })
.addKeyframe(1000, { opacity: 1, scale: 1 }, 'easeOutBack')
.addKeyframe(4000, { opacity: 1, scale: 1 })
.addKeyframe(5000, { opacity: 0, scale: 0.5 }, 'easeInQuad');
timeline.onUpdate((params) => {
experience.setParams(params);
});
timeline.play();
timeline.pause();
timeline.seek(2500); // Jump to 2.5sReact Integration
import { useExperience, ExperienceView } from '@hypertools/sdk/react';
function MyVisualization() {
const {
experience,
isReady,
isPlaying,
params,
setParam,
play,
pause,
toggle,
} = useExperience({
paramDefs: {
color: { type: 'color', value: '#ff0000' },
speed: { type: 'number', value: 5, min: 1, max: 10 },
},
setup(context) {
// Your setup code
},
});
return (
<div>
<ExperienceView
experience={experience}
className="w-full h-96"
/>
<div className="controls">
<button onClick={toggle}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<input
type="color"
value={params.color}
onChange={(e) => setParam('color', e.target.value)}
/>
<input
type="range"
min={1}
max={10}
value={params.speed}
onChange={(e) => setParam('speed', Number(e.target.value))}
/>
</div>
</div>
);
}Controls Module (Tweakpane)
Add a visual control panel with Tweakpane:
import { HypertoolControls } from '@hypertools/sdk/controls';
const controls = new HypertoolControls({
container: document.getElementById('controls'),
definitions: {
color: { type: 'color', value: '#ff0000' },
speed: { type: 'number', value: 5, min: 1, max: 10 },
},
onChange: (key, value) => {
console.log(`${key} changed to ${value}`);
},
});
// Programmatic updates
controls.set('color', '#00ff00');
controls.refresh();
controls.dispose();Using Exported HyperTools Experiences
When you export a project from HyperTools, you get a standalone web component that can be embedded anywhere. Use ExperienceController from this SDK to control it programmatically.
Setup
Get your exported experience - Export from HyperTools to get a JS file (e.g.,
my-experience.js)Load the experience in your HTML:
<script src="./my-experience.js"></script>
<my-experience></my-experience>- Install the SDK to control it:
npm install @hypertools/sdkBasic Control
import { ExperienceController } from '@hypertools/sdk';
// Get reference to the web component
const element = document.querySelector('my-experience');
// Connect the controller
const controller = new ExperienceController({
element,
initialParams: {
speed: 5,
color: '#ff0000',
},
});
// Control parameters
controller.setParam('speed', 10);
controller.setParams({ speed: 8, color: '#00ff00' });
// Get current values
const params = controller.getParams();
const paramDefs = controller.getParamDefs();
// Reset to defaults
controller.resetParams();
// Listen to changes
controller.on('paramChange', (event) => {
console.log(`${event.key} changed to ${event.value}`);
});
// Cleanup when done
controller.destroy();Dispatching Events
Trigger interactions programmatically by dispatching synthetic events:
// Simulate a click
controller.dispatchToCanvas('click', { clientX: 400, clientY: 300 });
// Simulate a long press (e.g., for formation triggers)
controller.dispatchToCanvas('mousedown', { clientX: 400, clientY: 300 });
setTimeout(() => {
controller.dispatchToCanvas('mouseup', { clientX: 400, clientY: 300 });
}, 600);
// Keyboard events
controller.dispatchToCanvas('keydown', { key: 'Space' });
// Custom events
controller.dispatchCustomEvent('myEvent', { data: 'hello' });React Integration
import { useEffect, useRef, useState } from 'react';
import { ExperienceController } from '@hypertools/sdk';
import type { ExportedExperienceElement } from '@hypertools/sdk';
function App() {
const experienceRef = useRef<ExportedExperienceElement>(null);
const controllerRef = useRef<ExperienceController | null>(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const element = experienceRef.current;
if (!element) return;
const onReady = () => {
controllerRef.current = new ExperienceController({ element });
setIsReady(true);
};
element.addEventListener('ready', onReady, { once: true });
return () => {
element.removeEventListener('ready', onReady);
controllerRef.current?.destroy();
};
}, []);
return (
<div>
{/* @ts-expect-error - Custom element */}
<my-experience ref={experienceRef} />
{isReady && (
<button onClick={() => controllerRef.current?.setParam('speed', 10)}>
Speed Up
</button>
)}
</div>
);
}ExperienceController API
// Constructor options
interface ExperienceControllerConfig {
element: ExportedExperienceElement; // The web component
initialParams?: Record<string, unknown>; // Override initial values
autoConnect?: boolean; // Connect immediately (default: true)
}
// Methods
controller.connect(); // Connect to element
controller.disconnect(); // Disconnect from element
controller.destroy(); // Full cleanup
controller.setParam(key, value);
controller.setParams(params);
controller.getParams();
controller.getParamDefs();
controller.resetParams();
controller.on(event, handler); // Subscribe
controller.once(event, handler); // Subscribe once
controller.off(event, handler); // Unsubscribe
controller.dispatchToCanvas(eventType, eventInit); // Dispatch to canvas
controller.dispatchToElement(eventType, eventInit); // Dispatch to element
controller.dispatchCustomEvent(type, detail); // Custom event
controller.getCanvas(); // Get canvas element
controller.getMount(); // Get mount element
// Properties
controller.element; // The web component
controller.isConnected; // Connection status
controller.isDestroyed; // Destroyed status
// Static factories
ExperienceController.fromSelector('my-experience');
await ExperienceController.whenDefined('my-experience');Building Custom Features
See examples/react-landing/ for a complete example showing how to build custom features on top of exported experiences:
- Preset System - Pre-configured settings with visual selection
- Click-to-Form Mode - Click anywhere to trigger interactions at that position
- Auto-pilot Mode - Automatically cycle through presets
- Idle Screensaver - Start animations after inactivity
- Share Configuration - Generate shareable URLs with encoded settings
- Keyboard Shortcuts - Custom keyboard controls
Examples
See the /examples directory for complete working examples:
examples/react-landing/- React app with exported experience as background + custom featuresexamples/vanilla-canvas/- Pure Canvas APIexamples/p5js/- p5.js sketchexamples/threejs/- Three.js sceneexamples/react/- React integrationexamples/recording/- Video capture & timeline
Browser Support
- Chrome 80+
- Firefox 78+
- Safari 14+
- Edge 80+
TypeScript
Full TypeScript support with type definitions included.
import type {
Experience,
ExperienceConfig,
ExperienceContext,
ParamDefinitions,
ParamValues,
ExperienceEvent,
} from '@hypertools/sdk';Contributing
Contributions are welcome! Please read our Contributing Guide for details.
License
MIT License - see LICENSE for details.
