Mouth Trap Game
School Project
Mouth Trap is a coded game created as a playful introduction for class. The game is around teeth and the mouth, the project referencing my family’s connection to dentistry through a flappy bird inspired browser game.



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>MOUTH TRAP</title>
<meta name="description" content="MOUTH TRAP - portfolio showcase by HAZ">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap');
body.intro-active {
margin: 0;
padding: 0;
color: white;
font-family: "Roboto", sans-serif;
text-transform: uppercase;
}
#myVideo {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: -1;
}
#title {
font-family: "Orbitron", monospace;
font-size: 38px;
font-weight: 900;
color: #f32525;
text-transform: uppercase;
letter-spacing: 2px;
}
.index {
display: flex;
flex-direction: column;
position: absolute;
height: 90vh;
width: 90%;
gap: 10px;
padding: 3%;
justify-content: space-between;
}
#me {
display: flex;
width: 24%;
right: 5px;
bottom: 30px;
position: absolute;
}
.mouth {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
position: relative;
z-index: 1;
justify-content: center;
align-items: center;
}
.mouthpic {
display: inline-block;
animation-name: mouth;
animation-duration: 8s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
transform-origin: center center;
}
.mouthpic img {
display: block;
max-width: 100%;
height: auto;
}
@keyframes mouth {
0% { transform: scale(0.5) rotate(0deg); }
25% { transform: scale(1) rotate(0deg); }
50% { transform: scale(0.5) rotate(0deg); }
75% { transform: scale(1) rotate(360deg); }
100% { transform: scale(0.5) rotate(0deg); }
}
#startButton {
position: relative;
width: 220px;
height: 50px;
border: none;
outline: none;
background: #f32525;
color: #fff;
cursor: pointer;
border-radius: 10px;
font-family: "Orbitron", monospace;
font-size: 16px;
font-weight: 700;
overflow: hidden;
transition: all 0.3s ease;
}
.button-text {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.button-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
#startButton:hover .button-video {
opacity: 1;
}
#startButton:hover .button-text {
color: #fff;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
#game-section * {
box-sizing: border-box;
}
#game-section {
display: none;
margin: 0;
padding: 0;
height: 100%;
background: #0b1820;
font-family: 'Roboto', system-ui, -apple-system, Segoe UI, Arial, sans-serif;
color: #ffffff;
min-height: 100vh;
}
#game-section #landing-page {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
#game-section #landing-canvas {
display: block;
width: 100vw;
height: 100vh;
object-fit: cover;
background: transparent;
}
#game-section #game-container {
width: 100vw;
height: 100vh;
}
#game-section canvas#game {
display: block;
width: 100vw;
height: 100vh;
object-fit: cover;
background: transparent;
}
</style>
</head>
<body class="intro-active">
<div id="intro-page">
<div class="index">
<div id="title">MOUTH TRAP</div>
<div id="me">My name is HAZ, and I am graphic designer from the Middle East who resides in Brooklyn.I am attracted to striking & standalone visuals, intricate designs, and flowy motion graphics. ♉︎</div>
</div>
<div class="mouth">
<div class="mouthpic"><img src="mouth.png" alt="lips"></div>
<button id="startButton" type="button">
<span class="button-text">START</span>
<video class="button-video" muted loop playsinline>
<source src="buttonvideo.mp4" type="video/mp4">
</video>
</button>
</div>
<video autoplay muted loop playsinline id="myVideo">
<source src="backgroundvideo.mp4" type="video/mp4">
</video>
</div>
<div id="game-section">
<div id="landing-page">
<canvas id="landing-canvas" width="480" height="720" aria-label="Landing page" role="img"></canvas>
</div>
<div id="game-container" style="display: none;">
<canvas id="game" width="480" height="720" aria-label="Flapping Bird game" role="img"></canvas>
</div>
</div>
<script>
(function () {
const startButton = document.getElementById('startButton');
const buttonVideo = document.querySelector('.button-video');
const introPage = document.getElementById('intro-page');
const gameSection = document.getElementById('game-section');
const bgVideo = document.getElementById('myVideo');
startButton.addEventListener('mouseenter', function () {
buttonVideo.play();
});
startButton.addEventListener('mouseleave', function () {
buttonVideo.pause();
buttonVideo.currentTime = 0;
});
startButton.addEventListener('click', function () {
document.body.style.transition = 'opacity 0.5s ease-out';
document.body.style.opacity = '0';
setTimeout(function () {
introPage.style.display = 'none';
if (bgVideo) bgVideo.pause();
document.body.classList.remove('intro-active');
document.body.style.textTransform = 'none';
gameSection.style.display = 'block';
document.body.style.opacity = '1';
document.body.style.transition = '';
window.dispatchEvent(new Event('mouth-trap-start'));
}, 500);
});
})();
(function () {
'use strict';
const ASSET_BASE = 'show-off-project/';
const canvas = document.getElementById('game');
const landingCanvas = document.getElementById('landing-canvas');
if (!canvas || !landingCanvas) return;
const ctx = canvas.getContext('2d');
const landingCtx = landingCanvas.getContext('2d');
let showLanding = true;
let gameReady = false;
function resizeCanvas() {
const desiredWidth = Math.max(320, Math.floor(window.innerWidth));
const desiredHeightByAspect = Math.floor((3 / 2) * desiredWidth);
const maxHeight = window.innerHeight;
const desiredHeight = Math.min(desiredHeightByAspect, maxHeight);
if (canvas.width !== desiredWidth || canvas.height !== desiredHeight) {
canvas.width = desiredWidth;
canvas.height = desiredHeight;
}
if (landingCanvas.width !== desiredWidth || landingCanvas.height !== desiredHeight) {
landingCanvas.width = desiredWidth;
landingCanvas.height = desiredHeight;
}
}
window.addEventListener('resize', function () {
resizeCanvas();
if (!gameReady) return;
if (showLanding) {
drawLanding();
} else {
draw();
}
});
let bgPattern = null;
let bgImg = null;
(function loadBackgroundTexture() {
const img = new Image();
let triedJpg = false;
img.onload = function () {
bgImg = img;
try {
bgPattern = ctx.createPattern(img, 'repeat');
} catch (_) {
bgPattern = null;
}
if (showLanding && gameReady) drawLanding();
};
img.onerror = function () {
if (!triedJpg) {
triedJpg = true;
img.src = ASSET_BASE + 'backgroundtexture.jpg';
}
};
img.src = ASSET_BASE + 'backgroundtexture.png';
})();
const groundImg = new Image();
groundImg.onload = function () {
if (showLanding && gameReady) drawLanding();
};
groundImg.src = ASSET_BASE + 'teethbackground.png';
const mouthImg = new Image();
mouthImg.src = ASSET_BASE + 'tongue-img.png';
const GRAVITY = 0.45;
const FLAP_STRENGTH = -7.5;
const PIPE_GAP_MIN = 140;
const PIPE_GAP_MAX = 200;
const PIPE_WIDTH = 70;
const PIPE_INTERVAL_MS = 1400;
const SCROLL_SPEED = 2.6;
const MOUTH_SIZE = 32;
const GROUND_HEIGHT = 80;
let mouthY = canvas.height / 2;
let mouthX = canvas.width * 0.28;
let mouthVelY = 0;
let pipes = [];
let lastPipeAt = 0;
let score = 0;
let best = Number(localStorage.getItem('mouth_trap_best') || 0);
let isRunning = false;
let isGameOver = false;
let lastTimestamp = 0;
let backgroundOffset = 0;
function resetGame() {
mouthY = canvas.height / 2;
mouthX = canvas.width * 0.28;
mouthVelY = 0;
pipes = [];
lastPipeAt = 0;
score = 0;
isGameOver = false;
backgroundOffset = 0;
}
function flap() {
if (!isRunning) {
isRunning = true;
lastTimestamp = performance.now();
requestAnimationFrame(gameLoop);
}
if (isGameOver) {
resetGame();
return;
}
mouthVelY = FLAP_STRENGTH;
}
function spawnPipe() {
const gap = PIPE_GAP_MIN + Math.random() * (PIPE_GAP_MAX - PIPE_GAP_MIN);
const topLimit = 40;
const bottomLimit = canvas.height - GROUND_HEIGHT - 40 - gap;
const topHeight = topLimit + Math.random() * (bottomLimit - topLimit);
const x = canvas.width + PIPE_WIDTH;
pipes.push({ x, width: PIPE_WIDTH, topHeight, gap, passed: false });
}
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
function update(dt) {
if (isGameOver) return;
mouthVelY += GRAVITY;
mouthY += mouthVelY;
if (mouthY < 0) {
mouthY = 0;
mouthVelY = 0;
}
const groundY = canvas.height - GROUND_HEIGHT - MOUTH_SIZE;
if (mouthY > groundY) {
mouthY = groundY;
isGameOver = true;
}
backgroundOffset = (backgroundOffset + SCROLL_SPEED) % canvas.width;
for (let i = pipes.length - 1; i >= 0; i--) {
const p = pipes[i];
p.x -= SCROLL_SPEED;
if (p.x + p.width < -10) {
pipes.splice(i, 1);
continue;
}
const mouthCenter = mouthX + MOUTH_SIZE / 2;
const pipeCenter = p.x + p.width / 2;
if (!p.passed && pipeCenter < mouthCenter) {
p.passed = true;
score++;
if (score > best) {
best = score;
localStorage.setItem('mouth_trap_best', String(best));
}
}
const mouthTop = mouthY;
const topRectCollide = rectsOverlap(
mouthX, mouthTop, MOUTH_SIZE, MOUTH_SIZE,
p.x, 0, p.width, p.topHeight
);
const bottomRectCollide = rectsOverlap(
mouthX, mouthTop, MOUTH_SIZE, MOUTH_SIZE,
p.x, p.topHeight + p.gap, p.width,
canvas.height - GROUND_HEIGHT - (p.topHeight + p.gap)
);
if (topRectCollide || bottomRectCollide) {
isGameOver = true;
}
}
lastPipeAt += dt;
if (lastPipeAt > PIPE_INTERVAL_MS) {
spawnPipe();
lastPipeAt = 0;
}
}
function draw() {
if (bgPattern) {
ctx.globalAlpha = 0.6;
ctx.fillStyle = bgPattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1.0;
} else {
ctx.fillStyle = '#70c5ce';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8be0ec';
for (let x = 0; x < canvas.width; x += 120) {
ctx.fillRect(x, 120, 60, 10);
}
}
if (groundImg.complete) {
ctx.drawImage(groundImg, 0, canvas.height - GROUND_HEIGHT, canvas.width, GROUND_HEIGHT);
}
if (groundImg.complete) {
ctx.save();
ctx.scale(1, -1);
ctx.drawImage(groundImg, 0, -GROUND_HEIGHT, canvas.width, GROUND_HEIGHT);
ctx.restore();
}
for (const p of pipes) {
ctx.fillStyle = '#fff200';
ctx.fillRect(p.x, 0, p.width, p.topHeight);
ctx.fillRect(p.x, p.topHeight + p.gap, p.width, canvas.height - GROUND_HEIGHT - (p.topHeight + p.gap));
ctx.fillStyle = '#e0c700';
ctx.fillRect(p.x - 4, p.topHeight - 10, p.width + 8, 10);
ctx.fillRect(p.x - 4, p.topHeight + p.gap, p.width + 8, 10);
}
ctx.save();
ctx.translate(mouthX + MOUTH_SIZE / 2, mouthY + MOUTH_SIZE / 2);
const tilt = Math.max(-0.5, Math.min(1.0, mouthVelY / 9));
ctx.rotate(tilt);
if (mouthImg.complete) {
ctx.drawImage(mouthImg, -MOUTH_SIZE / 2, -MOUTH_SIZE / 2, MOUTH_SIZE, MOUTH_SIZE);
} else {
ctx.fillStyle = '#ffd166';
ctx.beginPath();
ctx.arc(0, 0, MOUTH_SIZE / 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 32px Orbitron, system-ui, -apple-system, Segoe UI, Arial';
ctx.textAlign = 'left';
ctx.fillText('SCORE: ' + score, 18, canvas.height / 2 - 20);
ctx.fillText('BEST: ' + best, 18, canvas.height / 2 + 20);
if (!isRunning) {
ctx.textAlign = 'center';
ctx.font = 'bold 24px Roboto, system-ui, -apple-system, Segoe UI, Arial';
ctx.fillText('CLICK OR PRESS SPACE TO START', canvas.width / 2, canvas.height * 0.42);
}
if (isGameOver) {
ctx.textAlign = 'center';
ctx.font = 'bold 36px Orbitron, system-ui, -apple-system, Segoe UI, Arial';
ctx.fillText('GAME OVER', canvas.width / 2, canvas.height * 0.45);
ctx.font = 'bold 20px Roboto, system-ui, -apple-system, Segoe UI, Arial';
ctx.fillText('CLICK OR PRESS SPACE TO RESTART', canvas.width / 2, canvas.height * 0.52);
}
}
function gameLoop(timestamp) {
const dt = Math.min(50, timestamp - lastTimestamp);
lastTimestamp = timestamp;
update(dt);
draw();
if (isRunning) requestAnimationFrame(gameLoop);
}
function onKeyDown(e) {
if (e.code === 'Space' || e.key === ' ') {
e.preventDefault();
flap();
}
}
function onPointer() {
flap();
}
document.addEventListener('keydown', onKeyDown);
canvas.addEventListener('mousedown', onPointer);
canvas.addEventListener('touchstart', function (e) {
e.preventDefault();
onPointer();
}, { passive: false });
function drawLanding() {
landingCtx.clearRect(0, 0, landingCanvas.width, landingCanvas.height);
if (bgPattern) {
landingCtx.globalAlpha = 0.6;
landingCtx.fillStyle = bgPattern;
landingCtx.fillRect(0, 0, landingCanvas.width, landingCanvas.height);
landingCtx.globalAlpha = 1.0;
} else if (bgImg && bgImg.complete) {
landingCtx.globalAlpha = 0.6;
landingCtx.drawImage(bgImg, 0, 0, landingCanvas.width, landingCanvas.height);
landingCtx.globalAlpha = 1.0;
} else {
landingCtx.fillStyle = '#70c5ce';
landingCtx.fillRect(0, 0, landingCanvas.width, landingCanvas.height);
landingCtx.fillStyle = '#8be0ec';
for (let x = 0; x < landingCanvas.width; x += 120) {
landingCtx.fillRect(x, 120, 60, 10);
}
}
if (groundImg.complete) {
landingCtx.drawImage(groundImg, 0, landingCanvas.height - GROUND_HEIGHT, landingCanvas.width, GROUND_HEIGHT);
}
if (groundImg.complete) {
landingCtx.save();
landingCtx.scale(1, -1);
landingCtx.drawImage(groundImg, 0, -GROUND_HEIGHT, landingCanvas.width, GROUND_HEIGHT);
landingCtx.restore();
}
landingCtx.fillStyle = '#ffffff';
landingCtx.font = 'bold 4rem Orbitron, system-ui, -apple-system, Segoe UI, Arial';
landingCtx.textAlign = 'center';
landingCtx.shadowColor = 'rgba(0,0,0,0.5)';
landingCtx.shadowBlur = 4;
landingCtx.shadowOffsetX = 2;
landingCtx.shadowOffsetY = 2;
landingCtx.fillText('MOUTH TRAP', landingCanvas.width / 2, landingCanvas.height * 0.4);
landingCtx.font = 'bold 1.2rem Roboto, system-ui, -apple-system, Segoe UI, Arial';
landingCtx.shadowBlur = 2;
landingCtx.fillText('CLICK ANYWHERE TO START PLAYING!', landingCanvas.width / 2, landingCanvas.height * 0.5);
const buttonWidth = 200;
const buttonHeight = 60;
const buttonX = (landingCanvas.width - buttonWidth) / 2;
const buttonY = landingCanvas.height * 0.6;
landingCtx.fillStyle = '#fff200';
landingCtx.fillRect(buttonX, buttonY, buttonWidth, buttonHeight);
landingCtx.fillStyle = '#000000';
landingCtx.font = 'bold 1.5rem Orbitron, system-ui, -apple-system, Segoe UI, Arial';
landingCtx.shadowColor = 'transparent';
landingCtx.shadowBlur = 0;
landingCtx.fillText('START GAME', landingCanvas.width / 2, buttonY + buttonHeight / 2 + 8);
}
function startGame() {
showLanding = false;
document.getElementById('landing-page').style.display = 'none';
document.getElementById('game-container').style.display = 'block';
draw();
}
landingCanvas.addEventListener('click', startGame);
landingCanvas.addEventListener('touchstart', function (e) {
e.preventDefault();
startGame();
}, { passive: false });
function waitForImages() {
if ((bgImg && bgImg.complete) || groundImg.complete) {
drawLanding();
} else {
landingCtx.fillStyle = '#0b1820';
landingCtx.fillRect(0, 0, landingCanvas.width, landingCanvas.height);
landingCtx.fillStyle = '#ffffff';
landingCtx.font = 'bold 2rem Roboto, system-ui, -apple-system, Segoe UI, Arial';
landingCtx.textAlign = 'center';
landingCtx.fillText('LOADING...', landingCanvas.width / 2, landingCanvas.height / 2);
setTimeout(waitForImages, 100);
}
}
window.addEventListener('mouth-trap-start', function () {
gameReady = true;
resizeCanvas();
waitForImages();
});
})();
</script>
</body>
</html>
Mouth Trap was created for my first creative coding project as a way to introduce myself to the class through an interactive website. Instead of making a traditional personal introduction, I built the project around the mouth, teeth, and dentistry, referencing the fact that my dad and sister are dentists. The game begins with a start screen that leads into a flappy bird inspired game. In this version, the “bird” becomes a mouth going through the pillars. The project turns a personal detail into a humorous and slightly strange digital experience, using play as a way to reveal something about my background. Beyond the visual concept, the site also focused on basic game logic and website performance. As the pillars move off screen, they are deleted so the browser does not become overloaded.

