// ====== SERVER SIDE GOOGLE APPS SCRIPT CODE ======
function doGet() {
// This serves the entire HTML interface from the string below
return HtmlService.createHtmlOutput(getHtmlContent())
.setTitle('Face Mold Booth')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function getHtmlContent() {
return `<!DOCTYPE htm
<html>
<head>
<meta charset="utf-8">
<title>Face Mold Booth</title>
<style>
body {
margin: 0;
font-family: 'Trebuchet MS', sans-serif;
background: linear-gradient(135deg, #6a5acd, #48c6ef);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
color: #222;
padding: 16px;
box-sizing: border-box;
}
h1 {
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
margin-bottom: 8px;
}
#stage {
position: relative;
background: #000;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
touch-action: none;
}
video, canvas {
display: block;
width: 480px;
height: 360px;
max-width: 90vw;
max-height: 60vh;
}
canvas {
cursor: grab;
}
canvas:active {
cursor: grabbing;
}
#video {
display: none;
}
.controls {
margin-top: 14px;
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
button {
font-size: 16px;
padding: 10px 18px;
border: none;
border-radius: 10px;
background: #ff6f61;
color: white;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transition: transform 0.1s, background-color 0.2s;
}
button:hover {
transform: scale(1.05);
}
button:disabled {
background: #999;
cursor: not-allowed;
transform: none;
}
.row {
margin-top: 15px;
display: flex;
align-items: center;
gap: 8px;
color: white;
font-weight: bold;
}
input[type=range] {
width: 140px;
}
#msg {
color: #fff8d0;
margin-top: 12px;
font-size: 14px;
text-align: center;
max-width: 500px;
font-weight: bold;
}
</style>
</head>
<body>
<h1>🫠 Face Mold Booth 🫠</h1>
<div id="stage">
<video id="video" autoplay playsinline></video>
<canvas id="canvas" width="480" height="360"></canvas>
</div>
<div class="controls">
<button id="startBtn">Start Camera</button>
<button id="snapBtn" disabled>Snap Photo</button>
<button id="resetBtn" disabled>Undo Mold</button>
<button id="retakeBtn" disabled>Retake Photo</button>
</div>
<div class="row">
<label for="brush">Mold Size:</label>
<input type="range" id="brush" min="20" max="150" value="60">
</div>
<div id="msg">Click "Start Camera" to begin, then snap a photo and drag your face around to mold it!</div>
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const startBtn = document.getElementById('startBtn');
const snapBtn = document.getElementById('snapBtn');
const resetBtn = document.getElementById('resetBtn');
const retakeBtn = document.getElementById('retakeBtn');
const brush = document.getElementById('brush');
const msg = document.getElementById('msg');
let stream = null;
let workingCanvas = document.createElement('canvas');
let workingCtx = workingCanvas.getContext('2d');
workingCanvas.width = canvas.width;
workingCanvas.height = canvas.height;
let history = [];
let hasPhoto = false;
let dragging = false;
let lastX = 0, lastY = 0;
let animationFrameId = null;
startBtn.addEventListener('click', async () => {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { width: 480, height: 360 },
audio: false
});
video.srcObject = stream;
snapBtn.disabled = false;
startBtn.disabled = true;
msg.textContent = "Camera ready! Smile and click 'Snap Photo'.";
hasPhoto = false;
drawVideoLoop();
} catch (e) {
msg.textContent = "Couldn't access camera. Please allow camera permission in Chrome and try again.";
}
});
function drawVideoLoop() {
if (!hasPhoto) {
ctx.save();
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
ctx.restore();
animationFrameId = requestAnimationFrame(drawVideoLoop);
}
}
snapBtn.addEventListener('click', () => {
hasPhoto = true;
cancelAnimationFrame(animationFrameId);
workingCtx.drawImage(canvas, 0, 0);
history = [];
saveHistory();
snapBtn.disabled = true;
resetBtn.disabled = false;
retakeBtn.disabled = false;
msg.textContent = "Now drag on your face to mold it like putty!";
if (stream) {
stream.getTracks().forEach(t => t.stop());
}
});
retakeBtn.addEventListener('click', async () => {
hasPhoto = false;
history = [];
resetBtn.disabled = true;
retakeBtn.disabled = true;
snapBtn.disabled = false;
msg.textContent = "Camera ready! Restarting video loop...";
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { width: 480, height: 360 },
audio: false
});
video.srcObject = stream;
drawVideoLoop();
} catch (e) {
msg.textContent = "Couldn't restart camera.";
}
});
function saveHistory() {
history.push(workingCtx.getImageData(0, 0, workingCanvas.width, workingCanvas.height));
if (history.length > 20) history.shift();
}
resetBtn.addEventListener('click', () => {
if (history.length > 1) {
history.pop();
const prev = history[history.length - 1];
workingCtx.putImageData(prev, 0, 0);
ctx.drawImage(workingCanvas, 0, 0);
msg.textContent = "Undid last mold!";
} else {
msg.textContent = "Original photo restored!";
}
});
function getPos(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
let clientX, clientY;
if (e.touches && e.touches.length) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
return {
x: (clientX - rect.left) * scaleX,
y: (clientY - rect.top) * scaleY
};
}
canvas.addEventListener('mousedown', (e) => {
if (!hasPhoto) return;
dragging = true;
const p = getPos(e);
lastX = p.x;
lastY = p.y;
});
canvas.addEventListener('touchstart', (e) => {
if (!hasPhoto) return;
dragging = true;
const p = getPos(e);
lastX = p.x;
lastY = p.y;
e.preventDefault();
}, { passive: false });
window.addEventListener('mousemove', (e) => {
if (!dragging || !hasPhoto) return;
const p = getPos(e);
moldFace(p.x, p.y);
});
canvas.addEventListener('touchmove', (e) => {
if (!dragging || !hasPhoto) return;
const p = getPos(e);
moldFace(p.x, p.y);
e.preventDefault();
}, { passive: false });
window.addEventListener('mouseup', () => {
if (dragging) {
dragging = false;
saveHistory();
}
});
window.addEventListener('touchend', () => {
if (dragging) {
dragging = false;
saveHistory();
}
});
function moldFace(currentX, currentY) {
const radius = parseInt(brush.value, 10);
const w = workingCanvas.width;
const h = workingCanvas.height;
const dx = currentX - lastX;
const dy = currentY - lastY;
if (dx === 0 && dy === 0) return;
const srcImgData = workingCtx.getImageData(0, 0, w, h);
const dstImgData = workingCtx.createImageData(w, h);
dstImgData.data.set(srcImgData.data);
const minX = Math.max(0, Math.floor(lastX - radius));
const maxX = Math.min(w - 1, Math.ceil(lastX + radius));