DEV Community

Cover image for Weekend Challenge: Dog Days Edition
John
John

Posted on

Weekend Challenge: Dog Days Edition

DEV Weekend Challenge: Dog Days Edition Submission 🐕

This is a submission for Weekend Challenge: Dog Days Edition

What I Built ðŸķ

Bark Gallery: Speed Sketch Challenge is an arcade-style, browser-based community drawing and voting game built to celebrate International Dog Day!

Players receive a randomized, humorous canine scenario generated using Google AI Studio's Gemini 3.7 Flash model, such as:

  • 🍕 A dog caught stealing a slice of pizza
  • ðŸĐš A dog doctor checking a heartbeat with a stethoscope
  • 🍝 A dog chef cooking spaghetti in a chaotic kitchen
  • 🔍 A dog detective inspecting clues with a magnifying glass
  • 🏖ïļ A dog and a duck playing on the beach (Duckmon crossover!)

ðŸŽŪ The Core Loop

  1. 90-Second Sprint: You get 90 seconds to sketch your masterpiece using our MS Paint-style spray tool, custom color palette, undo stack, and canvas controls.

  2. Arcade Dog Tag: When the timer ends, sign your artwork with a custom arcade dog tag.

  3. Community Voting: Your sketch uploads instantly to the live Community Bark Gallery, where visitors can vote in real time:

    • âĪïļ Like → +1 point
    • ðŸĶī Treat / Super Like → +3 points
  4. Top 10 Leaderboard: Scores recalculate on the fly and feed into the dynamic leaderboard!


Demo ðŸŽŪ

Code ðŸ’ŧ

The complete game is architected as a lightweight, single-file client application featuring an HTML5 <canvas> engine, custom particle spray physics, real-time client-side JPEG compression, voting state persistence, and dynamic leaderboard sorting.

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Bark Gallery: Community Dog Doodles — Quackwork Studios</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="robots" content="index,follow">
  <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Inter:wght@400;600;700;900&display=swap" rel="stylesheet" />
  <link rel="stylesheet" href="style.css">

  <style>
    .game-section {
      padding: 30px 16px 80px;
      max-width: 1120px;
      margin: 0 auto;
    }
    .pixel-title {
      font-family: 'Press Start 2P', monospace;
      color: #f59e0b;
      font-size: 1.45rem;
      letter-spacing: -1px;
      margin-bottom: 8px;
    }
    .rules-card {
      background: #0d121c;
      border: 1px solid #232d3f;
      border-left: 4px solid #f59e0b;
      border-radius: 12px;
      padding: 16px 20px;
      margin-bottom: 24px;
    }
    .rules-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 14px;
      margin-top: 10px;
    }
    .rule-item {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      font-size: 0.8rem;
      color: #cbd5e1;
    }
    .rule-num {
      background: #1a2233;
      color: #f59e0b;
      font-family: 'Press Start 2P', monospace;
      font-size: 0.7rem;
      width: 24px;
      height: 24px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 6px;
      flex-shrink: 0;
    }
    .game-layout {
      display: grid;
      grid-template-columns: 1fr;
      gap: 20px;
    }
    @media (min-width: 880px) {
      .game-layout {
        grid-template-columns: 1.35fr 0.65fr;
      }
    }
    .game-box, .leaderboard-box, .gallery-section {
      background: #121824;
      border: 2px solid #232d3f;
      border-radius: 16px;
      padding: 20px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
    }
    .status-bar {
      background: #1a2233;
      border: 1px solid #2e3a50;
      border-radius: 12px;
      padding: 12px 16px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      flex-wrap: wrap;
      gap: 12px;
      margin-bottom: 16px;
    }
    .prompt-container {
      flex: 1;
      min-width: 220px;
    }
    .target-title {
      font-size: 0.75rem;
      text-transform: uppercase;
      color: #8f9ba8;
      font-weight: 700;
    }
    .target-breed {
      font-family: 'Press Start 2P', monospace;
      font-size: 0.8rem;
      color: #fcd34d;
      margin-top: 4px;
      line-height: 1.4;
    }
    .timer-badge {
      font-family: 'Press Start 2P', monospace;
      font-size: 1rem;
      color: #38bdf8;
      background: #0f172a;
      padding: 6px 12px;
      border-radius: 8px;
      border: 1px solid #1e293b;
    }
    .timer-badge.warning {
      color: #ef4444;
      border-color: #ef4444;
      animation: pulse 1s infinite;
    }
    @keyframes pulse {
      0%, 100% { transform: scale(1); }
      50% { transform: scale(1.05); }
    }
    .canvas-container {
      position: relative;
      width: 100%;
      max-width: 480px;
      aspect-ratio: 1 / 1;
      margin: 0 auto 16px;
      background: #ffffff;
      border-radius: 12px;
      border: 3px solid #f59e0b;
      overflow: hidden;
      box-shadow: 0 0 20px rgba(245, 158, 11, 0.2);
    }
    #drawCanvas {
      width: 100%;
      height: 100%;
      touch-action: none;
      cursor: crosshair;
    }
    .overlay {
      position: absolute;
      inset: 0;
      background: rgba(10, 14, 23, 0.95);
      backdrop-filter: blur(4px);
      display: none;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 20px;
      text-align: center;
      z-index: 10;
    }
    .tools-bar {
      display: flex;
      flex-direction: column;
      gap: 12px;
      background: #1a2233;
      padding: 12px 14px;
      border-radius: 12px;
      border: 1px solid #2e3a50;
    }
    .tools-subrow {
      display: flex;
      justify-content: space-between;
      align-items: center;
      flex-wrap: wrap;
      gap: 8px;
    }
    .tool-selector, .size-selector {
      display: flex;
      align-items: center;
      gap: 4px;
      background: #0d121c;
      padding: 4px;
      border-radius: 8px;
      border: 1px solid #232d3f;
    }
    .tool-btn, .size-btn {
      background: none;
      border: none;
      color: #94a3b8;
      padding: 4px 8px;
      border-radius: 6px;
      font-size: 0.75rem;
      cursor: pointer;
      font-weight: bold;
      transition: all 0.15s ease;
    }
    .tool-btn.active, .size-btn.active {
      background: #f59e0b;
      color: #0f172a;
    }
    .color-swatches {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 6px;
    }
    .color-btn {
      width: 24px;
      height: 24px;
      border-radius: 6px;
      border: 2px solid transparent;
      cursor: pointer;
      transition: transform 0.15s ease;
    }
    .color-btn:hover {
      transform: scale(1.2);
    }
    .color-btn.active {
      border-color: #ffffff;
      box-shadow: 0 0 8px rgba(255, 255, 255, 0.9);
      transform: scale(1.25);
    }
    .btn-pixel {
      font-family: 'Press Start 2P', monospace;
      font-size: 0.6rem;
      padding: 8px 10px;
      border-radius: 8px;
      border: none;
      cursor: pointer;
      transition: all 0.15s ease;
    }
    .btn-action {
      background: #f59e0b;
      color: #0f172a;
      box-shadow: 0 4px 0 #b45309;
    }
    .btn-action:hover {
      background: #fbbf24;
      transform: translateY(-2px);
      box-shadow: 0 6px 0 #b45309;
    }
    .btn-action:active {
      transform: translateY(2px);
      box-shadow: 0 2px 0 #b45309;
    }
    .btn-secondary {
      background: #334155;
      color: #e2e8f0;
    }
    .btn-secondary:hover {
      background: #475569;
    }

    /* Leaderboard Styling */
    .leaderboard-table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 14px;
      font-size: 0.85rem;
    }
    .leaderboard-table th {
      text-align: left;
      font-family: 'Press Start 2P', monospace;
      font-size: 0.6rem;
      color: #8f9ba8;
      padding-bottom: 10px;
      border-bottom: 1px solid #232d3f;
    }
    .leaderboard-table td {
      padding: 8px 4px;
      border-bottom: 1px solid #1a2233;
      color: #cbd5e1;
    }
    .tag-highlight {
      font-family: 'Press Start 2P', monospace;
      font-size: 0.65rem;
      color: #fcd34d;
    }
    .score-highlight {
      font-family: 'Press Start 2P', monospace;
      font-size: 0.7rem;
      color: #10b981;
      text-align: right;
    }

    /* Gallery Styling */
    .gallery-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
      gap: 16px;
      margin-top: 20px;
      max-height: 650px;
      overflow-y: auto;
      padding-right: 6px;
    }
    .gallery-card {
      background: #0d121c;
      border: 1px solid #232d3f;
      border-radius: 12px;
      overflow: hidden;
      display: flex;
      flex-direction: column;
      transition: transform 0.2s ease, border-color 0.2s ease;
    }
    .gallery-card:hover {
      transform: translateY(-4px);
      border-color: #f59e0b;
    }
    .gallery-img-wrap {
      width: 100%;
      aspect-ratio: 1 / 1;
      background: #ffffff;
    }
    .gallery-img-wrap img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    .gallery-info {
      padding: 12px;
      display: flex;
      flex-direction: column;
      gap: 6px;
    }
    .gallery-user-row {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .gallery-user {
      font-family: 'Press Start 2P', monospace;
      font-size: 0.65rem;
      color: #fcd34d;
    }
    .gallery-prompt {
      font-size: 0.75rem;
      color: #94a3b8;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .vote-bar {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-top: 6px;
      padding-top: 8px;
      border-top: 1px solid #1a2233;
    }
    .vote-btn {
      background: #1a2233;
      border: 1px solid #2e3a50;
      color: #fff;
      padding: 4px 8px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 0.75rem;
      display: flex;
      align-items: center;
      gap: 4px;
      transition: all 0.15s ease;
    }
    .vote-btn:hover {
      background: #28364f;
      transform: scale(1.05);
    }
    .vote-btn:active {
      transform: scale(0.95);
    }

    /* Modal */
    .modal-backdrop {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.85);
      backdrop-filter: blur(5px);
      display: none;
      align-items: center;
      justify-content: center;
      z-index: 999;
      padding: 16px;
    }
    .modal-card {
      background: #121824;
      border: 2px solid #f59e0b;
      border-radius: 16px;
      max-width: 440px;
      width: 100%;
      padding: 24px;
      color: #e2e8f0;
      box-shadow: 0 0 30px rgba(245, 158, 11, 0.3);
      text-align: center;
    }
    .modal-input {
      width: 100%;
      background: #090d14;
      border: 1px solid #2e3a50;
      border-radius: 8px;
      padding: 10px 12px;
      color: #fff;
      font-size: 1rem;
      margin: 12px 0 20px 0;
      text-align: center;
      font-family: 'Press Start 2P', monospace;
      box-sizing: border-box;
    }
    .modal-input:focus {
      outline: none;
      border-color: #f59e0b;
    }
  </style>
</head>

<body>

  <header class="header-wrapper">
    <div class="top-notice-bar">
      <span class="notice-text">ðŸū DEV Weekend Challenge: Community Bark Gallery is Live!</span>
    </div>

    <nav class="navbar">
      <div class="navbar-container">
        <a href="/" class="brand-link">
          <div class="brand-pixel-badge">
            <span class="pixel-cell"></span>
            <span class="pixel-cell"></span>
            <span class="pixel-cell"></span>
            <span class="pixel-cell"></span>
          </div>
          <span class="brand-name">Quackwork Studios</span>
        </a>

        <ul class="navbar-links">
          <li><a href="/" class="navbar-link">Home</a></li>
          <li><a href="/#games" class="navbar-link">Games</a></li>
          <li><a href="/doggame.html" class="navbar-link" style="color: #f59e0b;">ðŸū Bark Gallery</a></li>
        </ul>
      </div>
    </nav>
  </header>

  <main>
    <section class="game-section">
      <!-- Title & Header -->
      <div class="section-header" style="margin-bottom: 16px; text-align: center;">
        <h1 class="pixel-title">Bark Gallery: Speed Sketch Challenge ðŸŽĻðŸķ</h1>
        <p class="section-subtitle">A community-driven dog doodle showcase built for International Dog Day!</p>
      </div>

      <!-- Rules Bar -->
      <div class="rules-card">
        <div style="font-family: 'Press Start 2P', monospace; font-size: 0.75rem; color: #f59e0b; margin-bottom: 8px;">
          📖 HOW TO PLAY & COMPETE:
        </div>
        <div class="rules-grid">
          <div class="rule-item">
            <span class="rule-num">1</span>
            <span>Get a random canine scenario prompt.</span>
          </div>
          <div class="rule-item">
            <span class="rule-num">2</span>
            <span>You have <strong>90s</strong> to sketch using the pen & spray tools.</span>
          </div>
          <div class="rule-item">
            <span class="rule-num">3</span>
            <span>Sign with a <strong>Dog Tag</strong> & upload to the gallery.</span>
          </div>
          <div class="rule-item">
            <span class="rule-num">4</span>
            <span>Vote below: <strong>âĪïļ Like (+1 pt)</strong> or <strong>ðŸĶī Treat (+3 pts)</strong> to top the leaderboard!</span>
          </div>
        </div>
      </div>

      <div class="game-layout">
        <!-- Drawing Area -->
        <div class="game-box">
          <div class="status-bar">
            <div class="prompt-container">
              <div class="target-title">Your 90s Prompt:</div>
              <div id="targetBreed" class="target-breed">Loading prompt...</div>
            </div>

            <div>
              <div class="target-title">Time</div>
              <div id="timerDisplay" class="timer-badge">90s</div>
            </div>
          </div>

          <!-- Canvas Frame -->
          <div class="canvas-container">
            <canvas id="drawCanvas" width="480" height="480"></canvas>

            <!-- Times Up Overlay -->
            <div id="overlay" class="overlay">
              <div style="font-size: 3rem; margin-bottom: 10px;">ðŸŽĻðŸū</div>
              <h2 style="font-family: 'Press Start 2P', monospace; font-size: 1rem; color: #fcd34d; margin-bottom: 8px;">TIME IS UP!</h2>
              <p style="font-size: 0.85rem; color: #cbd5e1; margin-bottom: 16px;">Ready to publish your artwork to the Bark Gallery?</p>

              <div style="display: flex; gap: 10px; flex-wrap: wrap; justify-content: center;">
                <button id="overlayRetryBtn" class="btn-pixel btn-secondary">🔄 Redraw</button>
                <button id="overlayUploadBtn" class="btn-pixel btn-action">ðŸ“Ī Upload Artwork</button>
              </div>
            </div>
          </div>

          <!-- Controls Bar -->
          <div class="tools-bar">
            <!-- Tool Type & Brush Size Subrow -->
            <div class="tools-subrow">
              <!-- Tool Type -->
              <div class="tool-selector">
                <button class="tool-btn active" id="penToolBtn">✏ïļ Pen</button>
                <button class="tool-btn" id="sprayToolBtn">ðŸ’Ļ Spray</button>
                <button class="tool-btn" id="eraserToolBtn">ðŸ§― Eraser</button>
              </div>

              <!-- Brush Sizes -->
              <div class="size-selector">
                <button class="size-btn" data-size="3">Thin</button>
                <button class="size-btn active" data-size="7">Med</button>
                <button class="size-btn" data-size="16">Thick</button>
              </div>
            </div>

            <!-- Expanded 18 Color Swatches -->
            <div class="color-swatches">
              <button class="color-btn active" style="background: #000000;" data-color="#000000" title="Black"></button>
              <button class="color-btn" style="background: #4b5563;" data-color="#4b5563" title="Dark Gray"></button>
              <button class="color-btn" style="background: #9ca3af;" data-color="#9ca3af" title="Light Gray"></button>
              <button class="color-btn" style="background: #ffffff; border: 1px solid #94a3b8;" data-color="#ffffff" title="White"></button>
              <button class="color-btn" style="background: #451a03;" data-color="#451a03" title="Dark Brown"></button>
              <button class="color-btn" style="background: #78350f;" data-color="#78350f" title="Brown"></button>
              <button class="color-btn" style="background: #b45309;" data-color="#b45309" title="Caramel / Tan"></button>
              <button class="color-btn" style="background: #f59e0b;" data-color="#f59e0b" title="Golden"></button>
              <button class="color-btn" style="background: #fef08a;" data-color="#fef08a" title="Cream / Yellow"></button>
              <button class="color-btn" style="background: #b91c1c;" data-color="#b91c1c" title="Dark Red"></button>
              <button class="color-btn" style="background: #ef4444;" data-color="#ef4444" title="Bright Red"></button>
              <button class="color-btn" style="background: #f97316;" data-color="#f97316" title="Orange"></button>
              <button class="color-btn" style="background: #84cc16;" data-color="#84cc16" title="Lime Green"></button>
              <button class="color-btn" style="background: #22c55e;" data-color="#22c55e" title="Green"></button>
              <button class="color-btn" style="background: #06b6d4;" data-color="#06b6d4" title="Cyan"></button>
              <button class="color-btn" style="background: #38bdf8;" data-color="#38bdf8" title="Sky Blue"></button>
              <button class="color-btn" style="background: #2563eb;" data-color="#2563eb" title="Royal Blue"></button>
              <button class="color-btn" style="background: #a855f7;" data-color="#a855f7" title="Purple"></button>
              <button class="color-btn" style="background: #ec4899;" data-color="#ec4899" title="Pink"></button>
            </div>

            <!-- Action Buttons Row -->
            <div class="tools-subrow">
              <div style="display: flex; gap: 6px; flex-wrap: wrap;">
                <button id="undoBtn" class="btn-pixel btn-secondary" title="Undo last stroke">â†Đïļ Undo</button>
                <button id="clearBtn" class="btn-pixel btn-secondary" title="Clear canvas">🗑ïļ Clear</button>
                <button id="resetTimerBtn" class="btn-pixel btn-secondary" title="Reset timer and clear canvas for this prompt">🔄 Reset</button>
                <button id="skipBtn" class="btn-pixel btn-secondary" title="Get a new prompt and reset timer">ðŸŽē Skip</button>
              </div>
              <button id="publishBtn" class="btn-pixel btn-action" style="padding: 10px 14px;">Upload Art ðŸ“Ī</button>
            </div>
          </div>
        </div>

        <!-- Top 10 Leaderboard -->
        <div class="leaderboard-box">
          <div style="border-bottom: 1px solid #232d3f; padding-bottom: 10px;">
            <h3 style="font-family: 'Press Start 2P', monospace; font-size: 0.85rem; color: #f59e0b; margin: 0;">🏆 Top 10 Dogs</h3>
            <p style="font-size: 0.75rem; color: #8f9ba8; margin: 4px 0 0 0;">âĪïļ Like (+1 pt) â€Ē ðŸĶī Treat (+3 pts)</p>
          </div>

          <table class="leaderboard-table">
            <thead>
              <tr>
                <th>#</th>
                <th>Dog Tag</th>
                <th style="text-align: right;">Points</th>
              </tr>
            </thead>
            <tbody id="leaderboardBody">
              <!-- Dynamically populated 10 items -->
            </tbody>
          </table>
        </div>
      </div>

      <!-- Live Community Gallery -->
      <div class="gallery-section" style="margin-top: 24px;">
        <div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #232d3f; padding-bottom: 12px;">
          <div>
            <h2 style="font-family: 'Press Start 2P', monospace; font-size: 1.1rem; color: #fcd34d; margin: 0;">🖞ïļ Community Bark Gallery</h2>
            <p style="font-size: 0.8rem; color: #94a3b8; margin: 4px 0 0 0;">Browse drawings and vote for your favorites!</p>
          </div>
          <span id="galleryCount" style="font-family: 'Press Start 2P', monospace; font-size: 0.7rem; color: #38bdf8;">0 ARTWORKS</span>
        </div>

        <div id="galleryGrid" class="gallery-grid">
          <!-- Dynamically populated cards -->
        </div>
      </div>
    </section>
  </main>

  <!-- Upload Modal -->
  <div id="uploadModal" class="modal-backdrop">
    <div class="modal-card">
      <div style="font-size: 2.5rem; margin-bottom: 8px;">🏷ïļ</div>
      <h2 style="font-family: 'Press Start 2P', monospace; font-size: 0.95rem; color: #f59e0b; margin-top: 0;">Sign Your Art!</h2>
      <p style="font-size: 0.85rem; color: #cbd5e1; margin: 0;">Enter your Dog Tag / Username (3–8 letters):</p>

      <input type="text" id="usernameInput" class="modal-input" maxlength="8" placeholder="BARKUS" />

      <div style="display: flex; justify-content: center; gap: 10px;">
        <button id="cancelModalBtn" class="btn-pixel btn-secondary">Cancel</button>
        <button id="confirmUploadBtn" class="btn-pixel btn-action">Post to Gallery 🚀</button>
      </div>
    </div>
  </div>

  <footer class="footer-wrapper">
    <div class="footer-container">
      <div class="footer-bottom">
        <p class="footer-copyright">
          ÂĐ 2026 Quackwork Studios â€Ē DEV Weekend Challenge: Dog Days Edition
        </p>
      </div>
    </div>
  </footer>

  <script>
    const PROMPTS = [
      "A dog and a duck playing on the beach 🏖ïļðŸĶ†",
      "A dog doctor checking a heartbeat with a stethoscope ðŸĐšðŸ•",
      "A dog driving a fast sports car with sunglasses 🏎ïļ",
      "A dog camping by a campfire roasting marshmallows 🏕ïļðŸ”Ĩ",
      "A dog playing cards with a poker face around a table 🃏♠ïļ",
      "A dog skydiving with a parachute in mid-air 🊂",
      "A dog chef cooking spaghetti in a chaotic kitchen 🍝ðŸ‘Ļ‍ðŸģ",
      "A dog detective inspecting clues with a magnifying glass 🔍ðŸ•ĩïļ",
      "A dog DJ rocking big headphones at a dance party 🎧ðŸŽķ",
      "An astronaut dog floating in outer space 🚀",
      "A dog wearing a Santa hat at Christmas 🎅",
      "A dog celebrating its birthday with a giant cake 🎂",
      "A dog dressed as a spooky Halloween ghost ðŸ‘ŧ",
      "A dog surfing a big ocean wave 🏄‍♂ïļ",
      "A dog caught red-handed stealing a slice of pizza 🍕",
      "A wrinkly Pug wearing retro sunglasses 😎",
      "A fluffy Corgi with stubby legs chasing a tennis ball ðŸŽū",
      "A long Dachshund (Wiener Dog) wearing a sweater 🌭",
      "A majestic Husky howling at the moon 🌕",
      "A fancy Poodle with a stylish fluffy haircut ðŸĐ",
      "A dog dramatically refusing to take a bath in the tub 🛁",
      "A puppy having intense mud zoomies in the rain 🌧ïļ"
    ];

    let currentPrompt = "";
    let isDrawing = false;
    let currentColor = "#000000";
    let brushSize = 7;
    let currentTool = "pen"; // "pen", "spray", "eraser"

    let timeLeft = 90;
    let timerInterval = null;
    let isRoundActive = false;

    // Undo Stack (keeps last 15 actions)
    let undoStack = [];

    // 10 Detailed Starter Dog Doodles (all 0 points)
    const DEFAULT_GALLERY = [
      {
        id: "doodle-1",
        name: "PUGGY",
        prompt: "A wrinkly Pug wearing retro sunglasses 😎",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><circle cx='240' cy='240' r='105' fill='%23b45309' stroke='%23000' stroke-width='6'/><ellipse cx='240' cy='285' rx='48' ry='32' fill='%23451a03' stroke='%23000' stroke-width='4'/><rect x='165' y='200' width='65' height='38' rx='8' fill='%23000'/><rect x='250' y='200' width='65' height='38' rx='8' fill='%23000'/><line x1='230' y1='216' x2='250' y2='216' stroke='%23000' stroke-width='6'/><polygon points='140,175 125,115 180,155' fill='%2378350f' stroke='%23000' stroke-width='4'/><polygon points='340,175 355,115 300,155' fill='%2378350f' stroke='%23000' stroke-width='4'/></svg>"
      },
      {
        id: "doodle-2",
        name: "CHOPPER",
        prompt: "A dog caught stealing a slice of pizza 🍕",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><ellipse cx='190' cy='260' rx='85' ry='55' fill='%23f59e0b' stroke='%23000' stroke-width='6'/><circle cx='130' cy='210' r='45' fill='%23f59e0b' stroke='%23000' stroke-width='6'/><circle cx='120' cy='200' r='6' fill='%23000'/><polygon points='145,225 300,265 290,170' fill='%23fef08a' stroke='%23b45309' stroke-width='6'/><circle cx='215' cy='225' r='10' fill='%23ef4444'/><circle cx='255' cy='235' r='10' fill='%23ef4444'/></svg>"
      },
      {
        id: "doodle-3",
        name: "BARKY",
        prompt: "A dog wearing a Santa hat at Christmas 🎅",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><ellipse cx='240' cy='275' rx='85' ry='75' fill='%23fef08a' stroke='%23000' stroke-width='6'/><circle cx='210' cy='255' r='8' fill='%23000'/><circle cx='270' cy='255' r='8' fill='%23000'/><ellipse cx='240' cy='295' rx='14' ry='10' fill='%23000'/><polygon points='165,210 240,85 315,210' fill='%23ef4444' stroke='%23000' stroke-width='5'/><rect x='155' y='200' width='170' height='26' rx='13' fill='%23ffffff' stroke='%23000' stroke-width='4'/><circle cx='240' cy='75' r='20' fill='%23ffffff' stroke='%23000' stroke-width='4'/></svg>"
      },
      {
        id: "doodle-4",
        name: "CAKEDOG",
        prompt: "A dog celebrating its birthday with a giant cake 🎂",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><ellipse cx='160' cy='260' rx='65' ry='55' fill='%239ca3af' stroke='%23000' stroke-width='6'/><circle cx='140' cy='245' r='6' fill='%23000'/><rect x='250' y='245' width='110' height='65' fill='%23ec4899' stroke='%23000' stroke-width='6'/><rect x='270' y='205' width='70' height='40' fill='%2338bdf8' stroke='%23000' stroke-width='6'/><line x1='305' y1='205' x2='305' y2='175' stroke='%23f59e0b' stroke-width='6'/><circle cx='305' cy='170' r='7' fill='%23ef4444'/></svg>"
      },
      {
        id: "doodle-5",
        name: "WAVERIDER",
        prompt: "A dog surfing a big ocean wave 🏄‍♂ïļ",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><path d='M20,380 Q150,220 280,330 T460,370 L460,480 L20,480 Z' fill='%2338bdf8' stroke='%232563eb' stroke-width='6'/><ellipse cx='230' cy='310' rx='95' ry='16' fill='%23f59e0b' stroke='%23000' stroke-width='5'/><ellipse cx='230' cy='240' rx='45' ry='35' fill='%2378350f' stroke='%23000' stroke-width='5'/><circle cx='215' cy='230' r='5' fill='%23fff'/><circle cx='245' cy='230' r='5' fill='%23fff'/></svg>"
      },
      {
        id: "doodle-6",
        name: "MOONDOG",
        prompt: "A majestic Husky howling at the moon 🌕",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><circle cx='360' cy='120' r='52' fill='%23fef08a' stroke='%23f59e0b' stroke-width='6'/><path d='M150,370 L195,240 L265,195 L245,260 L265,370 Z' fill='%234b5563' stroke='%23000' stroke-width='6'/><polygon points='235,215 255,165 275,215' fill='%23b91c1c' stroke='%23000' stroke-width='4'/><circle cx='215' cy='225' r='4' fill='%23000'/></svg>"
      },
      {
        id: "doodle-7",
        name: "DUCKPUP",
        prompt: "A dog and a duck playing on the beach 🏖ïļðŸĶ†",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><ellipse cx='160' cy='280' rx='60' ry='45' fill='%23f59e0b' stroke='%23000' stroke-width='5'/><circle cx='120' cy='230' r='30' fill='%23f59e0b' stroke='%23000' stroke-width='5'/><circle cx='310' cy='275' r='35' fill='%23fef08a' stroke='%23000' stroke-width='5'/><polygon points='345,270 380,278 345,286' fill='%23f97316' stroke='%23000' stroke-width='3'/><circle cx='240' cy='310' r='22' fill='%23ef4444' stroke='%23000' stroke-width='4'/></svg>"
      },
      {
        id: "doodle-8",
        name: "DR_BARK",
        prompt: "A dog doctor checking a heartbeat with a stethoscope ðŸĐšðŸ•",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><circle cx='240' cy='240' r='80' fill='%23b45309' stroke='%23000' stroke-width='5'/><rect x='170' y='140' width='140' height='35' rx='8' fill='%2306b6d4' stroke='%23000' stroke-width='4'/><path d='M190,260 Q240,350 290,260' fill='none' stroke='%234b5563' stroke-width='8'/><circle cx='240' cy='320' r='14' fill='%239ca3af' stroke='%23000' stroke-width='3'/></svg>"
      },
      {
        id: "doodle-9",
        name: "CAMP_DOG",
        prompt: "A dog camping by a campfire roasting marshmallows 🏕ïļðŸ”Ĩ",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><polygon points='280,330 310,240 340,330' fill='%23ef4444' stroke='%23000' stroke-width='4'/><polygon points='295,330 310,270 325,330' fill='%23f59e0b'/><circle cx='170' cy='270' r='55' fill='%2378350f' stroke='%23000' stroke-width='5'/><line x1='170' y1='270' x2='290' y2='270' stroke='%23b45309' stroke-width='4'/><rect x='280' y='260' width='22' height='18' rx='4' fill='%23ffffff' stroke='%23000' stroke-width='2'/></svg>"
      },
      {
        id: "doodle-10",
        name: "SPEEDY",
        prompt: "A dog driving a fast sports car with sunglasses 🏎ïļ",
        likes: 0,
        superLikes: 0,
        img: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'><rect width='480' height='480' fill='%23ffffff'/><circle cx='220' cy='210' r='45' fill='%23f59e0b' stroke='%23000' stroke-width='4'/><rect x='195' y='195' width='50' height='18' rx='4' fill='%23000'/><rect x='100' y='250' width='280' height='80' rx='20' fill='%23ef4444' stroke='%23000' stroke-width='6'/><circle cx='160' cy='330' r='30' fill='%23000'/><circle cx='320' cy='330' r='30' fill='%23000'/></svg>"
      }
    ];

    let gallery = JSON.parse(localStorage.getItem("doggame_community_gallery")) || DEFAULT_GALLERY;

    const canvas = document.getElementById("drawCanvas");
    const ctx = canvas.getContext("2d");
    const targetBreedEl = document.getElementById("targetBreed");
    const timerDisplay = document.getElementById("timerDisplay");
    const overlay = document.getElementById("overlay");

    // Tools & Action Buttons
    const penToolBtn = document.getElementById("penToolBtn");
    const sprayToolBtn = document.getElementById("sprayToolBtn");
    const eraserToolBtn = document.getElementById("eraserToolBtn");
    const undoBtn = document.getElementById("undoBtn");
    const clearBtn = document.getElementById("clearBtn");
    const resetTimerBtn = document.getElementById("resetTimerBtn");
    const skipBtn = document.getElementById("skipBtn");
    const publishBtn = document.getElementById("publishBtn");
    const overlayRetryBtn = document.getElementById("overlayRetryBtn");
    const overlayUploadBtn = document.getElementById("overlayUploadBtn");

    const galleryGrid = document.getElementById("galleryGrid");
    const galleryCount = document.getElementById("galleryCount");
    const leaderboardBody = document.getElementById("leaderboardBody");

    const uploadModal = document.getElementById("uploadModal");
    const usernameInput = document.getElementById("usernameInput");
    const cancelModalBtn = document.getElementById("cancelModalBtn");
    const confirmUploadBtn = document.getElementById("confirmUploadBtn");

    function saveState() {
      undoStack.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
      if (undoStack.length > 15) undoStack.shift();
    }

    function clearCanvas() {
      saveState();
      ctx.fillStyle = "#ffffff";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    }
    clearCanvas();

    function startTimer() {
      clearInterval(timerInterval);
      timeLeft = 90;
      timerDisplay.textContent = `${timeLeft}s`;
      timerDisplay.classList.remove("warning");
      isRoundActive = true;

      timerInterval = setInterval(() => {
        timeLeft--;
        timerDisplay.textContent = `${timeLeft}s`;

        if (timeLeft <= 15) {
          timerDisplay.classList.add("warning");
        }

        if (timeLeft <= 0) {
          clearInterval(timerInterval);
          isRoundActive = false;
          overlay.style.display = "flex";
        }
      }, 1000);
    }

    function stopTimer() {
      clearInterval(timerInterval);
      isRoundActive = false;
    }

    function getCoords(e) {
      const rect = canvas.getBoundingClientRect();
      const scaleX = canvas.width / rect.width;
      const scaleY = canvas.height / rect.height;
      const clientX = e.touches ? e.touches[0].clientX : e.clientX;
      const clientY = e.touches ? e.touches[0].clientY : e.clientY;
      return {
        x: (clientX - rect.left) * scaleX,
        y: (clientY - rect.top) * scaleY
      };
    }

    // MS Paint Style Spray Function
    function drawSpray(x, y) {
      const density = brushSize * 3;
      const radius = brushSize * 2.2;
      ctx.fillStyle = currentTool === "eraser" ? "#ffffff" : currentColor;

      for (let i = 0; i < density; i++) {
        const offsetX = (Math.random() - 0.5) * 2 * radius;
        const offsetY = (Math.random() - 0.5) * 2 * radius;
        if (offsetX * offsetX + offsetY * offsetY <= radius * radius) {
          ctx.fillRect(x + offsetX, y + offsetY, 1.5, 1.5);
        }
      }
    }

    function startDraw(e) {
      if (!isRoundActive) return;
      e.preventDefault();
      saveState();
      isDrawing = true;
      const { x, y } = getCoords(e);

      if (currentTool === "spray") {
        drawSpray(x, y);
      } else {
        ctx.beginPath();
        ctx.moveTo(x, y);
      }
    }

    function draw(e) {
      if (!isDrawing || !isRoundActive) return;
      e.preventDefault();
      const { x, y } = getCoords(e);

      if (currentTool === "spray") {
        drawSpray(x, y);
      } else {
        ctx.lineTo(x, y);
        ctx.strokeStyle = currentTool === "eraser" ? "#ffffff" : currentColor;
        ctx.lineWidth = currentTool === "eraser" ? brushSize * 3 : brushSize;
        ctx.lineCap = "round";
        ctx.lineJoin = "round";
        ctx.stroke();
      }
    }

    function stopDraw() {
      isDrawing = false;
    }

    canvas.addEventListener("mousedown", startDraw);
    canvas.addEventListener("mousemove", draw);
    canvas.addEventListener("mouseup", stopDraw);
    canvas.addEventListener("mouseleave", stopDraw);

    canvas.addEventListener("touchstart", startDraw, { passive: false });
    canvas.addEventListener("touchmove", draw, { passive: false });
    canvas.addEventListener("touchend", stopDraw);

    // Color Swatches
    document.querySelectorAll(".color-btn").forEach(btn => {
      btn.addEventListener("click", () => {
        document.querySelectorAll(".color-btn").forEach(b => b.classList.remove("active"));
        btn.classList.add("active");
        currentColor = btn.dataset.color;
        if (currentTool === "eraser") {
          currentTool = "pen";
          penToolBtn.classList.add("active");
          eraserToolBtn.classList.remove("active");
        }
      });
    });

    // Tool Selector
    penToolBtn.addEventListener("click", () => {
      currentTool = "pen";
      penToolBtn.classList.add("active");
      sprayToolBtn.classList.remove("active");
      eraserToolBtn.classList.remove("active");
    });

    sprayToolBtn.addEventListener("click", () => {
      currentTool = "spray";
      sprayToolBtn.classList.add("active");
      penToolBtn.classList.remove("active");
      eraserToolBtn.classList.remove("active");
    });

    eraserToolBtn.addEventListener("click", () => {
      currentTool = "eraser";
      eraserToolBtn.classList.add("active");
      penToolBtn.classList.remove("active");
      sprayToolBtn.classList.remove("active");
    });

    // Brush Sizes
    document.querySelectorAll(".size-btn").forEach(btn => {
      btn.addEventListener("click", () => {
        document.querySelectorAll(".size-btn").forEach(b => b.classList.remove("active"));
        btn.classList.add("active");
        brushSize = parseInt(btn.dataset.size);
      });
    });

    // Undo Action
    undoBtn.addEventListener("click", () => {
      if (undoStack.length > 0) {
        const lastState = undoStack.pop();
        ctx.putImageData(lastState, 0, 0);
      }
    });

    clearBtn.addEventListener("click", clearCanvas);

    // Reset Timer (Keeps same prompt)
    resetTimerBtn.addEventListener("click", () => {
      overlay.style.display = "none";
      clearCanvas();
      undoStack = [];
      startTimer();
    });

    // Skip (New Prompt + Reset Timer)
    function nextPrompt() {
      stopTimer();
      overlay.style.display = "none";
      clearCanvas();
      undoStack = [];

      currentPrompt = PROMPTS[Math.floor(Math.random() * PROMPTS.length)];
      targetBreedEl.textContent = currentPrompt;
      startTimer();
    }

    skipBtn.addEventListener("click", nextPrompt);
    overlayRetryBtn.addEventListener("click", nextPrompt);

    // Upload Modal Flow
    function openUploadModal() {
      stopTimer();
      usernameInput.value = localStorage.getItem("doggame_username") || "BARKUS";
      uploadModal.style.display = "flex";
    }

    publishBtn.addEventListener("click", openUploadModal);
    overlayUploadBtn.addEventListener("click", () => {
      overlay.style.display = "none";
      openUploadModal();
    });
    cancelModalBtn.addEventListener("click", () => uploadModal.style.display = "none");

    confirmUploadBtn.addEventListener("click", () => {
      let cleanName = usernameInput.value.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "");
      if (cleanName.length < 3) cleanName = "DOGE";
      cleanName = cleanName.slice(0, 8);
      localStorage.setItem("doggame_username", cleanName);

      const dataUrl = canvas.toDataURL("image/jpeg", 0.7);

      const newArtwork = {
        id: "art-" + Date.now(),
        name: cleanName,
        prompt: currentPrompt,
        likes: 0,
        superLikes: 0,
        img: dataUrl
      };

      gallery.unshift(newArtwork);
      localStorage.setItem("doggame_community_gallery", JSON.stringify(gallery));

      uploadModal.style.display = "none";
      renderGalleryAndLeaderboard();
      nextPrompt();

      document.querySelector(".gallery-section").scrollIntoView({ behavior: "smooth" });
    });

    // Voting System
    window.vote = function(id, type) {
      const item = gallery.find(g => g.id === id);
      if (!item) return;

      if (type === "like") {
        item.likes += 1;
      } else if (type === "super") {
        item.superLikes += 1;
      }

      localStorage.setItem("doggame_community_gallery", JSON.stringify(gallery));
      renderGalleryAndLeaderboard();
    };

    function calculateScore(item) {
      return (item.likes * 1) + (item.superLikes * 3);
    }

    function renderGalleryAndLeaderboard() {
      galleryCount.textContent = `${gallery.length} ARTWORKS`;
      galleryGrid.innerHTML = gallery.map(item => `
        <div class="gallery-card">
          <div class="gallery-img-wrap">
            <img src="${item.img}" alt="${item.prompt}" />
          </div>
          <div class="gallery-info">
            <div class="gallery-user-row">
              <span class="gallery-user">${item.name}</span>
              <span style="font-size: 0.75rem; color: #10b981; font-weight: bold;">${calculateScore(item)} PTS</span>
            </div>
            <div class="gallery-prompt" title="${item.prompt}">${item.prompt}</div>
            <div class="vote-bar">
              <button class="vote-btn" onclick="vote('${item.id}', 'like')" title="Give a Like (+1 pt)">
                âĪïļ <span>${item.likes}</span>
              </button>
              <button class="vote-btn" style="border-color: #f59e0b;" onclick="vote('${item.id}', 'super')" title="Super Like with a Bone! (+3 pts)">
                ðŸĶī <span>${item.superLikes}</span>
              </button>
            </div>
          </div>
        </div>
      `).join("");

      const sorted = [...gallery].sort((a, b) => calculateScore(b) - calculateScore(a));
      leaderboardBody.innerHTML = sorted.slice(0, 10).map((item, idx) => `
        <tr>
          <td style="color: ${idx === 0 ? '#fcd34d' : idx === 1 ? '#cbd5e1' : idx === 2 ? '#b45309' : '#8f9ba8'}; font-weight: bold;">
            ${idx + 1}.
          </td>
          <td class="tag-highlight">${item.name}</td>
          <td class="score-highlight">${calculateScore(item)} PTS</td>
        </tr>
      `).join("");
    }

    renderGalleryAndLeaderboard();
    nextPrompt();
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

How I Built It 🛠ïļ

🏗ïļ Architecture & Hosting Transparency

I chose to host this project on my existing studio domain (Quackwork Studios) and custom backend infrastructure because it was the fastest, most reliable way to deliver a friction-free demo.

  • Pre-Existing Infrastructure: My website already had a working deployment pipeline, SSL certificates, edge caching, and a clean base template ready to go.

  • Built 100% from Scratch for this Challenge: The Bark Gallery game itself (doggame.html), its canvas engine, voting logic, and Gemini-generated assets did not exist before this hackathon.

  • Zero Friction for Judges: Deploying on my own infrastructure let me skip third-party sandbox restrictions and ensure judges and players could jump right in with instant load times and zero configuration.

ðŸĪ– Solo Dev Pairing Workflow with Google AI Studio

All development, prototyping, prompt generation, debugging, and code creation was done inside Google AI Studio using the Gemini 3.7 Flash model from the Featured tab. Gemini acted as a real-time technical co-pilot throughout the build:

  1. Ideation & Prototyping: We brainstormed dog-themed concepts tailored to the 48-hour hackathon timeframe, taking the idea from initial concept to a playable browser prototype in under an hour.

  2. Full-Stack Frontend Generation: Gemini generated the single-file architecture (doggame.html), including the touch/mouse event listeners, undo state history, and particle-based MS Paint spray effects.

  3. Asset Generation in Code: Gemini generated 10 hand-drawn SVG dog illustrations directly in code to pre-populate the community gallery with starter doodles.

  4. Live Debugging & Refactoring: Diagnosed server 404s, resolved edge caching issues, and refactored the canvas engine during development.


ðŸŽĻ Creative Features & Toolkit

  • 🖌ïļ Drawing Suite:
    • ðŸ’Ļ MS Paint-Style Spray Can: Realistic particle dispersion physics.
    • ✏ïļ Pen & Eraser: 3 stroke thicknesses (Thin, Med, Thick).
    • ðŸŽĻ 18-Color Swatch Palette: From vibrant neon accents to natural fur tones.
    • â†Đïļ 15-Step Undo Stack: Fast state restoration to undo misclicks.
  • ðŸ•đïļ Canvas Controls:
    • ðŸŽē Skip: Pulls a fresh prompt and resets the 90s countdown.
    • 🔄 Reset: Clears canvas and restarts the timer for the same prompt.
    • 🗑ïļ Clear: Wipes the current sketch immediately.

🛑 Roadblocks, Errors & Solutions

  1. The Linux Case Sensitivity 404:

    • Bug: Deploying to GitHub Pages gave a 404 Not Found.
    • Cause: The file was committed as Doggame.html (capital D) while the link targeted /doggame.html.
    • Fix: Renamed to all-lowercase doggame.html and redeployed.
  2. Aggressive Edge Caching:

    • Bug: Fresh commits weren't showing up immediately for players.
    • Fix: Added cache-busting version query parameters (?v=2, ?v=3) and purged edge caches.
  3. Client-Side API Deprecation & Key Friction:

    • Bug: Querying external models client-side required exposing or asking users for API keys.
    • Fix: Pivoted to a 100% frictionless Community Speed-Sketch Showcase: client-side JPEG compression (~15KB) paired with instantaneous real-time voting and leaderboard ranking. Zero backend latency, zero friction, and 100% uptime.

Prize Categories 🏆

I am submitting this project for:

🌟 Best Use of Google AI

Bark Gallery was developed end-to-end with Google AI Studio (Gemini 3.7 Flash):

  • Generating all humorous, chaotic prompt scenarios
  • Generating the core HTML5 canvas engine and particle spray physics
  • Generating the embedded starter SVG illustrations
  • Debugging deployment, caching, and state management logic

ðŸū About the Developer

I'm John, a self-taught, solo indie game developer building games at Quackwork Studios (currently developing the multiverse card battler Duckmon).

Grab your digital spray can, post your dog sketch, don't forget to vote and enter, and let's celebrate International Dog Day! 🐕ðŸŽĻ

Edit - Adjusted Title

Top comments (0)