|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * Build Meta Horizon Store screenshots at exactly 2560×1440 with no extra logos, captions, |
| 4 | + * badges, or marketing copy (VRC.Quest.Asset.5). Sources are real Quest compositor captures. |
| 5 | + * Square captures are cover-cropped so the 16:9 frame is filled (no letterboxing). |
| 6 | + * |
| 7 | + * node apps/quest-universal-qr-scanner/scripts/gen-store-screenshots.mjs |
| 8 | + */ |
| 9 | +import { createHash } from 'node:crypto'; |
| 10 | +import { mkdirSync, writeFileSync } from 'node:fs'; |
| 11 | +import { dirname, join } from 'node:path'; |
| 12 | +import { fileURLToPath } from 'node:url'; |
| 13 | +import sharp from 'sharp'; |
| 14 | + |
| 15 | +const here = dirname(fileURLToPath(import.meta.url)); |
| 16 | +const repoRoot = join(here, '..', '..', '..'); |
| 17 | +const captureDir = join(repoRoot, '.scratch', '2026-07-26-holoqr-quest'); |
| 18 | +const outDir = join(here, '..', 'store-assets', 'screenshots'); |
| 19 | +mkdirSync(outDir, { recursive: true }); |
| 20 | + |
| 21 | +const WIDTH = 2560; |
| 22 | +const HEIGHT = 1440; |
| 23 | + |
| 24 | +const slides = [ |
| 25 | + { file: '01-welcome.png', source: 'meta-compositor-welcome.jpg' }, |
| 26 | + { file: '02-how-it-works.png', source: 'meta-compositor-tutorial.jpg' }, |
| 27 | + { file: '03-link-found.png', source: 'meta-compositor-link.jpg' }, |
| 28 | + { file: '04-tutorial-scan.png', source: 'agent-panel-tutorial-scanning-fast.png' }, |
| 29 | + { file: '05-saved-links.png', source: 'meta-compositor-saved-links.jpg' }, |
| 30 | +]; |
| 31 | + |
| 32 | +async function toStoreScreenshot(sourcePath) { |
| 33 | + return sharp(sourcePath) |
| 34 | + .resize(WIDTH, HEIGHT, { fit: 'cover', position: 'centre' }) |
| 35 | + .png({ compressionLevel: 9, adaptiveFiltering: true }) |
| 36 | + .toBuffer(); |
| 37 | +} |
| 38 | + |
| 39 | +const artifacts = []; |
| 40 | +for (const slide of slides) { |
| 41 | + const sourcePath = join(captureDir, slide.source); |
| 42 | + const bytes = await toStoreScreenshot(sourcePath); |
| 43 | + const info = await sharp(bytes).metadata(); |
| 44 | + if (info.width !== WIDTH || info.height !== HEIGHT) { |
| 45 | + throw new Error(`${slide.file} is ${info.width}x${info.height}, expected ${WIDTH}x${HEIGHT}`); |
| 46 | + } |
| 47 | + writeFileSync(join(outDir, slide.file), bytes); |
| 48 | + artifacts.push({ |
| 49 | + file: slide.file, |
| 50 | + source: slide.source, |
| 51 | + width: info.width, |
| 52 | + height: info.height, |
| 53 | + bytes: bytes.byteLength, |
| 54 | + sha256: createHash('sha256').update(bytes).digest('hex'), |
| 55 | + }); |
| 56 | + console.log(` ${slide.file}`); |
| 57 | +} |
| 58 | + |
| 59 | +const { unlinkSync, existsSync } = await import('node:fs'); |
| 60 | +for (const staleName of ['04-tutorial-scanning.png', '04-scanning.png']) { |
| 61 | + const stale = join(outDir, staleName); |
| 62 | + if (existsSync(stale)) unlinkSync(stale); |
| 63 | +} |
| 64 | + |
| 65 | +const receipt = { |
| 66 | + schemaVersion: 'holoscript.holoqr-store-screenshot-receipt.v0.3.0', |
| 67 | + kind: 'HoloQrStoreScreenshotReceipt', |
| 68 | + createdAt: new Date().toISOString(), |
| 69 | + rule: 'VRC.Quest.Asset.5 — unembellished in-experience frames, no extra logos/text/iconography', |
| 70 | + size: { width: WIDTH, height: HEIGHT }, |
| 71 | + overlays: [], |
| 72 | + artifacts, |
| 73 | +}; |
| 74 | +writeFileSync(join(outDir, 'store-screenshot-receipt.json'), `${JSON.stringify(receipt, null, 2)}\n`); |
| 75 | +console.log(`gen-store-screenshots: wrote ${artifacts.length} screenshot(s) → store-assets/screenshots/`); |
0 commit comments