crt-shader
CRT rendering for images and 3D scenes. Requires WebGL 2 and a browser app with a bundler, such as Vite.
pnpm add crt-shaderLive example · Click to inspect
Save as crt.js in your browser app.
import { createRuntime, prepareInput } from 'crt-shader';
export function mount(host) { const source = document.createElement('canvas'); source.width = 96; source.height = 60; const ctx = source.getContext('2d'); ctx.fillStyle = '#101018'; ctx.fillRect(0, 0, 96, 60); ['#ef6868', '#f4ca70', '#83c99a', '#7fa7ef'].forEach((color, i) => { ctx.fillStyle = color; ctx.fillRect(8 + i * 22, 8, 14, 44); });
const canvas = document.createElement('canvas'); canvas.setAttribute('aria-label', 'CRT color bars'); canvas.style.cssText = 'display:block;width:100%;height:auto'; host.append(canvas); const runtime = createRuntime(); const ready = runtime.render({ source: prepareInput(source), output: canvas, preset: 'reference', width: 640, height: 400, });
return { canvas, ready, dispose() { runtime.dispose(); canvas.remove(); }, };}Then mount it from your entry point:
import { mount } from './crt.js';const demo = mount(document.body);Await demo.ready before reading the output; call demo.dispose() when removing it.
pnpm add crt-shader react@19 react-dom@19Live example · Click to inspect
Save as Artwork.jsx, then import and render <Artwork /> in your existing React app.
'use client';
import { CRTImage } from 'crt-shader/react';
// An included image: replace this data URL with your own image URL.const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="96" height="60"> <path fill="#101018" d="M0 0h96v60H0z"/> <path fill="#ef6868" d="M8 8h14v44H8z"/> <path fill="#f4ca70" d="M30 8h14v44H30z"/> <path fill="#83c99a" d="M52 8h14v44H52z"/> <path fill="#7fa7ef" d="M74 8h14v44H74z"/></svg>`;const src = `data:image/svg+xml,${encodeURIComponent(svg)}`;
export default function Artwork({ onLoad, onError = console.error }) { return ( <div style={{ width: '100%', maxWidth: 640, aspectRatio: '8 / 5' }}> <CRTImage src={src} alt="CRT color bars" preset="reference" width={640} height={400} dpr={1} style={{ maxWidth: '100%', maxHeight: '100%' }} onLoad={onLoad} onError={onError} /> </div> );}CRTImage releases its runtime lease on unmount.
pnpm add crt-shader three@^0.186.0Live example · Click to inspect
Save as crt.js in your browser app.
import * as THREE from 'three';import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';import { CRTPass } from 'crt-shader/three';
export function mount(host) { const renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true }); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.NoToneMapping; renderer.setPixelRatio(1); renderer.setSize(640, 400); renderer.domElement.style.cssText = 'display:block;width:100%;height:auto'; renderer.domElement.setAttribute('aria-label', 'CRT torus knot'); host.append(renderer.domElement);
const scene = new THREE.Scene(); scene.background = new THREE.Color('black'); const camera = new THREE.PerspectiveCamera(40, 640 / 400, 0.1, 100); camera.position.z = 5; const geometry = new THREE.TorusKnotGeometry(0.8, 0.25, 128, 16); const material = new THREE.MeshNormalMaterial(); scene.add(new THREE.Mesh(geometry, material));
const composer = new EffectComposer(renderer); const render = new RenderPass(scene, camera); const output = new OutputPass(); const crt = new CRTPass({ preset: 'reference', inputResolution: 160 }); composer.addPass(render); composer.addPass(output); // Tone map and encode before native CRT. composer.addPass(crt); // Last: do not encode again. composer.render(); // For animation, call this in your render loop.
return { canvas: renderer.domElement, dispose() { crt.dispose(); output.dispose(); render.dispose(); composer.dispose(); geometry.dispose(); material.dispose(); renderer.dispose(); renderer.domElement.remove(); }, };}Then mount it from your entry point:
import { mount } from './crt.js';const demo = mount(document.body);Call demo.dispose() when removing the scene. Reuse your existing renderer if you have one.
pnpm add crt-shader three@^0.186.0 postprocessing@^6.39.5Live example · Click to inspect
Save as crt.js in your browser app.
import * as THREE from 'three';import { EffectComposer, EffectPass, RenderPass, ToneMappingEffect, ToneMappingMode,} from 'postprocessing';import { CRTPass } from 'crt-shader/postprocessing';
export function mount(host) { const renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true }); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.NoToneMapping; renderer.setPixelRatio(1); renderer.setSize(640, 400); renderer.domElement.style.cssText = 'display:block;width:100%;height:auto'; renderer.domElement.setAttribute('aria-label', 'CRT torus knot'); host.append(renderer.domElement);
const scene = new THREE.Scene(); scene.background = new THREE.Color('black'); const camera = new THREE.PerspectiveCamera(40, 640 / 400, 0.1, 100); camera.position.z = 5; const geometry = new THREE.TorusKnotGeometry(0.8, 0.25, 128, 16); const material = new THREE.MeshNormalMaterial(); scene.add(new THREE.Mesh(geometry, material));
const composer = new EffectComposer(renderer, { frameBufferType: THREE.HalfFloatType, multisampling: 0, }); composer.addPass(new RenderPass(scene, camera)); composer.addPass(new EffectPass(camera, new ToneMappingEffect({ mode: ToneMappingMode.LINEAR }), )); // Neutral tone mapping; CRT receives linear RGB, no OutputPass. composer.addPass(new CRTPass({ preset: 'reference', inputResolution: 160 })); composer.render(); // For animation, call this in your render loop.
return { canvas: renderer.domElement, dispose() { composer.dispose(); // pmndrs also disposes its passes. geometry.dispose(); material.dispose(); renderer.dispose(); renderer.domElement.remove(); }, };}Then mount it from your entry point:
import { mount } from './crt.js';const demo = mount(document.body);Call demo.dispose() when removing the scene. Reuse your existing renderer if you have one.
pnpm add crt-shader react@19 react-dom@19 three@^0.186.0 postprocessing@^6.39.5 @react-three/fiber@^9.7.0 @react-three/postprocessing@^3.1.1Live example · Click to inspect
Save as CRTScene.jsx, then import and render <CRTScene /> in your existing React app.
'use client';
import { Canvas } from '@react-three/fiber';import { EffectComposer, ToneMapping } from '@react-three/postprocessing';import { HalfFloatType } from 'three';import { ToneMappingMode } from 'postprocessing';import { CRT } from 'crt-shader/r3f';
export default function CRTScene({ onCreated, crtRef }) { return ( <Canvas flat frameloop="demand" dpr={1} camera={{ position: [0, 0, 5], fov: 40 }} gl={{ antialias: false, preserveDrawingBuffer: true }} style={{ width: '100%', aspectRatio: '8 / 5' }} onCreated={onCreated} > <color attach="background" args={['black']} /> <mesh> <torusKnotGeometry args={[0.8, 0.25, 128, 16]} /> <meshNormalMaterial /> </mesh> <EffectComposer frameBufferType={HalfFloatType} multisampling={0}> <ToneMapping mode={ToneMappingMode.LINEAR} /> <CRT ref={crtRef} preset="reference" inputResolution={160} /> </EffectComposer> </Canvas> );}Fiber owns the renderer and mesh resources; CRT handles its pass cleanup on unmount.
By Brooklyn. MIT · Image credits