import ShaderPad from 'shaderpad';
import autosize from 'shaderpad/plugins/autosize';
import face from 'shaderpad/plugins/face';
import helpers from 'shaderpad/plugins/helpers';
import pose from 'shaderpad/plugins/pose';
import { createFullscreenCanvas } from 'shaderpad/util';
import { getWebcamVideo, stopVideoStream } from '@/examples/demo-utils';
import type { ExampleContext } from '@/examples/runtime';
const MODE_FACE = 0;
const MODE_BODY = 1;
let shader: ShaderPad | null = null;
let video: HTMLVideoElement | null = null;
let outputCanvas: HTMLCanvasElement | null = null;
let controls: HTMLDivElement | null = null;
let faceButton: HTMLButtonElement | null = null;
let bodyButton: HTMLButtonElement | null = null;
function updateButtonStyles(mode: number) {
if (!faceButton || !bodyButton) return;
const activeStyle = 'rgba(255, 255, 255, 0.95)';
const inactiveStyle = 'rgba(255, 255, 255, 0.16)';
const activeColor = '#0f172a';
const inactiveColor = 'rgba(255, 255, 255, 0.88)';
faceButton.style.background = mode === MODE_FACE ? activeStyle : inactiveStyle;
faceButton.style.color = mode === MODE_FACE ? activeColor : inactiveColor;
faceButton.setAttribute('aria-pressed', String(mode === MODE_FACE));
bodyButton.style.background = mode === MODE_BODY ? activeStyle : inactiveStyle;
bodyButton.style.color = mode === MODE_BODY ? activeColor : inactiveColor;
bodyButton.setAttribute('aria-pressed', String(mode === MODE_BODY));
}
function setMode(mode: number) {
shader?.updateUniforms({ u_mode: mode });
updateButtonStyles(mode);
}
function createModeButton(label: string) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = label;
button.style.border = '0';
button.style.borderRadius = '999px';
button.style.padding = '10px 16px';
button.style.font = '600 14px/1.2 system-ui, sans-serif';
button.style.cursor = 'pointer';
button.style.transition = 'background 160ms ease, color 160ms ease';
return button;
}
export async function init({ mount, overlay }: ExampleContext) {
const fragmentShaderSrc = `#version 300 es
precision mediump float;
in vec2 v_uv;
out vec4 outColor;
uniform sampler2D u_webcam;
uniform highp sampler2DArray u_history;
uniform int u_historyFrameOffset;
uniform int u_mode;
vec2 fitCoverInverse(vec2 uv, vec2 textureSize) {
vec2 scale = u_resolution.xy * textureSize.yx / (u_resolution.yx * textureSize.xy);
return (uv - 0.5) / min(scale, vec2(1.0)) + 0.5;
}
vec3 sampleHistoryAtWebcamUV(vec2 uv) {
vec2 historyUV = clamp(fitCoverInverse(uv, vec2(textureSize(u_webcam, 0))), 0.0, 1.0);
float z = historyZ(u_history, u_historyFrameOffset, 1);
return texture(u_history, vec3(historyUV, z)).rgb;
}
vec3 applyFaceCamo(vec2 uv, vec3 color, vec2 pixel) {
float closestCenter = 2.0;
for (int i = 0; i < u_nFaces; ++i) {
vec2 faceCenterPos = vec2(faceLandmark(i, FACE_LANDMARK_FACE_CENTER));
vec2 dir = uv - faceCenterPos;
float lenDir = length(dir);
if (lenDir >= closestCenter) continue;
closestCenter = lenDir;
if (lenDir < 1e-5) {
dir = vec2(0.0, 1.0);
} else {
dir /= lenDir;
}
vec2 uvNearerFaceCenter = uv - dir * 80.0 * pixel;
float faceConfidence = inFace(uv) + inFace(uvNearerFaceCenter);
if (faceConfidence > 0.0) {
vec2 target = clamp(uv + dir * (20.0 * pixel), 0.0, 1.0);
color = sampleHistoryAtWebcamUV(target);
}
}
return color;
}
vec3 applyBodyCamo(vec2 uv, vec3 color, vec2 pixel) {
float closestCenter = 2.0;
for (int i = 0; i < u_nPoses; ++i) {
vec2 dir = uv - vec2(poseLandmark(i, POSE_LANDMARK_BODY_CENTER));
float lenDir = length(dir);
if (lenDir >= closestCenter) continue;
closestCenter = lenDir;
if (lenDir < 1e-5) {
dir = vec2(0.0, 1.0);
} else {
dir /= lenDir;
}
vec2 uvNearerPoseCenter = uv - dir * 80.0 * pixel;
vec2 pose = poseAt(uv);
vec2 nearerPose = poseAt(uvNearerPoseCenter);
if ((pose.x > 0.0 && int(pose.y) == i) || (nearerPose.x > 0.0 && int(nearerPose.y) == i)) {
vec2 target = clamp(uv + dir * (20.0 * pixel), 0.0, 1.0);
color = sampleHistoryAtWebcamUV(target);
}
}
return color;
}
void main() {
vec2 texSize = vec2(textureSize(u_webcam, 0));
vec2 uv = fitCover(v_uv, texSize);
vec2 pixel = vec2(1.0) / texSize;
vec3 color = texture(u_webcam, uv).rgb;
if (u_mode == ${MODE_FACE}) {
color = applyFaceCamo(uv, color, pixel);
} else {
color = applyBodyCamo(uv, color, pixel);
}
outColor = vec4(color, 1.0);
}`;
video = await getWebcamVideo();
outputCanvas = createFullscreenCanvas(mount);
outputCanvas.style.transform = 'scaleX(-1)';
outputCanvas.style.transformOrigin = 'center';
controls = document.createElement('div');
controls.style.position = 'fixed';
controls.style.top = 'calc(env(safe-area-inset-top, 0px) + 120px)';
controls.style.left = '16px';
controls.style.display = 'flex';
controls.style.flexWrap = 'wrap';
controls.style.gap = '8px';
controls.style.padding = '8px';
controls.style.borderRadius = '999px';
controls.style.background = 'rgba(15, 23, 42, 0.55)';
controls.style.backdropFilter = 'blur(10px)';
controls.style.pointerEvents = 'auto';
controls.style.zIndex = '1000';
controls.style.maxWidth = 'calc(100vw - 32px)';
faceButton = createModeButton('Face');
bodyButton = createModeButton('Body');
faceButton.addEventListener('click', () => setMode(MODE_FACE));
bodyButton.addEventListener('click', () => setMode(MODE_BODY));
controls.appendChild(faceButton);
controls.appendChild(bodyButton);
overlay.appendChild(controls);
shader = new ShaderPad(fragmentShaderSrc, {
canvas: outputCanvas,
history: 1,
plugins: [
autosize(),
helpers(),
face({
textureName: 'u_webcam',
options: { maxFaces: 3 },
}),
pose({
textureName: 'u_webcam',
options: { maxPoses: 2 },
}),
],
});
shader.initializeUniform('u_mode', 'int', MODE_FACE);
shader.initializeTexture('u_webcam', video);
updateButtonStyles(MODE_FACE);
shader.play(() => {
shader!.updateTextures({ u_webcam: video! });
});
}
export function destroy() {
if (shader) {
shader.destroy();
shader = null;
}
if (video) {
stopVideoStream(video);
video = null;
}
if (outputCanvas) {
outputCanvas.remove();
outputCanvas = null;
}
if (controls) {
controls.remove();
controls = null;
}
faceButton = null;
bodyButton = null;
}