From 0ceaf74b0065963e5597f058fe563a12bba1eacf Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Sun, 18 Oct 2026 19:48:46 +0000 Subject: [PATCH] ventures: Cartridge lives at cartridge.yolopreneur.lol; box-art-night redirects there (owner's call) --- ventures/cartridge/app.js | 176 ++++++++++++++++++++++++++++++++++ ventures/cartridge/index.html | 123 ++++++++++++++++++++++++ ventures/cartridge/style.css | 66 +++++++++++++ 3 files changed, 365 insertions(+) create mode 100644 ventures/cartridge/app.js create mode 100644 ventures/cartridge/index.html create mode 100644 ventures/cartridge/style.css diff --git a/ventures/cartridge/app.js b/ventures/cartridge/app.js new file mode 100644 index 0000000..3b71813 --- /dev/null +++ b/ventures/cartridge/app.js @@ -0,0 +1,176 @@ +'use strict'; +// Cartridge: everything runs in this page. No network calls, no libraries. +const W = 600, H = 840; +const STYLES = { + snes: { label: '16-BIT', bg: '#c5c3cf', ink: '#1b1630', band: '#4b3a8c', accent: '#e0433a', bandInk: '#ffffff', font: 'Impact, "Arial Black", sans-serif', px: 150, smooth: false, levels: 5 }, + ps1: { label: '32-BIT CD', bg: '#0b0b0f', ink: '#f2f2f2', band: '#2a2d3a', accent: '#9aa7c7', bandInk: '#e8ecf8', font: '"Arial Black", Impact, sans-serif', px: 320, smooth: true, levels: 8 }, + hand: { label: 'HANDHELD', bg: '#a9b08c', ink: '#1e2a14', band: '#3b4a2a', accent: '#b3263e', bandInk: '#dbe5b0', font: '"Courier New", monospace', px: 96, smooth: false, pal: [[15, 56, 15], [48, 98, 48], [139, 172, 15], [155, 188, 15]] } +}; +const RATINGS = [['E', 'EVERYONE'], ['T', 'TEEN'], ['M', 'MATURE'], ['?', 'RATING PENDING']]; + +// Pure helpers (self-checked in test.js). +function hash(s) { let h = 2166136261; for (const c of s) { h ^= c.codePointAt(0); h = Math.imul(h, 16777619); } return h >>> 0; } +function parseStats(t, n) { + return t.split('\n').map(l => l.trim()).filter(Boolean).slice(0, n).map(l => { + const i = l.indexOf(':'); + return i < 0 ? ['', l] : [l.slice(0, i).trim(), l.slice(i + 1).trim()]; + }); +} +// shortcut: a single word wider than max is not broken, upgrade if long unbroken strings matter. +function wrap(measure, text, max, maxLines) { + const lines = []; let cur = ''; + for (const w of text.split(/\s+/).filter(Boolean)) { + const t = cur ? cur + ' ' + w : w; + if (!cur || measure(t) <= max) cur = t; else { lines.push(cur); cur = w; } + } + if (cur) lines.push(cur); + if (lines.length > maxLines) { + lines.length = maxLines; + let l = lines[maxLines - 1]; + while (l && measure(l + '…') > max) l = l.slice(0, -1); + lines[maxLines - 1] = l.trimEnd() + '…'; + } + return lines; +} +if (typeof module !== 'undefined') module.exports = { hash, parseStats, wrap }; + +if (typeof document !== 'undefined') (function () { + const $ = id => document.getElementById(id); + const cv = $('cv'), form = document.querySelector('#try form'), status = $('status'); + let photo = null; + + // Photo cropped to w x h, reduced to the console's resolution and palette, then scaled up. + function art(g, x, y, w, h, st, zoom, fx, fy) { + const sw = Math.max(8, Math.round(st.px * w / W)), sh = Math.max(8, Math.round(sw * h / w)); + const c = document.createElement('canvas'); c.width = sw; c.height = sh; + const k = c.getContext('2d'); + k.fillStyle = '#fff'; k.fillRect(0, 0, sw, sh); + const s = Math.max(sw / photo.width, sh / photo.height) * zoom; + k.drawImage(photo, (sw - photo.width * s) * fx, (sh - photo.height * s) * fy, photo.width * s, photo.height * s); + const d = k.getImageData(0, 0, sw, sh), a = d.data; + for (let i = 0; i < a.length; i += 4) { + if (st.pal) { + const l = (0.299 * a[i] + 0.587 * a[i + 1] + 0.114 * a[i + 2]) / 255, p = st.pal[Math.min(3, (l * 4) | 0)]; + a[i] = p[0]; a[i + 1] = p[1]; a[i + 2] = p[2]; + } else { + const q = 255 / (st.levels - 1); + for (let j = 0; j < 3; j++) a[i + j] = Math.round(a[i + j] / q) * q; + } + } + k.putImageData(d, 0, 0); + g.imageSmoothingEnabled = st.smooth; + g.drawImage(c, x, y, w, h); + } + function fit(g, text, font, max, size) { + for (; size > 12; size -= 2) { g.font = size + 'px ' + font; if (g.measureText(text).width <= max) break; } + } + function top(g, m, st) { + g.fillStyle = st.bg; g.fillRect(0, 0, W, H); + g.fillStyle = st.band; g.fillRect(0, 0, W, 84); + g.fillStyle = st.accent; g.fillRect(0, 84, W, 8); + g.textBaseline = 'middle'; g.fillStyle = st.bandInk; + } + function front(g, m) { + const st = m.st; + top(g, m, st); + g.textAlign = 'left'; g.font = 'bold 34px ' + st.font; g.fillText(st.label, 30, 44); + g.textAlign = 'right'; fit(g, 'PLAYER 1: ' + m.player.toUpperCase(), st.font, 300, 22); g.fillText('PLAYER 1: ' + m.player.toUpperCase(), 570, 44); + g.fillStyle = st.ink; g.fillRect(24, 112, 552, 552); + art(g, 30, 118, 540, 540, st, m.zoom, m.fx, m.fy); + const r = RATINGS[m.rating], x = 42, y = 530; + g.fillStyle = '#fff'; g.fillRect(x, y, 84, 114); + g.strokeStyle = '#000'; g.lineWidth = 4; g.strokeRect(x + 2, y + 2, 80, 110); + g.fillStyle = '#000'; g.textAlign = 'center'; + g.font = 'bold 70px Arial, sans-serif'; g.fillText(r[0], x + 42, y + 52); + g.font = 'bold 11px Arial, sans-serif'; g.fillText(r[1], x + 42, y + 98, 74); + g.fillStyle = st.band; g.fillRect(24, 684, 552, 96); + g.fillStyle = st.bandInk; fit(g, m.title, st.font, 520, 76); g.fillText(m.title, W / 2, 733); + g.fillStyle = st.ink; fit(g, m.tag, st.font, 552, 24); g.fillText(m.tag, W / 2, 812); + } + function shot(g, x, y, w, h, m, zoom, fx, fy, n) { + const st = m.st; + g.fillStyle = st.ink; g.fillRect(x, y, w, h); + art(g, x + 3, y + 3, w - 6, h - 6, st, zoom, fx, fy); + g.save(); g.beginPath(); g.rect(x + 3, y + 3, w - 6, h - 6); g.clip(); + g.font = 'bold 16px "Courier New", monospace'; g.textBaseline = 'top'; g.textAlign = 'left'; + const t = (s, tx, ty) => { g.fillStyle = '#000'; g.fillText(s, tx + 1, ty + 1); g.fillStyle = '#fff'; g.fillText(s, tx, ty); }; + t('P1 ' + m.player.slice(0, 10).toUpperCase(), x + 10, y + 10); + t('SCORE ' + String((hash(m.title) * (n + 1)) % 1000000).padStart(6, '0'), x + 10, y + h - 26); + g.fillStyle = '#e0433a'; for (let i = 0; i < 3; i++) g.fillRect(x + w - 24 - i * 18, y + 12, 12, 12); + g.restore(); g.textBaseline = 'middle'; + } + function back(g, m) { + const st = m.st; + top(g, m, st); + g.textAlign = 'left'; fit(g, m.title, st.font, 380, 40); g.fillText(m.title, 30, 44); + g.textAlign = 'right'; g.font = 'bold 22px ' + st.font; g.fillText(st.label, 570, 44); + shot(g, 30, 112, 540, 230, m, m.zoom * 1.6, 0.3, 0.4, 0); + shot(g, 30, 358, 262, 150, m, m.zoom * 2.4, 0.8, 0.25, 1); + shot(g, 308, 358, 262, 150, m, m.zoom * 2.4, 0.5, 0.95, 2); + g.fillStyle = st.ink; g.textAlign = 'left'; g.textBaseline = 'top'; g.font = '20px Arial, sans-serif'; + wrap(s => g.measureText(s).width, m.blurb, 540, 4).forEach((l, i) => g.fillText(l, 30, 526 + i * 26)); + g.fillStyle = st.band; g.fillRect(30, 664, 410, 140); + g.fillStyle = st.bandInk; g.font = '19px "Courier New", monospace'; + m.stats.forEach(([a, b], i) => { + g.textAlign = 'left'; g.fillText(a.toUpperCase(), 44, 678 + i * 30, 150); + g.textAlign = 'right'; g.fillText(b, 426, 678 + i * 30, 220); + }); + g.fillStyle = '#fff'; g.fillRect(460, 664, 110, 140); + g.fillStyle = '#000'; let s = hash(m.title) || 1; + for (let x = 470; x < 560; ) { s = Math.imul(s, 1664525) + 1013904223 >>> 0; const w = 2 + (s >>> 28) % 4; g.fillRect(x, 676, w, 100); x += w + 2 + (s >>> 24) % 3; } + g.textBaseline = 'middle'; + } + + function read() { + const f = form.elements; + return { + st: STYLES[f.style.value], title: (f.ttl.value.trim() || 'Untitled').toUpperCase(), + player: f.player.value.trim() || 'Player', tag: f.tag.value.trim(), blurb: f.blurb.value.trim(), + stats: parseStats(f.stats.value, 4), rating: +f.rating.value, + zoom: +f.zoom.value, fx: +f.fx.value, fy: +f.fy.value + }; + } + function render() { + if (!photo) return; + const g = cv.getContext('2d'), m = read(); + g.setTransform(1, 0, 0, 1, 0, 0); + g.fillStyle = '#14141c'; g.fillRect(0, 0, cv.width, cv.height); + g.save(); g.translate(20, 20); front(g, m); g.restore(); + g.save(); g.translate(660, 20); back(g, m); g.restore(); + } + let queued = false; + const later = () => queued || (queued = requestAnimationFrame(() => { queued = false; render(); })); + + // Same path for a user's file and the built-in sample. + function loadPhoto(blob, label) { + return new Promise(res => { + const url = URL.createObjectURL(blob), img = new Image(); + img.onload = () => { photo = img; URL.revokeObjectURL(url); status.textContent = label; render(); res(); }; + img.onerror = () => { URL.revokeObjectURL(url); status.textContent = 'That file could not be read as an image.'; res(); }; + img.src = url; + }); + } + const pick = f => f && loadPhoto(f, 'Using ' + f.name + '. It stays in this page.'); + + function save(kind) { + let c = cv; + if (kind) { c = document.createElement('canvas'); c.width = W; c.height = H; (kind === 'front' ? front : back)(c.getContext('2d'), read()); } + const name = (read().title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'cartridge') + '-' + (kind || 'front-back') + '.png'; + c.toBlob(b => { + const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = name; a.click(); + setTimeout(() => URL.revokeObjectURL(a.href), 1e4); + }, 'image/png'); + } + + form.addEventListener('submit', e => e.preventDefault()); + form.addEventListener('input', later); + $('file').addEventListener('change', e => pick(e.target.files[0])); + const tryEl = $('try'); + tryEl.addEventListener('dragover', e => { e.preventDefault(); tryEl.classList.add('over'); }); + tryEl.addEventListener('dragleave', () => tryEl.classList.remove('over')); + tryEl.addEventListener('drop', e => { e.preventDefault(); tryEl.classList.remove('over'); pick(e.dataTransfer.files[0]); }); + document.querySelectorAll('[data-k]').forEach(b => b.addEventListener('click', () => save(b.dataset.k))); + + const SAMPLE = ''; + loadPhoto(new Blob([SAMPLE], { type: 'image/svg+xml' }), 'Showing the built-in sample image. Drop your own photo to replace it.'); +})(); diff --git a/ventures/cartridge/index.html b/ventures/cartridge/index.html new file mode 100644 index 0000000..ce255ad --- /dev/null +++ b/ventures/cartridge/index.html @@ -0,0 +1,123 @@ + + + + + +Cartridge: a fake 16-bit game box cover from one photo + + + + +
+
+
+ + CartridgeYour photo on a 16-bit game box +
+
+ Join early access + Opens a public issue on Teahub. No email, no payment. +
+
+ +
+
+ + +
+ + + + +
+
+
+ +
+ + + + + +
+
Console style + + + +
+
+ + + + +
+
+
+
+ +
+
+

How it works

+
    +
  1. Drop in a photo and type a title, a player name and a tagline.
  2. +
  3. Pick SNES-era, PS1-era or handheld, then zoom and pan the cover art.
  4. +
  5. Download the PNG: front and back together, or either one alone.
  6. +
+
+ +
+

Why this exists

+

Cartridge rides the wave of two repos trending on GitHub this week. It does not use their code and is not affiliated with them.

+ +
+ +
+

Pricing

+

Nothing is charged today.

+
+
+

Free

$0

+

For anyone who wants a funny cover to send or post today.

+

Includes unlimited covers in every console style (SNES-era, PS1-era, handheld), front and back cover with fake screenshots, rating badge and spec list, watermark-free PNG download at screen resolution, all rendered in your browser with nothing uploaded and no account.

+ Use it free +
+
+

Print Pack (early access, founding price)

$6 one-time per cover, founding price. Nothing is charged today.

+

For the gift-giver who wants to print, frame or fold the cover into a real box.

+

Includes 300 dpi CMYK-safe front, back and spine exports, a printable cut-and-fold physical box template sized to each console style, bleed and trim guides for print shops, and priority access when it opens.

+ Join early access +
+
+

Family Shelf (early access, founding price)

$4/mo, founding price. Nothing is charged today.

+

For a family, office or friend group making covers together for a reunion, retirement or milestone birthday.

+

Includes everything in Print Pack for every cover on the shelf, plus a shareable 'family library' link where several people add their own covers to one shelf.

+ Join early access +
+
+
+ +
+

Honest limits

+
    +
  • The PNG is screen resolution (1280×880 for the pair, 600×840 for one side), RGB. It is not 300 dpi or CMYK, and there is no box template.
  • +
  • Cropping is a centered zoom with two pan sliders. No face detection, no rotation.
  • +
  • The three styles are original looks inspired by their eras. They are not copies of real boxes or logos, and the rating badges are not official ratings.
  • +
  • The fake screenshots are pixelated crops of your photo, not game footage.
  • +
  • Nothing is saved. Reload the page and you start over. Text uses your device's fonts, so it can look slightly different from one device to another.
  • +
+
+
+ + + + + diff --git a/ventures/cartridge/style.css b/ventures/cartridge/style.css new file mode 100644 index 0000000..20e9026 --- /dev/null +++ b/ventures/cartridge/style.css @@ -0,0 +1,66 @@ +*{box-sizing:border-box} +html{background:#0e0f15} +body{margin:0;color:#e7e9f2;font:14px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:#0e0f15;overflow-x:hidden} +a{color:#7fc4ff} +.first{min-height:100vh;min-height:100dvh;display:flex;flex-direction:column} +.top{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:6px 16px;padding:8px 14px;background:#171923;border-bottom:1px solid #2a2d3b} +.brand{display:flex;align-items:center;gap:8px;min-width:0} +.brand b{font-size:17px;letter-spacing:.04em;text-transform:uppercase} +.brand span{color:#9aa0b5} +.cta{display:flex;align-items:center;gap:10px} +.cta small{color:#9aa0b5;font-size:11px;line-height:1.25;max-width:230px} +.btn{display:inline-block;border:1px solid #3a3e52;background:#222536;color:#e7e9f2;border-radius:6px;padding:7px 12px;font:600 13px system-ui,sans-serif;text-decoration:none;cursor:pointer} +.btn:hover{background:#2c3047} +.btn.primary{background:#ffb020;border-color:#ffb020;color:#1b1200;white-space:nowrap} +.btn.primary:hover{background:#ffc24d} +.btn.dl{background:#3ddc97;border-color:#3ddc97;color:#04210f} +.btn.dl:hover{background:#6ae8b3} +:focus-visible{outline:2px solid #ffb020;outline-offset:2px} + +#try{flex:1;display:flex;flex-direction:column;gap:12px;padding:12px 14px} +#try.over{outline:2px dashed #ffb020;outline-offset:-6px} +.stage{min-width:0} +canvas{display:block;width:100%;height:auto;background:#14141c;border:1px solid #2a2d3b;border-radius:6px;object-fit:contain} +.bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px} +#status{color:#9aa0b5;font-size:12px;flex:1 1 200px} +.nojs{color:#ffb020} +.panel{background:#171923;border:1px solid #2a2d3b;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:10px;min-width:0} +.drop{display:block;border:2px dashed #3a3e52;border-radius:8px;padding:10px;text-align:center;cursor:pointer;position:relative} +.drop:hover,.drop:focus-within{border-color:#ffb020} +.drop input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%} +.drop small{display:block;color:#9aa0b5;font-size:11px} +.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px} +.grid label,.panel>label{display:flex;flex-direction:column;gap:3px;font-size:11px;color:#9aa0b5;text-transform:uppercase;letter-spacing:.04em;min-width:0} +.wide{grid-column:1/-1} +input:not([type=range]):not([type=file]):not([type=radio]),textarea,select{width:100%;background:#0e0f15;color:#e7e9f2;border:1px solid #3a3e52;border-radius:6px;padding:6px 8px;font:14px system-ui,sans-serif;text-transform:none;letter-spacing:0} +textarea{resize:vertical} +input[type=range]{width:100%;accent-color:#ffb020} +fieldset{border:0;margin:0;padding:0;display:flex;gap:6px;flex-wrap:wrap} +legend{font-size:11px;color:#9aa0b5;text-transform:uppercase;letter-spacing:.04em;padding:0 0 3px} +.styles label{flex:1} +.styles input{position:absolute;opacity:0} +.styles span{display:block;text-align:center;padding:7px 4px;border:1px solid #3a3e52;border-radius:6px;background:#222536;font-weight:600;cursor:pointer} +.styles input:checked+span{background:#ffb020;border-color:#ffb020;color:#1b1200} +.styles input:focus-visible+span{outline:2px solid #fff} + +@media (min-width:860px){ + #try{flex-direction:row;align-items:flex-start} + .stage{flex:1} + canvas{max-height:calc(100vh - 130px);max-height:calc(100dvh - 130px)} + .panel{width:340px;flex:none} +} +@media (max-width:600px){ + .brand span{font-size:12px} + .cta{width:100%} +} + +.below{max-width:1040px;padding:20px 14px 0} +.below h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:#9aa0b5;margin:28px 0 8px;border-top:1px solid #2a2d3b;padding-top:14px} +.below h3{margin:0;font-size:15px} +.below p,.below li{max-width:70ch} +.tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px} +.tiers article{background:#171923;border:1px solid #2a2d3b;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:6px;align-items:flex-start} +.tiers article p{margin:0} +.tiers .btn{margin-top:auto} +.price{color:#ffb020;font-weight:700} +footer{display:flex;flex-wrap:wrap;gap:8px 24px;padding:24px 14px 32px;color:#9aa0b5;font-size:12px}