import ShaderPad from 'shaderpad';
import autosize from 'shaderpad/plugins/autosize';
import hands from 'shaderpad/plugins/hands';
import helpers from 'shaderpad/plugins/helpers';
import { createFullscreenCanvas } from 'shaderpad/util';
import { getWebcamVideo, stopVideoStream } from '@/examples/demo-utils';
import type { ExampleContext } from '@/examples/runtime';
const N_HISTORY_FRAMES = 100;
let shader: ShaderPad | null = null;
let video: HTMLVideoElement | null = null;
let outputCanvas: HTMLCanvasElement | null = null;
export async function init({ mount }: ExampleContext) {
const fragmentShaderSrc = `#version 300 es
precision mediump float;
in vec2 v_uv;
out vec4 outColor;
uniform sampler2D u_webcam;
// Fingertip landmark indices
#define THUMB_TIP 4
#define INDEX_TIP 8
#define MIDDLE_TIP 12
#define RING_TIP 16
#define PINKY_TIP 20
#define HISTORY_FRAMES ${N_HISTORY_FRAMES}
// Fingertip colors
const vec3 THUMB_COLOR = vec3(1.0, 0.3, 0.3); // Red
const vec3 INDEX_COLOR = vec3(1.0, 0.6, 0.2); // Orange
const vec3 MIDDLE_COLOR = vec3(1.0, 1.0, 0.3); // Yellow
const vec3 RING_COLOR = vec3(0.3, 1.0, 0.5); // Green
const vec3 PINKY_COLOR = vec3(0.4, 0.6, 1.0); // Blue
float distToSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a;
vec2 ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
// Check if hand 0 was valid at the given historical frame
bool wasHandValid(int framesAgo) {
return nHandsAt(framesAgo) > 0;
}
const float THUMB_PROXIMITY_THRESHOLD = 0.06;
int getActiveFinger(int framesAgo) {
if (!wasHandValid(framesAgo)) return -1;
vec2 thumbPos = vec2(handLandmark(0, THUMB_TIP, framesAgo));
vec2 indexPos = vec2(handLandmark(0, INDEX_TIP, framesAgo));
vec2 middlePos = vec2(handLandmark(0, MIDDLE_TIP, framesAgo));
vec2 ringPos = vec2(handLandmark(0, RING_TIP, framesAgo));
vec2 pinkyPos = vec2(handLandmark(0, PINKY_TIP, framesAgo));
float indexDist = distance(indexPos, thumbPos);
float middleDist = distance(middlePos, thumbPos);
float ringDist = distance(ringPos, thumbPos);
float pinkyDist = distance(pinkyPos, thumbPos);
float maxDist = indexDist;
int activeFinger = 0;
if (middleDist > maxDist) { maxDist = middleDist; activeFinger = 1; }
if (ringDist > maxDist) { maxDist = ringDist; activeFinger = 2; }
if (pinkyDist > maxDist) { maxDist = pinkyDist; activeFinger = 3; }
if (maxDist < THUMB_PROXIMITY_THRESHOLD) return -1;
return activeFinger;
}
vec2 getFingerPos(int fingerIdx, int framesAgo) {
if (fingerIdx == 0) return vec2(handLandmark(0, INDEX_TIP, framesAgo));
if (fingerIdx == 1) return vec2(handLandmark(0, MIDDLE_TIP, framesAgo));
if (fingerIdx == 2) return vec2(handLandmark(0, RING_TIP, framesAgo));
return vec2(handLandmark(0, PINKY_TIP, framesAgo));
}
vec3 getFingerColor(int fingerIdx) {
if (fingerIdx == 0) return INDEX_COLOR;
if (fingerIdx == 1) return MIDDLE_COLOR;
if (fingerIdx == 2) return RING_COLOR;
return PINKY_COLOR;
}
vec4 getAllPenIntensities(vec2 uv, float lineWidthUv) {
vec4 intensities = vec4(0.0);
vec2 prevIndex = vec2(-999.0);
vec2 prevMiddle = vec2(-999.0);
vec2 prevRing = vec2(-999.0);
vec2 prevPinky = vec2(-999.0);
int prevIndexFrame = -1;
int prevMiddleFrame = -1;
int prevRingFrame = -1;
int prevPinkyFrame = -1;
for (int i = HISTORY_FRAMES - 1; i >= 0; --i) {
int activeFinger = getActiveFinger(i);
if (activeFinger < 0) continue;
vec2 currentPos = getFingerPos(activeFinger, i);
vec2 prevPos = vec2(-999.0);
int prevFrame = -1;
if (activeFinger == 0) { prevPos = prevIndex; prevFrame = prevIndexFrame; }
else if (activeFinger == 1) { prevPos = prevMiddle; prevFrame = prevMiddleFrame; }
else if (activeFinger == 2) { prevPos = prevRing; prevFrame = prevRingFrame; }
else { prevPos = prevPinky; prevFrame = prevPinkyFrame; }
if (prevPos.x > -500.0 && prevFrame >= 0) {
float dist = distToSegment(uv, prevPos, currentPos);
float ageFade = 1.0 - float(i) / float(HISTORY_FRAMES);
float segmentIntensity = smoothstep(lineWidthUv * 2.0, 0.0, dist) * ageFade;
if (activeFinger == 0) intensities.x = max(intensities.x, segmentIntensity);
else if (activeFinger == 1) intensities.y = max(intensities.y, segmentIntensity);
else if (activeFinger == 2) intensities.z = max(intensities.z, segmentIntensity);
else intensities.w = max(intensities.w, segmentIntensity);
}
if (activeFinger == 0) { prevIndex = currentPos; prevIndexFrame = i; }
else if (activeFinger == 1) { prevMiddle = currentPos; prevMiddleFrame = i; }
else if (activeFinger == 2) { prevRing = currentPos; prevRingFrame = i; }
else { prevPinky = currentPos; prevPinkyFrame = i; }
}
return intensities;
}
void main() {
vec2 uv = fitCover(vec2(1.0 - v_uv.x, v_uv.y), vec2(textureSize(u_webcam, 0)));
vec4 webcamColor = texture(u_webcam, uv);
vec3 color = webcamColor.rgb;
float pxPerUv = u_resolution.y;
float lineWidthPx = 3.0;
float lineWidthUv = lineWidthPx / pxPerUv;
vec4 intensities = getAllPenIntensities(uv, lineWidthUv);
color = mix(color, INDEX_COLOR, intensities.x);
color = mix(color, MIDDLE_COLOR, intensities.y);
color = mix(color, RING_COLOR, intensities.z);
color = mix(color, PINKY_COLOR, intensities.w);
if (u_nHands > 0) {
int activeFinger = getActiveFinger(0);
if (activeFinger >= 0) {
vec2 activePos = getFingerPos(activeFinger, 0);
vec3 activeColor = getFingerColor(activeFinger);
float dotSize = 0.015;
float dotEdge = 0.005;
color = mix(color, activeColor, smoothstep(dotSize, dotEdge, distance(uv, activePos)));
}
}
outColor = vec4(color, 1.0);
}`;
video = await getWebcamVideo();
outputCanvas = createFullscreenCanvas(mount);
shader = new ShaderPad(fragmentShaderSrc, {
canvas: outputCanvas,
plugins: [
autosize(),
helpers(),
hands({
textureName: 'u_webcam',
options: {
maxHands: 1,
history: N_HISTORY_FRAMES,
},
}),
],
});
shader.initializeTexture('u_webcam', video);
shader.play((_time, frame) => {
const options = { skipHistoryWrite: !!(frame % 5) };
shader!.updateTextures({ u_webcam: video! }, options);
return options;
});
}
export function destroy() {
if (shader) {
shader.destroy();
shader = null;
}
if (video) {
stopVideoStream(video);
video = null;
}
if (outputCanvas) {
outputCanvas.remove();
outputCanvas = null;
}
}