| 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 }; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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), |
| ]; |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
|
|
| 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 = []; |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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 = []; |
|
|
| |
| 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; |
| } |
|
|
| |
| 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())); |
| } |
| |
| 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; |
| } |
|
|
| |
| 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())); |
| } |
| |
| 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())); |
| } |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| for (let i = 0; i < 5; i++) { |
| strokes.push(roughLine(cx - 22, cy + 20 + i * 30, cx + 22, cy + 20 + i * 30, "#ef4444", pickSize())); |
| } |
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| } |
| } |
| |
| strokes.push(...roughRect(cx - 14, cy + 170, 28, 20, c3, pickSize(), 4)); |
| strokes.push(...roughEllipse(cx, cy + 180, 2, 2, "#eab308", pickSize(), 6)); |
| |
| strokes.push(roughLine(cx - 50, cy + 190, cx + 50, cy + 190, c1, pickSize())); |
| return strokes; |
| } |
|
|
| |
| 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)); |
| |
| strokes.push(roughLine(cx - 28, cy + 20, cx, cy - 25, c3, 8)); |
| strokes.push(roughLine(cx, cy - 25, cx + 28, cy + 20, c3, 8)); |
| |
| 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())); |
| |
| strokes.push(...roughRect(cx - 10, cy + 110, 20, 30, c3, pickSize(), 4)); |
| |
| 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())); |
| } |
| |
| strokes.push(...roughEllipse(cx, cy - 5, 6, 6, "#eab308", pickSize(), 8)); |
| return strokes; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| } |
| } |
| |
| strokes.push(...roughEllipse(cx + 55, cy + 85, 20, 18, c3, pickSize(), 12)); |
| strokes.push(...roughEllipse(cx + 55, cy + 85, 12, 10, c1, pickSize(), 10)); |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| |
| strokes.push(roughLine(cx, cy - 90, cx, cy + 50, c3, pickSize())); |
| |
| 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())); |
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| 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 = []; |
|
|
| |
| 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())); |
| } |
| |
| 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)); |
| |
| for (let i = 0; i < 4; i++) { |
| strokes.push(...roughEllipse(cx - 50 + i * 30, ground - 8, 18, 8, "#6b7280", pickSize(), 8)); |
| } |
| return strokes; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| } |
| |
| strokes.push(...roughEllipse(cx, ground - 122, 8, 6, "#ef4444", pickSize(), 10)); |
| strokes.push(...roughEllipse(cx + 5, ground - 125, 4, 4, "#eab308", pickSize(), 8)); |
| |
| 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; |
| } |
|
|
| |
| 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())); |
| |
| strokes.push(roughLine(sx, sy, sx + Math.cos(a) * 22, sy + Math.sin(a) * 22, "#93c5fd", pickSize())); |
| } |
| |
| strokes.push(...roughEllipse(sx, sy, 8, 8, "#ffffff", pickSize(), 10)); |
| strokes.push(...roughEllipse(sx, sy, 4, 4, "#93c5fd", pickSize(), 8)); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("flower")) { |
| |
| 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())); |
| |
| 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())); |
| } |
| |
| 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)); |
| } |
| |
| strokes.push(...roughEllipse(cx, ground - 155, 12, 12, "#eab308", 8, 12)); |
| strokes.push(...hatchEllipse(cx, ground - 155, 8, 8, "#fef08a", 4)); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("tree")) { |
| |
| strokes.push(...roughRect(cx - 10, ground - 100, 20, 100, c1, 8, 4)); |
| strokes.push(...hatchRect(cx - 7, ground - 97, 14, 94, "#8B4513", 5)); |
| |
| 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())); |
| } |
| |
| 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)); |
| } |
| |
| strokes.push(roughLine(cx - 80, ground, cx + 80, ground, c1, 6)); |
| strokes.push(roughLine(cx - 60, ground + 3, cx + 60, ground + 3, c2, pickSize())); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("mushroom")) { |
| |
| strokes.push(...roughRect(cx - 14, ground - 80, 28, 80, c1, 8, 4)); |
| strokes.push(...hatchRect(cx - 10, ground - 76, 20, 72, c2, 5)); |
| |
| 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)); |
| |
| 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)); |
| } |
| |
| strokes.push(roughLine(cx - 45, ground - 85, cx + 45, ground - 85, c1, pickSize())); |
| |
| strokes.push(roughLine(cx - 80, ground, cx + 80, ground, c1, 6)); |
| strokes.push(roughLine(cx - 60, ground + 3, cx + 60, ground + 3, c2, pickSize())); |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| 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)); |
| } |
| |
| 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) { |
| |
| strokes.push(...roughEllipse(cx, ground - 205, 18, 8, "#ef4444", 8, 10)); |
| strokes.push(...roughEllipse(cx, ground - 205, 10, 5, "#eab308", pickSize(), 8)); |
| |
| 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)); |
| } |
| } |
| |
| 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; |
| } |
|
|
| |
| 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() |
| )); |
| } |
| |
| strokes.push(roughLine(cx - 100, ground, cx + 100, ground, c1, 6)); |
| return strokes; |
| } |
|
|
| |
| 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)); |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| } |
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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 = []; |
|
|
| |
| 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)); |
| |
| strokes.push(...roughEllipse(cx, cy - 20, 16, 16, c3, 8, 14)); |
| strokes.push(...roughEllipse(cx, cy - 20, 8, 8, "#3b82f6", pickSize(), 10)); |
| |
| 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())); |
| |
| 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)); |
| |
| 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())); |
| } |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| |
| 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)); |
| } |
| |
| for (let i = 0; i < 3; i++) { |
| strokes.push(...roughEllipse(cx - 50 + i * 40, cy + 38, 10, 8, c2, pickSize(), 8)); |
| } |
| return strokes; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| |
| strokes.push(...roughEllipse(cx + 32, cy - 36, 8, 4, c1, pickSize(), 8)); |
| |
| 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())); |
| } |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("parachute")) { |
| strokes.push(...roughEllipse(cx, cy, 90, 50, c1, 8, 20)); |
| strokes.push(...hatchEllipse(cx, cy, 80, 42, c2, 6)); |
| |
| 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())); |
| } |
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("sailboat")) { |
| |
| 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)); |
| |
| strokes.push(roughLine(cx, cy + 30, cx, cy - 60, c1, pickSize())); |
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| } |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("skateboard")) { |
| |
| 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())); |
| |
| 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())); |
| |
| 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)); |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| } |
| |
| 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)); |
| |
| 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)); |
| |
| 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())); |
| |
| if (!isBus) { |
| strokes.push(roughLine(cx + 80, cy + 6, cx + 80, cy + bodyH - 4, c3, pickSize())); |
| } |
| |
| 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())); |
| } |
| |
| 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())); |
| } |
| |
| 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())); |
| |
| 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 = []; |
|
|
| |
| 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)); |
| |
| 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)); |
| } |
| |
| 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)); |
| } |
| |
| 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())); |
| |
| strokes.push(roughLine(cx, cy, cx + 60, cy - 50, c3, pickSize())); |
| strokes.push(roughLine(cx, cy, cx - 40, cy + 60, c3, pickSize())); |
| return strokes; |
| } |
|
|
| |
| 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)); |
| |
| strokes.push(roughLine(cx - 65, cy - 10, cx + 65, cy - 10, "#ef4444", 8)); |
| strokes.push(...roughEllipse(cx, cy - 12, 60, 8, "#ef4444", pickSize(), 14)); |
| |
| 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)); |
| } |
| |
| 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())); |
| } |
| |
| strokes.push(roughLine(cx - 65, cy + 45, cx + 65, cy + 45, c3, pickSize())); |
| |
| strokes.push(...roughEllipse(cx, cy + 50, 70, 10, c1, pickSize(), 14)); |
| return strokes; |
| } |
|
|
| |
| if (word.includes("ice cream") || word.includes("icecream")) { |
| |
| 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)); |
| |
| 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)); |
| |
| 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())); |
| } |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("popcorn")) { |
| |
| 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)); |
| |
| 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)); |
| } |
| |
| 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)); |
| } |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| 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)); |
| |
| 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())); |
| |
| strokes.push(roughLine(cx - 10, cy - 15, cx - 5, cy - 10, "#ffffff", pickSize())); |
| return strokes; |
| } |
|
|
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("pineapple")) { |
| strokes.push(...roughEllipse(cx, cy, 35, 50, c3, 8, 16)); |
| strokes.push(...hatchEllipse(cx, cy, 28, 42, c2, 5)); |
| |
| 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())); |
| } |
| } |
| |
| 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; |
| } |
|
|
| |
| 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 = []; |
|
|
| |
| 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)); |
| |
| strokes.push(...roughRect(cx - 18, cy - 18, 36, 8, "#1e293b", pickSize(), 4)); |
| strokes.push(...roughRect(cx - 12, cy - 38, 24, 22, "#1e293b", pickSize(), 4)); |
| |
| strokes.push(...roughEllipse(cx - 9, cy - 5, 4, 4, "#1e293b", pickSize(), 8)); |
| strokes.push(...roughEllipse(cx + 9, cy - 5, 4, 4, "#1e293b", pickSize(), 8)); |
| |
| 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())); |
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| |
| strokes.push(roughLine(cx - 60, cy + 130, cx + 60, cy + 130, c1, pickSize())); |
| return strokes; |
| } |
|
|
| |
| if (word.includes("robot")) { |
| |
| strokes.push(...roughRect(cx - 30, cy - 30, 60, 60, c1, 8, 4)); |
| strokes.push(...hatchRect(cx - 26, cy - 26, 52, 52, c2, 5)); |
| |
| strokes.push(...roughRect(cx - 38, cy + 30, 76, 85, c1, 8, 4)); |
| strokes.push(...hatchRect(cx - 34, cy + 34, 68, 77, c3, 5)); |
| |
| 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)); |
| |
| 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())); |
| |
| strokes.push(roughLine(cx, cy - 30, cx, cy - 45, c4, pickSize())); |
| strokes.push(...roughEllipse(cx, cy - 48, 5, 5, "#ef4444", pickSize(), 8)); |
| |
| 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())); |
| |
| 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())); |
| |
| strokes.push(...roughRect(cx - 28, cy + 148, 18, 8, c2, pickSize(), 4)); |
| strokes.push(...roughRect(cx + 10, cy + 148, 18, 8, c2, pickSize(), 4)); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("astronaut")) { |
| |
| 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)); |
| |
| strokes.push(...roughRect(cx - 30, cy + 30, 60, 80, "#ffffff", 8, 4)); |
| strokes.push(...hatchRect(cx - 26, cy + 34, 52, 72, "#e2e8f0", 5)); |
| |
| strokes.push(...roughRect(cx + 30, cy + 35, 20, 50, c2, pickSize(), 4)); |
| |
| 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())); |
| |
| 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)); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("firefighter")) { |
| |
| 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)); |
| |
| 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())); |
| |
| strokes.push(...roughRect(cx - 25, cy + 24, 50, 75, "#ef4444", 8, 4)); |
| strokes.push(...hatchRect(cx - 21, cy + 28, 42, 67, "#dc2626", 5)); |
| |
| 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())); |
| |
| 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())); |
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("wizard")) { |
| |
| 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)); |
| |
| 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() |
| )); |
| } |
| |
| 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())); |
| |
| 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())); |
| } |
| |
| strokes.push(...roughRect(cx - 28, cy + 28, 56, 70, c3, 8, 4)); |
| strokes.push(...hatchRect(cx - 24, cy + 32, 48, 62, c4, 5)); |
| |
| 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())); |
| |
| 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)); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("superhero")) { |
| |
| 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)); |
| |
| strokes.push(...roughRect(cx - 24, cy + 25, 48, 60, "#3b82f6", 8, 4)); |
| strokes.push(...hatchRect(cx - 20, cy + 29, 40, 52, "#2563eb", 5)); |
| |
| strokes.push(...roughEllipse(cx, cy + 45, 10, 12, "#eab308", pickSize(), 10)); |
| |
| strokes.push(...roughEllipse(cx, cy, 28, 28, c3, 8, 14)); |
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("mountaineer")) { |
| |
| strokes.push(...roughEllipse(cx, cy - 5, 30, 8, "#1e293b", pickSize(), 12)); |
| strokes.push(...roughRect(cx - 16, cy - 30, 32, 28, "#1e293b", pickSize(), 4)); |
| |
| 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)); |
| |
| strokes.push(...roughRect(cx - 22, cy + 26, 44, 65, "#ef4444", 8, 4)); |
| strokes.push(...hatchRect(cx - 18, cy + 30, 36, 57, "#dc2626", 5)); |
| |
| strokes.push(...roughRect(cx + 22, cy + 30, 18, 45, c2, pickSize(), 4)); |
| |
| 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())); |
| |
| strokes.push(roughLine(cx + 45, cy + 32, cx + 55, cy + 95, c1, pickSize())); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("beekeeper")) { |
| |
| 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)); |
| |
| 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())); |
| |
| strokes.push(...roughRect(cx - 24, cy + 22, 48, 60, "#eab308", 8, 4)); |
| strokes.push(...hatchRect(cx - 20, cy + 26, 40, 52, "#ca8a04", 5)); |
| |
| for (let i = 0; i < 3; i++) { |
| strokes.push(roughLine(cx - 24, cy + 30 + i * 14, cx + 24, cy + 30 + i * 14, "#1e293b", pickSize())); |
| } |
| |
| 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())); |
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("mermaid")) { |
| |
| 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())); |
| } |
| |
| 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())); |
| |
| strokes.push(roughLine(cx, cy + 24, cx, cy + 55, c2, pickSize())); |
| |
| strokes.push(...roughEllipse(cx - 10, cy + 30, 10, 8, c4, pickSize(), 10)); |
| strokes.push(...roughEllipse(cx + 10, cy + 30, 10, 8, c4, pickSize(), 10)); |
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| } |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("scarecrow")) { |
| |
| 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())); |
| |
| 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)); |
| |
| 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())); |
| |
| strokes.push(...roughEllipse(cx, cy - 42, 30, 6, c2, pickSize(), 12)); |
| strokes.push(...roughRect(cx - 16, cy - 56, 32, 16, c2, pickSize(), 4)); |
| |
| strokes.push(...roughRect(cx - 18, cy - 6, 36, 40, "#3b82f6", 8, 4)); |
| strokes.push(...hatchRect(cx - 14, cy - 2, 28, 32, c4, 4)); |
| |
| strokes.push(...roughRect(cx - 12, cy + 4, 8, 8, "#eab308", pickSize(), 4)); |
| strokes.push(...roughRect(cx + 4, cy + 12, 8, 8, "#ef4444", pickSize(), 4)); |
| |
| strokes.push(...roughRect(cx - 16, cy + 34, 14, 35, c2, pickSize(), 4)); |
| strokes.push(...roughRect(cx + 2, cy + 34, 14, 35, c2, pickSize(), 4)); |
| |
| 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; |
| } |
|
|
| |
| 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 = []; |
|
|
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| } |
| |
| 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())); |
| |
| strokes.push(...roughEllipse(cx, cy, 6, 6, c1, pickSize(), 8)); |
| strokes.push(...roughEllipse(cx, cy, 3, 3, c3, pickSize(), 6)); |
| |
| strokes.push(roughLine(cx - 20, cy - 30, cx - 10, cy - 20, "#ffffff", pickSize())); |
| return strokes; |
| } |
|
|
| |
| 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 |
| )); |
| } |
| |
| 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() |
| )); |
| } |
| |
| 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())); |
| |
| strokes.push(...roughEllipse(cx, cy - 50, 4, 4, c4, pickSize(), 6)); |
| return strokes; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| |
| strokes.push(roughLine(cx - 26, cy + 78, cx + 26, cy + 78, c1, pickSize())); |
| |
| 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; |
| } |
|
|
| |
| 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())); |
| |
| strokes.push(roughLine(cx - 39, cy + 40, cx + 39, cy + 40, c1, pickSize())); |
| |
| 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; |
| } |
|
|
| |
| 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())); |
| |
| strokes.push(roughLine(cx, cy - 4, cx + 8, cy - 4, c4, pickSize())); |
| strokes.push(roughLine(cx, cy - 1, cx + 8, cy - 1, c4, pickSize())); |
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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())); |
| |
| strokes.push(...roughEllipse(cx + 14, cy + 52, 3, 4, "#eab308", pickSize(), 8)); |
| |
| 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; |
| } |
|
|
| |
| 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())); |
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| strokes.push(roughLine(cx - 25, cy - 5, cx + 25, cy - 5, "#ef4444", pickSize())); |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("kite")) { |
| |
| 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)); |
| |
| strokes.push(roughLine(cx - 35, cy, cx + 35, cy, c3, pickSize())); |
| strokes.push(roughLine(cx, cy - 50, cx, cy + 50, c3, pickSize())); |
| |
| 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)); |
| } |
| |
| strokes.push(roughLine(cx, cy - 55, cx - wobble(5, 10), cy - 90, c3, pickSize())); |
| return strokes; |
| } |
|
|
| |
| 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)); |
| |
| 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)); |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| 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())); |
| |
| strokes.push(...roughEllipse(cx, cy - 63, 4, 4, "#eab308", pickSize(), 8)); |
| |
| strokes.push(...roughRect(cx - 6, cy + 58, 12, 8, c1, pickSize(), 4)); |
| return strokes; |
| } |
|
|
| |
| 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())); |
| |
| 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())); |
| |
| strokes.push(...roughEllipse(cx, cy + 54, 30, 8, c1, pickSize(), 12)); |
| strokes.push(...hatchEllipse(cx, cy + 54, 24, 5, c3, 3)); |
| |
| strokes.push(roughLine(cx - 16, cy + 8, cx + 16, cy + 8, c2, pickSize())); |
| |
| 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; |
| } |
|
|
| |
| 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())); |
| |
| strokes.push(roughLine(cx - 8, cy - 12, cx - 3, cy - 8, "#ffffff", pickSize())); |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("guitar")) { |
| |
| 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)); |
| |
| 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())); |
| |
| for (let i = 0; i < 4; i++) { |
| const sx = cx - 3 + i * 2; |
| strokes.push(roughLine(sx, cy - 68, sx, cy + 50, c3, pickSize())); |
| } |
| |
| strokes.push(...roughEllipse(cx, cy, 10, 10, c1, pickSize(), 10)); |
| strokes.push(...roughEllipse(cx, cy, 6, 6, c4, pickSize(), 8)); |
| |
| for (let i = 0; i < 4; i++) { |
| strokes.push(...roughEllipse(cx - 4 + i * 2, cy - 72, 2, 2, c4, pickSize(), 6)); |
| } |
| return strokes; |
| } |
|
|
| |
| 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)); |
| |
| strokes.push(...roughRect(cx + 18, cy + 2, 16, 12, c2, pickSize(), 4)); |
| strokes.push(...roughRect(cx + 20, cy + 4, 12, 8, "#eab308", pickSize(), 4)); |
| |
| 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)); |
| |
| strokes.push(...roughRect(cx - 22, cy + 4, 10, 8, c3, pickSize(), 4)); |
| |
| strokes.push(...roughEllipse(cx, cy + 2, 4, 3, c4, pickSize(), 6)); |
| return strokes; |
| } |
|
|
| |
| 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 = []; |
|
|
| |
| 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())); |
| |
| 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; |
| } |
|
|
| |
| 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 |
| )); |
| } |
| |
| 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() |
| )); |
| } |
| |
| strokes.push(...roughEllipse(cx, cy, 6, 6, "#eab308", pickSize(), 8)); |
| |
| 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; |
| } |
|
|
| |
| 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() |
| )); |
| } |
| } |
| |
| 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)); |
| } |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| |
| strokes.push(roughLine(bx - br * 0.3, by - br * 0.3, bx - br * 0.1, by - br * 0.1, "#ffffff", pickSize())); |
| } |
| return strokes; |
| } |
|
|
| |
| if (word.includes("ghost")) { |
| strokes.push(...roughEllipse(cx, cy, 52, 62, c1, 8, 18)); |
| strokes.push(...hatchEllipse(cx, cy, 42, 52, c2, 5)); |
| |
| 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())); |
| } |
| |
| 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)); |
| |
| strokes.push(...roughEllipse(cx, cy + 6, 8, 6, "#1e293b", pickSize(), 8)); |
| |
| 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; |
| } |
|
|
| |
| 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)); |
| } |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("earthquake")) { |
| |
| 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())); |
| } |
| |
| 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())); |
| } |
| |
| 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)); |
| } |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("avalanche")) { |
| |
| strokes.push(roughLine(cx - 80, cy + 80, cx, cy - 40, c1, 8)); |
| strokes.push(roughLine(cx, cy - 40, cx + 80, cy + 80, c1, 8)); |
| |
| strokes.push(roughLine(cx - 40, cy + 20, cx, cy - 40, "#ffffff", pickSize())); |
| strokes.push(roughLine(cx, cy - 40, cx + 40, cy + 20, "#ffffff", pickSize())); |
| |
| 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)); |
| } |
| |
| 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())); |
| } |
| |
| 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; |
| } |
|
|
| |
| if (word.includes("shipwreck")) { |
| |
| 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)); |
| |
| 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())); |
| |
| 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())); |
| |
| 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())); |
| } |
| |
| 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())); |
| } |
| |
| 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; |
| } |
|
|
| |
| 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(); |
|
|
| |
| if (qdSamples[w] && qdSamples[w].length > 0) { |
| const drawing = qdSamples[w][Math.floor(Math.random() * qdSamples[w].length)]; |
| return quickDrawToStrokes(drawing); |
| } |
|
|
| |
| 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; |
| let lastColor = null; |
| let lastPos = null; |
| let consecutiveQuick = 0; |
|
|
| strokes.forEach((s) => { |
| const isColorChange = s.color !== lastColor; |
|
|
| |
| 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; |
| consecutiveQuick = 0; |
| } else if (isNewSection) { |
| delay += 300 + Math.random() * 600; |
| consecutiveQuick = 0; |
| } |
|
|
| |
| delay += 60 + Math.random() * 180; |
| consecutiveQuick++; |
|
|
| |
| if (Math.random() < 0.08) delay += 200 + Math.random() * 400; |
|
|
| |
| const len = Math.sqrt((s.x2 - s.x1) ** 2 + (s.y2 - s.y1) ** 2); |
| if (len > 90) delay += 80 + Math.random() * 200; |
|
|
| |
| if (Math.random() < 0.03) delay += 400 + Math.random() * 800; |
|
|
| |
| 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(""); |
| }); |
|
|