// Gera a textura "site vivo" da tela do celular: um mini-portfolio desenhado em // canvas 2D, com os prints reais dos projetos em produção rolando verticalmente // conforme o progresso do reel — como se o site estivesse aberto no aparelho. // Fotos dos projetos: capturadas ao vivo dos deployments (headless Chrome). // CC0 model: "Phone" by Quaternius (https://quaternius.com) via poly.pizza. type Shot = { img: HTMLImageElement; name: string; tag: string }; const SHOTS: Array<{ src: string; name: string; tag: string }> = [ { src: "/assets/prints/print-nutriscan.png", name: "NutriScan", tag: "Open Food Facts · PWA" }, { src: "/assets/prints/print-scplus.png", name: "SCPLUS", tag: "Educação + IoT · Garuva-SC" }, { src: "/assets/prints/print-sev.png", name: "SEV Moda Íntima", tag: "E-commerce · Next.js" }, { src: "/assets/prints/print-asservicos.png", name: "AS Serviços", tag: "Institucional · SSR" }, ]; export type ScreenPainter = { canvas: HTMLCanvasElement; /** desenha o frame para o progresso p∈[0,1] do reel; retorna true se mudou */ paint: (p: number, t: number) => boolean; ready: () => boolean; }; const W = 512; const H = 1024; const INK = "#0a0a0a"; const PAPER = "#f4f2ed"; const SLASH = "#ccff00"; export function createPhoneScreen(): ScreenPainter { const canvas = document.createElement("canvas"); canvas.width = W; canvas.height = H; const ctx = canvas.getContext("2d"); const images: Shot[] = []; let lastKey = ""; for (const s of SHOTS) { const img = new Image(); img.decoding = "async"; img.onload = () => images.push({ img, name: s.name, tag: s.tag }); img.src = s.src; } function roundRect(x: number, y: number, w: number, h: number, r: number) { if (!ctx) return; ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); } function wrap(text: string, x: number, y: number, maxW: number, lh: number) { if (!ctx) return; const words = text.split(" "); let line = ""; let yy = y; for (const word of words) { const candidate = line ? `${line} ${word}` : word; if (ctx.measureText(candidate).width > maxW) { ctx.fillText(line, x, yy); line = word; yy += lh; } else line = candidate; } if (line) ctx.fillText(line, x, yy); } function drawHero(offset: number, t: number) { if (!ctx) return; if (offset > H || offset < -H) return; ctx.save(); ctx.translate(0, offset); const pulse = 0.6 + 0.4 * Math.sin(t / 600); ctx.fillStyle = SLASH; ctx.globalAlpha = 0.14 + pulse * 0.06; ctx.fillRect(0, 60, W, 2); ctx.globalAlpha = 1; ctx.fillStyle = "rgba(244,242,237,.55)"; ctx.font = "600 20px ui-monospace, monospace"; ctx.fillText("PORTFÓLIO · AO VIVO", 36, 150); ctx.fillStyle = PAPER; ctx.font = "800 72px system-ui, sans-serif"; ctx.fillText("Software", 36, 260); ctx.fillText("que", 36, 340); ctx.fillStyle = SLASH; ctx.fillText("opera.", 36, 420); ctx.fillStyle = "rgba(244,242,237,.66)"; ctx.font = "400 28px system-ui, sans-serif"; wrap("Engenharia de sistemas que precisam funcionar de verdade.", 36, 500, W - 72, 40); ctx.fillStyle = SLASH; roundRect(36, 700, 250, 72, 36); ctx.fill(); ctx.fillStyle = INK; ctx.font = "700 26px ui-monospace, monospace"; ctx.fillText("VER PROJETOS ↓", 60, 744); ctx.restore(); } function drawProject(shot: Shot, offset: number, i: number, t: number) { if (!ctx) return; ctx.save(); ctx.translate(0, offset); ctx.fillStyle = "rgba(244,242,237,.5)"; ctx.font = "600 18px ui-monospace, monospace"; ctx.fillText(`EM PRODUÇÃO · 0${i + 1}`, 36, 110); ctx.fillStyle = PAPER; ctx.font = "800 46px system-ui, sans-serif"; ctx.fillText(shot.name, 36, 168); ctx.fillStyle = "rgba(204,255,0,.85)"; ctx.font = "400 22px system-ui, sans-serif"; ctx.fillText(shot.tag, 36, 204); // o print real, recortado, com leve auto-scroll dentro do card const cardX = 36; const cardY = 236; const cardW = W - 72; const cardH = 620; ctx.save(); roundRect(cardX, cardY, cardW, cardH, 28); ctx.clip(); ctx.fillStyle = "#111"; ctx.fillRect(cardX, cardY, cardW, cardH); const scale = cardW / shot.img.width; const drift = (Math.sin(t / 1400 + i * 2.1) * 0.5 + 0.5) * Math.max(0, shot.img.height * scale - cardH); ctx.drawImage(shot.img, cardX, cardY - drift, cardW, shot.img.height * scale); const grd = ctx.createLinearGradient(cardX, cardY, cardX, cardY + cardH); grd.addColorStop(0, "rgba(255,255,255,.08)"); grd.addColorStop(0.12, "rgba(255,255,255,0)"); ctx.fillStyle = grd; ctx.fillRect(cardX, cardY, cardW, cardH); ctx.restore(); ctx.strokeStyle = "rgba(255,255,255,.12)"; ctx.lineWidth = 2; roundRect(cardX, cardY, cardW, cardH, 28); ctx.stroke(); ctx.restore(); } function paint(p: number, t: number) { if (!ctx || images.length === 0) return false; const key = `${p.toFixed(4)}|${Math.floor(t / 250)}|${images.length}`; if (key === lastKey) return false; lastKey = key; ctx.fillStyle = INK; ctx.fillRect(0, 0, W, H); // status bar do "aparelho" ctx.fillStyle = PAPER; ctx.font = "600 22px ui-monospace, monospace"; ctx.fillText("21:37", 28, 38); ctx.fillStyle = SLASH; ctx.fillRect(W - 96, 22, 44, 14); ctx.fillStyle = PAPER; ctx.font = "600 16px ui-monospace, monospace"; ctx.fillText("E:/", W - 40, 36); // o mini-site rolando: hero + página por projeto const pageH = H - 60; const y = p * images.length * pageH; drawHero(-y, t); images.forEach((shot, i) => { const top = (i + 1) * pageH - y; if (top > H || top < -pageH) return; drawProject(shot, top, i, t); }); return true; } return { canvas, paint, ready: () => images.length > 0, }; } export const PHONE_SCREEN_W = W; export const PHONE_SCREEN_H = H;