From bf4a4ba7f89d621611cb95246b1ef0a699ab36e7 Mon Sep 17 00:00:00 2001
From: adripo <26493496+adripo@users.noreply.github.com>
Date: Sat, 25 Oct 2025 00:31:03 +0000
Subject: [PATCH] Add Mirror Painter project
---
projects/mirror-painter/index.html | 65 +++
projects/mirror-painter/main.js | 674 +++++++++++++++++++++++++++++
projects/mirror-painter/style.css | 327 ++++++++++++++
3 files changed, 1066 insertions(+)
create mode 100644 projects/mirror-painter/index.html
create mode 100644 projects/mirror-painter/main.js
create mode 100644 projects/mirror-painter/style.css
diff --git a/projects/mirror-painter/index.html b/projects/mirror-painter/index.html
new file mode 100644
index 0000000..fdf3048
--- /dev/null
+++ b/projects/mirror-painter/index.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+ Mirror Painter - VanillaVerse
+
+
+
+
+
+
+
+
+
Level 1: Vertical Line
+
Draw a straight vertical line
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 4px
+
+
+
+
+
+
+ 🎉 Perfect! You matched the template!
+
+
+
+ 🏆 Congratulations! You completed all levels!
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/mirror-painter/main.js b/projects/mirror-painter/main.js
new file mode 100644
index 0000000..b534cb8
--- /dev/null
+++ b/projects/mirror-painter/main.js
@@ -0,0 +1,674 @@
+/*
+================================================================================
+MIRROR PAINTER - MAIN JAVASCRIPT FILE
+================================================================================
+This file contains all the JavaScript logic for the Mirror Painter application.
+It demonstrates separation of concerns: HTML for structure, CSS for styling,
+and JavaScript for behavior and interactivity.
+
+Educational Purpose: This code is structured to help beginners understand:
+- DOM manipulation and element selection
+- HTML5 Canvas API and 2D rendering context
+- Event-driven programming with mouse/touch events
+- Coordinate transformations (mirroring)
+- Image data processing and pixel comparison
+- State management in web applications
+================================================================================
+*/
+
+/*
+================================================================================
+1. DOM ELEMENT SELECTION
+================================================================================
+We start by selecting all the HTML elements we need to work with.
+Using getElementById() is the most common way to access specific elements.
+*/
+
+// Canvas elements - These are where we'll draw
+const drawCanvas = document.getElementById('drawCanvas');
+const mirrorCanvas = document.getElementById('mirrorCanvas');
+
+// Canvas contexts - The 2D rendering context provides drawing methods
+// Think of context as the "paintbrush" we use to draw on the canvas
+const drawCtx = drawCanvas.getContext('2d');
+const mirrorCtx = mirrorCanvas.getContext('2d');
+
+// UI Control elements - Buttons and inputs the user interacts with
+const clearBtn = document.getElementById('clearBtn');
+const nextLevelBtn = document.getElementById('nextLevelBtn');
+const colorPicker = document.getElementById('colorPicker');
+const brushSize = document.getElementById('brushSize');
+const brushSizeValue = document.getElementById('brushSizeValue');
+
+// Display elements - Elements we update to show information to the user
+const levelTitle = document.getElementById('levelTitle');
+const levelDescription = document.getElementById('levelDescription');
+const matchPercentage = document.getElementById('matchPercentage');
+const encouragement = document.getElementById('encouragement');
+const successMessage = document.getElementById('successMessage');
+const completionMessage = document.getElementById('completionMessage');
+
+/*
+================================================================================
+2. GAME STATE VARIABLES
+================================================================================
+These variables track the current state of our application.
+In a real app, you might use more sophisticated state management,
+but for learning purposes, global variables work fine.
+*/
+
+let currentLevel = 0; // Which level the player is on (0-indexed)
+let isDrawing = false; // Is the user currently drawing?
+let lastX = 0; // Last X position of the mouse
+let lastY = 0; // Last Y position of the mouse
+let currentColor = '#2563eb'; // Current drawing color (blue)
+let currentBrushSize = 4; // Current brush thickness in pixels
+let levelComplete = false; // Has the current level been completed?
+
+/*
+================================================================================
+3. LEVEL TEMPLATES DATA
+================================================================================
+This array defines all the levels in our game.
+Each level has a shape that the player needs to match.
+*/
+
+const levels = [
+ {
+ id: 1,
+ name: "Vertical Line",
+ description: "Draw a straight vertical line",
+ template_type: "line",
+ points: [[200, 50], [200, 350]] // Start and end coordinates
+ },
+ {
+ id: 2,
+ name: "Horizontal Line",
+ description: "Draw a straight horizontal line",
+ template_type: "line",
+ points: [[50, 200], [350, 200]]
+ },
+ {
+ id: 3,
+ name: "Cross",
+ description: "Draw a plus sign",
+ template_type: "multi_line",
+ lines: [
+ [[200, 50], [200, 350]], // Vertical line
+ [[50, 200], [350, 200]] // Horizontal line
+ ]
+ },
+ {
+ id: 4,
+ name: "Triangle",
+ description: "Draw a triangle",
+ template_type: "polygon",
+ points: [[200, 80], [320, 320], [80, 320], [200, 80]]
+ },
+ {
+ id: 5,
+ name: "Heart",
+ description: "Draw a simple heart shape",
+ template_type: "polygon",
+ points: [
+ [200, 300], [150, 250], [150, 200], [175, 175],
+ [200, 185], [225, 175], [250, 200], [250, 250], [200, 300]
+ ]
+ }
+];
+
+// Encouraging messages to motivate the player
+const encouragementMessages = [
+ "Keep going!",
+ "You're doing great!",
+ "Almost there!",
+ "Nice work!"
+];
+
+/*
+================================================================================
+4. INITIALIZATION FUNCTION
+================================================================================
+This function sets up the canvas contexts with the properties we want.
+We call this once when the page loads.
+*/
+
+function initCanvas() {
+ // Configure both canvas contexts with the same drawing properties
+ // lineCap: 'round' makes the ends of lines rounded instead of square
+ // lineJoin: 'round' makes corners rounded when lines connect
+ [drawCtx, mirrorCtx].forEach(ctx => {
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ });
+}
+
+/*
+================================================================================
+5. UTILITY FUNCTIONS
+================================================================================
+*/
+
+/**
+ * Get the mouse position relative to the canvas
+ * This is necessary because mouse events give us coordinates relative to the
+ * entire page, but we need coordinates relative to the canvas element.
+ *
+ * @param {HTMLCanvasElement} canvas - The canvas element
+ * @param {MouseEvent|Touch} evt - The mouse or touch event
+ * @returns {Object} Object with x and y coordinates
+ */
+function getMousePos(canvas, evt) {
+ const rect = canvas.getBoundingClientRect();
+ return {
+ x: evt.clientX - rect.left,
+ y: evt.clientY - rect.top
+ };
+}
+
+/*
+================================================================================
+6. CANVAS DRAWING FUNCTIONS
+================================================================================
+*/
+
+/**
+ * Clear both canvases and redraw the template
+ * This function is called when the user clicks the Clear button
+ * or when starting a new level
+ */
+function clearCanvases() {
+ // clearRect removes all drawings from a rectangular area
+ // We clear the entire canvas by using its full width and height
+ drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);
+ mirrorCtx.clearRect(0, 0, mirrorCanvas.width, mirrorCanvas.height);
+
+ // Redraw the template so the user knows what to draw
+ drawTemplate();
+}
+
+/**
+ * Draw the template shape on the mirror canvas
+ * The template shows the user what they need to draw
+ * It appears as a light gray shape on the right canvas
+ */
+function drawTemplate() {
+ const level = levels[currentLevel];
+
+ // Set template drawing style - semi-transparent gray
+ mirrorCtx.strokeStyle = 'rgba(200, 200, 200, 0.5)';
+ mirrorCtx.lineWidth = 8;
+ mirrorCtx.lineCap = 'round';
+ mirrorCtx.lineJoin = 'round';
+
+ // Draw different shapes based on the template type
+ if (level.template_type === 'line') {
+ // Draw a single line from point A to point B
+ mirrorCtx.beginPath(); // Start a new path
+ mirrorCtx.moveTo(level.points[0][0], level.points[0][1]); // Move to start point
+ mirrorCtx.lineTo(level.points[1][0], level.points[1][1]); // Draw line to end point
+ mirrorCtx.stroke(); // Actually draw the line
+ }
+ else if (level.template_type === 'multi_line') {
+ // Draw multiple separate lines
+ level.lines.forEach(line => {
+ mirrorCtx.beginPath();
+ mirrorCtx.moveTo(line[0][0], line[0][1]);
+ mirrorCtx.lineTo(line[1][0], line[1][1]);
+ mirrorCtx.stroke();
+ });
+ }
+ else if (level.template_type === 'polygon') {
+ // Draw a shape by connecting multiple points
+ mirrorCtx.beginPath();
+ mirrorCtx.moveTo(level.points[0][0], level.points[0][1]);
+ // Loop through remaining points and draw lines to each
+ for (let i = 1; i < level.points.length; i++) {
+ mirrorCtx.lineTo(level.points[i][0], level.points[i][1]);
+ }
+ mirrorCtx.stroke();
+ }
+}
+
+/**
+ * Main drawing function - draws on both canvases
+ * This is called during mousemove when the user is drawing
+ *
+ * @param {number} x - Current X coordinate
+ * @param {number} y - Current Y coordinate
+ */
+function draw(x, y) {
+ /*
+ * PART 1: Draw on the left (drawing) canvas
+ * This is straightforward - just draw a line from the last position to the current one
+ */
+ drawCtx.strokeStyle = currentColor;
+ drawCtx.lineWidth = currentBrushSize;
+ drawCtx.beginPath();
+ drawCtx.moveTo(lastX, lastY);
+ drawCtx.lineTo(x, y);
+ drawCtx.stroke();
+
+ /*
+ * PART 2: Draw the MIRRORED version on the right canvas
+ * This is more complex because we need to flip it horizontally
+ *
+ * COORDINATE TRANSFORMATION EXPLANATION:
+ * - save() saves the current canvas state (transformations, styles, etc.)
+ * - translate(width, 0) moves the origin to the right edge
+ * - scale(-1, 1) flips horizontally (negative X scale)
+ * - Now when we draw, it appears mirrored!
+ * - restore() returns to the saved state, undoing our transformations
+ */
+ mirrorCtx.save(); // Save the current state
+
+ // Transform the coordinate system for mirroring
+ mirrorCtx.translate(mirrorCanvas.width, 0); // Move origin to right edge
+ mirrorCtx.scale(-1, 1); // Flip horizontally
+
+ // Draw the mirrored stroke (same coordinates, but transformed space)
+ mirrorCtx.strokeStyle = currentColor;
+ mirrorCtx.lineWidth = currentBrushSize;
+ mirrorCtx.beginPath();
+ mirrorCtx.moveTo(lastX, lastY);
+ mirrorCtx.lineTo(x, y);
+ mirrorCtx.stroke();
+
+ mirrorCtx.restore(); // Restore the original state
+
+ // Redraw the template on top so it's always visible
+ drawTemplate();
+
+ // Calculate how well the drawing matches the template
+ calculateMatch();
+
+ // Update last position for next draw call
+ lastX = x;
+ lastY = y;
+}
+
+/*
+================================================================================
+7. SHAPE MATCHING ALGORITHM
+================================================================================
+This is one of the most complex parts of the code.
+We compare the user's drawing to the template pixel by pixel.
+*/
+
+/**
+ * Calculate how well the user's drawing matches the template
+ * Uses pixel-level comparison with getImageData()
+ *
+ * HOW IT WORKS:
+ * 1. Create a temporary canvas with just the template
+ * 2. Create another temporary canvas with the user's drawing (mirrored)
+ * 3. Get the pixel data from both canvases
+ * 4. Count how many pixels match
+ * 5. Calculate the percentage
+ */
+function calculateMatch() {
+ // Don't recalculate if level is already complete
+ if (levelComplete) return;
+
+ /*
+ * STEP 1: Create template canvas
+ * We need the template by itself (without the user's drawing)
+ */
+ const tempCanvas = document.createElement('canvas');
+ tempCanvas.width = mirrorCanvas.width;
+ tempCanvas.height = mirrorCanvas.height;
+ const tempCtx = tempCanvas.getContext('2d');
+
+ // Draw the template shape
+ const level = levels[currentLevel];
+ tempCtx.strokeStyle = 'rgba(255, 255, 255, 1)';
+ tempCtx.lineWidth = 8;
+ tempCtx.lineCap = 'round';
+ tempCtx.lineJoin = 'round';
+
+ // Draw the same shape as in drawTemplate()
+ if (level.template_type === 'line') {
+ tempCtx.beginPath();
+ tempCtx.moveTo(level.points[0][0], level.points[0][1]);
+ tempCtx.lineTo(level.points[1][0], level.points[1][1]);
+ tempCtx.stroke();
+ } else if (level.template_type === 'multi_line') {
+ level.lines.forEach(line => {
+ tempCtx.beginPath();
+ tempCtx.moveTo(line[0][0], line[0][1]);
+ tempCtx.lineTo(line[1][0], line[1][1]);
+ tempCtx.stroke();
+ });
+ } else if (level.template_type === 'polygon') {
+ tempCtx.beginPath();
+ tempCtx.moveTo(level.points[0][0], level.points[0][1]);
+ for (let i = 1; i < level.points.length; i++) {
+ tempCtx.lineTo(level.points[i][0], level.points[i][1]);
+ }
+ tempCtx.stroke();
+ }
+
+ /*
+ * STEP 2: Get template pixel data
+ * getImageData returns an object containing pixel data as a Uint8ClampedArray
+ * Each pixel has 4 values: Red, Green, Blue, Alpha (transparency)
+ */
+ const templateData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
+
+ /*
+ * STEP 3: Create user drawing canvas (mirrored)
+ * We need to mirror the user's drawing to match the template orientation
+ */
+ const userCanvas = document.createElement('canvas');
+ userCanvas.width = mirrorCanvas.width;
+ userCanvas.height = mirrorCanvas.height;
+ const userCtx = userCanvas.getContext('2d');
+
+ // Mirror the drawing canvas onto the user canvas
+ userCtx.save();
+ userCtx.translate(userCanvas.width, 0);
+ userCtx.scale(-1, 1);
+ userCtx.drawImage(drawCanvas, 0, 0); // Copy the drawing canvas
+ userCtx.restore();
+
+ const userData = userCtx.getImageData(0, 0, userCanvas.width, userCanvas.height);
+
+ /*
+ * STEP 4: Count matching pixels
+ * We loop through all pixels and check if they match
+ *
+ * IMAGE DATA FORMAT:
+ * The data array contains 4 values per pixel: [R, G, B, A, R, G, B, A, ...]
+ * So we increment by 4 to go from one pixel to the next
+ * The Alpha channel (index + 3) tells us if a pixel is drawn (>128) or empty (<=128)
+ */
+ let templatePixels = 0; // Total number of pixels in the template
+ let matchingPixels = 0; // Number of pixels that match
+
+ for (let i = 0; i < templateData.data.length; i += 4) {
+ const templateAlpha = templateData.data[i + 3]; // Template pixel transparency
+ const userAlpha = userData.data[i + 3]; // User pixel transparency
+
+ // If the template has a pixel here (alpha > 128 means it's drawn)
+ if (templateAlpha > 128) {
+ templatePixels++;
+
+ // Check if the user also drew something here
+ if (userAlpha > 128) {
+ matchingPixels++;
+ }
+ }
+ }
+
+ /*
+ * STEP 5: Calculate match percentage
+ * If there are no template pixels (shouldn't happen), return 0%
+ * Otherwise, calculate what percentage of template pixels are matched
+ */
+ const matchPercent = templatePixels > 0
+ ? Math.round((matchingPixels / templatePixels) * 100)
+ : 0;
+
+ updateMatchPercentage(matchPercent);
+
+ // Check if level is complete (75% match threshold)
+ if (matchPercent >= 75 && !levelComplete) {
+ levelComplete = true;
+ onLevelComplete();
+ }
+}
+
+/*
+================================================================================
+8. UI UPDATE FUNCTIONS
+================================================================================
+*/
+
+/**
+ * Update the match percentage display and encouragement message
+ * Changes color based on how close the user is to completing the level
+ *
+ * @param {number} percent - The match percentage (0-100)
+ */
+function updateMatchPercentage(percent) {
+ matchPercentage.textContent = `${percent}%`;
+
+ // Update color and message based on percentage
+ matchPercentage.classList.remove('low', 'medium', 'high');
+
+ if (percent < 50) {
+ // Low score - show in red
+ matchPercentage.classList.add('low');
+ encouragement.textContent = encouragementMessages[0];
+ }
+ else if (percent < 75) {
+ // Medium score - show in yellow
+ matchPercentage.classList.add('medium');
+ encouragement.textContent = encouragementMessages[Math.floor(Math.random() * 2) + 1];
+ }
+ else {
+ // High score - show in green
+ matchPercentage.classList.add('high');
+ encouragement.textContent = encouragementMessages[3];
+ }
+}
+
+/**
+ * Called when the user completes a level (match >= 75%)
+ * Shows success message and next level button
+ */
+function onLevelComplete() {
+ // Add a flash animation to celebrate
+ mirrorCanvas.classList.add('success-flash');
+ setTimeout(() => {
+ mirrorCanvas.classList.remove('success-flash');
+ }, 600);
+
+ // Show success message
+ successMessage.classList.add('show');
+
+ // Show next level button or completion message
+ if (currentLevel < levels.length - 1) {
+ // More levels remain
+ nextLevelBtn.classList.add('show');
+ } else {
+ // All levels complete!
+ completionMessage.classList.add('show');
+ }
+}
+
+/*
+================================================================================
+9. LEVEL MANAGEMENT FUNCTIONS
+================================================================================
+*/
+
+/**
+ * Start a new level
+ * Resets the canvases and updates the UI
+ */
+function startLevel() {
+ levelComplete = false;
+ clearCanvases();
+
+ // Update level information display
+ const level = levels[currentLevel];
+ levelTitle.textContent = `Level ${level.id}: ${level.name}`;
+ levelDescription.textContent = level.description;
+
+ // Draw the template
+ drawTemplate();
+
+ // Reset match percentage
+ updateMatchPercentage(0);
+
+ // Hide success messages
+ successMessage.classList.remove('show');
+ nextLevelBtn.classList.remove('show');
+ completionMessage.classList.remove('show');
+}
+
+/*
+================================================================================
+10. EVENT LISTENERS - MOUSE EVENTS FOR DRAWING
+================================================================================
+Event listeners are the core of interactive web applications.
+They "listen" for user actions and execute code when those actions occur.
+*/
+
+/**
+ * MOUSEDOWN EVENT
+ * Triggered when the user presses the mouse button down on the canvas
+ * This starts the drawing process
+ */
+drawCanvas.addEventListener('mousedown', (e) => {
+ isDrawing = true; // Set flag to true
+ const pos = getMousePos(drawCanvas, e);
+ lastX = pos.x; // Store starting position
+ lastY = pos.y;
+});
+
+/**
+ * MOUSEMOVE EVENT
+ * Triggered when the mouse moves over the canvas
+ * If we're drawing (mouse button is down), draw a line
+ */
+drawCanvas.addEventListener('mousemove', (e) => {
+ if (!isDrawing) return; // Only draw if mouse is pressed
+ const pos = getMousePos(drawCanvas, e);
+ draw(pos.x, pos.y);
+});
+
+/**
+ * MOUSEUP EVENT
+ * Triggered when the user releases the mouse button
+ * This stops the drawing process
+ */
+drawCanvas.addEventListener('mouseup', () => {
+ isDrawing = false;
+});
+
+/**
+ * MOUSELEAVE EVENT
+ * Triggered when the mouse leaves the canvas area
+ * We stop drawing to prevent weird behavior
+ */
+drawCanvas.addEventListener('mouseleave', () => {
+ isDrawing = false;
+});
+
+/*
+================================================================================
+11. EVENT LISTENERS - TOUCH EVENTS FOR MOBILE
+================================================================================
+Touch events work similarly to mouse events but are for touchscreens.
+e.preventDefault() stops the default touch behavior (like scrolling).
+*/
+
+drawCanvas.addEventListener('touchstart', (e) => {
+ e.preventDefault(); // Prevent scrolling
+ isDrawing = true;
+ const touch = e.touches[0]; // Get first touch point
+ const pos = getMousePos(drawCanvas, touch);
+ lastX = pos.x;
+ lastY = pos.y;
+});
+
+drawCanvas.addEventListener('touchmove', (e) => {
+ e.preventDefault();
+ if (!isDrawing) return;
+ const touch = e.touches[0];
+ const pos = getMousePos(drawCanvas, touch);
+ draw(pos.x, pos.y);
+});
+
+drawCanvas.addEventListener('touchend', (e) => {
+ e.preventDefault();
+ isDrawing = false;
+});
+
+/*
+================================================================================
+12. EVENT LISTENERS - UI CONTROLS
+================================================================================
+These listeners handle button clicks and input changes.
+*/
+
+/**
+ * COLOR PICKER CHANGE EVENT
+ * When the user selects a new color, update the current color
+ */
+colorPicker.addEventListener('change', (e) => {
+ currentColor = e.target.value;
+});
+
+/**
+ * BRUSH SIZE INPUT EVENT
+ * When the user moves the slider, update the brush size
+ * 'input' event fires continuously as the slider moves
+ */
+brushSize.addEventListener('input', (e) => {
+ currentBrushSize = parseInt(e.target.value);
+ brushSizeValue.textContent = `${currentBrushSize}px`;
+});
+
+/**
+ * CLEAR BUTTON CLICK EVENT
+ * Clear the canvases and reset the level
+ */
+clearBtn.addEventListener('click', () => {
+ clearCanvases();
+ updateMatchPercentage(0);
+ levelComplete = false;
+ successMessage.classList.remove('show');
+ nextLevelBtn.classList.remove('show');
+});
+
+/**
+ * NEXT LEVEL BUTTON CLICK EVENT
+ * Move to the next level
+ */
+nextLevelBtn.addEventListener('click', () => {
+ currentLevel++;
+ if (currentLevel < levels.length) {
+ startLevel();
+ }
+});
+
+/*
+================================================================================
+13. APPLICATION INITIALIZATION
+================================================================================
+This code runs when the page loads.
+It sets up the canvases and starts the first level.
+*/
+
+initCanvas(); // Set up canvas properties
+startLevel(); // Start level 1
+
+/*
+================================================================================
+END OF FILE
+================================================================================
+Congratulations! You've reached the end of the Mirror Painter code.
+
+KEY CONCEPTS YOU LEARNED:
+1. DOM Manipulation - Selecting and modifying HTML elements
+2. Canvas API - Drawing shapes and lines on HTML5 canvas
+3. Event-Driven Programming - Responding to user actions
+4. Coordinate Transformations - Mirroring using translate() and scale()
+5. Image Data Processing - Comparing pixels to calculate match percentage
+6. State Management - Tracking application state with variables
+7. Modular Code - Organizing code into logical functions
+
+NEXT STEPS:
+Try modifying the code to:
+- Add more levels with custom shapes
+- Change the match percentage threshold
+- Add different brush styles
+- Implement an undo feature
+- Add sound effects
+
+Happy coding!
+================================================================================
+*/
\ No newline at end of file
diff --git a/projects/mirror-painter/style.css b/projects/mirror-painter/style.css
new file mode 100644
index 0000000..39916f7
--- /dev/null
+++ b/projects/mirror-painter/style.css
@@ -0,0 +1,327 @@
+/* Design System Variables */
+:root {
+ --color-white: rgba(255, 255, 255, 1);
+ --color-black: rgba(0, 0, 0, 1);
+ --color-cream-50: rgba(252, 252, 249, 1);
+ --color-cream-100: rgba(255, 255, 253, 1);
+ --color-gray-200: rgba(245, 245, 245, 1);
+ --color-gray-300: rgba(167, 169, 169, 1);
+ --color-gray-400: rgba(119, 124, 124, 1);
+ --color-slate-500: rgba(98, 108, 113, 1);
+ --color-brown-600: rgba(94, 82, 64, 1);
+ --color-charcoal-700: rgba(31, 33, 33, 1);
+ --color-charcoal-800: rgba(38, 40, 40, 1);
+ --color-slate-900: rgba(19, 52, 59, 1);
+ --color-teal-300: rgba(50, 184, 198, 1);
+ --color-teal-400: rgba(45, 166, 178, 1);
+ --color-teal-500: rgba(33, 128, 141, 1);
+ --color-teal-600: rgba(29, 116, 128, 1);
+ --color-teal-700: rgba(26, 104, 115, 1);
+ --color-red-400: rgba(255, 84, 89, 1);
+ --color-red-500: rgba(192, 21, 47, 1);
+ --color-orange-400: rgba(230, 129, 97, 1);
+ --color-orange-500: rgba(168, 75, 47, 1);
+ --color-brown-600-rgb: 94, 82, 64;
+ --color-teal-500-rgb: 33, 128, 141;
+ --color-slate-900-rgb: 19, 52, 59;
+ --color-background: var(--color-cream-50);
+ --color-surface: var(--color-cream-100);
+ --color-text: var(--color-slate-900);
+ --color-text-secondary: var(--color-slate-500);
+ --color-primary: var(--color-teal-500);
+ --color-primary-hover: var(--color-teal-600);
+ --color-border: rgba(var(--color-brown-600-rgb), 0.2);
+ --color-card-border: rgba(var(--color-brown-600-rgb), 0.12);
+ --font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ --font-size-sm: 12px;
+ --font-size-base: 14px;
+ --font-size-lg: 16px;
+ --font-size-xl: 18px;
+ --font-size-2xl: 20px;
+ --font-size-3xl: 24px;
+ --space-8: 8px;
+ --space-12: 12px;
+ --space-16: 16px;
+ --space-20: 20px;
+ --space-24: 24px;
+ --space-32: 32px;
+ --radius-base: 8px;
+ --radius-lg: 12px;
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.04), 0 2px 4px -1px rgba(0, 0, 0, 0.02);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --color-background: var(--color-charcoal-700);
+ --color-surface: var(--color-charcoal-800);
+ --color-text: var(--color-gray-200);
+ --color-text-secondary: rgba(167, 169, 169, 0.7);
+ --color-primary: var(--color-teal-300);
+ --color-primary-hover: var(--color-teal-400);
+ --color-border: rgba(119, 124, 124, 0.3);
+ --color-card-border: rgba(119, 124, 124, 0.2);
+ }
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: var(--font-family-base);
+ background-color: var(--color-background);
+ color: var(--color-text);
+ padding: var(--space-24);
+ min-height: 100vh;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+header {
+ text-align: center;
+ margin-bottom: var(--space-32);
+}
+
+h1 {
+ font-size: var(--font-size-3xl);
+ margin-bottom: var(--space-8);
+ color: var(--color-text);
+}
+
+.subtitle {
+ font-size: var(--font-size-base);
+ color: var(--color-text-secondary);
+}
+
+.game-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--space-24);
+ padding: var(--space-16);
+ background-color: var(--color-surface);
+ border-radius: var(--radius-base);
+ border: 1px solid var(--color-card-border);
+}
+
+.level-info {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-8);
+}
+
+.level-title {
+ font-size: var(--font-size-xl);
+ font-weight: 600;
+}
+
+.level-description {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+}
+
+.match-info {
+ text-align: right;
+}
+
+.match-percentage {
+ font-size: var(--font-size-2xl);
+ font-weight: 600;
+ margin-bottom: var(--space-8);
+}
+
+.match-percentage.low {
+ color: #ef4444;
+}
+
+.match-percentage.medium {
+ color: #eab308;
+}
+
+.match-percentage.high {
+ color: #22c55e;
+}
+
+.encouragement {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+}
+
+.canvas-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-32);
+ margin-bottom: var(--space-24);
+}
+
+.canvas-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--space-12);
+}
+
+.canvas-label {
+ font-size: var(--font-size-lg);
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+canvas {
+ border: 2px solid var(--color-border);
+ border-radius: var(--radius-base);
+ background-color: var(--color-white);
+ cursor: crosshair;
+ box-shadow: var(--shadow-md);
+ transition: box-shadow 0.3s ease;
+}
+
+canvas:hover {
+ box-shadow: var(--shadow-md), 0 0 0 2px var(--color-primary);
+}
+
+canvas.success-flash {
+ animation: successPulse 0.6s ease;
+}
+
+@keyframes successPulse {
+ 0%, 100% { box-shadow: var(--shadow-md); }
+ 50% { box-shadow: 0 0 20px #22c55e, var(--shadow-md); }
+}
+
+.controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-16);
+ justify-content: center;
+ align-items: center;
+ padding: var(--space-20);
+ background-color: var(--color-surface);
+ border-radius: var(--radius-base);
+ border: 1px solid var(--color-card-border);
+}
+
+.control-group {
+ display: flex;
+ align-items: center;
+ gap: var(--space-8);
+}
+
+label {
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ color: var(--color-text);
+}
+
+input[type="color"] {
+ width: 50px;
+ height: 36px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-base);
+ cursor: pointer;
+}
+
+input[type="range"] {
+ width: 120px;
+}
+
+.brush-size-value {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+ min-width: 30px;
+}
+
+button {
+ padding: var(--space-12) var(--space-24);
+ font-size: var(--font-size-base);
+ font-weight: 500;
+ border: none;
+ border-radius: var(--radius-base);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.btn-primary {
+ background-color: var(--color-primary);
+ color: white;
+}
+
+.btn-primary:hover {
+ background-color: var(--color-primary-hover);
+ transform: translateY(-1px);
+}
+
+.btn-secondary {
+ background-color: var(--color-surface);
+ color: var(--color-text);
+ border: 1px solid var(--color-border);
+}
+
+.btn-secondary:hover {
+ background-color: var(--color-background);
+}
+
+.success-message {
+ display: none;
+ text-align: center;
+ padding: var(--space-20);
+ margin-top: var(--space-24);
+ background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
+ color: white;
+ border-radius: var(--radius-lg);
+ font-size: var(--font-size-xl);
+ font-weight: 600;
+ animation: slideIn 0.5s ease;
+}
+
+.success-message.show {
+ display: block;
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.completion-message {
+ display: none;
+ text-align: center;
+ padding: var(--space-32);
+ margin-top: var(--space-24);
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
+ color: white;
+ border-radius: var(--radius-lg);
+ font-size: var(--font-size-2xl);
+ font-weight: 600;
+}
+
+.completion-message.show {
+ display: block;
+}
+
+#nextLevelBtn {
+ display: none;
+ margin-top: var(--space-16);
+}
+
+#nextLevelBtn.show {
+ display: inline-block;
+}
+
+@media (max-width: 900px) {
+ .canvas-container {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file