@actis/core
v26.9.0
Published
A versatile WebGL renderer designed with multipass support in mind.
Maintainers
Readme
SOHNE | Actis
Actis is a lightweight WebGL rendering library designed to make it easy to work with WebGL fragment shaders, passes, and textures. It integrates seamlessly with React for modern web development.
Features
- Simple API for setting up WebGL rendering contexts
- Support for multiple rendering passes and shaders
- Integration with React for easy use in web applications
Installation
You can install Actis via npm:
npm install @actis/coreor via yarn:
yarn add @actis/coreUsage
Basic Usage
Here's a simple example to get you started:
import { WebGLRenderer } from '@actis/core'
import React, { useEffect, useRef } from 'react'
// Main React functional component
function App() {
const canvasRef = useRef<HTMLCanvasElement>(null) // Reference to the canvas element
const rendererRef = useRef<WebGLRenderer>() // Reference to the WebGL renderer
useEffect(() => {
rendererRef.current = new WebGLRenderer(canvasRef.current) // Initialize the renderer with the canvas element
const passes = {
passes: [
{
name: 'bufferA',
fragmentShader: `
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
float sdCircle(in vec2 p, in float r) {
return length(p) - r;
}
void main() {
vec2 p = (2. * gl_FragCoord.xy - u_resolution.xy) / u_resolution.y;
vec2 m = (2. * u_mouse.xy - u_resolution.xy) / u_resolution.y;
vec3 color = vec3(.0);
float d = sdCircle(p - m, .125);
color = mix(color, vec3(1.), 1.0 - smoothstep(0.0, 0.01, d));
gl_FragColor = vec4(color, 1.);
}
`,
textures: [],
},
{
name: 'bufferB',
fragmentShader: `
precision highp float;
uniform sampler2D u_texture0;
uniform vec2 u_resolution;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec4 color = texture2D(u_texture0, uv);
float smoothValue = smoothstep(0.0, 1.0, color.r);
gl_FragColor = vec4(smoothValue, 0.0, 0.0, 1.0);
}
`,
textures: ['bufferA'],
},
{
name: 'MainBuffer',
fragmentShader: `
precision highp float;
uniform sampler2D u_texture0;
uniform vec2 u_resolution;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec4 color = texture2D(u_texture0, uv);
gl_FragColor = color;
}
`,
textures: ['bufferB'],
},
],
}
rendererRef.current.setup(passes) // Setup the renderer with the passes
requestAnimationFrame(rendererRef.current.render) // Start the rendering loop
}, [])
// Render the canvas element
return <canvas ref={canvasRef} width={800} height={600} />
}
export default AppAdvanced Usage
For more advanced usage, such as adding multiple passes and using textures, refer to the API documentation ~in a near future~.
Offscreen rendering (workers)
createRenderer runs the same renderer inside a dedicated worker against an
OffscreenCanvas, keeping shader compilation, uniform resolution, and the
pass loop off the main thread. It is worker-first by default and falls back
to the main-thread WebGLRenderer automatically:
import { createRenderer } from '@actis/core'
const renderer = await createRenderer(canvas, {
mode: 'auto', // 'auto' | 'worker' | 'main'
onFallback: reason => console.info('main-thread fallback:', reason),
})
renderer.setup({ passes: [/* ... */] }) // same API on both paths
renderer.play()When your bundler owns worker bundling (recommended for apps), pass a pre-constructed worker instead of a URL — this is correct in both dev and prod builds (Vite example):
import RendererWorker from './worker-entry.ts?worker' // or an aliased path
const renderer = await createRenderer(canvas, {
worker: new RendererWorker(),
})Fallback order: worker + WebGL2 → worker + WebGL1 → main thread. The
onFallback reason is one of no-worker, no-offscreen-canvas, no-webgl,
worker-spawn-failed, worker-handshake-timeout, worker-version-mismatch,
or worker-gl-unavailable. mode: 'worker' throws WorkerUnsupportedError
instead of falling back; mode: 'main' pins the legacy path (also used
automatically for pinned CDN builds, where the worker entry cannot be
resolved reliably). If the worker dies after the canvas was transferred
(the surface cannot be recovered), creation throws even in auto mode and
onFallback reports worker-transferred-fatal.
One canvas, one renderer: transferControlToOffscreen is irreversible. Mount
a fresh canvas element per renderer (e.g. on framework remounts/HMR) and
dispose the previous renderer first — reusing a transferred canvas throws
WorkerUnsupportedError (canvas-already-bound) instead of failing obscurely.
Worker-path notes:
- Reads (
getMetrics,getPassNames,getContextState) are served from caches the worker pushes — same sync signatures, ≤ ~500ms staleness. capturePassDataURLreturns the last pushed thumbnail; callrequestPassCapture(name)first (e.g. on a poll interval) for fresh frames.- Uniform providers cross the boundary only as static descriptors:
registerUniformProvider({ id, values }). Function providers,Passobjects (addPass/getPass/getPasses/forEachPass), and directnew WebGLRenderer(canvas)(deprecated, still supported) require the main thread. - Dispose with
renderer.dispose()to terminate the worker.
Contributing
Contributions are welcome! Please open an issue or submit a pull request on GitHub.
