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);
vec2 smin( float a, float b, float k )
float h = 1.0 - min( abs(a-b)/(6.0*k), 1.0 );
return (a<b) ? vec2(a-s,m) : vec2(b-s,1.0-m);
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 distortion = min(1. - closestNextR/maxR, 1. - closestPrevR/maxR);
// vec2 distortionAngle = normalize(closestPrev - closestNext);
// https://iquilezles.org/articles/smin/
// vec2 smoothMin = smin(closestPrevR, closestNextR, maxR);
// float smoothR = clamp(smoothMin.x, -maxR, maxR);
// float blendFactor = smoothMin.y;
float smoothR = clamp(opSmoothUnion(closestNextR, closestPrevR, maxR), -maxR, maxR);
// float smoothR = opSmoothUnion(closestNextR, closestPrevR, maxR);
// closestNextR = max(0., closestNextR);
// closestPrevR = max(0., closestPrevR);
// vec2 samplePrev = closestPrev + (smoothR) * (abs(closestPrevR) == maxR ? vec2(0.) : normalize(closestPrev - vTexCoord));
// vec2 sampleNext = closestNext + abs(smoothR - closestNextR) * (abs(closestNextR) == maxR ? vec2(0.) : normalize(closestNext - vTexCoord));
// float prevMix = abs(smoothR - closestPrevR)/maxR;
// vec2 samplePrev = mix(vTexCoord, closestPrev, prevMix);
// float nextMix = abs(smoothR - closestNextR)/maxR;
// vec2 sampleNext = mix(vTexCoord, closestNext, nextMix);
// vec2 sampleNext = vTexCoord + smoothstep(0., maxR, abs(closestNext - vTexCoord)) * nextMix;
// float diff = map(closestNextR - smoothR, 0., maxR, 0., 1.);
/*vec2 sampleNext = vTexCoord + (closestNextR > 0.
? (closestNext - vTexCoord) * map(closestNextR, 0., maxR, 2.5, 1.)
: (vTexCoord - closestNext) * -map(closestNextR, -maxR, 0., 0., 2.)
// vec2 dirPrev = closestPrevR > 0. ? (closestPrev - vTexCoord) : (closestNext - vTexCoord);
vec2 dirPrev = closestPrevAngle * (closestPrevR > 0. ? 1. : -1.);
if (dirPrev.x != 0. && dirPrev.y != 0.) {
dirPrev = normalize(dirPrev);
// vec2 samplePrev = vTexCoord + dirPrev * 0.5 * (closestNextR + closestPrevR) * (smoothR < 0. ? 1. : 0.);
vec2 samplePrev = vTexCoord + dirPrev * maxR * (1. - abs(closestNextR/maxR)) * (1. - abs(closestPrevR/maxR)) * (smoothR < 0. ? 1. : 0.);
// vec2 dirNext = closestNextR > 0. ? (closestNext - vTexCoord) : (closestPrev - vTexCoord);
vec2 dirNext = closestNextAngle * (closestNextR > 0. ? 1. : -1.);
if (dirNext.x != 0. && dirNext.y != 0.) {
dirNext = normalize(dirNext);
// vec2 sampleNext = vTexCoord + dirNext * (clamp(closestNextR, 0., maxR) + clamp(closestPrevR, 0., maxR)) * (smoothR < 0. ? 1. : 0.);
vec2 sampleNext = vTexCoord + dirNext * maxR * (1. - abs(closestNextR/maxR)) * (1. - abs(closestPrevR/maxR)) * (smoothR < 0. ? 1. : 0.);
// float t = first ? 1. : (prevMix / (prevMix + nextMix + 0.0001));
vec4 prevColor = texture2D(prev, samplePrev);
vec4 nextColor = texture2D(next, sampleNext);
float weightPrev = map(closestPrevR, maxR, 0., 0., 1.) * (map(closestPrevR, -maxR, maxR*0.1, 1., 0.)); // * prevColor.a;
float weightNext = map(closestNextR, maxR, 0., 0., 1.) * (map(closestNextR, -maxR, maxR*0.1, 1., 0.)); // * nextColor.a;
float blendFactor = (weightPrev == 0. && weightNext == 0.) ? 0.5 : (weightNext / (weightPrev + weightNext));
// float blendFactor = 0.;
// float tPrev = 1. - prevColor.a;
// float tNext = 1. - nextColor.a;
// float t = (tPrev) / (tPrev + tNext + 0.0001);
gl_FragColor = mix(prevColor, nextColor, clamp(blendFactor, 0., 1.)) * (smoothR < 0. ? 1. : 0.);
// gl_FragColor = vec4(vec3(length(sampleNext - vTexCoord)), 1.);
// gl_FragColor = texture2D(next, vTexCoord);
// gl_FragColor = (prevColor + nextColor);
// if (prevColor.a + nextColor.a > 0.) gl_FragColor /= prevColor.a + nextColor.a;
// gl_FragColor = (prevColor * (1. - nextColor.a) + nextColor) * (smoothR <= 0. ? 1. : 0.);
// gl_FragColor = nextColor * (smoothR <= 0. ? 1. : 0.);
// gl_FragColor = vec4(1.) * clamp(abs(smoothR - closestNextR)/maxR, 0., 1.);
// gl_FragColor = texture2D(next, closestNext);
font = loadFont('https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZhrib2Bg-4.ttf')
createCanvas(800, 800, WEBGL)
const isMobile = window.navigator.userAgent && /Mobi|Android/i.test(window.navigator.userAgent)
cam = createCapture(isMobile ? {
} : VIDEO, { flipped: !isMobile })
fboSize = { width: 800, height: 800 }
fg1 = createFramebuffer()
fg2 = createFramebuffer()
captureData = createGraphics(fboSize.width, fboSize.height)
maskData = createGraphics(fboSize.width, fboSize.height)
prevMaskData = createGraphics(fboSize.width, fboSize.height)
fbo = createFramebuffer(fboSize)
maskShader = createShader(...maskShaderSource())
warpShader = createShader(...warpShaderSource())
stampShader = createShader(...stampShaderSource())
segmenter = await bodySegmentation.createSegmenter(model, segmenterConfig)
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()
prevMaskData.image(maskData, 0, 0)
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)
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() {