Duck-Powered Posture Police
Build a browser app that quacks at you when AI detects you slouching.
Introduction
30 Second Summary
You know that feeling when you've been hunched over your laptop for an hour and your back starts screaming at you? What if your computer could catch you in the act and embarrass you back into good posture?
In this project, you will build a web app that watches you through your webcam and uses AI to detect the moment you start slouching. When it catches you, a duck explodes onto your screen with a loud quack sound and refuses to leave until you sit up straight.
What You'll Build
A single-page web application that runs entirely in your browser, using your webcam and a pre-trained AI model to monitor your posture in real-time.
By the end of this project, you'll have:
- A complete HTML, CSS, and JavaScript web application built from scratch that accesses your webcam and runs AI in the browser.
- Real-time pose detection powered by TensorFlow.js and the MoveNet model, tracking your nose and shoulders at 30+ FPS.
- A posture calibration system that learns your "good posture" baseline and detects slouching with configurable thresholds and a debounce timer.
- Secret Mission: a slouch scoreboard with escalating duck severity levels that get more chaotic the more you slouch.
Are there any prerequisites?
No prior experience is needed. This guide walks you through installing everything from scratch, including your code editor and all required extensions.
Set Up Your Project and Tools
Before building your posture checker, you need a project folder with the right file structure in place. Every web application has three core file types working together: HTML for the page structure, CSS for visual styling, and JavaScript for interactive behavior.
In this step, you'll create the project folder, gather your duck image and sound assets, and confirm everything loads correctly in the browser.
In this step, get ready to:
- Create the quack-attack project folder with all required files.
- Download duck PNG images and a quack sound effect into the assets/ folder.
- Verify your project loads in the browser using Live Server.
Create the project folder structure
Your project needs a main folder with three code files and a subfolder for images and sounds. You'll create all of this from the terminal.
- In VS Code, click Terminal in the top menu bar.
- Click New Terminal to open the integrated terminal panel.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir quack-attack
cd quack-attack
- Create the assets subfolder by running:
mkdir assets
Now create the three empty code files. The command differs depending on your operating system.
macOS
- Create the empty files by running:
touch index.html style.css app.js
Windows
- Create the empty files by running:
New-Item -ItemType "File" -Name "index.html", "style.css", "app.js"
- Confirm the files exist by running:
ls
You should see app.js, assets, index.html, and style.css in the output.
Not seeing all four items?
Make sure you ran the file creation command from inside the quack-attack folder. If you're in the wrong directory, run cd ~/Desktop/quack-attack and try the creation command again.
Help me troubleshoot my folder setup.
- Now open this folder in VS Code. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the quack-attack folder, and click Open.
The VS Code Explorer sidebar now shows your project files and the assets folder.
Download your duck assets
Your app needs three duck PNG images and one quack sound effect. These are the files that will ambush your screen when you slouch.
- Search for "rubber duck clipart transparent PNG" on a free image site like Pixabay or OpenClipart.
- Download three different duck images. Any rubber duck or cartoon duck will work.
- Save them into your assets folder with these exact names: duck1.png, duck2.png, and duck3.png.
- Search for "quack sound effect" on a free sound site like Pixabay Audio or Freesound.
- Download one quack sound file and save it into your assets folder as quack.mp3.
Why do the file names matter?
Your JavaScript code will reference these exact file names to load the images and sound. If you name a file my_duck.png instead of duck1.png, the browser won't find it and your duck attack won't work.
Convert other formats to MP3
If the sound file you download is .wav or .ogg instead of .mp3, use a free online converter like CloudConvert to convert it to MP3 before saving it.
- Confirm your assets are in place. In the VS Code Explorer sidebar, expand the assets folder.
You should see duck1.png, duck2.png, duck3.png, and quack.mp3 listed inside the folder.
Assets not showing in the Explorer?
Make sure you saved the files directly into the assets subfolder inside quack-attack, not into a separate Downloads folder. Drag and drop them from your Downloads into the assets folder in the VS Code sidebar if needed.
Help me find my missing asset files.
Verify the setup with Live Server
The Live Server extension runs a local web server and opens your page in the browser. It also auto-refreshes whenever you save a file, so you'll see changes instantly throughout this project.
- In the VS Code Explorer sidebar, right-click index.html.
- Click Open with Live Server.
Your default browser opens a new tab at http://127.0.0.1:5500 showing a blank white page. This is expected because your index.html file is still empty.
What is 127.0.0.1:5500?
This is a local web address that points to your own computer. 127.0.0.1 means "this machine" and 5500 is the port number Live Server uses. Nothing is being uploaded to the internet.
Browser didn't open?
Check the VS Code status bar at the bottom. If you see Port: 5500, the server is running. Open Google Chrome manually and type http://127.0.0.1:5500 in the address bar.
If you don't see the port indicator, make sure you right-clicked index.html specifically, not a different file or the folder itself.
Help me get Live Server working.
Your project folder is structured and ready to go. Next up, you'll build the HTML page that holds your webcam feed, calibration button, and duck attack zone.
Build the HTML Structure and Load TensorFlow.js
Your project folder is set up and Live Server is running. Now it is time to give your app a face.
Every web page starts with an HTML file that defines the page structure. In this step, you will build the skeleton of Quack Attack: a webcam feed area, a calibrate button, a status message, and a zone where ducks will appear. You will also load the TensorFlow.js AI library so your browser can run pose detection.
In this step, get ready to:
- Build the HTML page structure with a video element, canvas overlay, button, and status display.
- Load TensorFlow.js and the MoveNet pose-detection model from a CDN.
- Verify the page renders correctly in your browser with no console errors.
Create the page structure
Your index.html file is currently empty. You will fill it with the foundational elements that every part of Quack Attack relies on.
- In VS Code, open index.html from the Explorer sidebar.
- Paste the following code into the file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quack Attack - Posture Police</title>
<!-- Link to external stylesheet -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Main app container -->
<div id="app">
<h1>Quack Attack</h1>
<p id="subtitle">The Duck-Powered Posture Police</p>
<!-- Webcam feed and pose overlay -->
<div id="webcam-container">
<video id="webcam" autoplay playsinline></video>
<canvas id="overlay"></canvas>
</div>
<!-- User controls -->
<div id="controls">
<button id="calibrate-btn" disabled>Calibrate Posture</button>
<p id="status">Loading AI model...</p>
</div>
<!-- Ducks appear here during a slouch attack -->
<div id="duck-zone"></div>
</div>
</body>
</html>
What does this code do?
This sets up the visual skeleton of your app. Here is what each piece does:
- video#webcam is where the live webcam feed will display. The autoplay and playsinline attributes let it play automatically without going fullscreen.
- canvas#overlay sits on top of the video. Your JavaScript will draw pose keypoints (dots on your nose and shoulders) here.
- button#calibrate-btn starts disabled because the AI model has not loaded yet. Once it is ready, JavaScript will enable it.
- div#duck-zone is an empty container. When you slouch, JavaScript will inject duck images into this zone.
How to think about HTML elements
If you have used Python, think of each HTML element like a function call that creates a visual object. Attributes like id="webcam" and disabled work like keyword arguments that configure that object.
If you have not used Python, just think of elements as labeled containers. The label (tag name) says what kind of thing it is. The attributes say how it should behave.
- Save index.html.
- Switch to Chrome. Your Live Server tab should auto-refresh and show the heading "Quack Attack", the subtitle, and the "Calibrate Posture" button.
Don't see any content in the browser?
- Make sure you saved index.html in VS Code. Unsaved files show a dot on the tab.
- Check that Live Server is still running. Look for the port number (5500) in the VS Code status bar at the bottom.
- Confirm your file is named exactly index.html (not index.html.txt or similar).
Ask for help: My index.html is not showing anything in the browser with Live Server running.
Load TensorFlow.js and the pose-detection model
Your page structure is ready, but it has no intelligence yet. You need to load the MoveNet pose-detection model so the browser can track your body. These libraries load from a CDN (a server that hosts popular JavaScript libraries), so there is nothing to install.
- In index.html, find the closing </body> tag.
- Add the following code directly above </body>:
<!-- Quack sound effect -->
<audio id="quack-sound" src="assets/quack.mp3" preload="auto"></audio>
<!-- Load TensorFlow.js core library -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js"></script>
<!-- Load the pose-detection model -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/pose-detection@2.1.3"></script>
<!-- Load our app logic (must come last) -->
<script src="app.js"></script>
What does this code do?
- The audio element preloads the quack sound so it can play instantly when slouching is detected.
- The first script tag loads TensorFlow.js version 4.22.0. This is the engine that runs machine learning models in the browser.
- The second script tag loads the pose-detection library version 2.1.3. This gives you the MoveNet model that detects body keypoints.
- app.js is loaded last because it depends on the two libraries above it. Script order matters.
Why load scripts at the bottom of the body?
Placing script tags just before </body> ensures all HTML elements exist in the page before JavaScript tries to find them. If scripts were in the <head>, JavaScript would run before the button and video elements are created, causing errors.
- Save index.html.
- Switch to Chrome and open DevTools by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).
- Check the Console tab. You should see no red errors about missing scripts.
Seeing a 404 error in the console?
- A 404 on app.js means the file does not exist in the same folder as index.html. Confirm app.js is in the root of quack-attack, not inside assets/.
- A 404 on quack.mp3 means the file is not in assets/ or is named differently. Check that the filename matches exactly.
- A network error on the CDN scripts usually means you are offline. These libraries load from the internet.
Ask for help: I am seeing 404 or script loading errors in my browser console for my Quack Attack project.
✔️ Awesome, I've got everything!
Great. Double check you have saved index.html and your console is error-free.
ⓧ I'd like to double check the full code
Here is the complete index.html at this point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quack Attack - Posture Police</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<h1>Quack Attack</h1>
<p id="subtitle">The Duck-Powered Posture Police</p>
<div id="webcam-container">
<video id="webcam" autoplay playsinline></video>
<canvas id="overlay"></canvas>
</div>
<div id="controls">
<button id="calibrate-btn" disabled>Calibrate Posture</button>
<p id="status">Loading AI model...</p>
</div>
<div id="duck-zone"></div>
</div>
<audio id="quack-sound" src="assets/quack.mp3" preload="auto"></audio>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/pose-detection@2.1.3"></script>
<script src="app.js"></script>
</body>
</html>
Your page structure is complete and the AI libraries are loading. Next up, you will write JavaScript to access the webcam and start detecting your pose in real time.
Access the Webcam and Run Pose Detection
Your HTML page has all the structural elements in place, but it looks bare and does nothing yet. This step brings it to life with styling, a live webcam feed, and an AI model that can see your body.
You will style the dark-themed layout, connect your webcam using a browser API, and load the MoveNet pose detection model. By the end, you will see green dots tracking your nose and shoulders in real time.
In this step, get ready to:
- Style the page with a dark theme, webcam layout, and duck animation keyframes.
- Stream your webcam feed into the browser using JavaScript.
- Load the MoveNet AI model and start detecting body keypoints every frame.
Style the page layout and webcam area
Before wiring up the webcam, you need the page to look right. The CSS gives the page a dark background, centers everything, and sizes the webcam area to 640x480 pixels.
- Switch to the style.css tab in VS Code and paste the following layout styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #1a1a2e;
color: #ffffff;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#app {
text-align: center;
padding: 20px;
}
h1 {
font-size: 2.5rem;
color: #ffd700;
margin-bottom: 5px;
}
#subtitle {
font-size: 1rem;
color: #aaaaaa;
margin-bottom: 20px;
}
#webcam-container {
position: relative;
display: inline-block;
border: 3px solid #333;
border-radius: 10px;
overflow: hidden;
}
#webcam {
width: 640px;
height: 480px;
transform: scaleX(-1);
display: block;
}
#overlay {
position: absolute;
top: 0;
left: 0;
width: 640px;
height: 480px;
transform: scaleX(-1);
pointer-events: none;
}
What does this CSS do?
The * reset removes default browser margins and padding. The body uses flexbox to center the entire app vertically and horizontally on a dark navy background.
The transform: scaleX(-1) on both the video and canvas flips them horizontally. This makes the webcam act like a mirror, so your left hand appears on the left side of the screen.
- Below the #overlay block you just pasted, add the controls, status indicators, and duck animation styles:
#controls {
margin-top: 20px;
}
#calibrate-btn {
padding: 12px 30px;
font-size: 1.1rem;
background-color: #4caf50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s;
}
#calibrate-btn:hover:not(:disabled) {
background-color: #45a049;
}
#calibrate-btn:disabled {
background-color: #555;
cursor: not-allowed;
}
#status {
margin-top: 15px;
font-size: 1.2rem;
font-weight: bold;
}
.status-good {
color: #4caf50;
}
.status-bad {
color: #ff4444;
}
.status-loading {
color: #ffaa00;
}
#duck-zone {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
}
.duck-img {
position: absolute;
width: 200px;
height: auto;
animation: duck-wobble 0.3s infinite alternate;
}
@keyframes duck-wobble {
0% {
transform: rotate(-10deg) scale(1);
}
100% {
transform: rotate(10deg) scale(1.05);
}
}
.slouch-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(255, 0, 0, 0.15);
z-index: 999;
pointer-events: none;
}
What do these styles do?
The #calibrate-btn styles create a green button that greys out when disabled. The three .status-good, .status-bad, and .status-loading classes let JavaScript swap the status text color by changing the class name.
The #duck-zone is a full-screen invisible container where duck images will appear later. The @keyframes duck-wobble animation makes ducks rock back and forth when they attack.
- Save style.css and check your browser.
- You should see the page with a dark navy background, a gold "Quack Attack" heading, a rounded webcam area (black rectangle), and a greyed-out "Calibrate Posture" button.
Page still looks unstyled?
Make sure your style.css file is saved and is in the same folder as index.html (not inside the assets/ subfolder). Check for a typo in the <link rel="stylesheet" href="style.css"> tag in your HTML.
help me debug why my CSS styles are not applying to the page
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css before moving on.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #1a1a2e;
color: #ffffff;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#app {
text-align: center;
padding: 20px;
}
h1 {
font-size: 2.5rem;
color: #ffd700;
margin-bottom: 5px;
}
#subtitle {
font-size: 1rem;
color: #aaaaaa;
margin-bottom: 20px;
}
#webcam-container {
position: relative;
display: inline-block;
border: 3px solid #333;
border-radius: 10px;
overflow: hidden;
}
#webcam {
width: 640px;
height: 480px;
transform: scaleX(-1);
display: block;
}
#overlay {
position: absolute;
top: 0;
left: 0;
width: 640px;
height: 480px;
transform: scaleX(-1);
pointer-events: none;
}
#controls {
margin-top: 20px;
}
#calibrate-btn {
padding: 12px 30px;
font-size: 1.1rem;
background-color: #4caf50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s;
}
#calibrate-btn:hover:not(:disabled) {
background-color: #45a049;
}
#calibrate-btn:disabled {
background-color: #555;
cursor: not-allowed;
}
#status {
margin-top: 15px;
font-size: 1.2rem;
font-weight: bold;
}
.status-good {
color: #4caf50;
}
.status-bad {
color: #ff4444;
}
.status-loading {
color: #ffaa00;
}
#duck-zone {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
}
.duck-img {
position: absolute;
width: 200px;
height: auto;
animation: duck-wobble 0.3s infinite alternate;
}
@keyframes duck-wobble {
0% {
transform: rotate(-10deg) scale(1);
}
100% {
transform: rotate(10deg) scale(1.05);
}
}
.slouch-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(255, 0, 0, 0.15);
z-index: 999;
pointer-events: none;
}
Set up webcam access and load the AI model
Now for the JavaScript. Your app needs to request camera permission from the browser, stream the video feed to the page, and then load the MoveNet pose detection model so it can start analyzing frames.
- Switch to app.js in VS Code and paste the following state variables, configuration, and DOM references:
// --- State ---
let detector = null;
let isCalibrated = false;
let baselineDistance = 0;
let isSlouching = false;
let slouchTimer = null;
let duckIsActive = false;
// --- Configuration ---
const SLOUCH_THRESHOLD = 0.15;
const SLOUCH_DELAY_MS = 1500;
const MIN_CONFIDENCE = 0.3;
// --- DOM Elements ---
const video = document.getElementById('webcam');
const canvas = document.getElementById('overlay');
const ctx = canvas.getContext('2d');
const calibrateBtn = document.getElementById('calibrate-btn');
const statusText = document.getElementById('status');
const duckZone = document.getElementById('duck-zone');
const quackSound = document.getElementById('quack-sound');
// --- Duck images ---
const duckImages = ['assets/duck1.png', 'assets/duck2.png', 'assets/duck3.png'];
What do these declarations do?
The state variables track everything the app needs to remember: whether the model is loaded, whether the user has calibrated, and whether a duck is currently attacking.
The configuration constants control how sensitive the slouch detection is. SLOUCH_THRESHOLD of 0.15 means a 15% drop from your calibrated posture triggers a warning. MIN_CONFIDENCE of 0.3 filters out low-quality keypoint detections.
The DOM references grab handles to the HTML elements you built in the previous step, so JavaScript can read the video feed and update the page.
You will not see any change yet. The next chunk adds the functions that use these variables.
- Below the duckImages array, paste the init, setupWebcam, and loadModel functions:
// --- Start the app ---
async function init() {
await setupWebcam();
await loadModel();
detectPose();
}
// --- Webcam Setup ---
async function setupWebcam() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});
video.srcObject = stream;
await new Promise((resolve) => {
video.onloadedmetadata = () => {
resolve();
};
});
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
} catch (error) {
statusText.textContent = 'Camera access denied. Please allow camera access and reload.';
statusText.className = 'status-bad';
throw error;
}
}
// --- Load MoveNet Model ---
async function loadModel() {
statusText.textContent = 'Loading AI model...';
statusText.className = 'status-loading';
detector = await poseDetection.createDetector(
poseDetection.SupportedModels.MoveNet,
{
modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING
}
);
statusText.textContent = 'Model loaded! Click Calibrate while sitting up straight.';
statusText.className = 'status-good';
calibrateBtn.disabled = false;
}
What do these functions do?
The init() function is the entry point. It runs the webcam setup, then loads the model, then starts the detection loop (which you will write next).
The setupWebcam() function calls navigator.mediaDevices.getUserMedia() to request camera permission from the browser. If the user allows it, the video stream is piped into the <video> element. If they deny it, a red error message appears.
The loadModel() function downloads the MoveNet SINGLEPOSE_LIGHTNING model from the CDN. This model is optimised for speed over accuracy, running at 50+ FPS on most machines. Once loaded, it enables the Calibrate button.
Why will Chrome ask for camera permission?
Browsers require explicit user consent before any website can access your webcam. When your code calls getUserMedia(), Chrome shows a permission popup near the address bar asking you to Allow or Block camera access.
Since Live Server runs on 127.0.0.1 (localhost), Chrome treats it as a secure context and allows the permission request. You only need to grant it once per session.
Do not save yet. The init() function calls detectPose() which does not exist yet. If you save now, the console will show a reference error.
Create the pose detection loop
The detection loop runs every animation frame (roughly 60 times per second). Each iteration asks the MoveNet model to estimate body keypoints from the current video frame, then draws coloured dots on your nose and shoulders.
- Below the loadModel function in app.js, paste the detection loop and the keypoint drawing function:
// --- Pose Detection Loop ---
async function detectPose() {
if (!detector) {
requestAnimationFrame(detectPose);
return;
}
const poses = await detector.estimatePoses(video);
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (poses.length > 0) {
const keypoints = poses[0].keypoints;
const nose = keypoints[0];
const leftShoulder = keypoints[5];
const rightShoulder = keypoints[6];
if (nose.score > MIN_CONFIDENCE &&
leftShoulder.score > MIN_CONFIDENCE &&
rightShoulder.score > MIN_CONFIDENCE) {
drawKeypoints(nose, leftShoulder, rightShoulder);
const shoulderMidY = (leftShoulder.y + rightShoulder.y) / 2;
const currentDistance = shoulderMidY - nose.y;
if (isCalibrated) {
checkPosture(currentDistance);
}
}
}
requestAnimationFrame(detectPose);
}
// --- Draw keypoints on canvas ---
function drawKeypoints(nose, leftShoulder, rightShoulder) {
const points = [nose, leftShoulder, rightShoulder];
const color = isSlouching ? '#ff4444' : '#4caf50';
points.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 8, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
});
ctx.beginPath();
ctx.moveTo(leftShoulder.x, leftShoulder.y);
ctx.lineTo(rightShoulder.x, rightShoulder.y);
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.stroke();
}
// --- Start ---
init();
What does the detection loop do?
Each frame, detector.estimatePoses(video) passes the current video image to the MoveNet model and returns an array of detected poses. Each pose contains 17 keypoints (one for each body joint).
The code extracts the nose (index 0), left shoulder (index 5), and right shoulder (index 6). It only uses them if the model is confident enough (score > 0.3).
The drawKeypoints function paints green circles on those three points and a line connecting both shoulders. The requestAnimationFrame(detectPose) call at the bottom restarts the loop for the next frame.
- Save app.js and switch to Chrome.
- Chrome will display a camera permission popup near the address bar. Click Allow.
- You should see your live webcam feed with three green dots (one on your nose, one on each shoulder) and a green line connecting your shoulders. The status text should read "Model loaded! Click Calibrate while sitting up straight."
Not seeing the camera feed or green dots?
- If the status text says "Camera access denied", you clicked Block on the permission popup. Click the camera icon in Chrome's address bar, select Allow, then reload the page.
- On macOS, Chrome also needs OS-level camera permission. Open System Settings > Privacy & Security > Camera and make sure Google Chrome is toggled on.
- If the webcam shows but no green dots appear, open DevTools (F12) and check the Console tab for errors. A slow network may take a few seconds to download the model.
help me troubleshoot why my webcam or pose detection is not working
✔️ Awesome, I've got everything!
Great. Double check you have saved app.js before moving on.
ⓧ I'd like to double check the full code
// --- State ---
let detector = null;
let isCalibrated = false;
let baselineDistance = 0;
let isSlouching = false;
let slouchTimer = null;
let duckIsActive = false;
// --- Configuration ---
const SLOUCH_THRESHOLD = 0.15;
const SLOUCH_DELAY_MS = 1500;
const MIN_CONFIDENCE = 0.3;
// --- DOM Elements ---
const video = document.getElementById('webcam');
const canvas = document.getElementById('overlay');
const ctx = canvas.getContext('2d');
const calibrateBtn = document.getElementById('calibrate-btn');
const statusText = document.getElementById('status');
const duckZone = document.getElementById('duck-zone');
const quackSound = document.getElementById('quack-sound');
// --- Duck images ---
const duckImages = ['assets/duck1.png', 'assets/duck2.png', 'assets/duck3.png'];
// --- Start the app ---
async function init() {
await setupWebcam();
await loadModel();
detectPose();
}
// --- Webcam Setup ---
async function setupWebcam() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});
video.srcObject = stream;
await new Promise((resolve) => {
video.onloadedmetadata = () => {
resolve();
};
});
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
} catch (error) {
statusText.textContent = 'Camera access denied. Please allow camera access and reload.';
statusText.className = 'status-bad';
throw error;
}
}
// --- Load MoveNet Model ---
async function loadModel() {
statusText.textContent = 'Loading AI model...';
statusText.className = 'status-loading';
detector = await poseDetection.createDetector(
poseDetection.SupportedModels.MoveNet,
{
modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING
}
);
statusText.textContent = 'Model loaded! Click Calibrate while sitting up straight.';
statusText.className = 'status-good';
calibrateBtn.disabled = false;
}
// --- Pose Detection Loop ---
async function detectPose() {
if (!detector) {
requestAnimationFrame(detectPose);
return;
}
const poses = await detector.estimatePoses(video);
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (poses.length > 0) {
const keypoints = poses[0].keypoints;
const nose = keypoints[0];
const leftShoulder = keypoints[5];
const rightShoulder = keypoints[6];
if (nose.score > MIN_CONFIDENCE &&
leftShoulder.score > MIN_CONFIDENCE &&
rightShoulder.score > MIN_CONFIDENCE) {
drawKeypoints(nose, leftShoulder, rightShoulder);
const shoulderMidY = (leftShoulder.y + rightShoulder.y) / 2;
const currentDistance = shoulderMidY - nose.y;
if (isCalibrated) {
checkPosture(currentDistance);
}
}
}
requestAnimationFrame(detectPose);
}
// --- Draw keypoints on canvas ---
function drawKeypoints(nose, leftShoulder, rightShoulder) {
const points = [nose, leftShoulder, rightShoulder];
const color = isSlouching ? '#ff4444' : '#4caf50';
points.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 8, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
});
ctx.beginPath();
ctx.moveTo(leftShoulder.x, leftShoulder.y);
ctx.lineTo(rightShoulder.x, rightShoulder.y);
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.stroke();
}
// --- Start ---
init();
Your webcam is live and the AI model is tracking your body in real time. Next up, you will build the calibration system that learns your good posture and detects when you slouch.
Implement Posture Calibration and Slouch Detection
Your app can see keypoints on your body, but it has no idea what "good posture" means for you specifically. Everyone sits differently. A tall person's nose-to-shoulder distance looks nothing like a short person's.
In this step, you'll build a calibration system that snapshots your posture while sitting up straight. Then you'll add detection logic that compares every frame against that baseline and flags when you're slouching.
In this step, get ready to:
- Build the calibration function that captures your "good posture" baseline.
- Write slouch detection logic with a debounce timer to prevent false alarms.
Build the calibration function
The calibration function captures the vertical distance between your nose and your shoulders at the moment you click the button. This distance becomes the baseline that every future frame is compared against.
- In app.js, scroll to the bottom of the file (below the drawKeypoints function).
- Add the calibration event listener by pasting this code at the bottom:
// --- Calibration ---
calibrateBtn.addEventListener('click', () => {
isCalibrated = false;
const calibrate = async () => {
const poses = await detector.estimatePoses(video);
if (poses.length > 0) {
const keypoints = poses[0].keypoints;
const nose = keypoints[0];
const leftShoulder = keypoints[5];
const rightShoulder = keypoints[6];
if (nose.score > MIN_CONFIDENCE &&
leftShoulder.score > MIN_CONFIDENCE &&
rightShoulder.score > MIN_CONFIDENCE) {
const shoulderMidY = (leftShoulder.y + rightShoulder.y) / 2;
baselineDistance = shoulderMidY - nose.y;
isCalibrated = true;
statusText.textContent = 'Calibrated! Monitoring your posture...';
statusText.className = 'status-good';
calibrateBtn.textContent = 'Re-Calibrate';
} else {
statusText.textContent = 'Could not detect pose. Make sure your head and shoulders are visible.';
statusText.className = 'status-bad';
}
}
};
calibrate();
});
What does this code do?
When the learner clicks the Calibrate button, the code takes a fresh pose snapshot from the current video frame.
- It calculates shoulderMidY (the average Y position of both shoulders) and subtracts the nose's Y position. This gives the vertical distance between your nose and shoulders.
- That distance is saved to baselineDistance. Every future frame will be compared against this number.
- The button text changes to "Re-Calibrate" so you can recalibrate anytime your seating position changes.
Why is Y subtraction the right measurement?
In browser coordinates, Y increases downward. Your shoulders have a larger Y value than your nose (they're lower on screen). The difference (shoulder Y minus nose Y) represents how far above your shoulders your nose sits.
When you slouch, your head drops forward and down. The nose Y increases, shrinking the distance. The smaller the distance compared to your baseline, the worse your posture.
- Save app.js.
- Switch to your browser. Sit up straight and click Calibrate Posture.
- The status text should change to "Calibrated! Monitoring your posture..." and the button text should change to "Re-Calibrate".
Status still says 'Loading AI model'?
- Make sure you waited for the model to finish loading. The button should be clickable (not grayed out) before you try calibrating.
- Check that your head and both shoulders are clearly visible in the webcam frame.
- Open the browser console (F12) and check for errors. A red error message will tell you what went wrong.
Help me debug my calibration button
Write the slouch detection logic with debounce
Now that calibration captures a baseline, you need a function that runs on every frame and decides whether the current distance counts as slouching. A single dropped frame shouldn't trigger an alarm, so you'll add a 1.5-second debounce timer.
- In app.js, add the slouch detection function below the calibration code you just pasted:
// --- Slouch Detection ---
function checkPosture(currentDistance) {
const deviation = (baselineDistance - currentDistance) / baselineDistance;
if (deviation > SLOUCH_THRESHOLD) {
if (!isSlouching && !slouchTimer) {
slouchTimer = setTimeout(() => {
isSlouching = true;
triggerDuckAttack();
}, SLOUCH_DELAY_MS);
}
} else {
if (slouchTimer) {
clearTimeout(slouchTimer);
slouchTimer = null;
}
if (isSlouching) {
isSlouching = false;
removeDuckAttack();
}
}
}
What does this code do?
- It calculates deviation as the percentage drop from your calibrated baseline. A deviation of 0.15 means your nose-to-shoulder distance has shrunk by 15%.
- If deviation exceeds SLOUCH_THRESHOLD (0.15), a 1.5-second timer starts. This prevents false alarms from quick head movements like glancing at your phone.
- If you sit back up before the timer finishes, clearTimeout cancels it. No alarm fired.
- The function calls triggerDuckAttack() and removeDuckAttack() which you'll build in the next step.
Why use a debounce timer?
Without debouncing, any momentary head dip (reaching for your coffee, checking your phone) would trigger the duck attack instantly. The 1.5-second delay ensures only sustained slouching activates the alarm.
The timer also clears the moment you sit back up, so brief slouches during the countdown are forgiven.
- Save app.js.
- Switch to your browser. Click Re-Calibrate while sitting up straight.
- Deliberately slouch forward for about 2 seconds. Watch the keypoint dots on the canvas overlay. They should turn from green to red after 1.5 seconds.
- Sit back up straight. The dots should return to green.
Console errors are expected right now
You'll see console errors about triggerDuckAttack or removeDuckAttack not being defined. That's normal. You'll build those functions in the next step.
Dots not turning red when you slouch?
- Make sure you calibrated first. The detection only runs after isCalibrated is set to true.
- Try a more exaggerated slouch. Lean your head significantly forward and down. A subtle slump might not exceed the 15% threshold.
- Check that the checkPosture function is placed after the calibration event listener, not inside it.
Help me debug my slouch detection
✔️ Awesome, I've got everything!
Great. Double check you've saved app.js before moving on.
ⓧ I'd like to double check the full code
Here's the full app.js file as it should look right now:
// --- State ---
let detector = null;
let isCalibrated = false;
let baselineDistance = 0;
let isSlouching = false;
let slouchTimer = null;
let duckIsActive = false;
// --- Configuration ---
const SLOUCH_THRESHOLD = 0.15;
const SLOUCH_DELAY_MS = 1500;
const MIN_CONFIDENCE = 0.3;
// --- DOM Elements ---
const video = document.getElementById('webcam');
const canvas = document.getElementById('overlay');
const ctx = canvas.getContext('2d');
const calibrateBtn = document.getElementById('calibrate-btn');
const statusText = document.getElementById('status');
const duckZone = document.getElementById('duck-zone');
const quackSound = document.getElementById('quack-sound');
// --- Duck images ---
const duckImages = ['assets/duck1.png', 'assets/duck2.png', 'assets/duck3.png'];
// --- Start the app ---
async function init() {
await setupWebcam();
await loadModel();
detectPose();
}
// --- Webcam Setup ---
async function setupWebcam() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});
video.srcObject = stream;
await new Promise((resolve) => {
video.onloadedmetadata = () => {
resolve();
};
});
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
} catch (error) {
statusText.textContent = 'Camera access denied. Please allow camera access and reload.';
statusText.className = 'status-bad';
throw error;
}
}
// --- Load MoveNet Model ---
async function loadModel() {
statusText.textContent = 'Loading AI model...';
statusText.className = 'status-loading';
detector = await poseDetection.createDetector(
poseDetection.SupportedModels.MoveNet,
{
modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING
}
);
statusText.textContent = 'Model loaded! Click Calibrate while sitting up straight.';
statusText.className = 'status-good';
calibrateBtn.disabled = false;
}
// --- Pose Detection Loop ---
async function detectPose() {
if (!detector) {
requestAnimationFrame(detectPose);
return;
}
const poses = await detector.estimatePoses(video);
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (poses.length > 0) {
const keypoints = poses[0].keypoints;
const nose = keypoints[0];
const leftShoulder = keypoints[5];
const rightShoulder = keypoints[6];
if (nose.score > MIN_CONFIDENCE &&
leftShoulder.score > MIN_CONFIDENCE &&
rightShoulder.score > MIN_CONFIDENCE) {
drawKeypoints(nose, leftShoulder, rightShoulder);
const shoulderMidY = (leftShoulder.y + rightShoulder.y) / 2;
const currentDistance = shoulderMidY - nose.y;
if (isCalibrated) {
checkPosture(currentDistance);
}
}
}
requestAnimationFrame(detectPose);
}
// --- Draw keypoints on canvas ---
function drawKeypoints(nose, leftShoulder, rightShoulder) {
const points = [nose, leftShoulder, rightShoulder];
const color = isSlouching ? '#ff4444' : '#4caf50';
points.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 8, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
});
ctx.beginPath();
ctx.moveTo(leftShoulder.x, leftShoulder.y);
ctx.lineTo(rightShoulder.x, rightShoulder.y);
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.stroke();
}
// --- Calibration ---
calibrateBtn.addEventListener('click', () => {
isCalibrated = false;
const calibrate = async () => {
const poses = await detector.estimatePoses(video);
if (poses.length > 0) {
const keypoints = poses[0].keypoints;
const nose = keypoints[0];
const leftShoulder = keypoints[5];
const rightShoulder = keypoints[6];
if (nose.score > MIN_CONFIDENCE &&
leftShoulder.score > MIN_CONFIDENCE &&
rightShoulder.score > MIN_CONFIDENCE) {
const shoulderMidY = (leftShoulder.y + rightShoulder.y) / 2;
baselineDistance = shoulderMidY - nose.y;
isCalibrated = true;
statusText.textContent = 'Calibrated! Monitoring your posture...';
statusText.className = 'status-good';
calibrateBtn.textContent = 'Re-Calibrate';
} else {
statusText.textContent = 'Could not detect pose. Make sure your head and shoulders are visible.';
statusText.className = 'status-bad';
}
}
};
calibrate();
});
// --- Slouch Detection ---
function checkPosture(currentDistance) {
const deviation = (baselineDistance - currentDistance) / baselineDistance;
if (deviation > SLOUCH_THRESHOLD) {
if (!isSlouching && !slouchTimer) {
slouchTimer = setTimeout(() => {
isSlouching = true;
triggerDuckAttack();
}, SLOUCH_DELAY_MS);
}
} else {
if (slouchTimer) {
clearTimeout(slouchTimer);
slouchTimer = null;
}
if (isSlouching) {
isSlouching = false;
removeDuckAttack();
}
}
}
Your app now knows what good posture looks like and can detect when you're slouching. Next up, you'll unleash the ducks.
Unleash the Ducks
Your posture detection system can spot a slouch, but right now it only updates a text label. That is not nearly terrifying enough to fix your posture.
In this step, you will write the functions that bring the chaos. When the slouch detector fires, a random duck PNG will explode onto your screen with a wobble animation, a quack sound will blare, and a red overlay will tint the page. The moment you sit up straight, everything vanishes.
In this step, get ready to:
- Write the duck attack function that spawns a duck image, plays a quack, and adds a red overlay.
- Add the duck retreat function that clears everything when you sit up straight.
- Start the app and test the full slouch-to-duck experience end to end.
Write the duck attack function
The checkPosture() function you wrote in the previous step calls triggerDuckAttack() when a slouch is confirmed. That function does not exist yet. Time to build it.
- In app.js, scroll to the bottom of the file (below the checkPosture() function).
- Add the triggerDuckAttack() function by copying the following code at the bottom of your file:
// --- Duck Attack ---
function triggerDuckAttack() {
if (duckIsActive) return;
duckIsActive = true;
statusText.textContent = 'QUACK! Sit up straight!';
statusText.className = 'status-bad';
const overlay = document.createElement('div');
overlay.className = 'slouch-overlay';
overlay.id = 'slouch-overlay';
document.body.appendChild(overlay);
const randomDuck = duckImages[Math.floor(Math.random() * duckImages.length)];
const duckImg = document.createElement('img');
duckImg.src = randomDuck;
duckImg.className = 'duck-img';
duckImg.style.top = Math.random() * 50 + 10 + '%';
duckImg.style.left = Math.random() * 60 + 10 + '%';
duckZone.appendChild(duckImg);
quackSound.currentTime = 0;
quackSound.play().catch(() => {});
}
What does this code do?
- if (duckIsActive) return; prevents multiple ducks from stacking up during a single slouch event. The function exits immediately if an attack is already in progress.
- The red overlay is a new div with the class slouch-overlay that covers the entire viewport with a semi-transparent red background. The CSS for this class already exists in your stylesheet.
- Math.random() positions the duck image at a random spot on screen, so it appears unpredictably each time.
- quackSound.play().catch(() => {}) plays the quack sound. The .catch(() => {}) silently handles edge cases where the browser blocks audio playback.
How does the duck wobble?
The duck image gets the CSS class duck-img, which applies the duck-wobble keyframe animation from your style.css. That animation rapidly rotates the duck between -10 and 10 degrees while slightly scaling it up, creating a frantic shaking effect.
- Save app.js.
- You won't see a change yet. The removeDuckAttack() function is still missing, so testing now would leave a duck permanently stuck on screen.
Seeing a red underline on triggerDuckAttack?
If your editor shows a warning, it is likely because removeDuckAttack() is referenced in checkPosture() but does not exist yet. This is expected and will resolve in the next chunk.
Help me understand why my editor shows warnings for functions I haven't written yet.
Add the duck retreat logic
When the user corrects their posture, the checkPosture() function calls removeDuckAttack(). This function needs to undo everything: remove the duck, clear the red overlay, stop the sound, and restore the good-posture message.
- In app.js, add the removeDuckAttack() function directly below triggerDuckAttack(), followed by the init() call that starts the entire app:
function removeDuckAttack() {
duckIsActive = false;
statusText.textContent = 'Good posture! Keep it up!';
statusText.className = 'status-good';
const overlay = document.getElementById('slouch-overlay');
if (overlay) overlay.remove();
duckZone.innerHTML = '';
quackSound.pause();
quackSound.currentTime = 0;
}
// --- Start ---
init();
What does this code do?
- duckIsActive = false resets the guard flag so the next slouch can trigger a fresh attack.
- document.getElementById('slouch-overlay') finds the red overlay by its ID and removes it from the page.
- duckZone.innerHTML = '' wipes all duck images from the duck container in one line.
- quackSound.pause() stops the quack audio, and currentTime = 0 rewinds it to the beginning so it plays from the start on the next slouch.
- init() at the very bottom kicks off the entire app. It sets up the webcam, loads the AI model, and starts the detection loop.
- Save app.js.
Console errors after saving?
Make sure removeDuckAttack() is placed after the closing brace of triggerDuckAttack(), not inside it. Each function needs its own opening and closing braces.
Also confirm that init(); is the very last line in the file, outside of any function.
Help me fix JavaScript errors in my duck attack code.
Test the full duck attack experience
Your app is now fully wired. The pose detector feeds into the slouch checker, which triggers the duck attack, which cleans up when you sit back up. Time to see it all in action.
- Switch to your browser tab where Live Server is running.
- Wait for the status to show "Model loaded! Click Calibrate while sitting up straight."
- Sit up straight and click Calibrate Posture.
- Deliberately slouch forward in your chair and hold it for about 2 seconds.
After 1.5 seconds of continuous slouching, a duck PNG should appear on screen with a wobble animation, a quack sound should play, and the entire page should tint red.
- Sit back up straight.
The duck disappears, the sound stops, the red overlay clears, and the status returns to "Good posture! Keep it up!".
Duck not appearing when you slouch?
Open the browser console by pressing F12 and check for error messages. Common issues:
- If you see a 404 error for the duck images, double-check that your assets/ folder contains files named exactly duck1.png, duck2.png, and duck3.png.
- If the status text never changes to "SLOUCHING DETECTED", you may not be slouching far enough. Try exaggerating the slouch by dropping your head forward.
- If the quack sound does not play, make sure assets/quack.mp3 exists and that your browser volume is not muted.
Help me debug why my duck attack is not triggering.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.js and your ducks are quacking on cue.
ⓧ I'd like to double check the full code
Here is the complete app.js file. Compare yours against it line by line.
// --- State ---
let detector = null;
let isCalibrated = false;
let baselineDistance = 0;
let isSlouching = false;
let slouchTimer = null;
let duckIsActive = false;
// --- Configuration ---
const SLOUCH_THRESHOLD = 0.15;
const SLOUCH_DELAY_MS = 1500;
const MIN_CONFIDENCE = 0.3;
// --- DOM Elements ---
const video = document.getElementById('webcam');
const canvas = document.getElementById('overlay');
const ctx = canvas.getContext('2d');
const calibrateBtn = document.getElementById('calibrate-btn');
const statusText = document.getElementById('status');
const duckZone = document.getElementById('duck-zone');
const quackSound = document.getElementById('quack-sound');
// --- Duck images (place your PNGs in assets/) ---
const duckImages = ['assets/duck1.png', 'assets/duck2.png', 'assets/duck3.png'];
// --- Start the app ---
async function init() {
await setupWebcam();
await loadModel();
detectPose();
}
// --- Webcam Setup ---
async function setupWebcam() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});
video.srcObject = stream;
await new Promise((resolve) => {
video.onloadedmetadata = () => {
resolve();
};
});
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
} catch (error) {
statusText.textContent = 'Camera access denied. Please allow camera access and reload.';
statusText.className = 'status-bad';
throw error;
}
}
// --- Load MoveNet Model ---
async function loadModel() {
statusText.textContent = 'Loading AI model...';
statusText.className = 'status-loading';
detector = await poseDetection.createDetector(
poseDetection.SupportedModels.MoveNet,
{
modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING
}
);
statusText.textContent = 'Model loaded! Click Calibrate while sitting up straight.';
statusText.className = 'status-good';
calibrateBtn.disabled = false;
}
// --- Pose Detection Loop ---
async function detectPose() {
if (!detector) {
requestAnimationFrame(detectPose);
return;
}
const poses = await detector.estimatePoses(video);
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (poses.length > 0) {
const keypoints = poses[0].keypoints;
const nose = keypoints[0];
const leftShoulder = keypoints[5];
const rightShoulder = keypoints[6];
if (nose.score > MIN_CONFIDENCE &&
leftShoulder.score > MIN_CONFIDENCE &&
rightShoulder.score > MIN_CONFIDENCE) {
drawKeypoints(nose, leftShoulder, rightShoulder);
const shoulderMidY = (leftShoulder.y + rightShoulder.y) / 2;
const currentDistance = shoulderMidY - nose.y;
if (isCalibrated) {
checkPosture(currentDistance);
}
}
}
requestAnimationFrame(detectPose);
}
// --- Draw keypoints on canvas ---
function drawKeypoints(nose, leftShoulder, rightShoulder) {
const points = [nose, leftShoulder, rightShoulder];
const color = isSlouching ? '#ff4444' : '#4caf50';
points.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 8, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
});
ctx.beginPath();
ctx.moveTo(leftShoulder.x, leftShoulder.y);
ctx.lineTo(rightShoulder.x, rightShoulder.y);
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.stroke();
}
// --- Calibration ---
calibrateBtn.addEventListener('click', () => {
isCalibrated = false;
const calibrate = async () => {
const poses = await detector.estimatePoses(video);
if (poses.length > 0) {
const keypoints = poses[0].keypoints;
const nose = keypoints[0];
const leftShoulder = keypoints[5];
const rightShoulder = keypoints[6];
if (nose.score > MIN_CONFIDENCE &&
leftShoulder.score > MIN_CONFIDENCE &&
rightShoulder.score > MIN_CONFIDENCE) {
const shoulderMidY = (leftShoulder.y + rightShoulder.y) / 2;
baselineDistance = shoulderMidY - nose.y;
isCalibrated = true;
statusText.textContent = 'Calibrated! Monitoring your posture...';
statusText.className = 'status-good';
calibrateBtn.textContent = 'Re-Calibrate';
} else {
statusText.textContent = 'Could not detect pose. Make sure your head and shoulders are visible.';
statusText.className = 'status-bad';
}
}
};
calibrate();
});
// --- Slouch Detection ---
function checkPosture(currentDistance) {
const deviation = (baselineDistance - currentDistance) / baselineDistance;
if (deviation > SLOUCH_THRESHOLD) {
if (!isSlouching && !slouchTimer) {
slouchTimer = setTimeout(() => {
isSlouching = true;
triggerDuckAttack();
}, SLOUCH_DELAY_MS);
}
} else {
if (slouchTimer) {
clearTimeout(slouchTimer);
slouchTimer = null;
}
if (isSlouching) {
isSlouching = false;
removeDuckAttack();
}
}
}
// --- Duck Attack ---
function triggerDuckAttack() {
if (duckIsActive) return;
duckIsActive = true;
statusText.textContent = 'QUACK! Sit up straight!';
statusText.className = 'status-bad';
const overlay = document.createElement('div');
overlay.className = 'slouch-overlay';
overlay.id = 'slouch-overlay';
document.body.appendChild(overlay);
const randomDuck = duckImages[Math.floor(Math.random() * duckImages.length)];
const duckImg = document.createElement('img');
duckImg.src = randomDuck;
duckImg.className = 'duck-img';
duckImg.style.top = Math.random() * 50 + 10 + '%';
duckImg.style.left = Math.random() * 60 + 10 + '%';
duckZone.appendChild(duckImg);
quackSound.currentTime = 0;
quackSound.play().catch(() => {});
}
function removeDuckAttack() {
duckIsActive = false;
statusText.textContent = 'Good posture! Keep it up!';
statusText.className = 'status-good';
const overlay = document.getElementById('slouch-overlay');
if (overlay) overlay.remove();
duckZone.innerHTML = '';
quackSound.pause();
quackSound.currentTime = 0;
}
// --- Start ---
init();
Your posture police is officially on duty. Every slouch now earns you a wobbling duck, a quack to the face, and a red-tinted screen of shame. In the Secret Mission, you will add a scoreboard that tracks how many times the ducks have caught you and escalates the severity with each offence.
Secret mission
Slouch Scoreboard and Escalating Duck Levels
One duck is funny. But what if the ducks got angrier the more you slouched? In this extension, you'll add a live slouch counter and escalating severity levels that punish repeat offenders with increasingly chaotic duck swarms.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally in your browser. There are no cloud services, no accounts, and no ongoing costs.
Decide whether to keep your project files on your machine, pause your work session, or delete everything entirely.
Resources you used:
- The quack-attack project folder on your Desktop (contains index.html, style.css, app.js, and the assets/ folder).
- The Live Server extension installed in VS Code.
Keep everything running
No action needed. Choose this if you want to keep experimenting with your posture checker or show it off to friends.
- Your quack-attack folder stays on your Desktop. Open index.html with Live Server anytime to run it again.
- The Live Server extension remains available in VS Code for future projects.
- Nothing costs money. The TensorFlow.js model loads fresh from a CDN each time you open the page.
Pause - I'll come back to this later
Shut down Live Server to free up the local port. Your project files stay intact for next time.
- In VS Code, click the Port: 5500 button in the bottom status bar to stop Live Server.
- Close the browser tab running your app.
- Your quack-attack folder remains on your Desktop ready to reopen.
Delete - I don't want to use this again
Remove all project files and optionally uninstall the Live Server extension.
- Stop Live Server by clicking Port: 5500 in the VS Code status bar.
- Close the browser tab running your app.
- Delete the project folder by running this command in your terminal:
macOS
rm -rf ~/Desktop/quack-attack
Windows
rmdir /s /q %USERPROFILE%\Desktop\quack-attack
- To uninstall the Live Server extension, open the Extensions panel in VS Code (Cmd+Shift+X on macOS, Ctrl+Shift+X on Windows).
- Find Live Server in your installed extensions list, right-click it, and select Uninstall.
Nice Work!
Nice Work!
You just built a browser-based posture checker that uses AI to detect slouching and punishes you with angry ducks. That is genuinely hilarious and technically impressive.
You've learned how to:
- Built a complete HTML/CSS/JavaScript web application from scratch that accesses the webcam using the browser's MediaDevices API.
- Loaded a pre-trained machine learning model (MoveNet) via TensorFlow.js and ran real-time pose estimation that extracts body keypoints at 30+ FPS.
- Created a posture calibration system with configurable slouch thresholds, debounce timers, and a duck attack experience powered by CSS animations and DOM manipulation.
- Secret Mission: Added a slouch scoreboard with escalating duck severity levels.
Ready to quiz yourself?