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
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.
83 lines
4.0 KiB
JavaScript
83 lines
4.0 KiB
JavaScript
// 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');
|