diff --git a/src/scenes/judge.ts b/src/scenes/judge.ts index f5d5dad..3fa34ee 100644 --- a/src/scenes/judge.ts +++ b/src/scenes/judge.ts @@ -32,6 +32,8 @@ export class JudgeView implements View { onNext: (() => void) | null = null; /** One-shot lines the game adds to this verdict (streaks, regulars' memory). */ extraLines: string[] = []; + /** What the polaroid prints under the photo. */ + private lastCaption = { day: 0, grade: 'S', total: 0, line: '' }; constructor(private app: App) { this.pedestal = new THREE.Group(); @@ -81,10 +83,81 @@ export class JudgeView implements View { const btns = el('div', 'judge-btns', foot); this.viewBtn = el('button', 'btn ghost', btns, 'HEATMAP'); this.viewBtn.addEventListener('click', () => this.cycleHeatmap()); + const polaroid = el('button', 'btn ghost', btns, 'POLAROID'); + polaroid.addEventListener('click', () => this.takePolaroid()); this.nextBtn = el('button', 'btn', btns, 'NEXT ORDER'); this.nextBtn.addEventListener('click', () => this.onNext?.()); } + /** + * The polaroid: re-render the dish under the spotlight, frame it in white, + * stamp the day, the grade and the judge's line, and hand it over as a PNG. + * The judge's lines are the most quotable thing in the game — let them travel. + */ + private takePolaroid(): void { + // Force a fresh render so the drawing buffer is populated for capture. + this.app.renderer.render(this.app.scene, this.app.camera); + const src = this.app.renderer.domElement; + const c = this.lastCaption; + const W = 720; + const H = 860; + const out = document.createElement('canvas'); + out.width = W; + out.height = H; + const g = out.getContext('2d')!; + // The frame. + g.fillStyle = '#f3ead9'; + g.fillRect(0, 0, W, H); + // The photo: centre-crop the render into the window. + const win = { x: 30, y: 30, w: W - 60, h: 560 }; + const srcAspect = src.width / src.height; + const winAspect = win.w / win.h; + let sw = src.width; + let sh = src.height; + if (srcAspect > winAspect) sw = Math.round(sh * winAspect); + else sh = Math.round(sw / winAspect); + const sx = Math.round((src.width - sw) / 2); + const sy = Math.round((src.height - sh) / 2); + g.drawImage(src, sx, sy, sw, sh, win.x, win.y, win.w, win.h); + g.strokeStyle = 'rgba(40,30,20,0.25)'; + g.lineWidth = 2; + g.strokeRect(win.x, win.y, win.w, win.h); + // The caption, in the house hand. + g.fillStyle = '#2a2320'; + g.font = 'bold 34px Georgia, serif'; + g.fillText(`TOASTSIM — day ${c.day}`, 34, 648); + g.font = 'bold 58px Georgia, serif'; + g.fillStyle = c.grade === 'S' ? '#3f7d3f' : c.grade === 'A' ? '#5b8f5b' : c.grade === 'B' ? '#a0762a' : '#9c3a2a'; + g.fillText(c.grade, 34, 716); + g.font = '26px Georgia, serif'; + g.fillStyle = '#2a2320'; + g.fillText(`${c.total.toFixed(1)} / 10`, 100, 712); + g.font = 'italic 21px Georgia, serif'; + g.fillStyle = '#4a4038'; + // Wrap the judge's line to the frame. + const words = (c.line || '').split(' '); + let row = ''; + let y = 762; + for (const w of words) { + if (g.measureText(row + w).width > W - 70) { + g.fillText(`“${row.trim()}`.replace('““', '“'), 34, y); + row = ''; + y += 28; + if (y > H - 40) break; + } + row += w + ' '; + } + if (row.trim() && y <= H - 40) g.fillText((y === 762 ? '“' : '') + row.trim() + '”', 34, y); + g.font = '16px Georgia, serif'; + g.fillStyle = '#8a7f72'; + g.fillText('partly.party/toastsim', W - 210, H - 22); + // Hand it over. + const a = document.createElement('a'); + a.href = out.toDataURL('image/png'); + a.download = `toastsim-day${c.day}-${c.grade}.png`; + a.click(); + } + private cycleHeatmap(): void { this.heatmap = ((this.heatmap + 1) % 3) as 0 | 1 | 2; this.slice?.setHeatmap(this.heatmap); @@ -176,6 +249,7 @@ export class JudgeView implements View { for (const l of lines) el('p', undefined, this.linesEl, l); for (const l of this.extraLines) el('p', 'judge-extra', this.linesEl, l); this.extraLines = []; + this.lastCaption = { day: order.day, grade: verdict.grade, total: verdict.total, line: lines[1] ?? lines[0] ?? '' }; this.faceEl.src = assetUrl(`/assets/img/judge_${judgeFace(verdict.grade)}.png`); this.stampEl.textContent = verdict.grade;