const model = bodySegmentation.SupportedModels.MediaPipeSelfieSegmentation;
const segmenterConfig = {
solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation@1.0.2'
const segmentationConfig = { flipHorizontal: false, landscape: true };
OPC.button('myButton', 'Save Image')
attribute vec3 aPosition;
attribute vec2 aTexCoord;
attribute vec4 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0);
gl_Position = uProjectionMatrix * viewModelPosition;
function maskShaderSource() {
uniform sampler2D content;
gl_FragColor = vec4(texture2D(content, vTexCoord).xyz, 1.) * smoothstep(0.3, 0.9, texture2D(mask, vTexCoord).x);
function stampShaderSource() {
uniform sampler2D content;
vec4 c = texture2D(content, vTexCoord);
function warpShaderSource() {
float map(float value, float min1, float max1, float min2, float max2) {
min2 + (value - min1) * (max2 - min2) / (max1 - min1),
vec3 p3 = fract(vec3(p.xyx) * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
float opSmoothUnion( float d1, float d2, float k )
float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
return mix( d2, d1, h ) - k*h*(1.0-h);
gl_FragColor = texture2D(next, vTexCoord);
bool onPrev = texture2D(prev, vTexCoord).a == OPAQUE;
bool onNext = texture2D(next, vTexCoord).a == OPAQUE;
float aOff = random(vTexCoord * 123.456);
vec2 closestPrevAngle = vec2(0.);
float prevAngleSamples = 0.;
for (int i = 0; i < 100; i++) {
float a = (float(i)/100. + aOff) * ${2 * Math.PI};
vec2 off = vec2(cos(a), sin(a)) * maxR;
vec2 pt = vTexCoord + off;
float val = texture2D(prev, pt).a;
if (onPrev ? val < OPAQUE : val >= OPAQUE) {
if (dot(closestPrevAngle, closestPrevAngle) > 0.) {
closestPrevAngle = normalize(closestPrevAngle);
closestPrevAngle = vec2(0.);
float closestPrevR = maxR;
for (int i = 0; i < 100; i++) {
float r = maxR * ((float(i) + aOff)/100.);
vec2 pt = vTexCoord + closestPrevAngle * r;
float val = texture2D(prev, pt).a;
if (onPrev ? val < OPAQUE : val >= OPAQUE) {
closestPrevR *= onPrev ? -1. : 1.;
vec2 closestPrev = vTexCoord + closestPrevR * closestPrevAngle;
vec2 closestNextAngle = vec2(0.);
float nextAngleSamples = 0.;
for (int i = 0; i < 100; i++) {
float a = (float(i)/100. + aOff) * ${2 * Math.PI};
vec2 off = vec2(cos(a), sin(a)) * maxR;
vec2 pt = vTexCoord + off;
float val = texture2D(next, pt).a;
if (onNext ? val < OPAQUE : val >= OPAQUE) {
if (dot(closestNextAngle, closestNextAngle) > 0.) {
closestNextAngle = normalize(closestNextAngle);
closestNextAngle = vec2(0.);
float closestNextR = maxR;
for (int i = 0; i < 100; i++) {
float r = maxR * ((float(i) + aOff)/100.);
vec2 pt = vTexCoord + closestNextAngle * r;
float val = texture2D(next, pt).a;
if (onNext ? val < OPAQUE : val >= OPAQUE) {
closestNextR *= onNext ? -1. : 1.;
vec2 closestNext = vTexCoord + closestNextR * closestNextAngle;
float smoothR = clamp(opSmoothUnion(closestNextR, closestPrevR, maxR), -maxR, maxR);
vec2 dirPrev = closestPrevAngle * (closestPrevR > 0. ? 1. : -1.);
if (dirPrev.x != 0. && dirPrev.y != 0.) {
dirPrev = normalize(dirPrev);
vec2 samplePrev = vTexCoord + dirPrev * maxR * (1. - abs(closestNextR/maxR)) * (1. - abs(closestPrevR/maxR)) * (smoothR < 0. ? 1. : 0.);
vec2 dirNext = closestNextAngle * (closestNextR > 0. ? 1. : -1.);
if (dirNext.x != 0. && dirNext.y != 0.) {
dirNext = normalize(dirNext);
vec2 sampleNext = vTexCoord + dirNext * maxR * (1. - abs(closestNextR/maxR)) * (1. - abs(closestPrevR/maxR)) * (smoothR < 0. ? 1. : 0.);
vec4 prevColor = texture2D(prev, samplePrev);
vec4 nextColor = texture2D(next, sampleNext);
float weightPrev = map(closestPrevR, -maxR, 0., 1., 0.);
float weightNext = map(closestNextR, -maxR, 0., 1., 0.);
float blendFactor = (weightPrev == 0. && weightNext == 0.) ? 0.5 : (weightNext / (weightPrev + weightNext));
gl_FragColor = mix(prevColor, nextColor, clamp(blendFactor, 0., 1.)) * (smoothR < 0. ? 1. : 0.);
font = loadFont('https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZhrib2Bg-4.ttf')
createCanvas(windowWidth, windowHeight, WEBGL)
const isMobile = window.navigator.userAgent && /Mobi|Android/i.test(window.navigator.userAgent)
cam = createCapture(isMobile ? {
} : VIDEO, { flipped: !isMobile })
fboSize = { width: 800, height: 800 }
bg = createFramebuffer(fboSize)
fg1 = createFramebuffer(fboSize)
fg2 = createFramebuffer(fboSize)
result = createFramebuffer(fboSize)
captureData = createGraphics(fboSize.width, fboSize.height)
maskData = createGraphics(fboSize.width, fboSize.height)
fbo = createFramebuffer(fboSize)
maskShader = createShader(...maskShaderSource())
warpShader = createShader(...warpShaderSource())
stampShader = createShader(...stampShaderSource())
segmenter = await bodySegmentation.createSegmenter(model, segmenterConfig)
function windowResized() {
resizeCanvas(windowWidth, windowHeight)
if (doStamp && !running) {
stampShader.setUniform('content', fg1)
plane(fg1.width, fg1.height)
captureData.image(cam, 0, 0, fboSize.width, fboSize.height, 0, 0, cam.width, cam.height, COVER)
segmenter.segmentPeople(captureData.elt, segmentationConfig).then((res) => {
const data = res[0].mask.toImageData()
maskData.drawingContext.putImageData(img, 0, 0)
maskShader.setUniform('content', captureData)
maskShader.setUniform('mask', maskData)
plane(fboSize.width, fboSize.height)
requestAnimationFrame(() => {
warpShader.setUniform('prev', fg2)
warpShader.setUniform('next', fbo)
warpShader.setUniform('first', !tapped)
warpShader.setUniform('maxR', maxR)
plane(fg1.width, fg1.height)
scale(min(width/fboSize.width, height/fboSize.height))
bg.draw(() => image(cam, 0, 0, fboSize.width, fboSize.height, 0, 0, cam.width, cam.height, COVER))
textAlign(CENTER, CENTER)
text('Tap to stamp', 0, -4)
function mouseClicked() {
function buttonReleased() {