“Carpet” is a personalized adaptation of the Chrome Dinosaur game combined with an animated narrative sequence. The application presents a story covering “2019-2024” through sequential text animations, followed by the classic endless runner gameplay mechanics. This unique combination creates both a storytelling experience and an interactive game.
carpet/
├── index.html # Main application and narrative sequence
├── ground.js # Scrolling ground background system
├── dino.js # Player character animation and physics
├── cactus.js # Obstacle generation and collision system
├── updateCustomProperty.js # CSS custom property utility functions
└── imgs/ # Game sprite assets
├── ground.png
├── dino-stationary.png
├── dino-run-0.png
├── dino-run-1.png
├── dino-lose.png
└── cactus.png
// Sequential narrative covering 5-year period
const sentences = [
'2019 - 2024', // Time period header
'5 years | 910 days', // Duration quantification
'from global pandemics, to place 1', // Major events reference
'place 2, to Shuttlefast Farm', // Location/project references
'object 1, to projector remotes', // Object/activity mentions
'Placeholder sentence six', // Incomplete content
'Placeholder sentence seven', // Incomplete content
"don't be afraid to take risks", // Motivational message
'and do what you love.', // Life philosophy
'press to start...' // Game transition prompt
];
function showNextSentence() {
const sentence = sentences[currentSentenceIndex];
const words = sentence.split(' ');
// Create individual word spans for granular animation
container.innerHTML = words.map(word =>
`<span class="sentence-word">${word}</span>`
).join(' ');
const wordElements = container.querySelectorAll('.sentence-word');
container.style.opacity = 1;
// Staggered word appearance with overlapping timing
wordElements.forEach((wordElement, index) => {
setTimeout(() => {
wordElement.style.opacity = 1;
}, index * 1000 - (index * 400)); // 600ms stagger with 400ms overlap
});
// Calculate total animation time and transition to next sentence
const totalFadeInTime = words.length * 600;
const waitTime = totalFadeInTime + 1500; // 1.5s pause after completion
setTimeout(() => {
if (currentSentenceIndex < sentences.length - 1) {
container.style.opacity = 0; // Fade out current sentence
setTimeout(() => {
currentSentenceIndex++;
showNextSentence(); // Recursive progression
}, 500);
}
}, waitTime);
}
/* Large-scale narrative text with sophisticated timing */
.sentence-container {
position: absolute;
top: 2%;
width: 100%;
text-align: center;
font-size: 5vw; /* Viewport-responsive sizing */
opacity: 0;
color: #535353; /* Muted gray for subtlety */
transition: opacity 0.5s ease-in-out;
}
.sentence-word {
display: inline-block;
opacity: 0;
transition: opacity 1.0s ease-in-out; /* Smooth word transitions */
margin: 0 0.1em; /* Subtle word spacing */
color: #535353;
}
// Clean separation of concerns through modules
import { updateGround, setupGround } from "./ground.js"
import { updateDino, setupDino, getDinoRect, setDinoLose } from "./dino.js"
import { updateCactus, setupCactus, getCactusRects } from "./cactus.js"
// Core game constants
const WORLD_WIDTH = 100 // Virtual world width units
const WORLD_HEIGHT = 55 // Virtual world height units
const SPEED_SCALE_INCREASE = 0.00001 // Progressive difficulty increase
// updateCustomProperty.js - CSS custom property manipulation
export function getCustomProperty(elem, prop) {
return parseFloat(getComputedStyle(elem).getPropertyValue(prop)) || 0;
}
export function setCustomProperty(elem, prop, value) {
elem.style.setProperty(prop, value);
}
export function incrementCustomProperty(elem, prop, inc) {
setCustomProperty(elem, prop, getCustomProperty(elem, prop) + inc);
}
// dino.js - Player character with running animation and jump physics
const JUMP_SPEED = 0.5 // Initial jump velocity
const GRAVITY = 0.0017 // Gravitational acceleration
const DINO_FRAME_COUNT = 2 // Running animation frames
const FRAME_TIME = 100 // Animation frame duration (ms)
export function updateDino(delta, speedScale) {
handleRun(delta, speedScale); // Running animation system
handleJump(delta); // Jump physics simulation
}
// Sprite animation for running
function handleRun(delta, speedScale) {
if (isJumping) {
dinoElem.src = `imgs/dino-stationary.png`;
return;
}
if (currentFrameTime >= FRAME_TIME) {
dinoFrame = (dinoFrame + 1) % DINO_FRAME_COUNT;
dinoElem.src = `imgs/dino-run-${dinoFrame}.png`;
currentFrameTime -= FRAME_TIME;
}
currentFrameTime += delta * speedScale;
}
// Physics-based jumping with gravity
function handleJump(delta) {
if (!isJumping) return;
incrementCustomProperty(dinoElem, "--bottom", yVelocity * delta);
// Ground collision detection
if (getCustomProperty(dinoElem, "--bottom") <= 0) {
setCustomProperty(dinoElem, "--bottom", 0);
isJumping = false;
}
yVelocity -= GRAVITY * delta; // Apply gravitational deceleration
}
// ground.js - Seamless background scrolling
const SPEED = 0.05
const groundElems = document.querySelectorAll("[data-ground]");
export function setupGround() {
setCustomProperty(groundElems[0], "--left", 0);
setCustomProperty(groundElems[1], "--left", 300); // Second ground offset
}
export function updateGround(delta, speedScale) {
groundElems.forEach(ground => {
incrementCustomProperty(ground, "--left", delta * speedScale * SPEED * -1);
// Infinite scroll reset when ground exits screen
if (getCustomProperty(ground, "--left") <= -300) {
incrementCustomProperty(ground, "--left", 600); // Move to end
}
});
}
// cactus.js - Procedural obstacle spawning and collision
const CACTUS_INTERVAL_MIN = 500 // Minimum spawn interval
const CACTUS_INTERVAL_MAX = 2000 // Maximum spawn interval
export function updateCactus(delta, speedScale) {
// Move existing cacti and remove off-screen obstacles
document.querySelectorAll("[data-cactus]").forEach(cactus => {
incrementCustomProperty(cactus, "--left", delta * speedScale * SPEED * -1);
if (getCustomProperty(cactus, "--left") <= -100) {
cactus.remove(); // Cleanup for performance
}
});
// Spawn new obstacles at random intervals
if (nextCactusTime <= 0) {
createCactus();
nextCactusTime = randomNumberBetween(CACTUS_INTERVAL_MIN, CACTUS_INTERVAL_MAX) / speedScale;
}
nextCactusTime -= delta;
}
function createCactus() {
const cactus = document.createElement("img");
cactus.dataset.cactus = true;
cactus.src = "imgs/cactus.png";
cactus.classList.add("cactus");
setCustomProperty(cactus, "--left", 100); // Start off-screen right
worldElem.append(cactus);
}
function update(time) {
if (lastTime == null) {
lastTime = time;
window.requestAnimationFrame(update);
return;
}
const delta = time - lastTime; // Frame delta time
// Update all game systems
updateGround(delta, speedScale);
updateDino(delta, speedScale);
updateCactus(delta, speedScale);
updateSpeedScale(delta);
// Check for game over condition
if (checkLose()) return handleLose();
lastTime = time;
window.requestAnimationFrame(update); // Continue loop
}
function updateSpeedScale(delta) {
speedScale += delta * SPEED_SCALE_INCREASE; // Progressive difficulty
}
function checkLose() {
const dinoRect = getDinoRect();
return getCactusRects().some(rect => isCollision(rect, dinoRect));
}
function isCollision(rect1, rect2) {
return (
rect1.left < rect2.right &&
rect1.top < rect2.bottom &&
rect1.right > rect2.left &&
rect1.bottom > rect2.top
);
}
// Game start handling for both touch and keyboard
function handleStart(e) {
if (e.type === 'touchstart' || (e.type === 'keydown' && e.code === 'Space')) {
// Reset game state
lastTime = null;
speedScale = 1;
// Initialize game systems
setupGround();
setupDino();
setupCactus();
// Reposition dino from center to game position
const dinoElem = document.querySelector("[data-dino]");
dinoElem.style.left = "1%";
dinoElem.style.transform = "translateX(0)";
// Start game
startScreenElem.classList.add("hide");
window.requestAnimationFrame(update);
}
}
// Jump input handling
function onJump(e) {
if (e.code !== "Space" && e.type !== "touchstart") return;
if (isJumping) return;
yVelocity = JUMP_SPEED;
isJumping = true;
}
// Proper cleanup to prevent memory leaks
export function setupDino() {
// Remove existing listeners to avoid multiple bindings
document.removeEventListener("keydown", onJump);
document.removeEventListener("touchstart", onJump);
// Add fresh event listeners
document.addEventListener("keydown", onJump);
document.addEventListener("touchstart", onJump);
}
function setPixelToWorldScale() {
let worldToPixelScale;
// Calculate scale based on aspect ratio constraints
if (window.innerWidth / window.innerHeight < WORLD_WIDTH / WORLD_HEIGHT) {
worldToPixelScale = window.innerWidth / WORLD_WIDTH; // Width-constrained
} else {
worldToPixelScale = window.innerHeight / WORLD_HEIGHT; // Height-constrained
}
// Apply calculated scale to maintain aspect ratio
worldElem.style.width = `${WORLD_WIDTH * worldToPixelScale}px`;
worldElem.style.height = `${WORLD_HEIGHT * worldToPixelScale}px`;
}
// Responsive scaling on window resize
window.addEventListener("resize", setPixelToWorldScale);
/* Dynamic positioning using CSS custom properties */
.ground {
--left: 0;
position: absolute;
width: 300%; /* Extra width for seamless scrolling */
bottom: 0;
left: calc(var(--left) * 1%); /* JavaScript-controlled position */
}
.dino {
--bottom: 0;
position: absolute;
left: 50%; /* Initial center position */
transform: translateX(-50%); /* Perfect centering */
height: 25%;
bottom: calc(var(--bottom) * 1%);
transition: left 0.5s ease, transform 0.5s ease; /* Smooth repositioning */
}
.cactus {
position: absolute;
left: calc(var(--left) * 1%);
height: 18%;
bottom: 0;
}
body {
font-family: 'Helvetica', 'Arial', sans-serif;
font-weight: bold;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f0f0f0; /* Light neutral background */
}
.start-screen {
position: absolute;
font-size: 5vmin; /* Responsive text sizing */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.hide {
display: none; /* Clean state transitions */
}
.world {
overflow: hidden; /* Constrain game area */
position: relative;
}
// Efficient delta time calculations
const delta = time - lastTime;
// Minimal DOM queries with caching
const groundElems = document.querySelectorAll("[data-ground]");
const dinoElem = document.querySelector("[data-dino]");
The narrative appears to document a 5-year personal journey from 2019-2024, with references to:
// ES6 module system requirements
import { updateGround, setupGround } from "./ground.js"
// Modern JavaScript features
const sentences = [...]; // Array spread
document.addEventListener("keydown", handleStart, { once: true }); // Event options
“Carpet” represents an innovative fusion of personal narrative and classic game mechanics, creating a unique experience that combines storytelling with interactive entertainment. The technical implementation demonstrates sophisticated understanding of modern web development, modular architecture, and game programming principles.
Technical Rating: 8.8/10
The application successfully transforms the familiar Chrome Dinosaur game into a personalized experience, demonstrating both technical proficiency and creative vision in combining narrative content with interactive gameplay.