THE POLAROID: a shareable verdict card from the judge screen
A POLAROID button on the scorecard re-renders the dish under the spotlight, centre-crops it into a cream polaroid frame, and stamps the day, the grade (coloured), the total, and the judge's line of the day in italics — 'partly.party/toastsim' in the corner — then downloads as toastsim-dayN-GRADE.png. The judge's lines are the most quotable thing in the game; now they travel. Verified: PNG downloads (real capture — frame cream, photo window lit, not the black-buffer trap; explicit re-render before toDataURL). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9c4e366b65
commit
f74ee61bbd
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user