Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/EditableSketch/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface Props {
height?: number;
base?: string;
scripts?: string[];
includeWebGPU?: boolean;
}

const props = Astro.props as Props;
Expand Down Expand Up @@ -38,6 +39,7 @@ const scrollable = props.scrollable ?? false;
allowSideBySide={!vertical}
fullWidth={!vertical}
scripts={props.scripts}
includeWebGPU={props.includeWebGPU}
cssCode={props.cssCode}
htmlBodyCode={props.bodyCode}
scrollable={scrollable}
Expand Down
3 changes: 3 additions & 0 deletions src/content/examples/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const examplesCollection = defineCollection({
// Aria label used for the live example code
arialabel: z.string().optional(),
featured: z.boolean().optional(),
// Additional libraries required by the embedded sketch.
scripts: z.array(z.url()).optional(),
webgpu: z.boolean().optional(),
relatedReference: z.array(reference("reference")).optional(),
featuredImage: image(),
featuredImageAlt: z.string().optional().default(""),
Expand Down
120 changes: 120 additions & 0 deletions src/content/examples/en/16_Shaders/10_Body_Field/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Inspired by
* Golan's https://openprocessing.org/@golan/2405195
* and Dave's https://editor.p5js.org/davepagurek/sketches/dTizHPqbM
*/

let video;
let bodyPose;
let poses = [];
let connections;
let dataStorage;
let bodyFieldShader;

const loopN = 100;
const pointN = 100;

// Adapted from https://openprocessing.org/@golan/2405195
function bodyField() {
const points = uniformStorage(dataStorage);
const progress = uniformFloat("u_progress");
const green = uniformFloat("u_green");
filterColor.begin();
const coord = filterColor.texCoord;

// Find the distance from this pixel to the nearest body point.
let minDist = 1.0;
for (let i = 0; i < 100; i++) {
const point = [points[i].x, points[i].y];
minDist = min(minDist, length(coord - point));
}
const r = 6.0 * minDist;
const g = green * coord.y;
const b = 0.5 + 0.5 * cos(TWO_PI * progress);
filterColor.set([r, g, b, 1]);
filterColor.end();
}

async function setup() {
await createCanvas(640, 640, WEBGPU);

// Share a fixed-size array of body points with the GPU.
dataStorage = createStorage(emptyPoints());
bodyFieldShader = buildFilterShader(bodyField);
bodyPose = await ml5.bodyPose();
video = createCapture(VIDEO);
video.size(width, height);
video.hide();
connections = bodyPose.getSkeleton();
bodyPose.detectStart(video, gotPoses);
}

function draw() {
if (!connections || !dataStorage) return;
const points = emptyPoints();
let count = 0;
for (const pose of poses) {
for (const [a, b] of connections) {
if (count >= pointN) break;
const pointA = pose.keypoints[a];
const pointB = pose.keypoints[b];
if (pointA.confidence <= 0.1 || pointB.confidence <= 0.1) continue;

// Use each skeleton segment's midpoint as a point in the field.
const x = (pointA.x + pointB.x) / 2;
const y = (pointA.y + pointB.y) / 2;

// Normalize to shader coordinates and mirror the webcam horizontally.
points[count++] = { x: 1 - x / width, y: y / height };
}
}
dataStorage.update(points);
const t = millis() / 1000;
const progress = (frameCount % loopN) / loopN;
bodyFieldShader.setUniform("u_progress", progress);
bodyFieldShader.setUniform("u_green", noise(t + 1));
filter(bodyFieldShader);
}

function emptyPoints() {
// Keep unused points far outside the canvas.
return Array.from({ length: pointN }, () => ({ x: -10, y: -10 }));
}

function gotPoses(results) {
// Track one person and ease toward each new pose to reduce jitter.
const smoothing = 0.2;
poses = results.slice(0, 1).map((pose, i) => {
const previousPose = poses[i];
if (!previousPose) return pose;
return {
...pose,
keypoints: pose.keypoints.map((point, j) => {
const previousPoint = previousPose.keypoints[j];
if (!previousPoint) return point;
return {
...point,
x: lerp(previousPoint.x, point.x, smoothing),
y: lerp(previousPoint.y, point.y, smoothing),
confidence: lerp(previousPoint.confidence, point.confidence, smoothing),
};
}),
};
});

// Merge facial landmarks so the head contributes a single location.
for (const pose of poses) {
const leftEye = pose.keypoints.find((p) => p.name === "left_eye");
const rightEye = pose.keypoints.find((p) => p.name === "right_eye");
if (!(leftEye?.confidence > 0.1 && rightEye?.confidence > 0.1)) continue;
const x = (leftEye.x + rightEye.x) / 2;
const y = (leftEye.y + rightEye.y) / 2;
const merged = ["nose", "left_eye", "right_eye", "left_ear", "right_ear"];
for (const point of pose.keypoints) {
if (merged.includes(point.name)) {
point.x = x;
point.y = y;
}
}
}
}
34 changes: 34 additions & 0 deletions src/content/examples/en/16_Shaders/10_Body_Field/description.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
featuredImage: "../../../images/featured/16_Shaders_10_body_field.png"
featuredImageAlt: Soft blue and purple circles trace a person's body against a pink background.
title: Body Field (p5.strands)
oneLineDescription: Turn webcam body tracking into a colorful distance field with p5.strands and ml5.js.
featured: false
webgpu: true
scripts:
- https://unpkg.com/ml5@1/dist/ml5.js

remix:
- description: Created by
attribution:
- name: Bairui Su
URL: https://bairui.dev/
code:
- label: Body Field source
URL: https://github.com/processing/p5.js-website/tree/main/src/content/examples/en/16_Shaders/10_Body_Field
- description: Inspired by
attribution:
- name: Golan Levin
URL: https://openprocessing.org/@golan
- name: Dave Pagurek
URL: https://www.davepagurek.com/
- collectivelyAttributedSince: 2026
---

Move in front of your webcam to shape a colorful field. The ml5.js BodyPose model
tracks your body, and the midpoints of its skeleton connections are passed to a
p5.strands filter shader using a storage buffer. The shader colors each pixel
based on its distance to the nearest point. Pose smoothing keeps the movement fluid.

This example requires a browser with WebGPU support and access to your webcam.
Allow camera access when prompted, and wait for the body tracking model to load.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion src/layouts/ExampleLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,11 @@ const remixHistoryHasCodeLinks = remixData?.some(

</div>

<EditableSketch code={code} />
<EditableSketch
code={code}
scripts={example.data.scripts}
includeWebGPU={example.data.webgpu}
/>
<p>{example.data.arialabel}</p>

</div>
Expand Down
Loading