Josh Bruce Online

Carpet - Narrative Chrome Dino Game Documentation

Overview

“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.

Technical Architecture

Core Technologies

File Structure

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

Narrative System

Story Content and Progression

// 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
];

Word-by-Word Animation Engine

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);
}

Visual Typography System

/* 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;
}

Modular Game Architecture

ES6 Module System Implementation

// 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

Custom Property Utility System

// 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);
}

Game Physics and Mechanics

Dinosaur Character System

// 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
}

Infinite Scrolling Ground System

// 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
        }
    });
}

Dynamic Obstacle Generation

// 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);
}

Game Loop and State Management

RequestAnimationFrame Game Loop

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
}

Collision Detection System

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
    );
}

Input and Interaction System

Multi-Modal Input Support

// 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;
}

Event Listener Management

// 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);
}

Responsive Design System

Adaptive Viewport Scaling

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);

CSS Custom Properties for Animation

/* 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;
}

Visual Design and Aesthetics

Typography and Layout

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%);
}

State-Based Visual Feedback

.hide {
    display: none;                  /* Clean state transitions */
}

.world {
    overflow: hidden;               /* Constrain game area */
    position: relative;
}

Performance Optimization

Efficient Asset Management

Frame Rate Optimization

// 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]");

Narrative Content Analysis

Personal Timeline Elements

The narrative appears to document a 5-year personal journey from 2019-2024, with references to:

  1. Global Events: “global pandemics” (COVID-19 reference)
  2. Locations: “place 1”, “place 2”, “Shuttlefast Farm”
  3. Objects/Activities: “object 1”, “projector remotes”
  4. Incomplete Content: Several placeholder sentences suggest work in progress
  5. Motivational Conclusion: Life advice about risk-taking and following passions

Story Structure

Game Design Psychology

Engagement Mechanisms

  1. Narrative Hook: Personal story creates emotional investment
  2. Progressive Difficulty: Increasing speed maintains challenge
  3. Simple Controls: Low barrier to entry with space/touch input
  4. Immediate Restart: Quick retry loop maintains engagement
  5. Visual Feedback: Clear collision and animation responses

User Experience Flow

  1. Story Absorption: Passive narrative consumption
  2. Transition Prompt: Clear call-to-action for interaction
  3. Game Learning: Immediate gameplay mechanics understanding
  4. Challenge Progression: Difficulty ramp maintains interest
  5. Retry Loop: Instant restart capability

Browser Compatibility

Modern Web Standards

// 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

Responsive Web Design

Accessibility Considerations

Current Limitations

Potential Improvements

Future Enhancement Opportunities

Narrative Features

  1. Interactive Story: Clickable story elements with expanded details
  2. Photo Integration: Visual memories accompanying text
  3. Audio Narration: Voice-over for accessibility
  4. Customizable Timeline: User-editable personal narratives
  5. Social Sharing: Timeline sharing capabilities

Gameplay Enhancements

  1. Power-ups: Special abilities and temporary boosts
  2. Multiple Characters: Different playable sprites
  3. Environmental Variety: Changing backgrounds and themes
  4. Achievement System: Progress tracking and milestones
  5. High Score Persistence: Local storage leaderboards

Technical Improvements

  1. Progressive Web App: Offline functionality
  2. Service Worker: Asset caching and performance
  3. WebGL Rendering: Enhanced visual effects
  4. Audio Integration: Sound effects and music
  5. Analytics: Usage pattern tracking

Code Quality Assessment

Strengths

Areas for Enhancement

Conclusion

“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.