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