diff --git a/scripts/screenshots/bretzel-pixel.ts b/scripts/screenshots/bretzel-pixel.ts new file mode 100644 index 0000000..f5706fd --- /dev/null +++ b/scripts/screenshots/bretzel-pixel.ts @@ -0,0 +1,311 @@ +import { deflateSync } from 'zlib'; + +// Colors: [R, G, B, A] +const TRANSPARENT = [0, 0, 0, 0]; +const GOLD = [0xc8, 0x86, 0x0a, 255]; // #C8860A — bretzel body +const DARK = [0x8b, 0x69, 0x14, 255]; // #8B6914 — shading +const SALT = [0xf0, 0xe6, 0xd3, 255]; // #F0E6D3 — salt dots +const HIGHLIGHT = [0xe8, 0xa0, 0x20, 255]; // #E8A020 — highlight + +// Salt shaker colors +const SHAKER_BODY = [0xfa, 0xf5, 0xeb, 255]; // #FAF5EB — white body +const SHAKER_CAP = [0x6b, 0x62, 0x72, 255]; // #6B6272 — gray cap +const SHAKER_DOTS = [0xd4, 0xca, 0xbb, 255]; // #D4CABB — holes + +function encodePng(width: number, height: number, pixels: number[][]): Buffer { + // Build raw RGBA image data with filter byte per row + const rawData: number[] = []; + for (let y = 0; y < height; y++) { + rawData.push(0); // filter: None + for (let x = 0; x < width; x++) { + const color = pixels[y][x]; + rawData.push(color[0], color[1], color[2], color[3]); + } + } + + const compressed = deflateSync(Buffer.from(rawData)); + + // PNG signature + const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); + + // IHDR chunk + const ihdr = createChunk('IHDR', () => { + const buf = Buffer.alloc(13); + buf.writeUInt32BE(width, 0); + buf.writeUInt32BE(height, 4); + buf[8] = 8; // bit depth + buf[9] = 6; // color type: RGBA + buf[10] = 0; // compression + buf[11] = 0; // filter + buf[12] = 0; // interlace + return buf; + }); + + // IDAT chunk + const idat = createChunk('IDAT', () => compressed); + + // IEND chunk + const iend = createChunk('IEND', () => Buffer.alloc(0)); + + return Buffer.concat([signature, ihdr, idat, iend]); +} + +function createChunk(type: string, dataFn: () => Buffer): Buffer { + const data = dataFn(); + const length = Buffer.alloc(4); + length.writeUInt32BE(data.length, 0); + + const typeBuffer = Buffer.from(type, 'ascii'); + const crcInput = Buffer.concat([typeBuffer, data]); + + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(crcInput), 0); + + return Buffer.concat([length, typeBuffer, data, crc]); +} + +function crc32(buf: Buffer): number { + let crc = 0xffffffff; + for (let i = 0; i < buf.length; i++) { + crc ^= buf[i]; + for (let j = 0; j < 8; j++) { + crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0); + } + } + return (crc ^ 0xffffffff) >>> 0; +} + +/** + * 64x64 pixel-art bretzel in warm browns/golds. + * Shape: classic pretzel knot with salt dots. + */ +export function generateBretzelPng(): Buffer { + const S = 64; + const pixels: number[][][] = Array.from({ length: S }, () => + Array.from({ length: S }, () => TRANSPARENT) + ); + + // Draw a pretzel shape using circles and arcs + // The bretzel is two loops connected by a twist in the center + + function setPixel(x: number, y: number, color: number[]) { + if (x >= 0 && x < S && y >= 0 && y < S) { + pixels[y][x] = color; + } + } + + function fillCircle(cx: number, cy: number, r: number, color: number[]) { + for (let y = -r; y <= r; y++) { + for (let x = -r; x <= r; x++) { + if (x * x + y * y <= r * r) { + setPixel(cx + x, cy + y, color); + } + } + } + } + + function drawRing(cx: number, cy: number, outerR: number, innerR: number, color: number[]) { + for (let y = -outerR; y <= outerR; y++) { + for (let x = -outerR; x <= outerR; x++) { + const d = x * x + y * y; + if (d <= outerR * outerR && d >= innerR * innerR) { + setPixel(cx + x, cy + y, color); + } + } + } + } + + // Main pretzel shape: two overlapping loops + // Left loop + drawRing(22, 28, 16, 11, GOLD); + // Right loop + drawRing(42, 28, 16, 11, GOLD); + + // Top arm going up to center crossing + fillCircle(32, 14, 5, GOLD); + // Connect top to loops with thicker arms + for (let y = 12; y <= 22; y++) { + const spread = Math.floor((22 - y) * 0.6); + for (let x = -3; x <= 3; x++) { + setPixel(22 + spread + x, y, GOLD); + setPixel(42 - spread + x, y, GOLD); + } + } + + // Cross-over: two diagonal arms crossing at center + for (let i = -8; i <= 8; i++) { + for (let w = -2; w <= 2; w++) { + // Left arm crosses to right bottom + setPixel(32 + i, 24 + Math.floor(i * 0.5) + w, GOLD); + // Right arm crosses to left bottom + setPixel(32 + i, 24 - Math.floor(i * 0.5) + w, GOLD); + } + } + + // Bottom connection + for (let x = 18; x <= 46; x++) { + for (let y = 40; y <= 44; y++) { + const dx = x - 32; + const distFromCenter = Math.abs(dx); + if (distFromCenter <= 14) { + setPixel(x, y, GOLD); + } + } + } + + // Add shading (darker on bottom-right edges) + for (let y = 0; y < S; y++) { + for (let x = 0; x < S; x++) { + if (pixels[y][x] === GOLD) { + // Check if near edge + let nearEdge = false; + for (const [dx, dy] of [ + [1, 0], + [0, 1], + [1, 1] + ]) { + const nx = x + dx; + const ny = y + dy; + if (nx >= S || ny >= S || pixels[ny][nx] === TRANSPARENT) { + nearEdge = true; + break; + } + } + if (nearEdge) { + setPixel(x, y, DARK); + } + } + } + } + + // Add highlight on top-left edges + for (let y = 0; y < S; y++) { + for (let x = 0; x < S; x++) { + if (pixels[y][x] === GOLD) { + let nearTopEdge = false; + for (const [dx, dy] of [ + [-1, 0], + [0, -1] + ]) { + const nx = x + dx; + const ny = y + dy; + if (nx < 0 || ny < 0 || pixels[ny][nx] === TRANSPARENT) { + nearTopEdge = true; + break; + } + } + if (nearTopEdge) { + setPixel(x, y, HIGHLIGHT); + } + } + } + } + + // Salt dots scattered on the bretzel + const saltPositions = [ + [20, 24], + [24, 20], + [28, 16], + [36, 16], + [40, 20], + [44, 24], + [24, 32], + [40, 32], + [30, 28], + [34, 28], + [26, 40], + [32, 42], + [38, 40] + ]; + for (const [sx, sy] of saltPositions) { + if (pixels[sy]?.[sx] && pixels[sy][sx] !== TRANSPARENT) { + setPixel(sx, sy, SALT); + setPixel(sx + 1, sy, SALT); + setPixel(sx, sy + 1, SALT); + } + } + + return encodePng(S, S, pixels); +} + +/** + * 32x32 pixel-art salt shaker. + */ +export function generateSaltShakerPng(): Buffer { + const S = 32; + const pixels: number[][][] = Array.from({ length: S }, () => + Array.from({ length: S }, () => TRANSPARENT) + ); + + function setPixel(x: number, y: number, color: number[]) { + if (x >= 0 && x < S && y >= 0 && y < S) { + pixels[y][x] = color; + } + } + + function fillRect(x1: number, y1: number, x2: number, y2: number, color: number[]) { + for (let y = y1; y <= y2; y++) { + for (let x = x1; x <= x2; x++) { + setPixel(x, y, color); + } + } + } + + // Cap (top section) + fillRect(11, 4, 20, 8, SHAKER_CAP); + + // Cap holes (salt comes out) + setPixel(13, 5, SHAKER_DOTS); + setPixel(16, 5, SHAKER_DOTS); + setPixel(19, 5, SHAKER_DOTS); + setPixel(14, 7, SHAKER_DOTS); + setPixel(17, 7, SHAKER_DOTS); + + // Neck + fillRect(12, 9, 19, 10, SHAKER_BODY); + + // Body (wider, tapered) + fillRect(10, 11, 21, 26, SHAKER_BODY); + + // Body shading (right edge darker) + for (let y = 11; y <= 26; y++) { + setPixel(21, y, SHAKER_DOTS); + } + + // Body outline + for (let y = 11; y <= 26; y++) { + setPixel(9, y, DARK); + setPixel(22, y, DARK); + } + for (let x = 10; x <= 21; x++) { + setPixel(x, 27, DARK); + } + + // Bottom rim + fillRect(9, 27, 22, 28, SHAKER_CAP); + + // "S" label on the body + const sPattern = [ + [14, 16], + [15, 16], + [16, 16], + [13, 17], + [14, 18], + [15, 18], + [16, 19], + [13, 20], + [14, 20], + [15, 20] + ]; + for (const [x, y] of sPattern) { + setPixel(x, y, SHAKER_CAP); + } + + // Falling salt particles + setPixel(14, 2, SALT); + setPixel(17, 1, SALT); + setPixel(19, 3, SALT); + setPixel(12, 1, SALT); + + return encodePng(S, S, pixels); +} diff --git a/scripts/screenshots/constants.ts b/scripts/screenshots/constants.ts new file mode 100644 index 0000000..cb7f8ce --- /dev/null +++ b/scripts/screenshots/constants.ts @@ -0,0 +1,18 @@ +export const BASE_URL = 'http://localhost:4173'; +export const SCREENSHOT_DB = './data/screenshots.db'; +export const OUTPUT_DIR = 'website/assets'; + +export const DESKTOP_VIEWPORT = { width: 1280, height: 800 }; +export const MOBILE_VIEWPORT = { width: 390, height: 844 }; + +export const ADMIN = { + email: 'admin@bretzel.app', + displayName: 'Bretzel Admin', + password: 'bretzel2026!' +}; + +export const COLLABORATOR = { + email: 'alice@bretzel.app', + displayName: 'Alice Chen', + password: 'alice2026!!' +};