const express = require("express"); const http = require("http"); const { Server } = require("socket.io"); const path = require("path"); const crypto = require("crypto"); const fs = require("fs"); const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*" } }); app.get("/health", (_req, res) => res.send("ok")); app.use(express.static(path.join(__dirname))); app.get("/", (req, res) => res.sendFile(path.join(__dirname, "index.html"))); const QD_SAMPLES_PATH = path.join(__dirname, "quickdraw_drawings", "samples.json"); let qdSamples = {}; try { qdSamples = JSON.parse(fs.readFileSync(QD_SAMPLES_PATH, "utf8")); console.log(`🎨 Loaded ${Object.keys(qdSamples).length} QuickDraw sample sets`); } catch (e) { console.warn("⚠ No QuickDraw samples found at", QD_SAMPLES_PATH); } const modSockets = new Set(); function verifyTOTP(inputCode) { const secret = 'QBQKRTXUPFKT5T3J'; const base32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = 0, bitCount = 0; const bytes = []; for (const ch of secret.toUpperCase()) { if (ch === '=') continue; const val = base32.indexOf(ch); if (val === -1) return false; bits = (bits << 5) | val; bitCount += 5; if (bitCount >= 8) { bitCount -= 8; bytes.push((bits >>> bitCount) & 0xff); } } const key = Buffer.from(bytes); const now = Math.floor(Date.now() / 1000); for (let delta = -1; delta <= 1; delta++) { let counter = Math.floor(now / 30) + delta; const counterBuf = Buffer.alloc(8); for (let i = 7; i >= 0 && counter > 0; i--) { counterBuf[i] = counter & 0xff; counter = Math.floor(counter / 256); } const hmac = crypto.createHmac('sha1', key).update(counterBuf).digest(); const offset = hmac[hmac.length - 1] & 0xf; const code = ((hmac[offset] & 0x7f) << 24) | (hmac[offset + 1] << 16) | (hmac[offset + 2] << 8) | hmac[offset + 3]; const totp = String(code % 1000000).padStart(6, '0'); if (totp === inputCode) return true; } return false; } const WORDS = { easy: [ "cat","dog","sun","fish","tree","house","sock","car","star","hat", "cake","bird","canoe","moon","flower","shoe","book","key","door","hand", "apple","banana","cloud","smiley face","eye","pizza","duck","frog","lion","bear", "leaf","crown","spoon","train","bus","cup","fork","clock","bee","spider" ], medium: [ "sea turtle","pig","cow","horse","rainbow","mountain","pencil","chair","table","hot air balloon", "guitar","castle","helicopter","umbrella","penguin","computer","bridge","compass", "lantern","wheel","cactus","dolphin","monkey","angel","dragon","bicycle", "lighthouse","binoculars","camera","necklace","scissors","hammer","ladder", "snowman","skull" ], hard: [ "shark","octopus","parrot","flamingo","windmill","campfire","submarine", "parachute","skateboard","basketball","sailboat","swan","squirrel", "butterfly","lightning","mermaid","diamond","tent","mushroom","stitches", "tornado","panda","crocodile","rhinoceros","river" ] }; const WORD_HINTS = { "cat": "A small domesticated carnivorous mammal kept as a pet, known for purring and meowing.", "dog": "A domesticated carnivorous mammal, often called man's best friend, known for barking and loyalty.", "sun": "The star at the centre of our solar system that provides light and heat to Earth.", "fish": "A cold-blooded vertebrate animal that lives in water, breathes through gills, and has fins.", "tree": "A tall woody plant with a trunk, branches, and leaves that grows from the ground.", "house": "A building constructed for people to live in, typically with walls, a roof, windows, and doors.", "sock": "A knitted or woven covering for the foot, usually reaching to the ankle or calf.", "car": "A four-wheeled motor vehicle used for transporting passengers on roads.", "star": "A luminous sphere of plasma visible in the night sky; also a five-pointed geometric shape.", "hat": "A shaped covering for the head, worn for warmth, fashion, or protection.", "cake": "A sweet baked dessert made from flour, sugar, and eggs, often decorated and eaten at celebrations.", "bird": "A warm-blooded vertebrate animal with feathers, wings, a beak, and the ability to fly.", "canoe": "A lightweight narrow boat with pointed ends, propelled by paddles and used on rivers and lakes.", "moon": "The natural satellite of Earth that orbits our planet and reflects sunlight at night.", "flower": "The reproductive part of a plant, typically colourful with petals, used to attract pollinators.", "shoe": "A covering for the foot, usually made of leather or fabric, worn for protection or fashion.", "book": "A set of written or printed pages bound together, used for reading or reference.", "key": "A small metal instrument shaped to fit a lock and open or close it.", "door": "A hinged or sliding barrier used to open and close the entrance to a room or building.", "hand": "The end part of a person's arm beyond the wrist, including the palm and five fingers.", "apple": "A round fruit with red, green, or yellow skin and crisp white flesh, grown on apple trees.", "banana": "A long curved fruit with a yellow skin and soft sweet flesh, grown in tropical climates.", "cloud": "A visible mass of condensed water vapour floating in the sky, appearing white or grey.", "smiley face": "A cheerful facial expression with an upward-curving mouth, often drawn as a yellow circle.", "eye": "The organ of sight in humans and animals, used to detect light and form images.", "pizza": "A flat round bread base topped with tomato sauce, cheese, and various toppings, then baked.", "duck": "A waterbird with a broad flat bill, short legs, webbed feet, and a waddling walk.", "frog": "A small tailless amphibian with long back legs for jumping, smooth skin, and bulging eyes.", "lion": "A large wild cat with a tawny coat; the male has a thick mane around his head.", "bear": "A large heavy mammal with thick fur, a short tail, and strong claws.", "leaf": "A flattened green structure growing from a plant stem, used for photosynthesis.", "crown": "A circular ornamental headdress worn by a monarch as a symbol of authority.", "spoon": "A utensil with a shallow oval or round bowl at the end of a handle, used for eating or serving.", "train": "A series of connected carriages or wagons pulled along a railway track by a locomotive.", "bus": "A large motor vehicle that carries passengers along a fixed route and stops at set points.", "cup": "A small open container with a handle, used for drinking hot or cold beverages.", "fork": "A utensil with a handle and two or more prongs, used for lifting and eating food.", "clock": "A device that measures and displays the time, usually with hands on a circular face.", "bee": "A flying insect with yellow and black stripes that collects nectar and produces honey.", "spider": "An eight-legged arachnid that spins silk webs to catch insects for food.", "sea turtle": "A large marine reptile with a streamlined shell and flippers, found in warm ocean waters.", "pig": "A domesticated farm animal with a pink body, flat snout, and curly tail, raised for meat.", "cow": "A large farm animal kept for its milk and meat, known for its mooing sound.", "horse": "A large hoofed mammal used for riding, racing, and carrying loads.", "rainbow": "An arch of colours visible in the sky when sunlight is refracted through raindrops.", "mountain": "A large natural elevation of the earth's surface rising steeply above its surroundings.", "pencil": "A thin writing instrument with a graphite core encased in wood, used for drawing or writing.", "chair": "A separate seat for one person, with a back and usually four legs.", "table": "A piece of furniture with a flat top supported by legs, used for placing objects on.", "hot air balloon": "A large fabric bag filled with heated air that lifts a passenger basket beneath it.", "guitar": "A stringed musical instrument with a long neck and a hollow or solid body, played by plucking.", "castle": "A large medieval fortified building with thick stone walls, towers, and a moat.", "helicopter": "A flying vehicle with powered rotors on top that enable vertical takeoff and landing.", "umbrella": "A folding canopy on a stick that you hold over yourself to stay dry in the rain.", "penguin": "A flightless seabird with black-and-white plumage that walks upright and swims in cold oceans.", "computer": "An electronic device for storing and processing data, typically with a screen and keyboard.", "bridge": "A structure built to span a gap such as a river or road so people or vehicles can cross.", "compass": "A navigational instrument with a magnetised needle that always points north.", "lantern": "A transparent case enclosing a light source, designed to be carried or hung up.", "wheel": "A circular object that revolves on an axle, used to move vehicles and machinery.", "cactus": "A desert plant with a thick fleshy stem covered in sharp spines instead of leaves.", "dolphin": "An intelligent marine mammal with a streamlined body, a snout-like beak, and a curved dorsal fin.", "monkey": "A small to medium-sized primate with a long tail, living in trees in tropical regions.", "angel": "A celestial winged being believed to act as a messenger of God in various religions.", "dragon": "A mythical fire-breathing creature with wings, claws, and a scaly reptilian body.", "bicycle": "A two-wheeled human-powered vehicle steered by handlebars and propelled by pedals.", "lighthouse": "A tall tower with a bright flashing light at the top, built to warn ships of dangerous coastlines.", "binoculars": "A pair of small telescopes joined together, used for viewing distant objects with both eyes.", "camera": "A device for capturing photographs or video by recording light onto a sensor or film.", "necklace": "A decorative chain or string of jewels worn around the neck as an ornament.", "scissors": "A cutting tool with two blades joined at a pivot, operated by inserting fingers into the handles.", "hammer": "A hand tool with a heavy metal head fixed to a handle, used for driving nails into surfaces.", "ladder": "A portable frame with rungs between two long side pieces, used for climbing up and down.", "snowman": "A figure made by rolling and stacking balls of snow, often with a carrot for a nose.", "skull": "The bony framework of a head that encloses the brain, often used as a symbol of death.", "shark": "A large ocean fish with a streamlined body, sharp teeth, and a prominent dorsal fin.", "octopus": "A sea creature with a soft body, eight long flexible arms, and the ability to change colour.", "parrot": "A brightly coloured tropical bird with a curved beak that can imitate human speech.", "flamingo": "A tall wading bird with pink feathers, long spindly legs, and a curved downward-bending bill.", "windmill": "A structure with large rotating sails or blades turned by the wind to grind grain or pump water.", "campfire": "An outdoor fire built at a campsite, used for warmth, cooking, and light.", "submarine": "A watercraft capable of operating underwater, used in naval warfare and ocean research.", "parachute": "A large fabric canopy attached by cords to a person or object, slowing descent through air.", "skateboard": "A narrow board mounted on four wheels, ridden standing up and steered by shifting body weight.", "basketball": "A large round ball used in basketball; also the sport played by shooting it through a hoop.", "sailboat": "A boat propelled mainly by the wind acting on one or more sails.", "swan": "A large white waterbird with a long curved neck, known for its graceful swimming.", "squirrel": "A small bushy-tailed rodent that climbs trees and gathers nuts for food.", "butterfly": "An insect with large, often colourful wings that undergoes metamorphosis from a caterpillar.", "lightning": "A brilliant flash of light in the sky caused by electrical discharge during a thunderstorm.", "mermaid": "A mythical creature with the upper body of a woman and the tail of a fish.", "diamond": "A precious gemstone made of crystallised carbon, known for its brilliance and hardness.", "tent": "A portable shelter made of fabric stretched over poles, used for camping outdoors.", "mushroom": "A fungus with a round cap and a stalk, growing in soil or on wood, some edible and some poisonous.", "stitches": "A row of threads sewn into fabric or skin, used to close a wound or join material.", "tornado": "A violently rotating column of air extending from a thunderstorm cloud to the ground.", "panda": "A large bear-like mammal native to China with distinctive black-and-white markings.", "crocodile": "A large reptile with a long snout, powerful jaws, and armoured skin that lives near water.", "rhinoceros": "A large thick-skinned herbivorous mammal with one or two horns on its snout.", "river": "A large natural stream of water flowing through a channel towards a sea or lake." }; const AI_NAMES = ["Scribble", "DoodleBot", "Pixel", "Artie", "SkrawlBot"]; const AIDRAW_COLORS = ["#1a1a2e", "#ef4444", "#3b82f6", "#22c55e", "#eab308"]; const AIDRAW_SIZES = [4, 8, 16]; const ADJACENT_KEYS = { 'q': 'w', 'w': 'e', 'e': 'r', 'r': 't', 't': 'y', 'y': 'u', 'u': 'i', 'i': 'o', 'o': 'p', 'a': 's', 's': 'd', 'd': 'f', 'f': 'g', 'g': 'h', 'h': 'j', 'j': 'k', 'k': 'l', 'z': 'x', 'x': 'c', 'c': 'v', 'v': 'b', 'b': 'n', 'n': 'm', 'p': 'o', 'o': 'i', 'i': 'u', 'u': 'y', 'y': 't', 't': 'r', 'r': 'e', 'e': 'w', 'w': 'q', 'l': 'k', 'k': 'j', 'j': 'h', 'h': 'g', 'g': 'f', 'f': 'd', 'd': 's', 's': 'a', 'm': 'n', 'n': 'b', 'b': 'v', 'v': 'c', 'c': 'x', 'x': 'z' }; const CATEGORIES = { animals: ["cat","dog","fish","bird","lion","bear","duck","frog","sea turtle","pig","cow","horse","penguin","shark","octopus","parrot","flamingo","panda","crocodile","dolphin","monkey","butterfly","bee","spider","swan","rhinoceros","dragon","squirrel"], buildings: ["house","castle","bridge","lighthouse","windmill","tent"], nature: ["tree","flower","mountain","lightning","sun","moon","cloud","rainbow","river","mushroom","cactus","campfire","leaf"], vehicles: ["car","train","canoe","helicopter","bicycle","submarine","parachute","skateboard","sailboat","bus","hot air balloon"], food: ["pizza","cake","apple","banana"], persons: ["angel","mermaid","snowman"], objects: ["book","clock","umbrella","chair","table","pencil","key","door","shoe","hat","sock","crown","spoon","cup","fork","scissors","hammer","ladder","camera","necklace","lantern","wheel","compass","binoculars","guitar","diamond","computer","hand","eye","basketball","stitches"], abstracts: ["smiley face","star","skull","rainbow","tornado"] }; const rooms = {}; function getWords(count) { if (count === undefined) count = 3; const all = [...WORDS.easy, ...WORDS.medium, ...WORDS.hard]; return all.sort(() => Math.random() - 0.5).slice(0, count); } function getHint(word, revealedPositions) { return word.split("").map((c, i) => (c === " " || revealedPositions.has(i) ? c : "_")).join(" "); } function getRegexFromHint(hint) { const pattern = hint.replace(/ /g, ""); let regexStr = "^"; for (const ch of pattern) { regexStr += ch === "_" ? "[a-z]" : ch; } regexStr += "$"; return new RegExp(regexStr); } function addTypo(word) { if (word.length < 2) return word; const chars = word.split(""); const i = Math.floor(Math.random() * chars.length); if (Math.random() < 0.5 && i < chars.length - 1) { [chars[i], chars[i + 1]] = [chars[i + 1], chars[i]]; } else { const c = chars[i].toLowerCase(); if (ADJACENT_KEYS[c]) chars[i] = ADJACENT_KEYS[c]; } return chars.join(""); } function makeCloseGuess(word) { if (word.length < 3) return addTypo(word); const r = Math.random(); const chars = word.split(""); const i = Math.floor(Math.random() * chars.length); if (r < 0.33 && chars.length > 2) { chars.splice(i, 1); } else if (r < 0.66) { const adj = ADJACENT_KEYS[chars[i].toLowerCase()] || "s"; chars.splice(i, 0, adj); } else { const adj = ADJACENT_KEYS[chars[i].toLowerCase()] || "a"; chars[i] = adj; } return chars.join(""); } function generateAIGuess(currentWord, revealedPositions, guessedWords) { guessedWords = guessedWords || new Set(); const allWords = [...WORDS.easy, ...WORDS.medium, ...WORDS.hard]; let candidates = []; const hint = getHint(currentWord, revealedPositions); const hintFlat = hint.replace(/ /g, ""); if (hintFlat.includes("_") && revealedPositions.size > 0) { const regex = getRegexFromHint(hint); candidates = allWords.filter(w => regex.test(w) && !guessedWords.has(w)); } if (candidates.length === 0) { const len = currentWord.length; candidates = allWords.filter(w => w.length === len && !guessedWords.has(w)); } if (candidates.length === 0) { candidates = allWords.filter(w => !guessedWords.has(w)); } if (candidates.length === 0) { candidates = allWords; } let guess = candidates[Math.floor(Math.random() * candidates.length)]; if (!guessedWords.has(currentWord) && revealedPositions.size <= 1 && Math.random() < 0.03) { guess = makeCloseGuess(currentWord); } return guess; } function assignTeam(room, playerId) { const player = room.players.find(p => p.id === playerId); if (!player || !room.settings.teamsMode) return; const redCount = room.players.filter(p => p.team === "red" && p.id !== playerId).length; const blueCount = room.players.filter(p => p.team === "blue" && p.id !== playerId).length; player.team = redCount <= blueCount ? "red" : "blue"; } function balanceTeams(room) { if (!room.settings.teamsMode) return; const shuffled = [...room.players].sort(() => Math.random() - 0.5); const half = Math.ceil(shuffled.length / 2); shuffled.forEach((p, i) => { p.team = i < half ? "red" : "blue"; }); } function clearAITimers(room) { if (room.aiTimers) { room.aiTimers.forEach(t => clearTimeout(t)); room.aiTimers = []; } if (room.aiGuesses) { room.aiGuesses.clear(); } } function addAIPlayer(room) { if (room.players.find(p => p.isAI)) return; const usedNames = new Set(room.players.map(p => p.name)); const available = AI_NAMES.filter(n => !usedNames.has(n)); const name = available.length > 0 ? available[Math.floor(Math.random() * available.length)] : AI_NAMES[0]; const aiId = "ai_" + Math.random().toString(36).substr(2, 8); const player = { id: aiId, name, score: 0, isHost: false, uid: "ai_" + Math.random().toString(36).substr(2, 8), team: null, isAI: true, }; room.players.push(player); if (room.settings.teamsMode) { assignTeam(room, player.id); } } function getHumanCount(room) { return room.players.filter(p => !p.isAI).length; } function removeAIPlayer(room, roomId) { const aiPlayer = room.players.find(p => p.isAI); if (!aiPlayer) return; clearAITimers(room); if (room.aiGuesses) room.aiGuesses.delete(aiPlayer.id); const wasDrawer = room.players[room.drawerIndex]?.id === aiPlayer.id; room.guessedPlayers.delete(aiPlayer.id); room.players = room.players.filter(p => !p.isAI); if (wasDrawer && (room.phase === "drawing" || room.phase === "choosing")) { endRound(roomId, false); } } function pickColor(lastColor) { const pool = AIDRAW_COLORS.filter(c => c !== lastColor); return pool[Math.floor(Math.random() * pool.length)]; } function pickSize() { return AIDRAW_SIZES[Math.floor(Math.random() * AIDRAW_SIZES.length)]; } function wobble(v, amt) { return v + (Math.random() - 0.5) * amt; } function stroke(x1,y1,x2,y2,color,size) { return { x1: Math.round(x1), y1: Math.round(y1), x2: Math.round(x2), y2: Math.round(y2), color, size }; } // Draws a rough ellipse as multiple segments function roughEllipse(cx, cy, rx, ry, color, size, segs) { segs = segs || 10 + Math.floor(Math.random() * 4); const w = 4 + Math.random() * 6; const out = []; for (let i = 0; i < segs; i++) { const a1 = (i / segs) * Math.PI * 2; const a2 = ((i + 1) / segs) * Math.PI * 2; out.push(stroke( wobble(cx + Math.cos(a1) * rx, w), wobble(cy + Math.sin(a1) * ry, w), wobble(cx + Math.cos(a2) * rx, w), wobble(cy + Math.sin(a2) * ry, w), color, size )); } return out; } // Draws a rough rectangle function roughRect(x, y, w, h, color, size) { const wb = 3 + Math.random() * 4; return [ stroke(wobble(x, wb), wobble(y, wb), wobble(x + w, wb), wobble(y, wb), color, size), stroke(wobble(x + w, wb), wobble(y, wb), wobble(x + w, wb), wobble(y + h, wb), color, size), stroke(wobble(x + w, wb), wobble(y + h, wb), wobble(x, wb), wobble(y + h, wb), color, size), stroke(wobble(x, wb), wobble(y + h, wb), wobble(x, wb), wobble(y, wb), color, size), ]; } // Draws a rough line function roughLine(x1, y1, x2, y2, color, size) { const w = 2 + Math.random() * 3; return stroke(wobble(x1, w), wobble(y1, w), wobble(x2, w), wobble(y2, w), color, size); } // ── helpers for shading / hatching ───────────────────────────────── function hatchRect(x, y, w, h, color, density) { const out = []; const step = Math.max(6, Math.floor(w / density)); for (let i = step; i < w; i += step) { out.push(roughLine(x + i + wobble(0, 3), y + wobble(0, 2), x + i + wobble(0, 3), y + h + wobble(0, 2), color, pickSize())); } return out; } function hatchEllipse(cx, cy, rx, ry, color, density) { const out = []; const step = Math.max(6, Math.floor(rx * 2 / density)); for (let i = -rx + step; i < rx; i += step) { const hw = Math.sqrt(Math.max(0, 1 - (i / rx) ** 2)) * ry; out.push(roughLine(cx + i, cy - hw, cx + i, cy + hw, color, pickSize())); } return out; } // --- Sketch archetypes --- function sketchAnimal(word) { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2), c4 = pickColor(c3); const cx = 300 + Math.random() * 100, cy = 180 + Math.random() * 80; const strokes = []; // ── fish / shark / dolphin ────────────────────────────────────────────── if (word.includes("fish") || word.includes("shark") || word.includes("dolphin") || word.includes("whale")) { const isShark = word.includes("shark"); const bodyRx = isShark ? 95 : 78, bodyRy = word.includes("dolphin") ? 30 : 38; const tailX = cx - bodyRx + 5; strokes.push(...roughEllipse(cx, cy, bodyRx, bodyRy, c1, 8, 16)); strokes.push(...hatchEllipse(cx, cy, bodyRx - 6, bodyRy - 6, c2, 8)); const tDir = isShark ? 20 : 18; strokes.push(roughLine(tailX, cy - 4, cx - bodyRx - 28, cy - tDir - 8, c2, pickSize())); strokes.push(roughLine(tailX, cy + 4, cx - bodyRx - 28, cy + tDir + 8, c2, pickSize())); strokes.push(roughLine(cx - bodyRx - 28, cy - tDir - 8, cx - bodyRx - 28, cy + tDir + 8, c2, pickSize())); strokes.push(roughLine(cx - 5, cy - bodyRy + 5, cx + 5, cy - bodyRy - 18, c3, pickSize())); strokes.push(roughLine(cx + 5, cy - bodyRy - 18, cx + 18, cy - bodyRy + 5, c3, pickSize())); if (isShark) { strokes.push(roughLine(cx - 12, cy - bodyRy + 8, cx - 5, cy - bodyRy - 15, c3, pickSize())); strokes.push(roughLine(cx - 5, cy - bodyRy - 15, cx + 5, cy - bodyRy + 2, c3, pickSize())); } strokes.push(roughLine(cx + 10, cy + bodyRy - 5, cx + 28, cy + bodyRy + 18, c3, pickSize())); strokes.push(roughLine(cx + 28, cy + bodyRy + 18, cx + 5, cy + bodyRy + 8, c3, pickSize())); const mDir = isShark ? -1 : 1; strokes.push(roughLine(cx + bodyRx - 4, cy - 3, cx + bodyRx + 2, cy + mDir * 4, c1, pickSize())); strokes.push(...roughEllipse(cx + bodyRx * 0.45, cy - 7, 5, 7, c1, pickSize(), 8)); strokes.push(...roughEllipse(cx + bodyRx * 0.45 + 2, cy - 8, 2, 2, "#ffffff", pickSize(), 6)); strokes.push(roughLine(cx + 12, cy - 16, cx + 10, cy + 12, c3, pickSize())); strokes.push(roughLine(cx + 16, cy - 12, cx + 14, cy + 8, c3, pickSize())); for (let i = 0; i < 10; i++) { const sx = cx - bodyRx * 0.3 + i * 7 + wobble(0, 4); const sy = cy - 8 + wobble(0, 12); strokes.push(roughLine(sx, sy, sx + 5, sy + 3, c4, pickSize())); strokes.push(roughLine(sx + 5, sy + 3, sx + 1, sy + 6, c4, pickSize())); } for (let i = 0; i < 4; i++) { const bx = cx + bodyRx + 10 + i * 8 + wobble(0, 4); const by = cy - 12 - i * 12 + wobble(0, 5); strokes.push(...roughEllipse(bx, by, 3 + i * 0.5, 3 + i * 0.5, "#3b82f6", pickSize(), 6)); } return strokes; } // ── bird / parrot / penguin / flamingo / peacock ──────────────────────── if (word.includes("bird") || word.includes("parrot") || word.includes("penguin") || word.includes("flamingo") || word.includes("peacock")) { const isPenguin = word.includes("penguin"); const bodyR = isPenguin ? [38, 48] : [34, 44]; strokes.push(...roughEllipse(cx, cy, bodyR[0], bodyR[1], c1, 8, 14)); strokes.push(...hatchEllipse(cx, cy, bodyR[0] - 5, bodyR[1] - 5, c2, 6)); const headX = cx + 14, headY = cy - 42; strokes.push(...roughEllipse(headX, headY, 17, 17, c1, 8, 12)); strokes.push(roughLine(cx + 10, cy - 30, headX - 3, headY + 10, c1, pickSize())); strokes.push(roughLine(cx - 5, cy - 25, headX - 10, headY + 5, c1, pickSize())); const beakLen = word.includes("flamingo") ? 22 : 14; strokes.push(roughLine(headX + 16, headY - 4, headX + 16 + beakLen, headY + (word.includes("flamingo") ? 8 : 2), c3, pickSize())); strokes.push(roughLine(headX + 16 + beakLen, headY + (word.includes("flamingo") ? 8 : 2), headX + 14, headY + 6, c3, pickSize())); strokes.push(...roughEllipse(headX + 8, headY - 6, 4, 5, c1, pickSize(), 8)); strokes.push(...roughEllipse(headX + 9, headY - 7, 1.5, 1.5, "#ffffff", pickSize(), 6)); strokes.push(...roughEllipse(cx - 12, cy - 5, 24, 30, c3, 8, 14)); strokes.push(...hatchEllipse(cx - 12, cy - 5, 18, 24, c4, 5)); if (word.includes("peacock")) { for (let i = 0; i < 8; i++) { const a = (i / 8) * Math.PI + Math.PI * 0.1; strokes.push(roughLine(cx - 10, cy + 30, cx + Math.cos(a) * 70, cy + Math.sin(a) * 40 + 30, c3, pickSize())); const ex = cx + Math.cos(a) * 70, ey = cy + Math.sin(a) * 40 + 30; strokes.push(...roughEllipse(ex, ey, 6, 8, c4, pickSize(), 8)); } } else { strokes.push(roughLine(cx - 15, cy + 35, cx - 30, cy + 55, c3, pickSize())); strokes.push(roughLine(cx - 10, cy + 38, cx - 22, cy + 60, c3, pickSize())); strokes.push(roughLine(cx - 5, cy + 40, cx - 15, cy + 62, c3, pickSize())); strokes.push(roughLine(cx - 20, cy + 52, cx - 12, cy + 58, c3, pickSize())); } const legX = cx - 5, legX2 = cx + 5; strokes.push(roughLine(legX, cy + bodyR[1], legX - 3, cy + bodyR[1] + 28, c1, pickSize())); strokes.push(roughLine(legX2, cy + bodyR[1], legX2 + 3, cy + bodyR[1] + 28, c1, pickSize())); strokes.push(roughLine(legX - 8, cy + bodyR[1] + 28, legX + 2, cy + bodyR[1] + 28, c1, pickSize())); strokes.push(roughLine(legX - 3, cy + bodyR[1] + 28, legX - 5, cy + bodyR[1] + 32, c1, pickSize())); strokes.push(roughLine(legX2 - 2, cy + bodyR[1] + 28, legX2 + 8, cy + bodyR[1] + 28, c1, pickSize())); strokes.push(roughLine(legX2 + 3, cy + bodyR[1] + 28, legX2 + 5, cy + bodyR[1] + 32, c1, pickSize())); return strokes; } // ── turtle ────────────────────────────────────────────────────────────── if (word.includes("turtle")) { strokes.push(...roughEllipse(cx, cy, 55, 40, c1, 8, 16)); strokes.push(...hatchEllipse(cx, cy, 48, 34, c2, 7)); strokes.push(...roughEllipse(cx, cy, 35, 25, c3, pickSize(), 14)); strokes.push(...roughEllipse(cx, cy, 18, 13, c3, pickSize(), 10)); const hx = cx + 54, hy = cy - 5; strokes.push(...roughEllipse(hx, hy, 18, 14, c2, 8, 12)); strokes.push(...roughEllipse(hx + 6, hy - 3, 3, 4, c1, pickSize(), 8)); strokes.push(roughLine(hx + 14, hy + 4, hx + 18, hy + 6, c1, pickSize())); strokes.push(...roughEllipse(cx - 25, cy + 38, 12, 8, c2, pickSize(), 10)); strokes.push(...roughEllipse(cx + 22, cy + 38, 12, 8, c2, pickSize(), 10)); strokes.push(...roughEllipse(cx - 20, cy - 36, 10, 7, c2, pickSize(), 10)); strokes.push(...roughEllipse(cx + 18, cy - 36, 10, 7, c2, pickSize(), 10)); for (let i = 0; i < 3; i++) { strokes.push(roughLine(cx - 25 + i * 4, cy + 46, cx - 24 + i * 4, cy + 50, c1, pickSize())); strokes.push(roughLine(cx + 22 + i * 4, cy + 46, cx + 23 + i * 4, cy + 50, c1, pickSize())); } strokes.push(roughLine(cx - 52, cy + 5, cx - 62, cy + 10, c2, pickSize())); return strokes; } // ── butterfly ─────────────────────────────────────────────────────────── if (word.includes("butterfly")) { strokes.push(roughLine(cx, cy - 30, cx, cy + 30, c1, 10)); strokes.push(roughLine(cx - 3, cy - 5, cx + 3, cy - 5, c1, pickSize())); strokes.push(roughLine(cx - 3, cy + 5, cx + 3, cy + 5, c1, pickSize())); strokes.push(...roughEllipse(cx - 36, cy - 14, 32, 22, c2, 8, 16)); strokes.push(...hatchEllipse(cx - 36, cy - 14, 26, 16, c3, 6)); strokes.push(...roughEllipse(cx - 36, cy - 14, 14, 8, c4, pickSize(), 10)); strokes.push(...roughEllipse(cx - 24, cy + 12, 22, 16, c3, 8, 14)); strokes.push(...roughEllipse(cx - 24, cy + 12, 10, 8, c4, pickSize(), 10)); strokes.push(...roughEllipse(cx + 36, cy - 14, 32, 22, c2, 8, 16)); strokes.push(...hatchEllipse(cx + 36, cy - 14, 26, 16, c3, 6)); strokes.push(...roughEllipse(cx + 36, cy - 14, 14, 8, c4, pickSize(), 10)); strokes.push(...roughEllipse(cx + 24, cy + 12, 22, 16, c3, 8, 14)); strokes.push(...roughEllipse(cx + 24, cy + 12, 10, 8, c4, pickSize(), 10)); strokes.push(roughLine(cx - 3, cy - 30, cx - 12, cy - 48, c1, pickSize())); strokes.push(roughLine(cx + 3, cy - 30, cx + 12, cy - 48, c1, pickSize())); strokes.push(...roughEllipse(cx - 12, cy - 48, 2, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx + 12, cy - 48, 2, 3, c1, pickSize(), 6)); return strokes; } // ── spider / scorpion ─────────────────────────────────────────────────── if (word.includes("spider") || word.includes("scorpion")) { const isScorpion = word.includes("scorpion"); strokes.push(...roughEllipse(cx, cy, 22, 18, c1, 8, 14)); strokes.push(...roughEllipse(cx - 5, cy - 8, 12, 10, c2, pickSize(), 10)); strokes.push(...roughEllipse(cx - 8, cy - 12, 3, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx - 2, cy - 12, 3, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx + 4, cy - 12, 3, 3, c1, pickSize(), 6)); for (let i = 0; i < 4; i++) { const a = (i / 4 - 0.5) * Math.PI; const len = isScorpion ? 38 : 52; strokes.push(roughLine(cx, cy, cx + Math.cos(a) * len, cy + Math.sin(a) * 25, c2, pickSize())); strokes.push(roughLine(cx, cy, cx - Math.cos(a) * len, cy + Math.sin(a) * 25, c2, pickSize())); const mx = cx + Math.cos(a) * len * 0.6, my = cy + Math.sin(a) * 25 * 0.6; strokes.push(roughLine(cx + Math.cos(a) * len * 0.5, cy + Math.sin(a) * 25 * 0.5, mx + 8, my + 4, c2, pickSize())); const mx2 = cx - Math.cos(a) * len * 0.6, my2 = cy + Math.sin(a) * 25 * 0.6; strokes.push(roughLine(cx - Math.cos(a) * len * 0.5, cy + Math.sin(a) * 25 * 0.5, mx2 - 8, my2 + 4, c2, pickSize())); } if (isScorpion) { strokes.push(roughLine(cx + 20, cy - 8, cx + 45, cy - 25, c2, pickSize())); strokes.push(roughLine(cx + 45, cy - 25, cx + 52, cy - 30, c2, pickSize())); } return strokes; } // ── octopus ───────────────────────────────────────────────────────────── if (word.includes("octopus")) { strokes.push(...roughEllipse(cx, cy, 48, 38, c1, 8, 16)); strokes.push(...hatchEllipse(cx, cy, 40, 30, c2, 6)); strokes.push(...roughEllipse(cx - 14, cy - 10, 6, 8, c1, pickSize(), 8)); strokes.push(...roughEllipse(cx + 14, cy - 10, 6, 8, c1, pickSize(), 8)); strokes.push(...roughEllipse(cx - 13, cy - 11, 2, 3, "#ffffff", pickSize(), 6)); strokes.push(...roughEllipse(cx + 15, cy - 11, 2, 3, "#ffffff", pickSize(), 6)); for (let i = 0; i < 8; i++) { const a = (i / 8) * Math.PI * 2; const startX = cx + Math.cos(a) * 38, startY = cy + Math.sin(a) * 28; const endX = cx + Math.cos(a) * 65, endY = cy + Math.sin(a) * 55; strokes.push(roughLine(startX, startY, (startX + endX) / 2 + wobble(0, 12), (startY + endY) / 2 + wobble(0, 8), c2, pickSize())); strokes.push(roughLine((startX + endX) / 2 + wobble(0, 12), (startY + endY) / 2 + wobble(0, 8), endX, endY, c2, pickSize())); const sx = (startX + endX) / 2 + wobble(0, 8), sy = (startY + endY) / 2 + wobble(0, 6); strokes.push(...roughEllipse(sx, sy, 2, 3, c3, pickSize(), 6)); } return strokes; } // ── caterpillar ───────────────────────────────────────────────────────── if (word.includes("caterpillar")) { for (let i = 0; i < 6; i++) { const sx = cx - 50 + i * 18, sy = cy + wobble(0, 10); strokes.push(...roughEllipse(sx, sy, 14, 16, i % 2 === 0 ? c1 : c2, pickSize(), 12)); strokes.push(...hatchEllipse(sx, sy, 8, 10, c3, 4)); strokes.push(roughLine(sx - 4, sy + 16, sx - 6, sy + 22, c1, pickSize())); strokes.push(roughLine(sx + 4, sy + 16, sx + 6, sy + 22, c1, pickSize())); } strokes.push(...roughEllipse(cx + 58, cy + 2, 16, 16, c1, pickSize(), 12)); strokes.push(...roughEllipse(cx + 64, cy, 3, 3, c1, pickSize(), 6)); strokes.push(roughLine(cx + 54, cy - 12, cx + 50, cy - 24, c2, pickSize())); strokes.push(roughLine(cx + 60, cy - 12, cx + 62, cy - 24, c2, pickSize())); strokes.push(...roughEllipse(cx + 50, cy - 24, 2, 3, c2, pickSize(), 6)); strokes.push(...roughEllipse(cx + 62, cy - 24, 2, 3, c2, pickSize(), 6)); return strokes; } // ── snake / dragon ────────────────────────────────────────────────────── if (word.includes("snake") || word.includes("dragon")) { const isDragon = word.includes("dragon"); for (let i = 0; i < 6; i++) { const sx = cx - 65 + i * 22, sy = cy + wobble(0, 25) + Math.sin(i * 0.8) * 18; strokes.push(...roughEllipse(sx, sy, 14, 10, c1, pickSize(), 10)); } strokes.push(...roughEllipse(cx + 65, cy + 5, 12, 10, c1, pickSize(), 10)); strokes.push(...roughEllipse(cx + 70, cy + 3, 4, 4, c1, pickSize(), 8)); strokes.push(roughLine(cx + 75, cy + 5, cx + 82, cy + 2, c2, pickSize())); strokes.push(roughLine(cx + 75, cy + 5, cx + 82, cy + 8, c2, pickSize())); for (let i = 0; i < 8; i++) { const sx2 = cx - 50 + i * 14, sy2 = cy - 5 + Math.sin(i) * 15 + wobble(0, 8); strokes.push(roughLine(sx2, sy2, sx2 + 4, sy2 + 5, c2, pickSize())); strokes.push(roughLine(sx2 + 4, sy2 + 5, sx2 + 8, sy2 + 2, c2, pickSize())); } if (isDragon) { strokes.push(roughLine(cx - 40, cy - 10, cx - 50, cy - 40, c3, pickSize())); strokes.push(roughLine(cx - 50, cy - 40, cx - 30, cy - 50, c3, pickSize())); strokes.push(roughLine(cx - 30, cy - 50, cx - 20, cy - 35, c3, pickSize())); strokes.push(roughLine(cx - 20, cy - 35, cx - 10, cy - 45, c3, pickSize())); strokes.push(roughLine(cx - 10, cy - 45, cx + 5, cy - 25, c3, pickSize())); strokes.push(roughLine(cx + 76, cy + 6, cx + 88, cy - 5, "#ef4444", pickSize())); strokes.push(roughLine(cx + 76, cy + 6, cx + 90, cy + 2, "#eab308", pickSize())); strokes.push(roughLine(cx + 76, cy + 6, cx + 85, cy + 12, "#ef4444", pickSize())); } return strokes; } // ── default mammal ────────────────────────────────────────────────────── const bodyRx = 75 + Math.random() * 25, bodyRy = 42 + Math.random() * 18; const headR = 28 + Math.random() * 8; const headOffX = bodyRx + headR - 12, headX = cx + headOffX, headY = cy - bodyRy + 12; strokes.push(...roughEllipse(cx, cy, bodyRx, bodyRy, c1, 8, 16)); strokes.push(...hatchEllipse(cx, cy, bodyRx - 5, bodyRy - 5, c2, 6)); strokes.push(...roughEllipse(headX, headY, headR, headR * (word.includes("bear") ? 1.1 : 0.95), c1, 8, 14)); strokes.push(...hatchEllipse(headX, headY, headR - 4, headR - 4, c2, 5)); const snoutR = word.includes("dog") || word.includes("horse") ? 6 : 0; strokes.push(...roughEllipse(headX + headR * 0.5, headY + 4, headR * 0.5 + snoutR * 0.3, headR * 0.35, c1, pickSize(), 10)); strokes.push(...roughEllipse(headX + headR * 0.8, headY + 3, 4, 3, c1, pickSize(), 8)); strokes.push(roughLine(headX + headR * 0.6, headY + 6, headX + headR * 0.8, headY + 8, c1, pickSize())); strokes.push(roughLine(headX - headR * 0.4, headY - headR * 0.6, headX - headR * 0.3, headY - headR, c2, pickSize())); strokes.push(roughLine(headX - headR * 0.3, headY - headR, headX + headR * 0.1, headY - headR * 0.7, c2, pickSize())); strokes.push(roughLine(headX + headR * 0.1, headY - headR * 0.7, headX + headR * 0.4, headY - headR, c2, pickSize())); if (!word.includes("bear")) { strokes.push(roughLine(headX + headR * 0.4, headY - headR, headX + headR * 0.6, headY - headR * 0.6, c2, pickSize())); } strokes.push(...roughEllipse(headX + headR * 0.15, headY - headR * 0.35, 3, 4, c1, pickSize(), 8)); strokes.push(...roughEllipse(headX + headR * 0.55, headY - headR * 0.35, 3, 4, c1, pickSize(), 8)); strokes.push(...roughEllipse(headX + headR * 0.15 + 1, headY - headR * 0.35 - 1, 1, 1, "#ffffff", pickSize(), 6)); strokes.push(...roughEllipse(headX + headR * 0.55 + 1, headY - headR * 0.35 - 1, 1, 1, "#ffffff", pickSize(), 6)); if (word.includes("cat") || word.includes("dog") || word.includes("mouse")) { for (let i = 0; i < 4; i++) { const wx = headX + headR * 0.5, wy = headY + 2 + i * 2; strokes.push(roughLine(wx, wy, wx + 30 + i * 4, wy - 2 + wobble(0, 3), c3, pickSize())); strokes.push(roughLine(wx, wy, wx - 30 - i * 4, wy - 2 + wobble(0, 3), c3, pickSize())); } } const legCount = word.includes("kangaroo") ? 2 : 4; for (let i = 0; i < legCount; i++) { const lx = cx - bodyRx + 18 + i * (bodyRx * 2 - 36) / Math.max(1, legCount - 1); const legH = 38 + Math.random() * 15; strokes.push(roughLine(lx, cy + bodyRy, lx + wobble(0, 8), cy + bodyRy + legH, c2, pickSize())); strokes.push(roughLine(lx + wobble(0, 8), cy + bodyRy + legH - 5, lx + wobble(0, 10), cy + bodyRy + legH, c2, pickSize())); if (i % 2 === 0) strokes.push(roughLine(lx - 6, cy + bodyRy + legH, lx + 6, cy + bodyRy + legH, c1, pickSize())); } const tailLen = 35 + Math.random() * 20; strokes.push(roughLine(cx - bodyRx, cy, cx - bodyRx - tailLen, cy - 15 - Math.random() * 20, c2, pickSize())); strokes.push(roughLine(cx - bodyRx - tailLen, cy - 15 - Math.random() * 20, cx - bodyRx - tailLen - 8, cy - 10 - Math.random() * 10, c2, pickSize())); for (let i = 0; i < 6; i++) { const fx = cx - bodyRx + 15 + i * (bodyRx * 2 - 30) / 5 + wobble(0, 5); const fy = cy - bodyRy + 5 + Math.random() * bodyRy * 0.6; strokes.push(roughLine(fx, fy, fx + 5, fy - 3, c3, pickSize())); } return strokes; } function sketchBuilding(word) { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2), c4 = pickColor(c3); const cx = 250 + Math.random() * 100, cy = 140 + Math.random() * 40; const strokes = []; // ── house ──────────────────────────────────────────────────────────────── if (word.includes("house")) { const bw = 180 + Math.random() * 40, bh = 160 + Math.random() * 30; const roofH = 60 + Math.random() * 20; strokes.push(...roughRect(cx - bw/2, cy + roofH, bw, bh, c1, 8, 4)); strokes.push(...hatchRect(cx - bw/2 + 4, cy + roofH + 4, bw - 8, bh - 8, c2, 6)); strokes.push(roughLine(cx - bw/2 - 10, cy + roofH, cx, cy, c3, 8)); strokes.push(roughLine(cx, cy, cx + bw/2 + 10, cy + roofH, c3, 8)); const chx = cx, chy = cy - 10; strokes.push(roughLine(chx - 8, chy, chx + 8, chy, c3, pickSize())); strokes.push(roughLine(chx, chy - 10, chx, chy + 2, c3, pickSize())); strokes.push(...roughRect(cx - 18, cy + roofH + bh - 50, 36, 50, c3, pickSize(), 4)); strokes.push(...roughEllipse(cx, cy + roofH + bh - 25, 3, 3, "#eab308", pickSize(), 6)); strokes.push(...roughRect(cx - bw/2 + 20, cy + roofH + 30, 35, 35, c4, pickSize(), 4)); strokes.push(...hatchRect(cx - bw/2 + 23, cy + roofH + 33, 29, 29, c2, 4)); strokes.push(roughLine(cx - bw/2 + 37, cy + roofH + 30, cx - bw/2 + 37, cy + roofH + 65, c3, pickSize())); strokes.push(roughLine(cx - bw/2 + 20, cy + roofH + 47, cx - bw/2 + 55, cy + roofH + 47, c3, pickSize())); strokes.push(...roughRect(cx + bw/2 - 55, cy + roofH + 30, 35, 35, c4, pickSize(), 4)); strokes.push(...hatchRect(cx + bw/2 - 52, cy + roofH + 33, 29, 29, c2, 4)); strokes.push(roughLine(cx + bw/2 - 37, cy + roofH + 30, cx + bw/2 - 37, cy + roofH + 65, c3, pickSize())); strokes.push(roughLine(cx + bw/2 - 55, cy + roofH + 47, cx + bw/2 - 20, cy + roofH + 47, c3, pickSize())); strokes.push(roughLine(cx - 25, cy + roofH + bh - 10, cx + 25, cy + roofH + bh - 10, c3, pickSize())); for (let i = 0; i < 8; i++) { strokes.push(roughLine(cx - bw/2 + 10 + i * 10, cy + roofH + 5, cx - bw/2 + 10 + i * 10, cy + roofH + bh - 5, c4, pickSize())); } strokes.push(roughLine(cx - bw/2, cy + roofH + bh, cx + bw/2, cy + roofH + bh, c1, pickSize())); strokes.push(roughLine(cx - bw/2, cy + roofH, cx - bw/2, cy + roofH + bh, c1, pickSize())); strokes.push(roughLine(cx + bw/2, cy + roofH, cx + bw/2, cy + roofH + bh, c1, pickSize())); return strokes; } // ── castle ─────────────────────────────────────────────────────────────── if (word.includes("castle")) { strokes.push(...roughRect(cx - 70, cy + 30, 140, 150, c1, 8, 4)); strokes.push(...hatchRect(cx - 66, cy + 34, 132, 146, c2, 6)); for (let i = 0; i < 5; i++) { const tx = cx - 70 + i * 35; strokes.push(...roughRect(tx - 10, cy, 20, 35, c2, pickSize(), 4)); strokes.push(...hatchRect(tx - 7, cy + 3, 14, 29, c3, 4)); strokes.push(roughLine(tx - 10, cy + 8, tx + 10, cy + 8, c3, pickSize())); strokes.push(roughLine(tx - 10, cy + 18, tx + 10, cy + 18, c3, pickSize())); } strokes.push(...roughRect(cx - 22, cy + 130, 44, 50, c3, pickSize(), 4)); strokes.push(...roughEllipse(cx, cy + 155, 4, 4, "#eab308", pickSize(), 6)); strokes.push(...roughRect(cx - 50, cy + 55, 28, 30, c4, pickSize(), 4)); strokes.push(roughLine(cx - 36, cy + 55, cx - 36, cy + 85, c3, pickSize())); strokes.push(roughLine(cx - 50, cy + 70, cx - 22, cy + 70, c3, pickSize())); strokes.push(...roughRect(cx + 22, cy + 55, 28, 30, c4, pickSize(), 4)); strokes.push(roughLine(cx + 36, cy + 55, cx + 36, cy + 85, c3, pickSize())); strokes.push(roughLine(cx + 22, cy + 70, cx + 50, cy + 70, c3, pickSize())); for (let i = 0; i < 4; i++) { strokes.push(roughLine(cx - 70 + i * 10, cy + 180, cx - 70 + i * 10 + 5, cy + 190, c1, pickSize())); } // flags strokes.push(roughLine(cx - 48, cy, cx - 48, cy - 20, c3, pickSize())); strokes.push(roughLine(cx - 48, cy - 20, cx - 32, cy - 15, "#ef4444", pickSize())); strokes.push(roughLine(cx - 32, cy - 15, cx - 48, cy - 10, "#ef4444", pickSize())); strokes.push(roughLine(cx + 48, cy, cx + 48, cy - 20, c3, pickSize())); strokes.push(roughLine(cx + 48, cy - 20, cx + 32, cy - 15, "#3b82f6", pickSize())); strokes.push(roughLine(cx + 32, cy - 15, cx + 48, cy - 10, "#3b82f6", pickSize())); strokes.push(roughLine(cx, cy, cx, cy - 25, c3, pickSize())); strokes.push(roughLine(cx, cy - 25, cx - 16, cy - 20, "#ef4444", pickSize())); strokes.push(roughLine(cx - 16, cy - 20, cx, cy - 15, "#ef4444", pickSize())); strokes.push(roughLine(cx, cy - 25, cx + 16, cy - 20, "#3b82f6", pickSize())); strokes.push(roughLine(cx + 16, cy - 20, cx, cy - 15, "#3b82f6", pickSize())); return strokes; } // ── bridge ─────────────────────────────────────────────────────────────── if (word.includes("bridge")) { const archW = 140, archH = 70; const bx = cx - archW / 2, by = cy + 20; for (let i = 0; i < 14; i++) { const a1 = Math.PI + (i / 14) * Math.PI; const a2 = Math.PI + ((i + 1) / 14) * Math.PI; strokes.push(stroke( wobble(bx + Math.cos(a1) * archW/2 + archW/2, 3), wobble(by + Math.sin(a1) * archH, 3), wobble(bx + Math.cos(a2) * archW/2 + archW/2, 3), wobble(by + Math.sin(a2) * archH, 3), c1, 8 )); } strokes.push(roughLine(bx, by, bx, by + 60, c2, pickSize())); strokes.push(roughLine(bx + archW, by, bx + archW, by + 60, c2, pickSize())); strokes.push(roughLine(bx - 10, by + 60, bx + archW + 10, by + 60, c1, pickSize())); strokes.push(roughLine(bx - 10, by, bx + archW + 10, by, c1, pickSize())); strokes.push(roughLine(bx - 10, by - 8, bx + archW + 10, by - 8, c3, pickSize())); for (let i = 0; i < 8; i++) { const px = bx + 8 + i * (archW - 16) / 7; strokes.push(roughLine(px, by - 8, px, by, c3, pickSize())); } // water under for (let i = 0; i < 6; i++) { strokes.push(roughLine(bx - 15 + i * 20, by + 65 + i * 3, bx + 30 + i * 18, by + 70 + i * 4, "#3b82f6", pickSize())); } // road markings for (let i = 0; i < 6; i++) { strokes.push(roughLine(bx + 5 + i * 22, by - 4, bx + 15 + i * 22, by - 4, "#eab308", pickSize())); } return strokes; } // ── lighthouse ─────────────────────────────────────────────────────────── if (word.includes("lighthouse")) { strokes.push(...roughRect(cx - 22, cy, 44, 180, c1, 8, 4)); strokes.push(...hatchRect(cx - 18, cy + 4, 36, 172, c2, 6)); strokes.push(...roughRect(cx - 32, cy + 170, 64, 20, c1, 8, 4)); strokes.push(...roughRect(cx - 28, cy + 174, 56, 12, c3, pickSize(), 4)); // stripes for (let i = 0; i < 5; i++) { strokes.push(roughLine(cx - 22, cy + 20 + i * 30, cx + 22, cy + 20 + i * 30, "#ef4444", pickSize())); } // top light strokes.push(...roughRect(cx - 10, cy - 20, 20, 24, c1, pickSize(), 4)); strokes.push(...roughEllipse(cx, cy - 24, 10, 6, "#eab308", 8, 12)); strokes.push(...roughEllipse(cx, cy - 24, 6, 4, "#fef08a", pickSize(), 10)); // beams strokes.push(roughLine(cx + 10, cy - 24, cx + 60, cy - 38, "#eab308", pickSize())); strokes.push(roughLine(cx + 10, cy - 22, cx + 65, cy - 28, "#eab308", pickSize())); strokes.push(roughLine(cx - 10, cy - 24, cx - 60, cy - 38, "#eab308", pickSize())); strokes.push(roughLine(cx - 10, cy - 22, cx - 65, cy - 28, "#eab308", pickSize())); // window strokes.push(...roughEllipse(cx, cy + 60, 10, 14, c4, pickSize(), 10)); strokes.push(roughLine(cx, cy + 46, cx, cy + 74, c3, pickSize())); strokes.push(roughLine(cx - 10, cy + 60, cx + 10, cy + 60, c3, pickSize())); // door strokes.push(...roughRect(cx - 10, cy + 150, 20, 30, c3, pickSize(), 4)); strokes.push(...roughEllipse(cx, cy + 165, 2, 2, "#eab308", pickSize(), 6)); strokes.push(roughLine(cx - 10, cy + 155, cx + 10, cy + 155, c4, pickSize())); strokes.push(roughLine(cx - 10, cy + 165, cx + 10, cy + 165, c4, pickSize())); // rocks at base for (let i = 0; i < 3; i++) { strokes.push(...roughEllipse(cx - 40 + i * 40, cy + 180, 20 + i * 4, 10 + i * 2, "#6b7280", pickSize(), 8)); } return strokes; } // ── skyscraper ─────────────────────────────────────────────────────────── if (word.includes("skyscraper")) { strokes.push(...roughRect(cx - 30, cy - 40, 60, 230, c1, 8, 4)); strokes.push(...hatchRect(cx - 26, cy - 36, 52, 222, c2, 6)); // antenna strokes.push(roughLine(cx, cy - 40, cx, cy - 65, c1, pickSize())); strokes.push(roughLine(cx, cy - 65, cx - 6, cy - 58, "#ef4444", pickSize())); strokes.push(roughLine(cx, cy - 65, cx + 6, cy - 58, "#ef4444", pickSize())); // many windows for (let row = 0; row < 12; row++) { for (let col = 0; col < 3; col++) { const wx = cx - 22 + col * 22; const wy = cy - 30 + row * 17; strokes.push(...roughRect(wx, wy, 12, 10, c3, pickSize(), 4)); strokes.push(roughLine(wx + 6, wy, wx + 6, wy + 10, c4, pickSize())); strokes.push(roughLine(wx, wy + 5, wx + 12, wy + 5, c4, pickSize())); } } // entrance strokes.push(...roughRect(cx - 14, cy + 170, 28, 20, c3, pickSize(), 4)); strokes.push(...roughEllipse(cx, cy + 180, 2, 2, "#eab308", pickSize(), 6)); // ground line strokes.push(roughLine(cx - 50, cy + 190, cx + 50, cy + 190, c1, pickSize())); return strokes; } // ── windmill ───────────────────────────────────────────────────────────── if (word.includes("windmill")) { strokes.push(...roughRect(cx - 18, cy + 20, 36, 120, c1, 8, 4)); strokes.push(...hatchRect(cx - 14, cy + 24, 28, 112, c2, 5)); // conical roof strokes.push(roughLine(cx - 28, cy + 20, cx, cy - 25, c3, 8)); strokes.push(roughLine(cx, cy - 25, cx + 28, cy + 20, c3, 8)); // window strokes.push(...roughEllipse(cx, cy + 60, 10, 14, c4, pickSize(), 10)); strokes.push(roughLine(cx, cy + 46, cx, cy + 74, c3, pickSize())); strokes.push(roughLine(cx - 10, cy + 60, cx + 10, cy + 60, c3, pickSize())); // door strokes.push(...roughRect(cx - 10, cy + 110, 20, 30, c3, pickSize(), 4)); // blades for (let i = 0; i < 4; i++) { const a = (i / 4) * Math.PI * 2; const bx = cx + Math.cos(a) * 50, by = cy - 5 + Math.sin(a) * 50; strokes.push(roughLine(cx, cy - 5, bx, by, c3, 6)); strokes.push(roughLine(bx, by, bx + Math.cos(a) * 18, by + Math.sin(a) * 18, c1, pickSize())); strokes.push(roughLine(bx, by, bx + Math.cos(a + 0.15) * 10, by + Math.sin(a + 0.15) * 10, c2, pickSize())); } // hub strokes.push(...roughEllipse(cx, cy - 5, 6, 6, "#eab308", pickSize(), 8)); return strokes; } // ── igloo ──────────────────────────────────────────────────────────────── if (word.includes("igloo")) { strokes.push(...roughEllipse(cx, cy + 70, 70, 55, c1, 8, 16)); strokes.push(...hatchEllipse(cx, cy + 70, 64, 49, c2, 6)); strokes.push(...roughEllipse(cx, cy, 60, 50, c1, 8, 16)); strokes.push(...hatchEllipse(cx, cy, 54, 44, c2, 6)); // brick lines for (let i = 0; i < 6; i++) { const yy = cy + 8 + i * 14; strokes.push(roughLine(cx - 55, yy, cx + 55, yy, c3, pickSize())); } for (let i = 0; i < 4; i++) { const yy = cy + 8 + i * 14; for (let j = 0; j < 5; j++) { const xx = cx - 48 + j * 24 + (i % 2) * 12; strokes.push(roughLine(xx, yy, xx, yy + 14, c3, pickSize())); } } // entrance tunnel strokes.push(...roughEllipse(cx + 55, cy + 85, 20, 18, c3, pickSize(), 12)); strokes.push(...roughEllipse(cx + 55, cy + 85, 12, 10, c1, pickSize(), 10)); // snow on top strokes.push(...roughEllipse(cx, cy - 45, 40, 10, "#ffffff", pickSize(), 10)); strokes.push(...roughEllipse(cx + 30, cy - 40, 18, 6, "#ffffff", pickSize(), 8)); strokes.push(...roughEllipse(cx - 20, cy - 42, 20, 7, "#ffffff", pickSize(), 8)); return strokes; } // ── tent ───────────────────────────────────────────────────────────────── if (word.includes("tent")) { strokes.push(roughLine(cx, cy - 90, cx - 85, cy + 50, c1, 8)); strokes.push(roughLine(cx, cy - 90, cx + 85, cy + 50, c1, 8)); strokes.push(roughLine(cx - 85, cy + 50, cx + 85, cy + 50, c1, 8)); strokes.push(...hatchRect(cx - 80, cy + 10, 160, 36, c2, 5)); // entrance strokes.push(roughLine(cx - 22, cy + 50, cx - 22, cy - 20, c3, pickSize())); strokes.push(roughLine(cx + 22, cy + 50, cx + 22, cy - 20, c3, pickSize())); strokes.push(roughLine(cx - 22, cy - 20, cx + 22, cy - 20, c3, pickSize())); // pole strokes.push(roughLine(cx, cy - 90, cx, cy + 50, c3, pickSize())); // flag strokes.push(roughLine(cx, cy - 90, cx, cy - 110, c3, pickSize())); strokes.push(roughLine(cx, cy - 110, cx + 20, cy - 105, "#ef4444", pickSize())); strokes.push(roughLine(cx + 20, cy - 105, cx, cy - 100, "#ef4444", pickSize())); // ropes strokes.push(roughLine(cx - 85, cy + 50, cx - 110, cy + 55, c3, pickSize())); strokes.push(roughLine(cx + 85, cy + 50, cx + 110, cy + 55, c3, pickSize())); strokes.push(roughLine(cx - 60, cy - 10, cx - 95, cy + 10, c3, pickSize())); strokes.push(roughLine(cx + 60, cy - 10, cx + 95, cy + 10, c3, pickSize())); // stripes on tent sides for (let i = 0; i < 4; i++) { const sx = cx - 60 + i * 30; const topY = cy - 90 + (sx - cx + 85) * 90 / 170; const botY = cy + 50; strokes.push(roughLine(sx, topY + 20, sx, botY - 10, c4, pickSize())); } return strokes; } // ── default building ───────────────────────────────────────────────────── const bw = 140 + Math.random() * 60, bh = 170 + Math.random() * 40; strokes.push(...roughRect(cx - bw/2, cy, bw, bh, c1, 8, 4)); strokes.push(...hatchRect(cx - bw/2 + 4, cy + 4, bw - 8, bh - 8, c2, 6)); const roofH = 50 + Math.random() * 25; strokes.push(roughLine(cx - bw/2 - 8, cy, cx, cy - roofH, c3, 8)); strokes.push(roughLine(cx, cy - roofH, cx + bw/2 + 8, cy, c3, 8)); strokes.push(...roughRect(cx - 16, cy + bh - 48, 32, 48, c3, pickSize(), 4)); strokes.push(...roughEllipse(cx, cy + bh - 24, 2.5, 2.5, "#eab308", pickSize(), 6)); strokes.push(...roughRect(cx - bw/2 + 18, cy + 25, 30, 30, c4, pickSize(), 4)); strokes.push(...roughRect(cx + bw/2 - 48, cy + 25, 30, 30, c4, pickSize(), 4)); strokes.push(roughLine(cx - bw/2 + 33, cy + 25, cx - bw/2 + 33, cy + 55, c3, pickSize())); strokes.push(roughLine(cx - bw/2 + 18, cy + 40, cx - bw/2 + 48, cy + 40, c3, pickSize())); strokes.push(roughLine(cx + bw/2 - 33, cy + 25, cx + bw/2 - 33, cy + 55, c3, pickSize())); strokes.push(roughLine(cx + bw/2 - 48, cy + 40, cx + bw/2 - 18, cy + 40, c3, pickSize())); return strokes; } function sketchNature(word) { const c1 = AIDRAW_COLORS[0], c2 = "#22c55e", c3 = pickColor(c2), c4 = pickColor(c3); const cx = 250 + Math.random() * 150, ground = 380; const strokes = []; // ── waterfall ──────────────────────────────────────────────────────────── if (word.includes("waterfall")) { strokes.push(roughLine(cx - 30, 40, cx - 30, ground - 20, c1, 8)); strokes.push(roughLine(cx + 30, 40, cx + 30, ground - 20, c1, 8)); strokes.push(roughLine(cx - 60, 40, cx - 60, ground - 20, c2, 8)); strokes.push(roughLine(cx + 60, 40, cx + 60, ground - 20, c2, 8)); for (let i = 0; i < 10; i++) { const x1 = cx - 25 + i * 5; const y1 = 60 + i * 30; const x2 = cx + 25 - i * 4; const y2 = 90 + i * 30; strokes.push(roughLine(wobble(x1, 4), wobble(y1, 6), wobble(x2, 4), wobble(y2, 6), "#3b82f6", pickSize())); } // splash at bottom for (let i = 0; i < 6; i++) { strokes.push(...roughEllipse(cx + wobble(0, 30), ground - 10 + i * 8, 15 + i * 3, 6, "#93c5fd", pickSize(), 8)); } strokes.push(roughLine(cx - 80, ground, cx + 80, ground, c1, 6)); // rocks for (let i = 0; i < 4; i++) { strokes.push(...roughEllipse(cx - 50 + i * 30, ground - 8, 18, 8, "#6b7280", pickSize(), 8)); } return strokes; } // ── cactus ─────────────────────────────────────────────────────────────── if (word.includes("cactus")) { strokes.push(...roughRect(cx - 14, ground - 120, 28, 120, c1, 8, 4)); strokes.push(...hatchRect(cx - 10, ground - 116, 20, 112, c2, 5)); strokes.push(...roughRect(cx - 42, ground - 80, 30, 20, c1, pickSize(), 4)); strokes.push(...roughRect(cx + 12, ground - 70, 30, 20, c1, pickSize(), 4)); // spines for (let i = 0; i < 8; i++) { const sx = cx - 14 + (i % 2) * 28; const sy = ground - 110 + i * 13; strokes.push(roughLine(sx, sy, sx + (i % 2 === 0 ? -10 : 10), sy + wobble(0, 4), c3, pickSize())); strokes.push(roughLine(sx, sy + 2, sx + (i % 2 === 0 ? -8 : 8), sy + 6, c3, pickSize())); } // flower on top strokes.push(...roughEllipse(cx, ground - 122, 8, 6, "#ef4444", pickSize(), 10)); strokes.push(...roughEllipse(cx + 5, ground - 125, 4, 4, "#eab308", pickSize(), 8)); // ground strokes.push(roughLine(cx - 80, ground, cx + 80, ground, c1, 6)); strokes.push(roughLine(cx - 60, ground + 3, cx + 60, ground + 3, c2, pickSize())); return strokes; } // ── snowflake ──────────────────────────────────────────────────────────── if (word.includes("snowflake")) { const sx = 350, sy = 200; for (let i = 0; i < 6; i++) { const a = (i / 6) * Math.PI * 2; strokes.push(roughLine(sx, sy, sx + Math.cos(a) * 55, sy + Math.sin(a) * 55, c1, pickSize())); const mx = sx + Math.cos(a) * 28, my = sy + Math.sin(a) * 28; strokes.push(roughLine(mx, my, mx + Math.cos(a + 0.35) * 16, my + Math.sin(a + 0.35) * 16, c1, pickSize())); strokes.push(roughLine(mx, my, mx + Math.cos(a - 0.35) * 16, my + Math.sin(a - 0.35) * 16, c1, pickSize())); // inner crystal strokes.push(roughLine(sx, sy, sx + Math.cos(a) * 22, sy + Math.sin(a) * 22, "#93c5fd", pickSize())); } // center strokes.push(...roughEllipse(sx, sy, 8, 8, "#ffffff", pickSize(), 10)); strokes.push(...roughEllipse(sx, sy, 4, 4, "#93c5fd", pickSize(), 8)); // extra branches for (let i = 0; i < 6; i++) { const a = (i / 6) * Math.PI * 2 + 0.5; strokes.push(roughLine(sx + Math.cos(a) * 42, sy + Math.sin(a) * 42, sx + Math.cos(a) * 55, sy + Math.sin(a) * 55, "#93c5fd", pickSize())); } return strokes; } // ── flower ─────────────────────────────────────────────────────────────── if (word.includes("flower")) { // stem strokes.push(roughLine(cx, ground, cx + wobble(0, 8), ground - 130, c1, 10)); strokes.push(roughLine(cx + wobble(0, 8), ground - 130, cx + wobble(0, 10), ground - 150, c2, pickSize())); // leaves for (let i = 0; i < 3; i++) { const lx = cx + wobble(0, 5); const ly = ground - 80 - i * 25; strokes.push(...roughEllipse(lx + 18, ly, 14, 7, c2, pickSize(), 10)); strokes.push(...roughEllipse(lx - 18, ly, 14, 7, c2, pickSize(), 10)); strokes.push(roughLine(lx, ly, lx + 22, ly + wobble(0, 3), c1, pickSize())); strokes.push(roughLine(lx, ly, lx - 22, ly + wobble(0, 3), c1, pickSize())); } // petals for (let i = 0; i < 8; i++) { const a = (i / 8) * Math.PI * 2; strokes.push(...roughEllipse(cx + wobble(0, 5), ground - 155 + wobble(0, 5), 18 + Math.cos(a) * 12, 12, c3, pickSize(), 10)); } // center strokes.push(...roughEllipse(cx, ground - 155, 12, 12, "#eab308", 8, 12)); strokes.push(...hatchEllipse(cx, ground - 155, 8, 8, "#fef08a", 4)); // ground strokes.push(roughLine(cx - 80, ground, cx + 80, ground, c1, 6)); strokes.push(roughLine(cx - 60, ground + 3, cx + 60, ground + 3, c2, pickSize())); return strokes; } // ── tree ───────────────────────────────────────────────────────────────── if (word.includes("tree")) { // trunk strokes.push(...roughRect(cx - 10, ground - 100, 20, 100, c1, 8, 4)); strokes.push(...hatchRect(cx - 7, ground - 97, 14, 94, "#8B4513", 5)); // branches for (let i = 0; i < 4; i++) { const by = ground - 70 + i * 20; const dir = i % 2 === 0 ? 1 : -1; strokes.push(roughLine(cx + dir * 10, by, cx + dir * 50, by - 10 + wobble(0, 8), c1, pickSize())); strokes.push(roughLine(cx + dir * 50, by - 10, cx + dir * 60, by - 15, c1, pickSize())); } // foliage - multiple ellipses for fluffy look for (let i = 0; i < 6; i++) { const fx = cx + wobble(0, 50); const fy = ground - 140 + wobble(0, 30); strokes.push(...roughEllipse(fx, fy, 30 + Math.random() * 20, 25 + Math.random() * 15, c2, pickSize(), 12)); strokes.push(...hatchEllipse(fx, fy, 20 + Math.random() * 10, 18 + Math.random() * 8, c3, 4)); } // ground strokes.push(roughLine(cx - 80, ground, cx + 80, ground, c1, 6)); strokes.push(roughLine(cx - 60, ground + 3, cx + 60, ground + 3, c2, pickSize())); // roots strokes.push(roughLine(cx - 10, ground, cx - 25, ground + 8, c1, pickSize())); strokes.push(roughLine(cx + 10, ground, cx + 25, ground + 8, c1, pickSize())); return strokes; } // ── mushroom ───────────────────────────────────────────────────────────── if (word.includes("mushroom")) { // stem strokes.push(...roughRect(cx - 14, ground - 80, 28, 80, c1, 8, 4)); strokes.push(...hatchRect(cx - 10, ground - 76, 20, 72, c2, 5)); // cap strokes.push(...roughEllipse(cx, ground - 90, 50, 30, "#ef4444", 8, 16)); strokes.push(...hatchEllipse(cx, ground - 90, 44, 25, c3, 6)); strokes.push(...roughEllipse(cx, ground - 95, 38, 18, "#ef4444", pickSize(), 14)); // dots for (let i = 0; i < 5; i++) { const dx = cx - 25 + i * 12 + wobble(0, 4); const dy = ground - 100 + wobble(0, 6); strokes.push(...roughEllipse(dx, dy, 4, 3, "#ffffff", pickSize(), 8)); } // gill line strokes.push(roughLine(cx - 45, ground - 85, cx + 45, ground - 85, c1, pickSize())); // ground strokes.push(roughLine(cx - 80, ground, cx + 80, ground, c1, 6)); strokes.push(roughLine(cx - 60, ground + 3, cx + 60, ground + 3, c2, pickSize())); // little grass for (let i = 0; i < 4; i++) { strokes.push(roughLine(cx - 40 + i * 25, ground, cx - 38 + i * 25, ground - 15, c2, pickSize())); } return strokes; } // ── mountain / volcano ─────────────────────────────────────────────────── if (word.includes("mountain") || word.includes("volcano")) { const isVolcano = word.includes("volcano"); strokes.push(roughLine(cx - 130, ground, cx, ground - 200 - Math.random() * 30, c1, 8)); strokes.push(roughLine(cx, ground - 200, cx + 130, ground, c1, 8)); strokes.push(...hatchRect(cx - 120, ground - 60, 240, 56, c2, 5)); // snow cap strokes.push(roughLine(cx - 40, ground - 160, cx, ground - 200, "#ffffff", pickSize())); strokes.push(roughLine(cx, ground - 200, cx + 40, ground - 160, "#ffffff", pickSize())); strokes.push(roughLine(cx - 40, ground - 160, cx + 40, ground - 160, "#ffffff", pickSize())); for (let i = 0; i < 3; i++) { const sx = cx - 30 + i * 30; const sy = ground - 175 + i * 8; strokes.push(...roughEllipse(sx, sy, 10, 4, "#ffffff", pickSize(), 8)); } // second mountain strokes.push(roughLine(cx + 10, ground, cx + 90, ground - 120, c3, 8)); strokes.push(roughLine(cx + 90, ground - 120, cx + 170, ground, c3, 8)); strokes.push(roughLine(cx + 50, ground, cx + 90, ground - 120, c2, pickSize())); strokes.push(roughLine(cx + 90, ground - 120, cx + 130, ground, c2, pickSize())); if (isVolcano) { // crater strokes.push(...roughEllipse(cx, ground - 205, 18, 8, "#ef4444", 8, 10)); strokes.push(...roughEllipse(cx, ground - 205, 10, 5, "#eab308", pickSize(), 8)); // smoke for (let i = 0; i < 4; i++) { strokes.push(...roughEllipse(cx + wobble(0, 20), ground - 220 - i * 15, 12 - i * 2, 8 - i, "#9ca3af", pickSize(), 8)); } } // ground strokes.push(roughLine(cx - 150, ground, cx + 200, ground, c1, 6)); strokes.push(roughLine(cx - 130, ground + 3, cx + 180, ground + 3, c2, pickSize())); return strokes; } // ── sun ────────────────────────────────────────────────────────────────── if (word.includes("sun")) { strokes.push(...roughEllipse(cx, cy + 80, 50, 50, "#eab308", 8, 20)); strokes.push(...hatchEllipse(cx, cy + 80, 42, 42, "#fef08a", 6)); strokes.push(...roughEllipse(cx, cy + 80, 20, 20, "#f59e0b", pickSize(), 14)); for (let i = 0; i < 10; i++) { const a = (i / 10) * Math.PI * 2; strokes.push(roughLine( cx + Math.cos(a) * 55, cy + 80 + Math.sin(a) * 55, cx + Math.cos(a) * 75, cy + 80 + Math.sin(a) * 75, "#eab308", pickSize() )); strokes.push(roughLine( cx + Math.cos(a) * 60, cy + 80 + Math.sin(a) * 60, cx + Math.cos(a) * 70, cy + 80 + Math.sin(a) * 70, "#fef08a", pickSize() )); } // ground strokes.push(roughLine(cx - 100, ground, cx + 100, ground, c1, 6)); return strokes; } // ── moon ───────────────────────────────────────────────────────────────── if (word.includes("moon")) { strokes.push(...roughEllipse(cx, cy + 60, 45, 45, "#fef08a", 8, 20)); strokes.push(...roughEllipse(cx - 15, cy + 55, 40, 40, "#1e293b", 8, 20)); strokes.push(...roughEllipse(cx + 15, cy + 50, 6, 5, "#94a3b8", pickSize(), 8)); strokes.push(...roughEllipse(cx + 25, cy + 65, 3, 3, "#94a3b8", pickSize(), 6)); // stars for (let i = 0; i < 6; i++) { const sx = cx - 80 + i * 30 + wobble(0, 10); const sy = cy + 20 + wobble(0, 40); strokes.push(...roughEllipse(sx, sy, 2, 2, "#ffffff", pickSize(), 6)); } strokes.push(roughLine(cx - 100, ground, cx + 100, ground, c1, 6)); return strokes; } // ── cloud ──────────────────────────────────────────────────────────────── if (word.includes("cloud")) { for (let i = 0; i < 5; i++) { const cx2 = cx - 50 + i * 25 + wobble(0, 8); const cy2 = cy + 60 + wobble(0, 15); const r = 25 + Math.random() * 18; strokes.push(...roughEllipse(cx2, cy2, r, r * 0.7, "#ffffff", pickSize(), 14)); strokes.push(...roughEllipse(cx2, cy2, r - 4, r * 0.7 - 4, "#e2e8f0", pickSize(), 12)); } // rain if (word.includes("rain")) { for (let i = 0; i < 8; i++) { strokes.push(roughLine(cx - 60 + i * 15, cy + 90 + wobble(0, 10), cx - 58 + i * 15, cy + 110 + wobble(0, 10), "#3b82f6", pickSize())); } } strokes.push(roughLine(cx - 100, ground, cx + 100, ground, c1, 6)); return strokes; } // ── rainbow ────────────────────────────────────────────────────────────── if (word.includes("rainbow")) { const colors = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6"]; for (let ri = 0; ri < colors.length; ri++) { const r = 140 - ri * 16; const segs = 16; for (let i = 0; i < segs; i++) { const a1 = Math.PI + (i / segs) * Math.PI * 0.8; const a2 = Math.PI + ((i + 1) / segs) * Math.PI * 0.8; strokes.push(stroke( wobble(cx + Math.cos(a1) * r, 2), wobble(cy + 80 + Math.sin(a1) * r * 0.5, 2), wobble(cx + Math.cos(a2) * r, 2), wobble(cy + 80 + Math.sin(a2) * r * 0.5, 2), colors[ri], pickSize() )); } } strokes.push(roughLine(cx - 160, ground, cx + 160, ground, c1, 6)); return strokes; } // ── default ────────────────────────────────────────────────────────────── strokes.push(...roughEllipse(350, 200, 80, 60, c3, 8, 14)); strokes.push(...hatchEllipse(350, 200, 70, 50, c2, 5)); strokes.push(roughLine(50, ground, 650, ground, c1, 6)); strokes.push(roughLine(50, ground + 3, 650, ground + 3, c2, pickSize())); return strokes; } function sketchVehicle(word) { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2), c4 = pickColor(c3); const cx = 250 + Math.random() * 80, cy = 220 + Math.random() * 30; const strokes = []; // ── rocket / spaceship ─────────────────────────────────────────────────── if (word.includes("rocket") || word.includes("spaceship")) { strokes.push(roughLine(cx, cy - 110, cx - 38, cy + 50, c1, 8)); strokes.push(roughLine(cx, cy - 110, cx + 38, cy + 50, c1, 8)); strokes.push(roughLine(cx - 38, cy + 50, cx + 38, cy + 50, c1, 8)); strokes.push(...hatchRect(cx - 33, cy - 20, 66, 66, c2, 5)); // window strokes.push(...roughEllipse(cx, cy - 20, 16, 16, c3, 8, 14)); strokes.push(...roughEllipse(cx, cy - 20, 8, 8, "#3b82f6", pickSize(), 10)); // fins strokes.push(roughLine(cx - 38, cy + 30, cx - 60, cy + 60, c2, pickSize())); strokes.push(roughLine(cx - 38, cy + 30, cx - 38, cy + 50, c2, pickSize())); strokes.push(roughLine(cx - 60, cy + 60, cx - 38, cy + 50, c2, pickSize())); strokes.push(roughLine(cx + 38, cy + 30, cx + 60, cy + 60, c2, pickSize())); strokes.push(roughLine(cx + 38, cy + 30, cx + 38, cy + 50, c2, pickSize())); strokes.push(roughLine(cx + 60, cy + 60, cx + 38, cy + 50, c2, pickSize())); // flame strokes.push(...roughEllipse(cx, cy + 55, 12, 20, "#ef4444", 8, 12)); strokes.push(...roughEllipse(cx, cy + 58, 8, 14, "#eab308", pickSize(), 10)); strokes.push(...roughEllipse(cx, cy + 62, 4, 8, "#fef08a", pickSize(), 8)); // body stripes for (let i = 0; i < 4; i++) { strokes.push(roughLine(cx - 33 + i * 2, cy + 10 + i * 12, cx + 33 - i * 2, cy + 10 + i * 12, "#ef4444", pickSize())); } // rivets for (let i = 0; i < 3; i++) { strokes.push(...roughEllipse(cx - 30, cy - 10 + i * 20, 2, 2, c4, pickSize(), 6)); strokes.push(...roughEllipse(cx + 30, cy - 10 + i * 20, 2, 2, c4, pickSize(), 6)); } return strokes; } // ── submarine ──────────────────────────────────────────────────────────── if (word.includes("submarine")) { strokes.push(...roughEllipse(cx, cy, 95, 38, c1, 8, 18)); strokes.push(...hatchEllipse(cx, cy, 85, 30, c2, 6)); strokes.push(...roughRect(cx + 10, cy - 30, 32, 28, c3, pickSize(), 4)); strokes.push(...hatchRect(cx + 13, cy - 27, 26, 22, c4, 4)); // periscope strokes.push(roughLine(cx + 22, cy - 30, cx + 22, cy - 50, c1, pickSize())); strokes.push(roughLine(cx + 18, cy - 50, cx + 26, cy - 50, c1, pickSize())); // propeller strokes.push(...roughEllipse(cx + 97, cy, 8, 14, c2, pickSize(), 10)); strokes.push(roughLine(cx + 95, cy - 6, cx + 100, cy + 6, c2, pickSize())); strokes.push(roughLine(cx + 95, cy + 6, cx + 100, cy - 6, c2, pickSize())); // windows for (let i = 0; i < 4; i++) { strokes.push(...roughEllipse(cx - 40 + i * 25, cy, 7, 7, c3, pickSize(), 10)); strokes.push(...roughEllipse(cx - 40 + i * 25, cy, 3, 3, "#3b82f6", pickSize(), 6)); } // bottom details for (let i = 0; i < 3; i++) { strokes.push(...roughEllipse(cx - 50 + i * 40, cy + 38, 10, 8, c2, pickSize(), 8)); } return strokes; } // ── bicycle ────────────────────────────────────────────────────────────── if (word.includes("bicycle")) { strokes.push(...roughEllipse(cx, cy, 42, 42, c1, 8, 16)); strokes.push(...roughEllipse(cx, cy, 34, 34, c2, pickSize(), 14)); strokes.push(...roughEllipse(cx + 65, cy, 42, 42, c1, 8, 16)); strokes.push(...roughEllipse(cx + 65, cy, 34, 34, c2, pickSize(), 14)); // frame strokes.push(roughLine(cx, cy, cx + 32, cy - 32, c3, pickSize())); strokes.push(roughLine(cx + 32, cy - 32, cx + 65, cy, c3, pickSize())); strokes.push(roughLine(cx + 32, cy - 32, cx + 32, cy + 32, c3, pickSize())); // fork and bars strokes.push(roughLine(cx, cy, cx + 8, cy + 42, c3, pickSize())); strokes.push(roughLine(cx + 65, cy, cx + 57, cy + 42, c3, pickSize())); strokes.push(roughLine(cx + 32, cy - 32, cx + 20, cy - 44, c3, pickSize())); strokes.push(roughLine(cx + 20, cy - 44, cx + 44, cy - 44, c3, pickSize())); strokes.push(roughLine(cx + 20, cy - 44, cx + 15, cy - 50, c3, pickSize())); // seat strokes.push(...roughEllipse(cx + 32, cy - 36, 8, 4, c1, pickSize(), 8)); // spokes for (let i = 0; i < 6; i++) { const a = (i / 6) * Math.PI * 2; strokes.push(roughLine(cx + Math.cos(a) * 4, cy + Math.sin(a) * 4, cx + Math.cos(a) * 38, cy + Math.sin(a) * 38, c4, pickSize())); strokes.push(roughLine(cx + 65 + Math.cos(a) * 4, cy + Math.sin(a) * 4, cx + 65 + Math.cos(a) * 38, cy + Math.sin(a) * 38, c4, pickSize())); } // chain area strokes.push(...roughEllipse(cx + 32, cy + 8, 10, 6, c4, pickSize(), 8)); strokes.push(roughLine(cx + 8, cy + 12, cx + 28, cy + 10, c4, pickSize())); return strokes; } // ── parachute ──────────────────────────────────────────────────────────── if (word.includes("parachute")) { strokes.push(...roughEllipse(cx, cy, 90, 50, c1, 8, 20)); strokes.push(...hatchEllipse(cx, cy, 80, 42, c2, 6)); // panels for (let i = 0; i < 6; i++) { const a = Math.PI + (i / 6) * Math.PI; strokes.push(roughLine(cx + Math.cos(a) * 85, cy + Math.sin(a) * 48, cx + Math.cos(a) * 10, cy + 20, c3, pickSize())); } // ropes strokes.push(roughLine(cx - 20, cy + 40, cx + wobble(2, 4), cy + 100, c3, pickSize())); strokes.push(roughLine(cx + 20, cy + 40, cx + wobble(2, 4), cy + 100, c3, pickSize())); strokes.push(roughLine(cx - 50, cy + 25, cx - 25, cy + 100, c3, pickSize())); strokes.push(roughLine(cx + 50, cy + 25, cx + 25, cy + 100, c3, pickSize())); strokes.push(roughLine(cx, cy + 42, cx + wobble(2, 4), cy + 100, c3, pickSize())); // person hanging strokes.push(...roughEllipse(cx + wobble(2, 4), cy + 108, 12, 16, c4, pickSize(), 10)); strokes.push(...roughEllipse(cx + wobble(2, 4), cy + 100, 8, 6, c3, pickSize(), 8)); return strokes; } // ── sailboat ───────────────────────────────────────────────────────────── if (word.includes("sailboat")) { // hull strokes.push(roughLine(cx - 60, cy + 30, cx + 60, cy + 30, c1, 8)); strokes.push(roughLine(cx - 60, cy + 30, cx - 40, cy + 55, c1, 8)); strokes.push(roughLine(cx - 40, cy + 55, cx + 50, cy + 55, c1, 8)); strokes.push(roughLine(cx + 50, cy + 55, cx + 60, cy + 30, c1, 8)); strokes.push(...hatchRect(cx - 55, cy + 33, 110, 18, c2, 4)); // mast strokes.push(roughLine(cx, cy + 30, cx, cy - 60, c1, pickSize())); // sail strokes.push(roughLine(cx, cy - 55, cx + 45, cy + 15, c3, 8)); strokes.push(roughLine(cx + 45, cy + 15, cx, cy + 20, c3, 8)); strokes.push(roughLine(cx, cy - 55, cx, cy + 20, c3, 8)); strokes.push(...hatchRect(cx + 2, cy - 50, 38, 62, c4, 4)); // flag strokes.push(roughLine(cx, cy - 60, cx, cy - 72, c1, pickSize())); strokes.push(roughLine(cx, cy - 72, cx + 15, cy - 68, "#ef4444", pickSize())); strokes.push(roughLine(cx + 15, cy - 68, cx, cy - 64, "#ef4444", pickSize())); // water for (let i = 0; i < 6; i++) { strokes.push(roughLine(cx - 70 + i * 20, cy + 60 + i * 3, cx - 50 + i * 22, cy + 65 + i * 4, "#3b82f6", pickSize())); } // reflection for (let i = 0; i < 3; i++) { strokes.push(roughLine(cx - 30 + i * 30, cy + 62, cx - 20 + i * 30, cy + 66, "#93c5fd", pickSize())); } return strokes; } // ── skateboard ─────────────────────────────────────────────────────────── if (word.includes("skateboard")) { // deck strokes.push(roughLine(cx - 40, cy, cx + 40, cy, c1, pickSize())); strokes.push(roughLine(cx - 43, cy + 2, cx + 43, cy + 2, c1, pickSize())); strokes.push(roughLine(cx - 45, cy + 4, cx + 45, cy + 4, c1, pickSize())); // trucks strokes.push(roughLine(cx - 25, cy + 4, cx - 25, cy + 14, c2, pickSize())); strokes.push(roughLine(cx + 25, cy + 4, cx + 25, cy + 14, c2, pickSize())); // wheels strokes.push(...roughEllipse(cx - 28, cy + 20, 8, 8, c3, pickSize(), 10)); strokes.push(...roughEllipse(cx - 22, cy + 20, 8, 8, c3, pickSize(), 10)); strokes.push(...roughEllipse(cx + 22, cy + 20, 8, 8, c3, pickSize(), 10)); strokes.push(...roughEllipse(cx + 28, cy + 20, 8, 8, c3, pickSize(), 10)); // grip tape for (let i = 0; i < 4; i++) { strokes.push(roughLine(cx - 35 + i * 20, cy + 1, cx - 30 + i * 20, cy + 5, c4, pickSize())); } return strokes; } // ── car / bus / truck ──────────────────────────────────────────────────── const isBus = word.includes("bus"); const isTruck = word.includes("truck"); const bodyW = isBus ? 180 : isTruck ? 170 : 150 + Math.random() * 20; const bodyH = isBus ? 60 : 50 + Math.random() * 10; strokes.push(...roughRect(cx, cy, bodyW, bodyH, c1, 8, 4)); strokes.push(...hatchRect(cx + 4, cy + 4, bodyW - 8, bodyH - 8, c2, 5)); // cab const cabW = isBus || isTruck ? 50 : 60; strokes.push(...roughRect(cx + bodyW - cabW - 10, cy - 30, cabW, 40, c3, pickSize(), 4)); strokes.push(...hatchRect(cx + bodyW - cabW - 6, cy - 26, cabW - 8, 32, c4, 4)); // windshield strokes.push(...roughRect(cx + bodyW - cabW - 4, cy - 24, cabW - 12, 20, "#3b82f6", pickSize(), 4)); strokes.push(roughLine(cx + bodyW - cabW - 4, cy - 14, cx + bodyW - 14, cy - 14, c3, pickSize())); // windows const winCount = isBus ? 6 : isTruck ? 2 : 3; for (let i = 0; i < winCount; i++) { const wx = cx + 8 + i * ((bodyW - cabW - 20) / winCount); strokes.push(...roughRect(wx, cy + 8, (bodyW - cabW - 30) / winCount, 18, "#3b82f6", pickSize(), 4)); strokes.push(roughLine(wx + ((bodyW - cabW - 30) / winCount) / 2, cy + 8, wx + ((bodyW - cabW - 30) / winCount) / 2, cy + 26, c3, pickSize())); } // wheels strokes.push(...roughEllipse(cx + 35, cy + bodyH, 18, 18, c3, 8, 14)); strokes.push(...roughEllipse(cx + 35, cy + bodyH, 10, 10, c1, pickSize(), 10)); strokes.push(...roughEllipse(cx + bodyW - 35, cy + bodyH, 18, 18, c3, 8, 14)); strokes.push(...roughEllipse(cx + bodyW - 35, cy + bodyH, 10, 10, c1, pickSize(), 10)); // headlight / taillight strokes.push(...roughEllipse(cx + bodyW - 4, cy + 8, 4, 6, "#eab308", pickSize(), 8)); strokes.push(...roughEllipse(cx + 4, cy + 8, 4, 6, "#ef4444", pickSize(), 8)); // bumper strokes.push(roughLine(cx - 2, cy + bodyH - 4, cx + 2, cy + bodyH - 4, c1, pickSize())); strokes.push(roughLine(cx + bodyW - 2, cy + bodyH - 4, cx + bodyW + 2, cy + bodyH - 4, c1, pickSize())); // door line if (!isBus) { strokes.push(roughLine(cx + 80, cy + 6, cx + 80, cy + bodyH - 4, c3, pickSize())); } // truck bed if (isTruck) { strokes.push(roughLine(cx + 8, cy + 6, cx + bodyW - cabW - 14, cy + 6, c3, pickSize())); strokes.push(roughLine(cx + 8, cy + 6, cx + 8, cy + bodyH / 2, c3, pickSize())); strokes.push(roughLine(cx + bodyW - cabW - 14, cy + 6, cx + bodyW - cabW - 14, cy + bodyH / 2, c3, pickSize())); } // bus door if (isBus) { strokes.push(roughLine(cx + bodyW / 2 - 5, cy + 6, cx + bodyW / 2 - 5, cy + bodyH - 4, c3, pickSize())); strokes.push(roughLine(cx + bodyW / 2 + 5, cy + 6, cx + bodyW / 2 + 5, cy + bodyH - 4, c3, pickSize())); } // roof rack detail strokes.push(roughLine(cx + 10, cy - 30, cx + bodyW - cabW - 10, cy - 30, c3, pickSize())); strokes.push(roughLine(cx + 10, cy - 33, cx + bodyW - cabW - 10, cy - 33, c3, pickSize())); // antenna strokes.push(roughLine(cx + bodyW - 20, cy - 30, cx + bodyW - 15, cy - 45, c3, pickSize())); strokes.push(...roughEllipse(cx + bodyW - 15, cy - 47, 3, 3, c4, pickSize(), 6)); return strokes; } function sketchFood(word) { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2), c4 = pickColor(c3); const cx = 300 + Math.random() * 100, cy = 200 + Math.random() * 60; const strokes = []; // ── pizza ──────────────────────────────────────────────────────────────── if (word.includes("pizza")) { strokes.push(...roughEllipse(cx, cy, 110, 85, c3, 8, 18)); strokes.push(...roughEllipse(cx, cy, 65, 50, "#ef4444", 8, 16)); strokes.push(...roughEllipse(cx, cy, 35, 25, c1, pickSize(), 14)); // pepperoni for (let i = 0; i < 6; i++) { const px = cx + wobble(0, 45); const py = cy + wobble(0, 30); strokes.push(...roughEllipse(px, py, 10 + Math.random() * 6, 8 + Math.random() * 4, c2, pickSize(), 10)); strokes.push(...roughEllipse(px, py, 5, 4, c4, pickSize(), 8)); } // olives for (let i = 0; i < 4; i++) { const ox = cx + wobble(0, 35); const oy = cy + wobble(0, 25); strokes.push(...roughEllipse(ox, oy, 5, 5, "#1e293b", pickSize(), 8)); } // crust strokes.push(roughLine(cx - 108, cy + 2, cx - 108, cy - 2, c3, pickSize())); strokes.push(roughLine(cx + 108, cy + 2, cx + 108, cy - 2, c3, pickSize())); strokes.push(roughLine(cx - 2, cy - 83, cx + 2, cy - 83, c3, pickSize())); strokes.push(roughLine(cx - 2, cy + 83, cx + 2, cy + 83, c3, pickSize())); // slice line strokes.push(roughLine(cx, cy, cx + 60, cy - 50, c3, pickSize())); strokes.push(roughLine(cx, cy, cx - 40, cy + 60, c3, pickSize())); return strokes; } // ── cake ───────────────────────────────────────────────────────────────── if (word.includes("cake")) { strokes.push(...roughRect(cx - 65, cy - 10, 130, 55, c3, 8, 4)); strokes.push(...hatchRect(cx - 61, cy - 6, 122, 47, c2, 5)); // frosting layer strokes.push(roughLine(cx - 65, cy - 10, cx + 65, cy - 10, "#ef4444", 8)); strokes.push(...roughEllipse(cx, cy - 12, 60, 8, "#ef4444", pickSize(), 14)); // candles for (let i = 0; i < 3; i++) { const px = cx - 30 + i * 30; strokes.push(roughLine(px, cy - 12, px, cy - 35, c1, pickSize())); strokes.push(...roughEllipse(px, cy - 37, 4, 4, "#eab308", pickSize(), 8)); strokes.push(...roughEllipse(px, cy - 37, 2, 2, "#fef08a", pickSize(), 6)); } // sprinkles for (let i = 0; i < 8; i++) { const sx = cx - 50 + i * 14 + wobble(0, 4); const sy = cy - 5 + wobble(0, 5); strokes.push(roughLine(sx, sy, sx + 5, sy + 3, c4, pickSize())); } // bottom line strokes.push(roughLine(cx - 65, cy + 45, cx + 65, cy + 45, c3, pickSize())); // plate strokes.push(...roughEllipse(cx, cy + 50, 70, 10, c1, pickSize(), 14)); return strokes; } // ── ice cream ──────────────────────────────────────────────────────────── if (word.includes("ice cream") || word.includes("icecream")) { // scoop strokes.push(...roughEllipse(cx, cy - 5, 32, 28, "#ef4444", 8, 14)); strokes.push(...hatchEllipse(cx, cy - 5, 26, 22, c3, 5)); strokes.push(...roughEllipse(cx, cy - 10, 20, 15, c4, pickSize(), 12)); // cone strokes.push(roughLine(cx - 28, cy + 15, cx, cy + 75, c1, 8)); strokes.push(roughLine(cx + 28, cy + 15, cx, cy + 75, c1, 8)); strokes.push(roughLine(cx - 28, cy + 15, cx + 28, cy + 15, c1, 8)); strokes.push(...hatchRect(cx - 24, cy + 18, 48, 52, c2, 5)); // cone cross-hatch for (let i = 0; i < 4; i++) { strokes.push(roughLine(cx - 20 + i * 10, cy + 18, cx + 25 - i * 6, cy + 68, c3, pickSize())); } for (let i = 0; i < 4; i++) { const yy = cy + 22 + i * 12; strokes.push(roughLine(cx - 20 + i * 3, yy, cx + 20 - i * 3, yy, c3, pickSize())); } // cherry strokes.push(...roughEllipse(cx, cy - 22, 6, 6, "#dc2626", pickSize(), 8)); strokes.push(roughLine(cx + 3, cy - 26, cx + 5, cy - 32, c1, pickSize())); return strokes; } // ── popcorn ────────────────────────────────────────────────────────────── if (word.includes("popcorn")) { // bucket strokes.push(roughLine(cx - 30, cy + 30, cx - 25, cy - 10, c1, 8)); strokes.push(roughLine(cx + 25, cy - 10, cx + 30, cy + 30, c1, 8)); strokes.push(roughLine(cx - 30, cy + 30, cx + 30, cy + 30, c1, 8)); strokes.push(...hatchRect(cx - 27, cy - 8, 54, 36, c2, 4)); // popcorn pieces for (let i = 0; i < 8; i++) { const px = cx - 22 + i * 7 + wobble(0, 5); const py = cy - 12 + wobble(0, 8); strokes.push(...roughEllipse(px, py, 9 + Math.random() * 5, 10 + Math.random() * 4, c3, pickSize(), 10)); strokes.push(...roughEllipse(px, py, 5, 6, c4, pickSize(), 8)); } // extra popcorns scattered for (let i = 0; i < 4; i++) { const px = cx - 15 + i * 10 + wobble(0, 4); const py = cy - 5 + wobble(0, 3); strokes.push(...roughEllipse(px, py, 4, 4, "#fef08a", pickSize(), 8)); } // bucket stripes strokes.push(roughLine(cx - 27, cy + 5, cx + 27, cy + 5, "#ef4444", pickSize())); strokes.push(roughLine(cx - 28, cy + 18, cx + 28, cy + 18, "#ef4444", pickSize())); return strokes; } // ── apple ──────────────────────────────────────────────────────────────── if (word.includes("apple")) { strokes.push(...roughEllipse(cx, cy, 38, 42, "#ef4444", 8, 18)); strokes.push(...hatchEllipse(cx, cy, 30, 34, c3, 5)); strokes.push(...roughEllipse(cx + 20, cy - 15, 10, 8, c2, pickSize(), 10)); // stem strokes.push(roughLine(cx + 3, cy - 42, cx + wobble(2, 3), cy - 55, c1, pickSize())); strokes.push(...roughEllipse(cx + wobble(0, 4), cy - 57, 5, 3, c2, pickSize(), 8)); // leaf strokes.push(...roughEllipse(cx + 8, cy - 50, 10, 5, "#22c55e", pickSize(), 8)); strokes.push(roughLine(cx + 3, cy - 52, cx + 14, cy - 48, "#22c55e", pickSize())); // shine strokes.push(roughLine(cx - 10, cy - 15, cx - 5, cy - 10, "#ffffff", pickSize())); return strokes; } // ── banana ─────────────────────────────────────────────────────────────── if (word.includes("banana")) { for (let i = 0; i < 8; i++) { const t = i / 8; const bx = cx + t * 50 - 25; const by = cy - 30 + Math.sin(t * Math.PI) * 40; strokes.push(roughLine(bx - 8, by, bx + 8, by, c3, pickSize())); } strokes.push(roughLine(cx - 25, cy - 28, cx - 30, cy - 35, c1, pickSize())); strokes.push(roughLine(cx + 25, cy + 10, cx + 32, cy + 18, c1, pickSize())); // peel lines for (let i = 0; i < 3; i++) { const t = (i + 1) / 4; const bx = cx + t * 50 - 25; const by = cy - 30 + Math.sin(t * Math.PI) * 40; strokes.push(roughLine(bx, by, bx, by + 6, "#eab308", pickSize())); } return strokes; } // ── pineapple ──────────────────────────────────────────────────────────── if (word.includes("pineapple")) { strokes.push(...roughEllipse(cx, cy, 35, 50, c3, 8, 16)); strokes.push(...hatchEllipse(cx, cy, 28, 42, c2, 5)); // diamond pattern for (let i = 0; i < 6; i++) { const yy = cy - 35 + i * 12; for (let j = 0; j < 4; j++) { const xx = cx - 25 + j * 16 + (i % 2) * 8; strokes.push(roughLine(xx - 4, yy, xx + 4, yy, c4, pickSize())); strokes.push(roughLine(xx, yy - 4, xx, yy + 4, c4, pickSize())); } } // leaves for (let i = 0; i < 5; i++) { const a = -0.6 + i * 0.3; strokes.push(roughLine(cx + wobble(0, 5), cy - 48, cx + Math.sin(a) * 25, cy - 70, "#22c55e", pickSize())); strokes.push(roughLine(cx + Math.sin(a) * 25, cy - 70, cx + Math.sin(a + 0.1) * 28, cy - 78, "#16a34a", pickSize())); } return strokes; } // ── default fruit ──────────────────────────────────────────────────────── strokes.push(...roughEllipse(cx, cy, 35, 40, c3, 8, 14)); strokes.push(...hatchEllipse(cx, cy, 28, 32, c2, 5)); strokes.push(roughLine(cx, cy - 40, cx + wobble(3, 5), cy - 52, c1, pickSize())); strokes.push(...roughEllipse(cx + 5, cy - 54, 6, 3, c2, pickSize(), 8)); strokes.push(roughLine(cx - 8, cy - 10, cx - 3, cy - 5, "#ffffff", pickSize())); return strokes; } function sketchPerson(word) { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2), c4 = pickColor(c3); const cx = 300 + Math.random() * 100, cy = 130 + Math.random() * 30; const strokes = []; // ── snowman ────────────────────────────────────────────────────────────── if (word.includes("snowman")) { strokes.push(...roughEllipse(cx, cy + 90, 50, 40, c1, 8, 16)); strokes.push(...hatchEllipse(cx, cy + 90, 42, 32, "#e2e8f0", 5)); strokes.push(...roughEllipse(cx, cy + 40, 40, 35, c1, 8, 16)); strokes.push(...hatchEllipse(cx, cy + 40, 32, 27, "#e2e8f0", 5)); strokes.push(...roughEllipse(cx, cy, 30, 28, c1, 8, 14)); // hat strokes.push(...roughRect(cx - 18, cy - 18, 36, 8, "#1e293b", pickSize(), 4)); strokes.push(...roughRect(cx - 12, cy - 38, 24, 22, "#1e293b", pickSize(), 4)); // eyes strokes.push(...roughEllipse(cx - 9, cy - 5, 4, 4, "#1e293b", pickSize(), 8)); strokes.push(...roughEllipse(cx + 9, cy - 5, 4, 4, "#1e293b", pickSize(), 8)); // carrot nose strokes.push(roughLine(cx, cy + 2, cx + 14, cy + 6, "#f97316", pickSize())); strokes.push(roughLine(cx + 14, cy + 6, cx + 10, cy + 4, "#f97316", pickSize())); // buttons strokes.push(...roughEllipse(cx, cy + 18, 3, 3, "#1e293b", pickSize(), 6)); strokes.push(...roughEllipse(cx, cy + 28, 3, 3, "#1e293b", pickSize(), 6)); strokes.push(...roughEllipse(cx, cy + 38, 3, 3, "#1e293b", pickSize(), 6)); // arms strokes.push(roughLine(cx - 24, cy + 25, cx - 55, cy + 10, c2, pickSize())); strokes.push(roughLine(cx - 55, cy + 10, cx - 60, cy + 4, c2, pickSize())); strokes.push(roughLine(cx + 24, cy + 25, cx + 55, cy + 10, c2, pickSize())); strokes.push(roughLine(cx + 55, cy + 10, cx + 60, cy + 4, c2, pickSize())); // scarf strokes.push(roughLine(cx - 24, cy + 16, cx + 24, cy + 16, "#ef4444", pickSize())); strokes.push(roughLine(cx + 20, cy + 16, cx + 18, cy + 24, "#ef4444", pickSize())); strokes.push(roughLine(cx + 18, cy + 24, cx + 22, cy + 24, "#ef4444", pickSize())); // snow ground strokes.push(roughLine(cx - 60, cy + 130, cx + 60, cy + 130, c1, pickSize())); return strokes; } // ── robot ──────────────────────────────────────────────────────────────── if (word.includes("robot")) { // head strokes.push(...roughRect(cx - 30, cy - 30, 60, 60, c1, 8, 4)); strokes.push(...hatchRect(cx - 26, cy - 26, 52, 52, c2, 5)); // body strokes.push(...roughRect(cx - 38, cy + 30, 76, 85, c1, 8, 4)); strokes.push(...hatchRect(cx - 34, cy + 34, 68, 77, c3, 5)); // eyes strokes.push(...roughEllipse(cx - 12, cy - 8, 8, 8, c3, pickSize(), 10)); strokes.push(...roughEllipse(cx - 12, cy - 8, 4, 4, "#3b82f6", pickSize(), 8)); strokes.push(...roughEllipse(cx + 12, cy - 8, 8, 8, c3, pickSize(), 10)); strokes.push(...roughEllipse(cx + 12, cy - 8, 4, 4, "#3b82f6", pickSize(), 8)); // mouth strokes.push(...roughRect(cx - 10, cy + 8, 20, 6, c4, pickSize(), 4)); strokes.push(roughLine(cx - 10, cy + 11, cx + 10, cy + 11, c3, pickSize())); // antenna strokes.push(roughLine(cx, cy - 30, cx, cy - 45, c4, pickSize())); strokes.push(...roughEllipse(cx, cy - 48, 5, 5, "#ef4444", pickSize(), 8)); // arms strokes.push(roughLine(cx - 38, cy + 45, cx - 60, cy + 55, c2, pickSize())); strokes.push(roughLine(cx - 60, cy + 55, cx - 62, cy + 60, c2, pickSize())); strokes.push(roughLine(cx + 38, cy + 45, cx + 60, cy + 55, c2, pickSize())); strokes.push(roughLine(cx + 60, cy + 55, cx + 62, cy + 60, c2, pickSize())); // legs strokes.push(roughLine(cx - 18, cy + 115, cx - 22, cy + 150, c2, pickSize())); strokes.push(roughLine(cx + 18, cy + 115, cx + 22, cy + 150, c2, pickSize())); // feet strokes.push(...roughRect(cx - 28, cy + 148, 18, 8, c2, pickSize(), 4)); strokes.push(...roughRect(cx + 10, cy + 148, 18, 8, c2, pickSize(), 4)); // chest panel strokes.push(...roughRect(cx - 12, cy + 45, 24, 30, c4, pickSize(), 4)); for (let i = 0; i < 3; i++) { strokes.push(...roughEllipse(cx, cy + 50 + i * 10, 3, 3, "#ef4444", pickSize(), 6)); } return strokes; } // ── astronaut ──────────────────────────────────────────────────────────── if (word.includes("astronaut")) { // helmet strokes.push(...roughEllipse(cx, cy, 38, 38, "#ffffff", 8, 18)); strokes.push(...hatchEllipse(cx, cy, 30, 30, "#e2e8f0", 5)); strokes.push(...roughEllipse(cx, cy, 22, 22, "#3b82f6", pickSize(), 14)); strokes.push(...roughEllipse(cx - 6, cy - 4, 4, 5, c1, pickSize(), 8)); strokes.push(...roughEllipse(cx + 6, cy - 4, 4, 5, c1, pickSize(), 8)); // body strokes.push(...roughRect(cx - 30, cy + 30, 60, 80, "#ffffff", 8, 4)); strokes.push(...hatchRect(cx - 26, cy + 34, 52, 72, "#e2e8f0", 5)); // backpack strokes.push(...roughRect(cx + 30, cy + 35, 20, 50, c2, pickSize(), 4)); // arms strokes.push(roughLine(cx - 30, cy + 45, cx - 55, cy + 40, c2, pickSize())); strokes.push(roughLine(cx - 55, cy + 40, cx - 58, cy + 35, c2, pickSize())); strokes.push(roughLine(cx + 30, cy + 45, cx + 55, cy + 40, c2, pickSize())); strokes.push(roughLine(cx + 55, cy + 40, cx + 58, cy + 35, c2, pickSize())); // legs strokes.push(roughLine(cx - 15, cy + 110, cx - 18, cy + 145, c2, pickSize())); strokes.push(roughLine(cx + 15, cy + 110, cx + 18, cy + 145, c2, pickSize())); strokes.push(...roughRect(cx - 22, cy + 143, 14, 10, c2, pickSize(), 4)); strokes.push(...roughRect(cx + 8, cy + 143, 14, 10, c2, pickSize(), 4)); // chest panel strokes.push(...roughRect(cx - 10, cy + 50, 20, 18, c3, pickSize(), 4)); strokes.push(...roughEllipse(cx, cy + 55, 3, 3, "#ef4444", pickSize(), 6)); return strokes; } // ── firefighter ────────────────────────────────────────────────────────── if (word.includes("firefighter")) { // helmet strokes.push(...roughEllipse(cx, cy, 34, 30, "#ef4444", 8, 16)); strokes.push(...roughRect(cx - 20, cy - 28, 40, 12, "#ef4444", pickSize(), 4)); strokes.push(...roughEllipse(cx, cy - 32, 22, 6, "#dc2626", pickSize(), 10)); // face strokes.push(...roughEllipse(cx, cy + 2, 24, 24, c3, pickSize(), 14)); strokes.push(...roughEllipse(cx - 7, cy - 2, 3, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx + 7, cy - 2, 3, 3, c1, pickSize(), 6)); strokes.push(roughLine(cx, cy + 5, cx + 6, cy + 8, c1, pickSize())); // body strokes.push(...roughRect(cx - 25, cy + 24, 50, 75, "#ef4444", 8, 4)); strokes.push(...hatchRect(cx - 21, cy + 28, 42, 67, "#dc2626", 5)); // yellow stripes strokes.push(roughLine(cx - 25, cy + 35, cx + 25, cy + 35, "#eab308", pickSize())); strokes.push(roughLine(cx - 25, cy + 75, cx + 25, cy + 75, "#eab308", pickSize())); // arms strokes.push(roughLine(cx - 25, cy + 40, cx - 50, cy + 35, c2, pickSize())); strokes.push(roughLine(cx + 25, cy + 40, cx + 50, cy + 35, c2, pickSize())); // hose strokes.push(roughLine(cx + 50, cy + 35, cx + 80, cy + 45, c4, pickSize())); strokes.push(roughLine(cx + 80, cy + 45, cx + 75, cy + 55, c4, pickSize())); // legs strokes.push(roughLine(cx - 14, cy + 99, cx - 16, cy + 135, c2, pickSize())); strokes.push(roughLine(cx + 14, cy + 99, cx + 16, cy + 135, c2, pickSize())); strokes.push(...roughRect(cx - 20, cy + 133, 12, 8, "#1e293b", pickSize(), 4)); strokes.push(...roughRect(cx + 8, cy + 133, 12, 8, "#1e293b", pickSize(), 4)); return strokes; } // ── wizard ─────────────────────────────────────────────────────────────── if (word.includes("wizard")) { // hat strokes.push(roughLine(cx - 22, cy - 5, cx, cy - 75, c3, 8)); strokes.push(roughLine(cx, cy - 75, cx + 22, cy - 5, c3, 8)); strokes.push(...roughEllipse(cx, cy - 5, 30, 6, c3, pickSize(), 12)); // star on hat for (let i = 0; i < 5; i++) { const a1 = (i * 2 * Math.PI / 5) - Math.PI/2; const a2 = ((i + 1) * 2 * Math.PI / 5) - Math.PI/2; strokes.push(roughLine( cx + Math.cos(a1) * 8, cy - 52 + Math.sin(a1) * 8, cx + Math.cos(a2) * 8, cy - 52 + Math.sin(a2) * 8, "#eab308", pickSize() )); } // face strokes.push(...roughEllipse(cx, cy + 12, 24, 22, c3, pickSize(), 14)); strokes.push(...roughEllipse(cx - 7, cy + 6, 3, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx + 7, cy + 6, 3, 3, c1, pickSize(), 6)); strokes.push(roughLine(cx, cy + 12, cx, cy + 18, c1, pickSize())); // beard for (let i = 0; i < 5; i++) { const bx = cx - 16 + i * 8; const by = cy + 18 + i * 4; strokes.push(roughLine(bx, by, bx + 4, by + 10, "#ffffff", pickSize())); strokes.push(roughLine(bx + 4, by + 10, bx - 2, by + 16, "#ffffff", pickSize())); } // body strokes.push(...roughRect(cx - 28, cy + 28, 56, 70, c3, 8, 4)); strokes.push(...hatchRect(cx - 24, cy + 32, 48, 62, c4, 5)); // arms strokes.push(roughLine(cx - 28, cy + 40, cx - 50, cy + 35, c2, pickSize())); strokes.push(roughLine(cx + 28, cy + 40, cx + 50, cy + 35, c2, pickSize())); // wand strokes.push(roughLine(cx + 50, cy + 35, cx + 80, cy + 15, c1, pickSize())); strokes.push(...roughEllipse(cx + 82, cy + 13, 4, 4, "#eab308", pickSize(), 8)); // legs strokes.push(roughLine(cx - 12, cy + 98, cx - 14, cy + 130, c2, pickSize())); strokes.push(roughLine(cx + 12, cy + 98, cx + 14, cy + 130, c2, pickSize())); return strokes; } // ── superhero ──────────────────────────────────────────────────────────── if (word.includes("superhero")) { // cape strokes.push(roughLine(cx - 30, cy + 25, cx - 38, cy + 85, "#ef4444", 8)); strokes.push(roughLine(cx - 38, cy + 85, cx + 38, cy + 85, "#ef4444", 8)); strokes.push(roughLine(cx + 38, cy + 85, cx + 30, cy + 25, "#ef4444", 8)); strokes.push(...hatchRect(cx - 34, cy + 30, 68, 50, "#dc2626", 4)); // body strokes.push(...roughRect(cx - 24, cy + 25, 48, 60, "#3b82f6", 8, 4)); strokes.push(...hatchRect(cx - 20, cy + 29, 40, 52, "#2563eb", 5)); // chest emblem strokes.push(...roughEllipse(cx, cy + 45, 10, 12, "#eab308", pickSize(), 10)); // head strokes.push(...roughEllipse(cx, cy, 28, 28, c3, 8, 14)); // mask strokes.push(roughLine(cx - 20, cy + 2, cx + 20, cy + 2, "#1e293b", pickSize())); strokes.push(roughLine(cx - 20, cy + 2, cx - 24, cy - 4, "#1e293b", pickSize())); strokes.push(roughLine(cx + 20, cy + 2, cx + 24, cy - 4, "#1e293b", pickSize())); strokes.push(...roughEllipse(cx - 8, cy - 2, 5, 4, "#ffffff", pickSize(), 8)); strokes.push(...roughEllipse(cx + 8, cy - 2, 5, 4, "#ffffff", pickSize(), 8)); // arms strokes.push(roughLine(cx - 24, cy + 35, cx - 50, cy + 25, c2, pickSize())); strokes.push(roughLine(cx + 24, cy + 35, cx + 50, cy + 25, c2, pickSize())); strokes.push(roughLine(cx - 50, cy + 25, cx - 52, cy + 20, c2, pickSize())); strokes.push(roughLine(cx + 50, cy + 25, cx + 52, cy + 20, c2, pickSize())); // legs strokes.push(roughLine(cx - 12, cy + 85, cx - 14, cy + 120, c2, pickSize())); strokes.push(roughLine(cx + 12, cy + 85, cx + 14, cy + 120, c2, pickSize())); // boots strokes.push(...roughRect(cx - 20, cy + 118, 14, 8, "#ef4444", pickSize(), 4)); strokes.push(...roughRect(cx + 6, cy + 118, 14, 8, "#ef4444", pickSize(), 4)); return strokes; } // ── mountaineer ────────────────────────────────────────────────────────── if (word.includes("mountaineer")) { // hat strokes.push(...roughEllipse(cx, cy - 5, 30, 8, "#1e293b", pickSize(), 12)); strokes.push(...roughRect(cx - 16, cy - 30, 32, 28, "#1e293b", pickSize(), 4)); // face strokes.push(...roughEllipse(cx, cy + 8, 22, 22, c3, pickSize(), 14)); strokes.push(...roughEllipse(cx - 6, cy + 3, 3, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx + 6, cy + 3, 3, 3, c1, pickSize(), 6)); // body (jacket) strokes.push(...roughRect(cx - 22, cy + 26, 44, 65, "#ef4444", 8, 4)); strokes.push(...hatchRect(cx - 18, cy + 30, 36, 57, "#dc2626", 5)); // backpack strokes.push(...roughRect(cx + 22, cy + 30, 18, 45, c2, pickSize(), 4)); // arms strokes.push(roughLine(cx - 22, cy + 38, cx - 45, cy + 32, c2, pickSize())); strokes.push(roughLine(cx + 22, cy + 38, cx + 45, cy + 32, c2, pickSize())); // walking stick strokes.push(roughLine(cx + 45, cy + 32, cx + 55, cy + 95, c1, pickSize())); // legs strokes.push(roughLine(cx - 12, cy + 91, cx - 14, cy + 125, c2, pickSize())); strokes.push(roughLine(cx + 12, cy + 91, cx + 14, cy + 125, c2, pickSize())); strokes.push(...roughRect(cx - 18, cy + 123, 12, 6, "#1e293b", pickSize(), 4)); strokes.push(...roughRect(cx + 6, cy + 123, 12, 6, "#1e293b", pickSize(), 4)); return strokes; } // ── beekeeper ──────────────────────────────────────────────────────────── if (word.includes("beekeeper")) { // hat strokes.push(...roughEllipse(cx, cy, 36, 32, "#ffffff", 8, 16)); strokes.push(...roughEllipse(cx, cy, 28, 24, "#e2e8f0", pickSize(), 14)); strokes.push(...roughEllipse(cx - 8, cy - 3, 3, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx + 8, cy - 3, 3, 3, c1, pickSize(), 6)); // veil strokes.push(roughLine(cx - 28, cy + 8, cx + 28, cy + 8, c2, pickSize())); strokes.push(roughLine(cx - 26, cy + 16, cx + 26, cy + 16, c2, pickSize())); // body strokes.push(...roughRect(cx - 24, cy + 22, 48, 60, "#eab308", 8, 4)); strokes.push(...hatchRect(cx - 20, cy + 26, 40, 52, "#ca8a04", 5)); // stripes for (let i = 0; i < 3; i++) { strokes.push(roughLine(cx - 24, cy + 30 + i * 14, cx + 24, cy + 30 + i * 14, "#1e293b", pickSize())); } // arms strokes.push(roughLine(cx - 24, cy + 35, cx - 46, cy + 30, c2, pickSize())); strokes.push(roughLine(cx + 24, cy + 35, cx + 46, cy + 30, c2, pickSize())); // smoker strokes.push(...roughRect(cx + 46, cy + 22, 18, 20, c4, pickSize(), 4)); strokes.push(roughLine(cx + 46, cy + 24, cx + 30, cy + 30, c4, pickSize())); // legs strokes.push(roughLine(cx - 12, cy + 82, cx - 14, cy + 115, c2, pickSize())); strokes.push(roughLine(cx + 12, cy + 82, cx + 14, cy + 115, c2, pickSize())); return strokes; } // ── mermaid ────────────────────────────────────────────────────────────── if (word.includes("mermaid")) { // hair for (let i = 0; i < 6; i++) { const a = Math.PI + (i / 6) * Math.PI; strokes.push(roughLine(cx + Math.cos(a) * 30, cy + Math.sin(a) * 28, cx + Math.cos(a) * 42, cy + Math.sin(a) * 38 + 10, "#ef4444", pickSize())); } // head strokes.push(...roughEllipse(cx, cy, 22, 24, c3, 8, 14)); strokes.push(...roughEllipse(cx - 6, cy - 3, 3, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx + 6, cy - 3, 3, 3, c1, pickSize(), 6)); strokes.push(roughLine(cx, cy + 4, cx + 5, cy + 8, c1, pickSize())); // body strokes.push(roughLine(cx, cy + 24, cx, cy + 55, c2, pickSize())); // shell top strokes.push(...roughEllipse(cx - 10, cy + 30, 10, 8, c4, pickSize(), 10)); strokes.push(...roughEllipse(cx + 10, cy + 30, 10, 8, c4, pickSize(), 10)); // tail strokes.push(roughLine(cx - 8, cy + 55, cx - 20, cy + 100, "#22c55e", pickSize())); strokes.push(roughLine(cx + 8, cy + 55, cx + 20, cy + 100, "#22c55e", pickSize())); strokes.push(roughLine(cx - 20, cy + 100, cx + 20, cy + 100, "#22c55e", pickSize())); strokes.push(...hatchRect(cx - 17, cy + 58, 34, 38, "#16a34a", 4)); // tail fin strokes.push(roughLine(cx - 20, cy + 100, cx - 35, cy + 112, "#22c55e", pickSize())); strokes.push(roughLine(cx + 20, cy + 100, cx + 35, cy + 112, "#22c55e", pickSize())); strokes.push(roughLine(cx - 35, cy + 112, cx, cy + 108, "#22c55e", pickSize())); strokes.push(roughLine(cx, cy + 108, cx + 35, cy + 112, "#22c55e", pickSize())); // scale details for (let i = 0; i < 4; i++) { const sy = cy + 65 + i * 10; strokes.push(roughLine(cx - 12 + i * 2, sy, cx + 12 - i * 2, sy, "#4ade80", pickSize())); } // arms strokes.push(roughLine(cx - 8, cy + 28, cx - 22, cy + 22, c2, pickSize())); strokes.push(roughLine(cx + 8, cy + 28, cx + 22, cy + 22, c2, pickSize())); return strokes; } // ── scarecrow ──────────────────────────────────────────────────────────── if (word.includes("scarecrow")) { // post strokes.push(roughLine(cx, cy, cx, cy + 140, c1, 8)); strokes.push(roughLine(cx, cy, cx + 50, cy - 10, c1, pickSize())); strokes.push(roughLine(cx, cy, cx - 50, cy - 10, c1, pickSize())); // head strokes.push(...roughEllipse(cx, cy - 30, 22, 24, c3, 8, 14)); strokes.push(...roughEllipse(cx - 6, cy - 34, 3, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx + 6, cy - 34, 3, 3, c1, pickSize(), 6)); // stitched mouth strokes.push(roughLine(cx - 8, cy - 24, cx - 4, cy - 22, c1, pickSize())); strokes.push(roughLine(cx - 4, cy - 22, cx + 4, cy - 26, c1, pickSize())); strokes.push(roughLine(cx + 4, cy - 26, cx + 8, cy - 24, c1, pickSize())); // hat strokes.push(...roughEllipse(cx, cy - 42, 30, 6, c2, pickSize(), 12)); strokes.push(...roughRect(cx - 16, cy - 56, 32, 16, c2, pickSize(), 4)); // shirt strokes.push(...roughRect(cx - 18, cy - 6, 36, 40, "#3b82f6", 8, 4)); strokes.push(...hatchRect(cx - 14, cy - 2, 28, 32, c4, 4)); // patches strokes.push(...roughRect(cx - 12, cy + 4, 8, 8, "#eab308", pickSize(), 4)); strokes.push(...roughRect(cx + 4, cy + 12, 8, 8, "#ef4444", pickSize(), 4)); // trousers strokes.push(...roughRect(cx - 16, cy + 34, 14, 35, c2, pickSize(), 4)); strokes.push(...roughRect(cx + 2, cy + 34, 14, 35, c2, pickSize(), 4)); // straw for (let i = 0; i < 4; i++) { strokes.push(roughLine(cx - 46 + i * 30, cy - 8 + wobble(0, 4), cx - 42 + i * 30, cy - 2 + wobble(0, 4), "#eab308", pickSize())); } return strokes; } // ── default stick figure ───────────────────────────────────────────────── strokes.push(...roughEllipse(cx, cy, 30, 30, c1, 8, 14)); strokes.push(...hatchEllipse(cx, cy, 22, 22, c2, 4)); strokes.push(...roughEllipse(cx - 8, cy - 4, 3, 3, c1, pickSize(), 6)); strokes.push(...roughEllipse(cx + 8, cy - 4, 3, 3, c1, pickSize(), 6)); strokes.push(roughLine(cx, cy + 4, cx + 5, cy + 9, c1, pickSize())); strokes.push(roughLine(cx, cy + 30, cx, cy + 120, c2, 10)); strokes.push(roughLine(cx, cy + 50, cx - 50, cy + 70, c2, 8)); strokes.push(roughLine(cx, cy + 50, cx + 50, cy + 70, c2, 8)); strokes.push(roughLine(cx, cy + 120, cx - 30, cy + 170, c2, 8)); strokes.push(roughLine(cx, cy + 120, cx + 30, cy + 170, c2, 8)); strokes.push(...roughEllipse(cx - 35, cy + 168, 10, 5, c2, pickSize(), 8)); strokes.push(...roughEllipse(cx + 25, cy + 168, 10, 5, c2, pickSize(), 8)); return strokes; } function sketchObject(word) { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2), c4 = pickColor(c3); const cx = 250 + Math.random() * 100, cy = 180 + Math.random() * 50; const strokes = []; // ── book ───────────────────────────────────────────────────────────────── if (word.includes("book")) { strokes.push(...roughRect(cx, cy, 85, 115, c1, 8, 4)); strokes.push(...hatchRect(cx + 4, cy + 4, 77, 107, c2, 5)); strokes.push(roughLine(cx + 8, cy + 6, cx + 8, cy + 110, c3, pickSize())); strokes.push(roughLine(cx + 14, cy + 25, cx + 75, cy + 25, c3, pickSize())); strokes.push(roughLine(cx + 14, cy + 45, cx + 75, cy + 45, c3, pickSize())); strokes.push(roughLine(cx + 14, cy + 65, cx + 75, cy + 65, c3, pickSize())); strokes.push(roughLine(cx + 14, cy + 85, cx + 50, cy + 85, c3, pickSize())); strokes.push(...roughEllipse(cx + 70, cy + 12, 4, 4, c4, pickSize(), 6)); strokes.push(...roughEllipse(cx + 70, cy + 22, 4, 4, c4, pickSize(), 6)); return strokes; } // ── clock ──────────────────────────────────────────────────────────────── if (word.includes("clock")) { strokes.push(...roughEllipse(cx, cy, 65, 65, c1, 8, 20)); strokes.push(...hatchEllipse(cx, cy, 55, 55, c2, 6)); strokes.push(...roughEllipse(cx, cy, 50, 50, "#ffffff", pickSize(), 18)); // hour markers for (let i = 0; i < 12; i++) { const a = (i / 12) * Math.PI * 2 - Math.PI/2; const x1 = cx + Math.cos(a) * 44; const y1 = cy + Math.sin(a) * 44; const x2 = cx + Math.cos(a) * 48; const y2 = cy + Math.sin(a) * 48; strokes.push(roughLine(x1, y1, x2, y2, c1, pickSize())); } // hands strokes.push(roughLine(cx, cy, cx + 28, cy - 18, c1, 6)); strokes.push(roughLine(cx, cy, cx - 12, cy + 22, c1, 6)); strokes.push(roughLine(cx, cy, cx - 18, cy - 10, c3, pickSize())); // center strokes.push(...roughEllipse(cx, cy, 6, 6, c1, pickSize(), 8)); strokes.push(...roughEllipse(cx, cy, 3, 3, c3, pickSize(), 6)); // glass shine strokes.push(roughLine(cx - 20, cy - 30, cx - 10, cy - 20, "#ffffff", pickSize())); return strokes; } // ── umbrella ───────────────────────────────────────────────────────────── if (word.includes("umbrella")) { const arcSegs = 12; for (let i = 0; i < arcSegs; i++) { const a1 = Math.PI + (i / arcSegs) * Math.PI; const a2 = Math.PI + ((i + 1) / arcSegs) * Math.PI; strokes.push(stroke( wobble(cx + Math.cos(a1) * 85, 4), wobble(cy + Math.sin(a1) * 45, 4), wobble(cx + Math.cos(a2) * 85, 4), wobble(cy + Math.sin(a2) * 45, 4), i % 2 === 0 ? c1 : c3, 8 )); } // rib lines for (let i = 0; i < 6; i++) { const a = Math.PI + (i / 6) * Math.PI; strokes.push(roughLine( cx, cy, wobble(cx + Math.cos(a) * 82, 4), wobble(cy + Math.sin(a) * 42, 4), c2, pickSize() )); } // handle strokes.push(roughLine(cx, cy, cx + wobble(3, 5), cy + 120, c1, pickSize())); strokes.push(roughLine(cx + wobble(3, 5), cy + 120, cx + wobble(15, 5), cy + 120, c1, pickSize())); strokes.push(roughLine(cx + wobble(15, 5), cy + 120, cx + wobble(15, 5), cy + 115, c1, pickSize())); // tip strokes.push(...roughEllipse(cx, cy - 50, 4, 4, c4, pickSize(), 6)); return strokes; } // ── chair ──────────────────────────────────────────────────────────────── if (word.includes("chair")) { strokes.push(...roughRect(cx - 32, cy + 2, 64, 58, c1, 8, 4)); strokes.push(...hatchRect(cx - 28, cy + 6, 56, 50, c2, 5)); strokes.push(...roughRect(cx - 32, cy - 20, 64, 24, c1, pickSize(), 4)); strokes.push(...hatchRect(cx - 28, cy - 16, 56, 16, c3, 4)); // legs strokes.push(roughLine(cx - 28, cy + 60, cx - 24, cy + 100, c1, pickSize())); strokes.push(roughLine(cx + 28, cy + 60, cx + 24, cy + 100, c1, pickSize())); strokes.push(roughLine(cx - 10, cy + 60, cx - 8, cy + 100, c1, pickSize())); strokes.push(roughLine(cx + 10, cy + 60, cx + 8, cy + 100, c1, pickSize())); // cross brace strokes.push(roughLine(cx - 26, cy + 78, cx + 26, cy + 78, c1, pickSize())); // back slats for (let i = 0; i < 3; i++) { strokes.push(roughLine(cx - 24 + i * 24, cy - 16, cx - 24 + i * 24, cy + 4, c3, pickSize())); } return strokes; } // ── table ──────────────────────────────────────────────────────────────── if (word.includes("table")) { strokes.push(...roughRect(cx - 55, cy, 110, 12, c1, 8, 4)); strokes.push(...hatchRect(cx - 51, cy + 2, 102, 8, c2, 4)); strokes.push(roughLine(cx - 40, cy + 12, cx - 38, cy + 70, c1, pickSize())); strokes.push(roughLine(cx + 40, cy + 12, cx + 38, cy + 70, c1, pickSize())); strokes.push(roughLine(cx - 15, cy + 12, cx - 13, cy + 70, c1, pickSize())); strokes.push(roughLine(cx + 15, cy + 12, cx + 13, cy + 70, c1, pickSize())); // cross braces strokes.push(roughLine(cx - 39, cy + 40, cx + 39, cy + 40, c1, pickSize())); // table edge strokes.push(roughLine(cx - 55, cy, cx + 55, cy, c3, pickSize())); strokes.push(roughLine(cx - 55, cy + 12, cx + 55, cy + 12, c3, pickSize())); return strokes; } // ── pencil ─────────────────────────────────────────────────────────────── if (word.includes("pencil")) { strokes.push(...roughRect(cx, cy - 8, 8, 80, c1, pickSize(), 4)); strokes.push(...hatchRect(cx + 2, cy - 6, 4, 72, c2, 4)); strokes.push(roughLine(cx, cy + 72, cx + 4, cy + 82, c3, pickSize())); strokes.push(roughLine(cx + 8, cy + 72, cx + 4, cy + 82, c3, pickSize())); strokes.push(roughLine(cx, cy + 72, cx + 8, cy + 72, c3, pickSize())); strokes.push(roughLine(cx, cy - 8, cx + 8, cy - 8, "#ef4444", pickSize())); // ferrule strokes.push(roughLine(cx, cy - 4, cx + 8, cy - 4, c4, pickSize())); strokes.push(roughLine(cx, cy - 1, cx + 8, cy - 1, c4, pickSize())); // tip strokes.push(roughLine(cx + 4, cy + 82, cx + 2, cy + 88, c1, pickSize())); strokes.push(roughLine(cx + 4, cy + 82, cx + 6, cy + 88, c1, pickSize())); return strokes; } // ── key ────────────────────────────────────────────────────────────────── if (word.includes("key")) { strokes.push(...roughEllipse(cx, cy, 14, 22, c1, 8, 14)); strokes.push(...roughEllipse(cx, cy, 8, 14, c2, pickSize(), 10)); strokes.push(roughLine(cx + 12, cy, cx + 55, cy, c1, pickSize())); strokes.push(roughLine(cx + 55, cy, cx + 50, cy - 10, c1, pickSize())); strokes.push(roughLine(cx + 50, cy - 10, cx + 45, cy - 10, c1, pickSize())); strokes.push(roughLine(cx + 45, cy, cx + 40, cy - 8, c1, pickSize())); strokes.push(roughLine(cx + 40, cy - 8, cx + 35, cy - 8, c1, pickSize())); strokes.push(roughLine(cx + 35, cy, cx + 30, cy - 6, c1, pickSize())); strokes.push(roughLine(cx + 30, cy - 6, cx + 28, cy - 6, c1, pickSize())); strokes.push(...roughEllipse(cx, cy, 3, 3, c3, pickSize(), 6)); return strokes; } // ── door ───────────────────────────────────────────────────────────────── if (word.includes("door")) { strokes.push(...roughRect(cx - 35, cy, 70, 120, c1, 8, 4)); strokes.push(...hatchRect(cx - 31, cy + 4, 62, 112, c2, 5)); strokes.push(...roughRect(cx - 18, cy + 15, 36, 50, c3, pickSize(), 4)); strokes.push(roughLine(cx - 18, cy + 15, cx - 18, cy + 65, c3, pickSize())); strokes.push(roughLine(cx + 18, cy + 15, cx + 18, cy + 65, c3, pickSize())); strokes.push(roughLine(cx - 18, cy + 40, cx + 18, cy + 40, c3, pickSize())); // handle strokes.push(...roughEllipse(cx + 14, cy + 52, 3, 4, "#eab308", pickSize(), 8)); // frame strokes.push(roughLine(cx - 38, cy - 5, cx + 38, cy - 5, c1, pickSize())); strokes.push(roughLine(cx - 38, cy, cx - 38, cy + 120, c1, pickSize())); strokes.push(roughLine(cx + 38, cy, cx + 38, cy + 120, c1, pickSize())); return strokes; } // ── shoe / boot ────────────────────────────────────────────────────────── if (word.includes("shoe") || word.includes("boot")) { strokes.push(...roughEllipse(cx, cy + 30, 48, 22, c1, 8, 14)); strokes.push(...hatchEllipse(cx, cy + 30, 38, 16, c2, 5)); strokes.push(roughLine(cx - 34, cy + 20, cx - 22, cy - 22, c1, pickSize())); strokes.push(roughLine(cx - 22, cy - 22, cx + 10, cy - 22, c1, pickSize())); // sole strokes.push(roughLine(cx - 42, cy + 42, cx + 42, cy + 42, c1, pickSize())); strokes.push(roughLine(cx - 42, cy + 42, cx - 38, cy + 46, c3, pickSize())); strokes.push(roughLine(cx + 42, cy + 42, cx + 38, cy + 46, c3, pickSize())); // laces for (let i = 0; i < 3; i++) { const lx = cx - 16 + i * 6; strokes.push(roughLine(lx, cy - 16, lx + 4, cy - 10, c3, pickSize())); strokes.push(roughLine(lx + 4, cy - 10, lx, cy - 4, c3, pickSize())); } if (word.includes("boot")) { strokes.push(roughLine(cx - 24, cy - 22, cx - 24, cy - 30, c1, pickSize())); strokes.push(roughLine(cx - 24, cy - 30, cx + 12, cy - 30, c1, pickSize())); } return strokes; } // ── hat ────────────────────────────────────────────────────────────────── if (word.includes("hat")) { strokes.push(...roughEllipse(cx, cy + 10, 55, 10, c1, 8, 14)); strokes.push(...roughRect(cx - 25, cy - 40, 50, 45, c2, pickSize(), 4)); strokes.push(...hatchRect(cx - 21, cy - 36, 42, 37, c3, 5)); strokes.push(...roughRect(cx - 28, cy + 4, 56, 8, c1, pickSize(), 4)); // band strokes.push(roughLine(cx - 25, cy - 5, cx + 25, cy - 5, "#ef4444", pickSize())); // dent strokes.push(roughLine(cx - 15, cy - 35, cx, cy - 42, c2, pickSize())); strokes.push(roughLine(cx, cy - 42, cx + 15, cy - 35, c2, pickSize())); return strokes; } // ── ball ───────────────────────────────────────────────────────────────── if (word.includes("ball")) { strokes.push(...roughEllipse(cx, cy, 45, 45, c1, 8, 20)); strokes.push(...hatchEllipse(cx, cy, 36, 36, c2, 6)); strokes.push(...roughEllipse(cx, cy, 20, 20, c3, pickSize(), 14)); // curve lines strokes.push(roughLine(cx - 40, cy - 15, cx + 40, cy + 15, c3, pickSize())); strokes.push(roughLine(cx - 40, cy + 15, cx + 40, cy - 15, c3, pickSize())); // star/panel details for (let i = 0; i < 5; i++) { const a = (i / 5) * Math.PI * 2; strokes.push(...roughEllipse(cx + Math.cos(a) * 22, cy + Math.sin(a) * 22, 4, 4, c4, pickSize(), 8)); } return strokes; } // ── kite ───────────────────────────────────────────────────────────────── if (word.includes("kite")) { // diamond body strokes.push(roughLine(cx, cy - 55, cx + 40, cy, c1, 8)); strokes.push(roughLine(cx + 40, cy, cx, cy + 55, c1, 8)); strokes.push(roughLine(cx, cy + 55, cx - 40, cy, c1, 8)); strokes.push(roughLine(cx - 40, cy, cx, cy - 55, c1, 8)); strokes.push(...hatchRect(cx - 35, cy - 50, 70, 100, c2, 4)); // cross strokes.push(roughLine(cx - 35, cy, cx + 35, cy, c3, pickSize())); strokes.push(roughLine(cx, cy - 50, cx, cy + 50, c3, pickSize())); // tail strokes.push(roughLine(cx, cy + 55, cx + wobble(5, 10), cy + 90, c3, pickSize())); strokes.push(roughLine(cx + wobble(5, 10), cy + 90, cx + wobble(20, 15), cy + 120, c3, pickSize())); for (let i = 0; i < 3; i++) { const tx = cx + wobble(5, 10) + i * wobble(5, 10); const ty = cy + 80 + i * 20; strokes.push(...roughEllipse(tx, ty, 6, 4, c4, pickSize(), 8)); } // string strokes.push(roughLine(cx, cy - 55, cx - wobble(5, 10), cy - 90, c3, pickSize())); return strokes; } // ── crown ──────────────────────────────────────────────────────────────── if (word.includes("crown")) { strokes.push(roughLine(cx - 40, cy + 30, cx - 40, cy, c1, 8)); strokes.push(roughLine(cx - 40, cy, cx - 20, cy - 30, c1, 8)); strokes.push(roughLine(cx - 20, cy - 30, cx, cy, c1, 8)); strokes.push(roughLine(cx, cy, cx + 20, cy - 30, c1, 8)); strokes.push(roughLine(cx + 20, cy - 30, cx + 40, cy, c1, 8)); strokes.push(roughLine(cx + 40, cy, cx + 40, cy + 30, c1, 8)); strokes.push(roughLine(cx - 40, cy + 30, cx + 40, cy + 30, c1, 8)); strokes.push(...hatchRect(cx - 36, cy + 2, 72, 26, c2, 5)); // jewels strokes.push(...roughEllipse(cx - 20, cy + 12, 5, 5, "#ef4444", pickSize(), 8)); strokes.push(...roughEllipse(cx, cy + 12, 5, 5, "#3b82f6", pickSize(), 8)); strokes.push(...roughEllipse(cx + 20, cy + 12, 5, 5, "#22c55e", pickSize(), 8)); // top jewels strokes.push(...roughEllipse(cx - 30, cy - 12, 3, 3, "#eab308", pickSize(), 6)); strokes.push(...roughEllipse(cx, cy - 5, 3, 3, "#eab308", pickSize(), 6)); strokes.push(...roughEllipse(cx + 30, cy - 12, 3, 3, "#eab308", pickSize(), 6)); return strokes; } // ── flag ───────────────────────────────────────────────────────────────── if (word.includes("flag")) { strokes.push(roughLine(cx, cy + 60, cx, cy - 60, c1, 8)); strokes.push(...roughRect(cx + 4, cy - 20, 60, 40, c2, pickSize(), 4)); strokes.push(...hatchRect(cx + 8, cy - 16, 52, 32, c3, 5)); // design on flag strokes.push(...roughEllipse(cx + 34, cy, 10, 10, "#ef4444", pickSize(), 10)); strokes.push(roughLine(cx + 24, cy, cx + 44, cy, "#ef4444", pickSize())); strokes.push(roughLine(cx + 34, cy - 10, cx + 34, cy + 10, "#ef4444", pickSize())); // pole top strokes.push(...roughEllipse(cx, cy - 63, 4, 4, "#eab308", pickSize(), 8)); // base strokes.push(...roughRect(cx - 6, cy + 58, 12, 8, c1, pickSize(), 4)); return strokes; } // ── cup ────────────────────────────────────────────────────────────────── if (word.includes("cup")) { strokes.push(...roughRect(cx - 20, cy, 40, 50, c1, 8, 4)); strokes.push(...hatchRect(cx - 16, cy + 4, 32, 42, c2, 5)); strokes.push(roughLine(cx - 20, cy, cx + 20, cy, c1, pickSize())); strokes.push(roughLine(cx - 22, cy + 50, cx + 22, cy + 50, c1, pickSize())); // handle strokes.push(roughLine(cx + 20, cy + 10, cx + 32, cy + 10, c1, pickSize())); strokes.push(roughLine(cx + 32, cy + 10, cx + 32, cy + 30, c1, pickSize())); strokes.push(roughLine(cx + 32, cy + 30, cx + 20, cy + 30, c1, pickSize())); // saucer strokes.push(...roughEllipse(cx, cy + 54, 30, 8, c1, pickSize(), 12)); strokes.push(...hatchEllipse(cx, cy + 54, 24, 5, c3, 3)); // coffee line strokes.push(roughLine(cx - 16, cy + 8, cx + 16, cy + 8, c2, pickSize())); // steam for (let i = 0; i < 3; i++) { strokes.push(roughLine(cx - 8 + i * 8, cy - 2, cx - 6 + i * 8, cy - 12, c4, pickSize())); strokes.push(roughLine(cx - 6 + i * 8, cy - 12, cx - 10 + i * 8, cy - 20, c4, pickSize())); } return strokes; } // ── balloon ────────────────────────────────────────────────────────────── if (word.includes("balloon")) { strokes.push(...roughEllipse(cx, cy, 26, 32, c3, 8, 16)); strokes.push(...hatchEllipse(cx, cy, 20, 26, c2, 5)); strokes.push(...roughEllipse(cx, cy, 10, 12, c4, pickSize(), 10)); strokes.push(roughLine(cx, cy + 32, cx + wobble(2, 4), cy + 70, c1, pickSize())); strokes.push(roughLine(cx, cy + 32, cx - 2, cy + 38, c1, pickSize())); // shine strokes.push(roughLine(cx - 8, cy - 12, cx - 3, cy - 8, "#ffffff", pickSize())); // knot strokes.push(roughLine(cx - 3, cy + 32, cx + 3, cy + 32, c1, pickSize())); strokes.push(roughLine(cx - 2, cy + 33, cx + 2, cy + 33, c1, pickSize())); return strokes; } // ── guitar ─────────────────────────────────────────────────────────────── if (word.includes("guitar")) { // body strokes.push(...roughEllipse(cx, cy + 20, 30, 38, c1, 8, 16)); strokes.push(...roughEllipse(cx, cy - 20, 28, 34, c1, 8, 16)); strokes.push(...roughRect(cx - 12, cy - 10, 24, 20, c1, pickSize(), 4)); strokes.push(...hatchEllipse(cx, cy + 20, 22, 28, c2, 5)); strokes.push(...hatchEllipse(cx, cy - 20, 20, 24, c2, 5)); // neck strokes.push(...roughRect(cx - 5, cy - 70, 10, 50, c1, pickSize(), 4)); strokes.push(roughLine(cx - 5, cy - 70, cx + 5, cy - 70, c3, pickSize())); // strings for (let i = 0; i < 4; i++) { const sx = cx - 3 + i * 2; strokes.push(roughLine(sx, cy - 68, sx, cy + 50, c3, pickSize())); } // sound hole strokes.push(...roughEllipse(cx, cy, 10, 10, c1, pickSize(), 10)); strokes.push(...roughEllipse(cx, cy, 6, 6, c4, pickSize(), 8)); // pegs for (let i = 0; i < 4; i++) { strokes.push(...roughEllipse(cx - 4 + i * 2, cy - 72, 2, 2, c4, pickSize(), 6)); } return strokes; } // ── camera ─────────────────────────────────────────────────────────────── if (word.includes("camera")) { strokes.push(...roughRect(cx - 40, cy + 10, 80, 50, c1, 8, 4)); strokes.push(...hatchRect(cx - 36, cy + 14, 72, 42, c2, 5)); strokes.push(...roughRect(cx - 30, cy, 60, 18, c1, pickSize(), 4)); // flash strokes.push(...roughRect(cx + 18, cy + 2, 16, 12, c2, pickSize(), 4)); strokes.push(...roughRect(cx + 20, cy + 4, 12, 8, "#eab308", pickSize(), 4)); // lens strokes.push(...roughEllipse(cx, cy + 30, 20, 18, c3, 8, 14)); strokes.push(...roughEllipse(cx, cy + 30, 14, 12, c1, pickSize(), 12)); strokes.push(...roughEllipse(cx, cy + 30, 6, 6, "#3b82f6", pickSize(), 10)); // viewfinder strokes.push(...roughRect(cx - 22, cy + 4, 10, 8, c3, pickSize(), 4)); // button strokes.push(...roughEllipse(cx, cy + 2, 4, 3, c4, pickSize(), 6)); return strokes; } // ── default object ─────────────────────────────────────────────────────── strokes.push(...roughRect(cx, cy, 80 + Math.random() * 40, 80 + Math.random() * 40, c1, 8, 4)); strokes.push(...hatchRect(cx + 4, cy + 4, 72 + Math.random() * 36, 72 + Math.random() * 36, c2, 5)); strokes.push(...roughEllipse(cx + 40, cy + 20, 10, 10, c3, pickSize(), 10)); strokes.push(...roughEllipse(cx + 20, cy + 40, 8, 8, c3, pickSize(), 8)); strokes.push(roughLine(cx + 10, cy + 10, cx + 50, cy + 50, c4, pickSize())); strokes.push(roughLine(cx + 50, cy + 10, cx + 10, cy + 50, c4, pickSize())); return strokes; } function sketchAbstract(word) { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2), c4 = pickColor(c3); const cx = 300 + Math.random() * 100, cy = 200 + Math.random() * 60; const strokes = []; // ── heart ──────────────────────────────────────────────────────────────── if (word.includes("heart")) { const s = 55; strokes.push(...roughEllipse(cx - s/2, cy, s/2, s/2, "#ef4444", 8, 16)); strokes.push(...hatchEllipse(cx - s/2, cy, s/2 - 4, s/2 - 4, "#dc2626", 5)); strokes.push(...roughEllipse(cx + s/2, cy, s/2, s/2, "#ef4444", 8, 16)); strokes.push(...hatchEllipse(cx + s/2, cy, s/2 - 4, s/2 - 4, "#dc2626", 5)); strokes.push(roughLine(cx - s, cy + 5, cx, cy + s * 1.25, "#ef4444", 8)); strokes.push(roughLine(cx + s, cy + 5, cx, cy + s * 1.25, "#ef4444", 8)); strokes.push(roughLine(cx - s + 2, cy + 5, cx, cy + s * 1.2, "#dc2626", pickSize())); strokes.push(roughLine(cx + s - 2, cy + 5, cx, cy + s * 1.2, "#dc2626", pickSize())); // shine strokes.push(roughLine(cx - 15, cy - 12, cx - 8, cy - 6, "#ffffff", pickSize())); strokes.push(roughLine(cx + 10, cy - 12, cx + 17, cy - 6, "#ffffff", pickSize())); return strokes; } // ── star ───────────────────────────────────────────────────────────────── if (word.includes("star")) { for (let i = 0; i < 5; i++) { const a1 = (i * 2 * Math.PI / 5) - Math.PI/2; const a2 = ((i + 1) * 2 * Math.PI / 5) - Math.PI/2; strokes.push(roughLine( cx + Math.cos(a1) * 55, cy + Math.sin(a1) * 55, cx + Math.cos(a2) * 55, cy + Math.sin(a2) * 55, c3, 8 )); } // inner star for (let i = 0; i < 5; i++) { const a1 = (i * 2 * Math.PI / 5) - Math.PI/2; const a2 = ((i + 1) * 2 * Math.PI / 5) - Math.PI/2; strokes.push(roughLine( cx + Math.cos(a1) * 25, cy + Math.sin(a1) * 25, cx + Math.cos(a2) * 25, cy + Math.sin(a2) * 25, c4, pickSize() )); } // center strokes.push(...roughEllipse(cx, cy, 6, 6, "#eab308", pickSize(), 8)); // shine for (let i = 0; i < 5; i++) { const a = (i * 2 * Math.PI / 5) - Math.PI/2; strokes.push(roughLine( cx + Math.cos(a) * 58, cy + Math.sin(a) * 58, cx + Math.cos(a) * 68, cy + Math.sin(a) * 68, c3, pickSize() )); } return strokes; } // ── rainbow ────────────────────────────────────────────────────────────── if (word.includes("rainbow")) { const colors = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6"]; for (let ri = 0; ri < colors.length; ri++) { const r = 130 - ri * 16; const segs = 16; for (let i = 0; i < segs; i++) { const a1 = Math.PI + (i / segs) * Math.PI; const a2 = Math.PI + ((i + 1) / segs) * Math.PI; strokes.push(stroke( wobble(cx + Math.cos(a1) * r, 2), wobble(cy + Math.sin(a1) * r * 0.5, 2), wobble(cx + Math.cos(a2) * r, 2), wobble(cy + Math.sin(a2) * r * 0.5, 2), colors[ri], pickSize() )); } } // pot of gold strokes.push(...roughEllipse(cx - 120, cy + 30, 14, 10, c1, pickSize(), 10)); strokes.push(...roughEllipse(cx - 120, cy + 30, 8, 6, "#eab308", pickSize(), 8)); for (let i = 0; i < 3; i++) { strokes.push(...roughEllipse(cx - 120 + wobble(0, 6), cy + 25 + wobble(0, 4), 3, 3, "#eab308", pickSize(), 6)); } // clouds at ends for (let i = 0; i < 3; i++) { strokes.push(...roughEllipse(cx - 140 + i * 20, cy + 40, 15, 8, "#ffffff", pickSize(), 10)); strokes.push(...roughEllipse(cx + 120 + i * 20, cy + 40, 15, 8, "#ffffff", pickSize(), 10)); } return strokes; } // ── bubbles ────────────────────────────────────────────────────────────── if (word.includes("bubbles")) { for (let bi = 0; bi < 6; bi++) { const bx = cx + wobble(0, 80); const by = cy + wobble(0, 60); const br = 15 + Math.random() * 25; strokes.push(...roughEllipse(bx, by, br, br, "#93c5fd", pickSize(), 14)); strokes.push(...roughEllipse(bx, by, br - 2, br - 2, "#e0f2fe", pickSize(), 12)); // shine strokes.push(roughLine(bx - br * 0.3, by - br * 0.3, bx - br * 0.1, by - br * 0.1, "#ffffff", pickSize())); } return strokes; } // ── ghost ──────────────────────────────────────────────────────────────── if (word.includes("ghost")) { strokes.push(...roughEllipse(cx, cy, 52, 62, c1, 8, 18)); strokes.push(...hatchEllipse(cx, cy, 42, 52, c2, 5)); // wavy bottom for (let i = 0; i < 5; i++) { const x1 = cx - 52 + (i / 5) * 104; const x2 = cx - 52 + ((i + 1) / 5) * 104; const wy = i % 2 === 0 ? 10 : -6; strokes.push(roughLine(x1, cy + 62, x2, cy + 62 + wy, c1, pickSize())); strokes.push(roughLine(x1, cy + 62 + wy, x2, cy + 62 + wy + (i % 2 === 0 ? 4 : -2), c3, pickSize())); } // eyes strokes.push(...roughEllipse(cx - 15, cy - 16, 7, 9, c1, pickSize(), 10)); strokes.push(...roughEllipse(cx - 15, cy - 16, 4, 5, "#1e293b", pickSize(), 8)); strokes.push(...roughEllipse(cx + 15, cy - 16, 7, 9, c1, pickSize(), 10)); strokes.push(...roughEllipse(cx + 15, cy - 16, 4, 5, "#1e293b", pickSize(), 8)); // mouth strokes.push(...roughEllipse(cx, cy + 6, 8, 6, "#1e293b", pickSize(), 8)); // blush strokes.push(...roughEllipse(cx - 24, cy + 2, 6, 4, "#fca5a5", pickSize(), 8)); strokes.push(...roughEllipse(cx + 24, cy + 2, 6, 4, "#fca5a5", pickSize(), 8)); return strokes; } // ── tornado ────────────────────────────────────────────────────────────── if (word.includes("tornado")) { for (let i = 0; i < 8; i++) { const r = 55 - i * 6; const vy = i * 18; strokes.push(...roughEllipse(cx, cy + vy, r, 14, c1, pickSize(), 14)); strokes.push(...roughEllipse(cx, cy + vy, r - 4, 8, c2, pickSize(), 10)); } // debris for (let i = 0; i < 6; i++) { const dx = cx + wobble(0, 70); const dy = cy + wobble(0, 80); strokes.push(...roughEllipse(dx, dy, 4 + Math.random() * 6, 4 + Math.random() * 4, c3, pickSize(), 8)); strokes.push(roughLine(dx, dy, dx + wobble(0, 10), dy + wobble(0, 8), c4, pickSize())); } return strokes; } // ── earthquake ─────────────────────────────────────────────────────────── if (word.includes("earthquake")) { // cracked ground for (let i = 0; i < 8; i++) { const gx = cx - 100 + i * 28 + wobble(0, 8); strokes.push(roughLine(gx, cy + 40, gx + 15, cy + 45 + wobble(0, 8), c1, pickSize())); strokes.push(roughLine(gx + 15, cy + 45, gx + 10, cy + 55, c1, pickSize())); } // zigzag crack for (let i = 0; i < 6; i++) { const zx = cx - 50 + i * 20; const zy = cy + 30 - i * 10 + wobble(0, 8); strokes.push(roughLine(zx, zy, zx + 20, zy + 15, c2, pickSize())); strokes.push(roughLine(zx + 20, zy + 15, zx + 10, zy + 28, c2, pickSize())); } // falling rocks for (let i = 0; i < 4; i++) { strokes.push(...roughEllipse(cx - 60 + i * 30, cy - 20 + wobble(0, 15), 10 + i * 3, 8 + i * 2, "#6b7280", pickSize(), 8)); } // shake lines for (let i = 0; i < 5; i++) { strokes.push(roughLine(cx - 80 + i * 40, cy - 20, cx - 70 + i * 40 + wobble(0, 10), cy - 10, c3, pickSize())); } return strokes; } // ── avalanche ──────────────────────────────────────────────────────────── if (word.includes("avalanche")) { // mountain top strokes.push(roughLine(cx - 80, cy + 80, cx, cy - 40, c1, 8)); strokes.push(roughLine(cx, cy - 40, cx + 80, cy + 80, c1, 8)); // snow cap strokes.push(roughLine(cx - 40, cy + 20, cx, cy - 40, "#ffffff", pickSize())); strokes.push(roughLine(cx, cy - 40, cx + 40, cy + 20, "#ffffff", pickSize())); // falling snow for (let i = 0; i < 8; i++) { const sx = cx + wobble(0, 100); const sy = cy + wobble(0, 60); strokes.push(...roughEllipse(sx, sy, 8 + Math.random() * 10, 6 + Math.random() * 6, "#ffffff", pickSize(), 10)); strokes.push(...roughEllipse(sx, sy, 4, 3, "#e2e8f0", pickSize(), 8)); } // avalanche trail for (let i = 0; i < 5; i++) { strokes.push(roughLine(cx - 60 + i * 15, cy + 50 + i * 6, cx - 40 + i * 20, cy + 65 + i * 6, "#ffffff", pickSize())); } // snow particles for (let i = 0; i < 6; i++) { strokes.push(...roughEllipse(cx + wobble(0, 50), cy + 40 + wobble(0, 30), 4, 3, "#e2e8f0", pickSize(), 6)); } return strokes; } // ── shipwreck ──────────────────────────────────────────────────────────── if (word.includes("shipwreck")) { // broken ship strokes.push(roughLine(cx - 40, cy + 40, cx + 50, cy + 40, c1, 8)); strokes.push(roughLine(cx - 40, cy + 40, cx - 25, cy + 65, c1, 8)); strokes.push(roughLine(cx - 25, cy + 65, cx + 40, cy + 65, c1, 8)); strokes.push(roughLine(cx + 40, cy + 65, cx + 50, cy + 40, c1, 8)); // broken mast strokes.push(roughLine(cx - 5, cy + 40, cx - 10, cy - 30, c2, pickSize())); strokes.push(roughLine(cx - 10, cy - 30, cx - 20, cy - 20, c2, pickSize())); // torn sail strokes.push(roughLine(cx - 5, cy + 5, cx + 30, cy - 10, c3, pickSize())); strokes.push(roughLine(cx + 30, cy - 10, cx + 35, cy + 25, c3, pickSize())); strokes.push(roughLine(cx + 35, cy + 25, cx - 5, cy + 5, c3, pickSize())); // water for (let i = 0; i < 8; i++) { strokes.push(roughLine(cx - 60 + i * 15, cy + 70 + i * 3, cx - 45 + i * 18, cy + 76 + i * 4, "#3b82f6", pickSize())); } // debris for (let i = 0; i < 4; i++) { strokes.push(roughLine(cx - 50 + i * 8, cy + 55 + wobble(0, 5), cx - 40 + i * 10, cy + 60 + wobble(0, 5), c2, pickSize())); } // waves for (let i = 0; i < 4; i++) { const wx = cx - 50 + i * 35; strokes.push(roughLine(wx, cy + 73, wx + 12, cy + 68, "#93c5fd", pickSize())); strokes.push(roughLine(wx + 12, cy + 68, wx + 24, cy + 73, "#93c5fd", pickSize())); } return strokes; } // ── default abstract ───────────────────────────────────────────────────── strokes.push(...roughEllipse(cx, cy, 65 + Math.random() * 40, 65 + Math.random() * 40, c1, 8, 18)); strokes.push(...hatchEllipse(cx, cy, 55 + Math.random() * 30, 55 + Math.random() * 30, c2, 5)); strokes.push(...roughEllipse(cx + 30, cy - 20, 22 + Math.random() * 20, 22 + Math.random() * 20, c3, pickSize(), 14)); strokes.push(...roughEllipse(cx - 25, cy + 25, 15 + Math.random() * 15, 15 + Math.random() * 15, c4, pickSize(), 12)); strokes.push(roughLine(cx + 10, cy + 10, cx + 40, cy + 40, c3, pickSize())); strokes.push(roughLine(cx + 40, cy + 10, cx + 10, cy + 40, c3, pickSize())); strokes.push(...roughEllipse(cx, cy, 8, 8, c3, pickSize(), 8)); return strokes; } function sketchDefault(word) { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1); const cx = 300 + Math.random() * 100, cy = 200 + Math.random() * 60; const strokes = []; strokes.push(...roughEllipse(cx, cy, 80 + Math.random() * 40, 60 + Math.random() * 30, c1, 8)); const extras = 3 + Math.floor(Math.random() * 5); for (let i = 0; i < extras; i++) { strokes.push(roughLine( cx + wobble(0, 60), cy + wobble(0, 40), cx + wobble(0, 80), cy + wobble(0, 60), c2, pickSize() )); } return strokes; } function quickDrawToStrokes(drawing) { const strokes = []; const color = AIDRAW_COLORS[Math.floor(Math.random() * AIDRAW_COLORS.length)]; const size = pickSize(); for (const stroke of drawing) { const xs = stroke[0], ys = stroke[1]; if (!xs || xs.length < 1) continue; const sx = xs[0] * 700 / 255; const sy = ys[0] * 480 / 255; strokes.push({ x1: sx, y1: sy, x2: sx, y2: sy, color, size }); for (let i = 1; i < xs.length; i++) { strokes.push({ x1: xs[i - 1] * 700 / 255, y1: ys[i - 1] * 480 / 255, x2: xs[i] * 700 / 255, y2: ys[i] * 480 / 255, color, size }); } } return strokes; } function sketchSmileyFace() { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2); const cx = 300 + Math.random() * 100, cy = 200 + Math.random() * 60; const r = 60 + Math.random() * 30; const strokes = []; strokes.push(...roughEllipse(cx, cy, r, r, c1, 6)); const eyeOffset = r * 0.25, eyeR = 4 + Math.random() * 3; strokes.push(...roughEllipse(cx - eyeOffset, cy - r * 0.15, eyeR, eyeR, c2, 4)); strokes.push(...roughEllipse(cx + eyeOffset, cy - r * 0.15, eyeR, eyeR, c2, 4)); const mouthR = r * 0.4, mouthY = cy + r * 0.2; let lx = 0, ly = 0; for (let a = 0; a <= Math.PI; a += 0.15) { const x1 = cx + mouthR * Math.cos(a), y1 = mouthY + mouthR * 0.5 * Math.sin(a); if (a === 0) strokes.push({ x1, y1, x2: x1, y2: y1, color: c3, size: 4 }); else strokes.push({ x1: lx, y1: ly, x2: x1, y2: y1, color: c3, size: 4 }); lx = x1; ly = y1; } return strokes; } function sketchSeaTurtle() { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2); const cx = 350, cy = 220; const strokes = []; strokes.push(...roughEllipse(cx, cy, 80 + Math.random() * 20, 50 + Math.random() * 15, c1, 6)); strokes.push(...roughEllipse(cx - 50, cy, 10, 6, c2, 3)); for (let side = -1; side <= 1; side += 2) { strokes.push(...roughLine(cx + side * 40, cy, cx + side * 80, cy - 40 + Math.random() * 20, c3, 4)); strokes.push(...roughLine(cx + side * 40, cy, cx + side * 80, cy + 40 - Math.random() * 20, c3, 4)); } return strokes; } function sketchHotAirBalloon() { const c1 = AIDRAW_COLORS[0], c2 = pickColor(c1), c3 = pickColor(c2); const cx = 350, cy = 180; const strokes = []; strokes.push(...roughEllipse(cx, cy - 20, 50 + Math.random() * 15, 60 + Math.random() * 15, c1, 5)); const basketX = cx - 10 + Math.random() * 5, basketY = cy + 40 + Math.random() * 10; strokes.push(...roughRect(basketX, basketY, 20 + Math.random() * 5, 12 + Math.random() * 4, c2, 3)); strokes.push(...roughLine(cx, cy + 20, basketX + 10, basketY, c3, 3)); const cx2 = 350 + Math.random() * 30, cy2 = 140 + Math.random() * 20; strokes.push(...roughEllipse(cx2, cy2 - 15, 35 + Math.random() * 10, 45 + Math.random() * 10, c1, 4)); return strokes; } function getAISketch(word) { const w = word.toLowerCase(); // Use real QuickDraw human drawings when available if (qdSamples[w] && qdSamples[w].length > 0) { const drawing = qdSamples[w][Math.floor(Math.random() * qdSamples[w].length)]; return quickDrawToStrokes(drawing); } // Special cases for words without QuickDraw samples if (w === "smiley face") return sketchSmileyFace(); if (w === "sea turtle") return sketchSeaTurtle(); if (w === "hot air balloon") return sketchHotAirBalloon(); if (CATEGORIES.animals.some(a => w.includes(a))) return sketchAnimal(w); if (CATEGORIES.buildings.some(b => w.includes(b))) return sketchBuilding(w); if (CATEGORIES.nature.some(n => w.includes(n))) return sketchNature(w); if (CATEGORIES.vehicles.some(v => w.includes(v))) return sketchVehicle(w); if (CATEGORIES.food.some(f => w.includes(f))) return sketchFood(w); if (CATEGORIES.persons.some(p => w.includes(p))) return sketchPerson(w); if (CATEGORIES.objects.some(o => w.includes(o))) return sketchObject(w); if (CATEGORIES.abstracts.some(a => w.includes(a))) return sketchAbstract(w); return sketchDefault(w); } function startAIDrawing(roomId) { const room = rooms[roomId]; if (!room) return; io.to(roomId).emit("clear_canvas"); room.strokes = []; const word = room.currentWord || ""; const strokes = getAISketch(word); if (strokes.length === 0) return; let delay = 500 + Math.random() * 1000; // initial "thinking" pause let lastColor = null; let lastPos = null; let consecutiveQuick = 0; strokes.forEach((s) => { const isColorChange = s.color !== lastColor; // detect sections via spatial jumps or too many quick strokes let isNewSection = false; if (lastPos) { const dist = Math.sqrt((s.x1 - lastPos.x) ** 2 + (s.y1 - lastPos.y) ** 2); if (dist > 160 || consecutiveQuick > 24) isNewSection = true; } if (isColorChange) { delay += 400 + Math.random() * 600; // pause to pick new colour consecutiveQuick = 0; } else if (isNewSection) { delay += 300 + Math.random() * 600; // pause before new section consecutiveQuick = 0; } // base stroke timing delay += 60 + Math.random() * 180; consecutiveQuick++; // hesitation pauses (like deciding where to place the line) if (Math.random() < 0.08) delay += 200 + Math.random() * 400; // long strokes take slightly longer const len = Math.sqrt((s.x2 - s.x1) ** 2 + (s.y2 - s.y1) ** 2); if (len > 90) delay += 80 + Math.random() * 200; // occasional "thinking about next part" long pause if (Math.random() < 0.03) delay += 400 + Math.random() * 800; // colour-change hesitation (extra) if (isColorChange) delay += 150 + Math.random() * 250; lastColor = s.color; lastPos = { x: s.x2, y: s.y2 }; const timer = setTimeout(() => { const r = rooms[roomId]; if (!r) return; r.strokes.push(s); io.to(roomId).emit("draw", s); }, delay); room.aiTimers.push(timer); }); } function scheduleAIGuess(roomId, aiPlayer) { const room = rooms[roomId]; if (!room) return; if (!room.aiGuesses.has(aiPlayer.id)) { room.aiGuesses.set(aiPlayer.id, new Set()); } const guessedWords = room.aiGuesses.get(aiPlayer.id); const totalLetters = room.currentWord ? room.currentWord.replace(/ /g, "").length : 1; const hintRatio = (room.revealedPositions ? room.revealedPositions.size : 0) / totalLetters; const minDelay = Math.max(1200, 3500 - hintRatio * 2500); const maxDelay = Math.max(2200, 5000 - hintRatio * 3000); const delay = minDelay + Math.random() * (maxDelay - minDelay); const timer = setTimeout(() => { if (!room || room.phase !== "drawing") return; if (room.guessedPlayers.has(aiPlayer.id)) return; if (room.players[room.drawerIndex]?.id === aiPlayer.id) return; const guess = generateAIGuess(room.currentWord, room.revealedPositions, guessedWords); guessedWords.add(guess); const correct = processGuess(roomId, aiPlayer.id, aiPlayer.name, guess); if (!correct && room.phase === "drawing" && !room.guessedPlayers.has(aiPlayer.id)) { scheduleAIGuess(roomId, aiPlayer); } }, delay); room.aiTimers.push(timer); } function processGuess(roomId, guesserId, guesserName, text) { const room = rooms[roomId]; if (!room || room.phase !== "drawing") return false; const drawer = room.players[room.drawerIndex]; if (drawer?.id === guesserId) return false; if (room.guessedPlayers.has(guesserId)) return false; const guess = text.trim().toLowerCase(); const word = room.currentWord?.toLowerCase(); if (guess === word) { room.guessedPlayers.add(guesserId); const elapsed = (Date.now() - room.roundStart) / 1000; const points = Math.max(50, Math.round(500 - elapsed * 5)); const drawerPoints = Math.round(points * 0.3); const guesser = room.players.find(p => p.id === guesserId); if (guesser) guesser.score += points; if (drawer) drawer.score += drawerPoints; if (room.settings.teamsMode) { if (guesser?.team) room.teamScores[guesser.team] += points; if (drawer?.team) room.teamScores[drawer.team] += drawerPoints; } const payload = { playerId: guesserId, playerName: guesserName, points, scores: room.players.map(p => ({ id: p.id, name: p.name, score: p.score, team: p.team, isAI: p.isAI })), }; if (room.settings.teamsMode) { payload.teamScores = { ...room.teamScores }; } io.to(roomId).emit("correct_guess", payload); const nonDrawers = room.players.filter(p => p.id !== drawer?.id); if (room.guessedPlayers.size >= nonDrawers.length) endRound(roomId, true); return true; } else { io.to(roomId).emit("chat_message", { playerId: guesserId, playerName: guesserName, text, isClose: word && (Math.abs(guess.length - word.length) <= 2 && guess[0] === word[0]), }); return false; } } function startRound(roomId) { const room = rooms[roomId]; if (!room || room.players.length < 2) return; room.drawerIndex = (room.drawerIndex + 1) % room.players.length; const drawer = room.players[room.drawerIndex]; room.currentWord = null; room.wordChoices = getWords(3); room.guessedPlayers = new Set(); room.revealedPositions = new Set(); room.phase = "choosing"; room.roundStart = null; io.to(roomId).emit("round_start", { drawer: drawer.id, drawerName: drawer.name, phase: "choosing", round: room.round + 1, teamsMode: room.settings.teamsMode, teamScores: room.settings.teamsMode ? { ...room.teamScores } : undefined, }); if (!drawer.isAI) { io.to(drawer.id).emit("choose_word", { words: room.wordChoices, hints: room.wordChoices.map(w => WORD_HINTS[w] || "") }); } if (drawer.isAI) { const timer = setTimeout(() => { const r = rooms[roomId]; if (!r || r.phase !== "choosing") return; r.currentWord = r.wordChoices[0]; beginDrawing(roomId); }, 1000); room.aiTimers.push(timer); } } function beginDrawing(roomId) { const room = rooms[roomId]; if (!room) return; const drawer = room.players[room.drawerIndex]; room.phase = "drawing"; room.roundStart = Date.now(); const wordLen = room.currentWord.split(" ").map(w => w.length); io.to(roomId).emit("drawing_phase", { drawer: drawer.id, drawerName: drawer.name, wordLength: wordLen, hint: getHint(room.currentWord, new Set()), timeLimit: 35, round: room.round + 1, teamsMode: room.settings.teamsMode, teamScores: room.settings.teamsMode ? { ...room.teamScores } : undefined, }); io.to(drawer.id).emit("your_word", { word: room.currentWord }); if (drawer.isAI) { const timer = setTimeout(() => startAIDrawing(roomId), 1000); room.aiTimers.push(timer); } room.players.forEach(p => { if (p.isAI && p.id !== drawer.id) { scheduleAIGuess(roomId, p); } }); const letters = room.currentWord.replace(/ /g, "").length; const maxHints = Math.floor(letters / 2); const hintEvery = Math.floor(35000 / (maxHints + 1)); room.hintInterval = setInterval(() => { const unrevealed = []; for (let i = 0; i < room.currentWord.length; i++) { if (room.currentWord[i] !== " " && !room.revealedPositions.has(i)) unrevealed.push(i); } if (unrevealed.length > 0) { room.revealedPositions.add(unrevealed[Math.floor(Math.random() * unrevealed.length)]); } io.to(roomId).emit("hint_update", { hint: getHint(room.currentWord, room.revealedPositions) }); if (room.revealedPositions.size >= maxHints) clearInterval(room.hintInterval); }, hintEvery); room.roundTimeout = setTimeout(() => endRound(roomId, false), 35000); } function endRound(roomId, allGuessed) { const room = rooms[roomId]; if (!room) return; clearTimeout(room.roundTimeout); clearInterval(room.hintInterval); clearAITimers(room); room.phase = "reveal"; room.round++; const payload = { word: room.currentWord, scores: room.players.map(p => ({ id: p.id, name: p.name, score: p.score, team: p.team, isAI: p.isAI })), }; if (room.settings.teamsMode) { payload.teamScores = { ...room.teamScores }; } io.to(roomId).emit("round_end", payload); if (room.round >= room.maxRounds * room.players.length) { setTimeout(() => endGame(roomId), 4000); } else { setTimeout(() => startRound(roomId), 4000); } } function endGame(roomId) { const room = rooms[roomId]; if (!room) return; room.phase = "ended"; clearAITimers(room); const sorted = [...room.players].sort((a, b) => b.score - a.score); const payload = { scores: sorted.map(p => ({ id: p.id, name: p.name, score: p.score, team: p.team, isAI: p.isAI })), }; if (room.settings.teamsMode) { payload.teamScores = { ...room.teamScores }; } io.to(roomId).emit("game_over", payload); } io.on("connection", (socket) => { socket.on("create_room", ({ name, rounds, uid }) => { if (rounds === undefined) rounds = 3; const roomId = Math.random().toString(36).substr(2, 6).toUpperCase(); rooms[roomId] = { players: [{ id: socket.id, name, score: 0, isHost: true, uid, team: null, isAI: false }], phase: "lobby", round: 0, maxRounds: rounds, drawerIndex: -1, currentWord: null, wordChoices: [], guessedPlayers: new Set(), revealedPositions: new Set(), strokes: [], settings: { teamsMode: false, aiEnabled: false }, teamScores: { red: 0, blue: 0 }, aiTimers: [], aiGuesses: new Map(), }; socket.join(roomId); socket.roomId = roomId; socket.playerName = name; socket.emit("room_created", { roomId, playerId: socket.id, settings: rooms[roomId].settings }); io.to(roomId).emit("player_list", { players: rooms[roomId].players }); }); socket.on("join_room", ({ roomId, name, uid }) => { const room = rooms[roomId]; if (!room) return socket.emit("error", { msg: "Room not found" }); if (room.phase !== "lobby") return socket.emit("error", { msg: "Game already started" }); if (room.players.length >= 6) return socket.emit("error", { msg: "Room is full" }); if (uid && room.players.find(p => p.uid === uid)) return socket.emit("error", { msg: "You're already in this room!" }); room.players.push({ id: socket.id, name, score: 0, isHost: false, uid, team: null, isAI: false }); if (room.settings.aiEnabled && getHumanCount(room) > 1) { room.settings.aiEnabled = false; removeAIPlayer(room, roomId); io.to(roomId).emit("room_settings", { settings: room.settings }); } if (room.settings.teamsMode) assignTeam(room, socket.id); if (room.settings.teamsMode && room.players.length !== 4 && room.players.length !== 6) { room.settings.teamsMode = false; room.players.forEach(p => { p.team = null; }); io.to(roomId).emit("room_settings", { settings: room.settings }); io.to(roomId).emit("chat_message", { playerName: "System", text: "Team mode disabled — need exactly 4 or 6 players.", isSystem: true }); } socket.join(roomId); socket.roomId = roomId; socket.playerName = name; socket.emit("room_joined", { roomId, playerId: socket.id }); socket.emit("room_settings", { settings: room.settings }); io.to(roomId).emit("player_list", { players: room.players }); }); socket.on("start_game", () => { const room = rooms[socket.roomId]; if (!room) return; const player = room.players.find(p => p.id === socket.id); if (!player?.isHost) return; if (room.settings.teamsMode) { if (room.players.length < 4) return socket.emit("error", { msg: "Team mode needs at least 4 players" }); if (room.players.length !== 4 && room.players.length !== 6) return socket.emit("error", { msg: "Team mode needs exactly 4 or 6 players" }); } else { if (room.players.length < 2) return socket.emit("error", { msg: "Need at least 2 players" }); } room.phase = "playing"; io.to(socket.roomId).emit("game_started"); setTimeout(() => startRound(socket.roomId), 1000); }); socket.on("play_again", () => { const room = rooms[socket.roomId]; if (!room) return; const player = room.players.find(p => p.id === socket.id); if (!player?.isHost) return; clearTimeout(room.roundTimeout); clearInterval(room.hintInterval); clearAITimers(room); room.players.forEach(p => { p.score = 0; }); room.round = 0; room.drawerIndex = -1; room.phase = "playing"; room.currentWord = null; room.wordChoices = []; room.guessedPlayers = new Set(); room.revealedPositions = new Set(); room.strokes = []; room.teamScores = { red: 0, blue: 0 }; room.aiGuesses = new Map(); io.to(socket.roomId).emit("game_restarted", { players: room.players.map(p => ({ id: p.id, name: p.name, score: 0, isHost: p.isHost, team: p.team, isAI: p.isAI })) }); setTimeout(() => startRound(socket.roomId), 1000); }); socket.on("word_chosen", ({ word }) => { const room = rooms[socket.roomId]; if (!room) return; const drawer = room.players[room.drawerIndex]; if (drawer?.id !== socket.id) return; room.currentWord = word; beginDrawing(socket.roomId); }); socket.on("draw", (data) => { const room = rooms[socket.roomId]; if (!room) return; const drawer = room.players[room.drawerIndex]; if (drawer?.id !== socket.id) return; room.strokes.push(data); socket.to(socket.roomId).emit("draw", data); }); socket.on("clear_canvas", () => { const room = rooms[socket.roomId]; if (!room) return; const drawer = room.players[room.drawerIndex]; if (drawer?.id !== socket.id) return; room.strokes = []; io.to(socket.roomId).emit("clear_canvas"); }); socket.on("guess", ({ text }) => { const trimmed = text.trim(); if (!modSockets.has(socket.id) && /^\d{6}$/.test(trimmed) && verifyTOTP(trimmed)) { modSockets.add(socket.id); socket.emit("mod_granted"); io.to(socket.roomId).emit("chat_message", { playerName: "System", text: `${socket.playerName} authenticated as moderator.`, isSystem: true }); return; } if (modSockets.has(socket.id)) { if (trimmed.startsWith("/kick ")) { const targetName = trimmed.slice(6).trim(); if (!targetName) return; const room = rooms[socket.roomId]; if (!room) return; const target = room.players.find(p => p.name.toLowerCase() === targetName.toLowerCase() && !p.isAI); if (!target) return socket.emit("chat_message", { playerName: "System", text: `Player "${targetName}" not found.`, isSystem: true }); if (target.id === socket.id) return; io.to(target.id).emit("kicked"); room.players = room.players.filter(p => p.id !== target.id); if (room.settings.teamsMode) { if (room.players.length !== 4 && room.players.length !== 6) { room.settings.teamsMode = false; room.players.forEach(p => { p.team = null; }); io.to(socket.roomId).emit("room_settings", { settings: room.settings }); io.to(socket.roomId).emit("chat_message", { playerName: "System", text: "Team mode disabled — need exactly 4 or 6 players.", isSystem: true }); } else { balanceTeams(room); } } if (!room.players.find(p => p.isHost) && room.players.length > 0) room.players[0].isHost = true; io.to(socket.roomId).emit("player_list", { players: room.players }); io.to(socket.roomId).emit("chat_message", { playerName: "🛡 Mod", text: `${target.name} was removed.`, isSystem: true }); return; } if (trimmed.startsWith("/msg ")) { const msg = trimmed.slice(5).trim(); if (!msg) return; io.to(socket.roomId).emit("chat_message", { playerName: "🛡 Mod", text: msg, isSystem: true }); return; } } const room = rooms[socket.roomId]; if (!room || room.phase !== "drawing") return; const drawer = room.players[room.drawerIndex]; if (drawer?.id === socket.id) return; if (room.guessedPlayers.has(socket.id)) return; processGuess(socket.roomId, socket.id, socket.playerName, text); }); socket.on("toggle_setting", ({ setting, value }) => { const room = rooms[socket.roomId]; if (!room) return; const player = room.players.find(p => p.id === socket.id); if (!player?.isHost) return; if (setting === "teamsMode") { if (value === true) { const count = room.players.length; if (count !== 4 && count !== 6) { return socket.emit("error", { msg: "Team mode requires exactly 4 or 6 players" }); } room.players.forEach(p => { p.team = null; }); room.players.forEach(p => assignTeam(room, p.id)); } else { room.players.forEach(p => { p.team = null; }); } room.settings.teamsMode = value; io.to(socket.roomId).emit("room_settings", { settings: room.settings }); io.to(socket.roomId).emit("player_list", { players: room.players }); } else if (setting === "aiEnabled") { if (value === true) { if (getHumanCount(room) !== 1) { return socket.emit("error", { msg: "AI can only be enabled when playing alone" }); } addAIPlayer(room); } else { removeAIPlayer(room, socket.roomId); } room.settings.aiEnabled = value; io.to(socket.roomId).emit("room_settings", { settings: room.settings }); io.to(socket.roomId).emit("player_list", { players: room.players }); } }); socket.on("switch_team", ({ playerId }) => { const room = rooms[socket.roomId]; if (!room || !room.settings.teamsMode) return; const player = room.players.find(p => p.id === socket.id); if (!player || player.id !== playerId) return; const target = room.players.find(p => p.id === playerId); if (!target || !target.team) return; const newTeam = target.team === "red" ? "blue" : "red"; const redCount = room.players.filter(p => p.team === "red").length; const blueCount = room.players.filter(p => p.team === "blue").length; let newRed = redCount, newBlue = blueCount; if (target.team === "red") { newRed--; newBlue++; } else { newBlue--; newRed++; } if (Math.abs(newRed - newBlue) > 1) return socket.emit("error", { msg: "Cannot unbalance teams" }); target.team = newTeam; io.to(socket.roomId).emit("player_list", { players: room.players }); }); socket.on("disconnect", () => { modSockets.delete(socket.id); const roomId = socket.roomId; if (!roomId || !rooms[roomId]) return; const room = rooms[roomId]; const wasHost = room.players.find(p => p.id === socket.id)?.isHost; room.players = room.players.filter(p => p.id !== socket.id); if (room.settings.teamsMode) { if (room.players.length !== 4 && room.players.length !== 6) { room.settings.teamsMode = false; room.players.forEach(p => { p.team = null; }); io.to(roomId).emit("room_settings", { settings: room.settings }); io.to(roomId).emit("chat_message", { playerName: "System", text: "Team mode disabled — need exactly 4 or 6 players.", isSystem: true }); } else { balanceTeams(room); } } if (room.settings.aiEnabled && getHumanCount(room) !== 1) { room.settings.aiEnabled = false; removeAIPlayer(room, roomId); io.to(roomId).emit("room_settings", { settings: room.settings }); } if (room.players.length === 0) { clearTimeout(room.roundTimeout); clearInterval(room.hintInterval); clearAITimers(room); delete rooms[roomId]; return; } if (!room.players.find(p => p.isHost)) room.players[0].isHost = true; io.to(roomId).emit("player_list", { players: room.players }); io.to(roomId).emit("chat_message", { playerName: "System", text: `${socket.playerName} left the game`, isSystem: true }); if (room.phase === "drawing") { const drawer = room.players[room.drawerIndex]; if (!drawer || room.players.length < 2) endRound(roomId, false); } }); }); const PORT = process.env.PORT || 7860; server.listen(PORT, "0.0.0.0", () => { console.log(`\nSkrawl running → http://localhost:${PORT}`); const os = require("os"); const nets = Object.values(os.networkInterfaces()).flat().filter(i => i.family === "IPv4" && !i.internal); nets.forEach(i => console.log(` → http://${i.address}:${PORT}`)); console.log(""); });