app
Hero Studio
Public Made by Adomby adom
The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Every hero wears its P
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
import { generateCircuitTile, tileSvg } from '../vendor/circuit-trace-generators/circuit-background.js';
import { generateCircuitField, pathData } from '../vendor/circuit-trace-generators/circuit-geometry-v9.js';
// Work in hero coordinates, never viewport pixels: a thumbnail and a PNG use
// the same routes. Only the composition's measured keepouts change geometry.
export const WIDTH = 1000, HEIGHT = 625;
export const DEFAULTS = Object.freeze({ density: 0.8, scale: 1.4, strength: 0.22, clearance: 8 });
const attr = value => String(value).replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
const dataUrl = svg => 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
// Range supplies the browser's actual line breaks and positions, including
// inline accents, cap trimming and overflowing words. Canvas supplies ink at
// those positions, using the already loaded font (not the layout column).
function paintTextInk(ctx, el, origin, sx, sy, clearance) {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
const range = document.createRange();
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
for (let node; (node = walker.nextNode());) {
const style = getComputedStyle(node.parentElement);
if (style.visibility === 'hidden' || Number(style.opacity) === 0) continue;
ctx.save();
ctx.scale(sx, sy);
ctx.font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
ctx.fontKerning = style.fontKerning;
ctx.fontStretch = style.fontStretch;
ctx.fontVariantCaps = style.fontVariantCaps;
ctx.letterSpacing = style.letterSpacing === 'normal' ? '0px' : style.letterSpacing;
ctx.wordSpacing = style.wordSpacing;
ctx.direction = style.direction;
ctx.textAlign = style.direction === 'rtl' ? 'right' : 'left';
ctx.textBaseline = 'alphabetic';
ctx.lineJoin = 'round';
ctx.lineWidth = 2 * clearance / sx;
let line;
const flush = () => {
if (!line || !line.text.trim()) return;
const text = style.textTransform === 'uppercase' ? line.text.toUpperCase()
: style.textTransform === 'lowercase' ? line.text.toLowerCase() : line.text;
const metrics = ctx.measureText(text);
// Range's inline font box spans fontBoundingBoxAscent/Descent, even
// with text-box-trim. The alphabetic baseline is inside that box.
const fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
const y = line.top - origin.top + (line.height - fontHeight) / 2 + metrics.fontBoundingBoxAscent;
const x = (style.direction === 'rtl' ? line.right : line.left) - origin.left;
if (clearance > 0) ctx.strokeText(text, x, y);
ctx.fillText(text, x, y);
};
for (const { segment, index } of segmenter.segment(node.data)) {
range.setStart(node, index); range.setEnd(node, index + segment.length);
const rect = range.getBoundingClientRect();
if (!rect.width || !rect.height) continue;
if (line && Math.abs(line.top - rect.top) > 1) { flush(); line = null; }
if (!line) line = { text:'', top:rect.top, height:rect.height, left:rect.left, right:rect.right };
line.text += segment;
line.left = Math.min(line.left, rect.left); line.right = Math.max(line.right, rect.right);
}
flush(); ctx.restore();
}
}
// Convert the painted ink into disjoint scanline polygons on a two-unit grid.
// Merge equal spans vertically. This keeps curved/stepped letter contours and
// counters without a font parser, external service, or thousands of obstacles.
function inkRings(ctx) {
const step = 2, pixels = ctx.getImageData(0, 0, WIDTH, HEIGHT).data;
const rectangles = []; let active = new Map();
for (let y = 0; y < HEIGHT; y += step) {
const next = new Map(); let start = -1;
for (let x = 0; x <= WIDTH; x += step) {
let ink = false;
for (let dy = 0; !ink && dy < step && y + dy < HEIGHT; dy++) {
for (let dx = 0; dx < step && x + dx < WIDTH; dx++) {
if (pixels[((y + dy) * WIDTH + x + dx) * 4 + 3] > 16) { ink = true; break; }
}
}
if (ink && start < 0) start = x;
if (!ink && start >= 0) {
const key = `${start}:${x}`, rect = active.get(key) || { x:start, y, right:x, bottom:y };
rect.bottom = Math.min(HEIGHT, y + step); next.set(key, rect);
if (!active.has(key)) rectangles.push(rect);
start = -1;
}
}
active = next;
}
return rectangles.map(r => [[r.x,r.y],[r.right,r.y],[r.right,r.bottom],[r.x,r.bottom],[r.x,r.y]]);
}
export function keepouts(hero, clearance = DEFAULTS.clearance) {
const base = hero.getBoundingClientRect();
const sx = WIDTH / (base.width - hero.clientLeft * 2);
const sy = HEIGHT / (base.height - hero.clientTop * 2);
const boxes = [];
const visible = el => el.getClientRects().length && getComputedStyle(el).visibility !== 'hidden';
const canvas = document.createElement('canvas'); canvas.width = WIDTH; canvas.height = HEIGHT;
const ctx = canvas.getContext('2d', { willReadFrequently:true });
const origin = { left:base.left + hero.clientLeft, top:base.top + hero.clientTop };
for (const selector of ['.h-title', '.h-sub', '.h-desc', '.c-name', '.c-mpn', '.c-spec', '.slug-br']) {
const el = hero.querySelector(selector);
if (el && visible(el)) paintTextInk(ctx, el, origin, sx, sy, clearance);
}
const rings = inkRings(ctx);
if (rings.length) boxes.push({ id:'text-ink', type:'path', space:'world', rings });
// These objects really are solid shapes. Never include a text container or
// the simulated homepage overlay, which adds its own scrim at display time.
const selectors = ['.window', '.comp-side', '.pill-tr', '.c-chips .c-family', '.wordmark:not(.noadom) .wmark'];
for (const selector of selectors) {
for (const el of hero.querySelectorAll(selector)) {
if (!visible(el)) continue;
const r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
// Conservative 4-unit grid removes subpixel differences between surfaces.
const x = Math.max(0, Math.floor(((r.left - base.left - hero.clientLeft) * sx - clearance) / 4) * 4);
const y = Math.max(0, Math.floor(((r.top - base.top - hero.clientTop) * sy - clearance) / 4) * 4);
const right = Math.min(WIDTH, Math.ceil(((r.right - base.left - hero.clientLeft) * sx + clearance) / 4) * 4);
const bottom = Math.min(HEIGHT, Math.ceil(((r.bottom - base.top - hero.clientTop) * sy + clearance) / 4) * 4);
if (right > x && bottom > y) boxes.push({ id: selector.slice(1), x, y, width: right - x, height: bottom - y });
}
}
return boxes;
}
export function composeTraces({ theme, seed, settings = {}, boxes = [], color = '#8b949e', background = '#0d1117' }) {
const config = { ...DEFAULTS, ...settings };
const { density, scale, strength } = config;
let art, defs = '', count;
if (theme === 'circuit-tile') {
const tile = generateCircuitTile({ seed: String(seed), density });
const svg = tileSvg(tile, { traceColor: color, holeColor: background });
const w = tile.width * scale, h = tile.height * scale;
defs = `<pattern id="tile" width="${w}" height="${h}" patternUnits="userSpaceOnUse"><image width="${w}" height="${h}" href="${attr(dataUrl(svg))}"/></pattern>`;
art = `<rect width="${WIDTH}" height="${HEIGHT}" fill="url(#tile)"/>`;
count = tile.traces.length;
} else {
const field = generateCircuitField({
seed: String(seed), width: WIDTH + 24, height: HEIGHT + 24,
featureScale: scale, fill: 0.668 * density, obstaclePreset: 'none',
obstacleMode: 'avoid', obstacleClearance: 0,
obstacles: boxes.map(b => b.rings ? b : ({ ...b, type: 'rect', space: 'world', x: b.x + b.width / 2, y: b.y + b.height / 2 })),
});
art = field.traces.map(t => `<path d="${pathData(t.points)}"/>`
+ [t.points[0], t.points[t.points.length - 1]].map(([x,y]) => `<circle cx="${x}" cy="${y}" r="${2.55 * scale}" stroke-width="${1.58 * scale}"/>`).join('')).join('');
art = `<g fill="none" stroke="${attr(color)}" stroke-width="${1.58 * scale}" stroke-linecap="butt" stroke-linejoin="miter">${art}</g>`;
count = field.traces.length;
}
// Tile geometry remains seamless; soften it around the same keepouts the
// field router avoids. Masked SVG is self-contained inside one cloned img.
const holes = boxes.map(b => b.rings
? `<path d="${b.rings.map(r => pathData(r) + 'Z').join('')}" fill="black"/>`
: `<rect x="${b.x}" y="${b.y}" width="${b.width}" height="${b.height}" rx="8" fill="black"/>`).join('');
if (theme === 'circuit-tile') defs += `<filter id="soft" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur stdDeviation="3"/></filter><mask id="quiet" maskUnits="userSpaceOnUse" x="0" y="0" width="${WIDTH}" height="${HEIGHT}"><rect width="${WIDTH}" height="${HEIGHT}" fill="white"/><g filter="url(#soft)">${holes}</g>${holes}</mask>`;
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}"><defs>${defs}</defs><g opacity="${strength}"${theme === 'circuit-tile' ? ' mask="url(#quiet)"' : ''}>${art}</g></svg>`;
return { src: dataUrl(svg), count };
}