Files
bass-foundry/website/scripts/og-image.mjs
T
nvrl 7e1776bb04
Check and Windows preview / check (anvil) (push) Successful in 49s
Check and Windows preview / check (crucible) (push) Successful in 48s
Check and Windows preview / check (knurl) (push) Successful in 40s
Check and Windows preview / check (lathe) (push) Successful in 41s
Check and Windows preview / check (patina) (push) Successful in 56s
Check and Windows preview / check (rivet) (push) Successful in 45s
Check and Windows preview / check (temper) (push) Successful in 43s
Check and Windows preview / check (weld) (push) Successful in 1m20s
Check and Windows preview / previews (push) Successful in 1m15s
Check and Windows preview / windows (push) Successful in 2m31s
feat(website): add the fl.narl.io site
Astro site for the suite: a home page with the plugin cards, install
steps, principles, the metalwork names and the ideas on the bench, one
page per plugin, and the Impressum and privacy policy. Plugin names,
versions, download links and editor previews come from the catalog, the
wrapper manifests and docs/*/assets at build time.

Served by unprivileged nginx in Docker behind Traefik on fl.narl.io,
the same setup as proton.narl.io: no access logs, CSP 'self', no
JavaScript, legal pages filled from the environment at start.
2026-11-06 11:30:12 +01:00

83 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Renders public/og.png (1200×630), the preview image for link shares: the name, the
// waveform from the hero and three editor previews. Run with `npm run og` after the look
// changes. The text is set in Roboto Condensed, loaded from the plugins' own font folder.
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const root = new URL('../', import.meta.url);
const repo = new URL('../', root);
const W = 1200;
const H = 630;
// librsvg finds fonts through fontconfig; point it at the embedded editor fonts.
const fonts = await mkdtemp(join(tmpdir(), 'og-fonts-'));
await writeFile(
join(fonts, 'fonts.conf'),
`<?xml version="1.0"?><!DOCTYPE fontconfig SYSTEM "fonts.dtd"><fontconfig>
<dir>${new URL('plugins/common/ui/assets/', repo).pathname}</dir><cachedir>${fonts}</cachedir>
</fontconfig>`,
);
process.env.FONTCONFIG_FILE = join(fonts, 'fonts.conf');
const { default: sharp } = await import('sharp');
// Knurl's Fry pattern on a tanh saw, as in src/components/Hero.astro.
const pattern = [0, -14, -5, -14].map((db) => 10 ** (db / 20));
const points = [];
for (let i = 0; i <= 700; i++) {
const phase = (i / 700) * 6;
const cycle = Math.floor(phase);
const frac = phase - cycle;
let g = pattern[cycle % 4];
if (frac > 0.85) {
const t = (frac - 0.85) / 0.15;
g += (pattern[(cycle + 1) % 4] - g) * (0.5 - 0.5 * Math.cos(Math.PI * t));
}
const y = Math.tanh(2.2 * (2 * frac - 1)) / Math.tanh(2.2);
points.push(`${(70 + (i / 700) * 1060).toFixed(1)},${(492 - y * g * 68).toFixed(1)}`);
}
const background = Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}">
<defs>
<radialGradient id="glow" cx="0.5" cy="1" r="0.75">
<stop offset="0" stop-color="#ff0160" stop-opacity="0.32"/>
<stop offset="1" stop-color="#ff0160" stop-opacity="0"/>
</radialGradient>
<linearGradient id="title" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#ff0160"/>
<stop offset="0.6" stop-color="#ff5c95"/>
<stop offset="1" stop-color="#b8c0cc"/>
</linearGradient>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M40 0H0V40" fill="none" stroke="#b8c0cc" stroke-opacity="0.06"/>
</pattern>
<filter id="blur"><feGaussianBlur stdDeviation="7"/></filter>
</defs>
<rect width="${W}" height="${H}" fill="#0b0c0e"/>
<rect width="${W}" height="${H}" fill="url(#grid)"/>
<rect width="${W}" height="${H}" fill="url(#glow)"/>
<text x="70" y="118" font-family="Roboto Condensed" font-weight="500" font-size="26" letter-spacing="6" fill="#ff0160">CLAP PLUGINS FOR FL STUDIO</text>
<text x="66" y="236" font-family="Roboto Condensed" font-weight="500" font-size="128" fill="#e8e9ec">BASS</text>
<text x="66" y="348" font-family="Roboto Condensed" font-weight="500" font-size="128" fill="url(#title)">FOUNDRY</text>
<path d="M${points.join('L')}" fill="none" stroke="#ff0160" stroke-width="9" opacity="0.4" filter="url(#blur)"/>
<path d="M${points.join('L')}" fill="none" stroke="#ff0160" stroke-width="3" stroke-linejoin="round"/>
<text x="70" y="596" font-family="Roboto Condensed" font-size="24" letter-spacing="3" fill="#8c929c">RIDDIM · DUBSTEP · BASS MUSIC — FL.NARL.IO</text>
</svg>`);
const shot = async (name, width) => {
const img = sharp(await readFile(new URL(`docs/${name}/assets/editor.png`, repo))).resize(width);
const { height } = await img.clone().toBuffer({ resolveWithObject: true }).then((r) => r.info);
const mask = Buffer.from(`<svg width="${width}" height="${height}"><rect width="${width}" height="${height}" rx="10"/></svg>`);
return img.composite([{ input: mask, blend: 'dest-in' }]).png().toBuffer();
};
await sharp(background)
.composite([
{ input: await shot('lathe', 430), left: 700, top: 36 },
{ input: await shot('knurl', 430), left: 640, top: 116 },
])
.png({ compressionLevel: 9 })
.toFile(new URL('public/og.png', root).pathname);
await rm(fonts, { recursive: true });
console.log('wrote public/og.png');