Files
KantSites/scripts/build-ebsolar-options.mjs
T
kantarciandClaude Fable 5.1 d0019c80af Arrow icons, cascading own CSS, a preload for the hero image, and EB Solar
Built while setting up the redesign of ebsolar.de as the sixth local
installation (scripts/seed-ebsolar.php, reset-ebsolar.sh, ports 8096/8097).
The rebuild surfaced four things in the core:

- The icon set knows three arrows now: arrow-right, arrow-left,
  arrow-up-right. The button block and the post navigation render them as
  inline SVG inside the same aria-hidden span as before, so a template that
  hides the arrow with "& span{display:none}" keeps working. Until now the
  arrow was a text glyph that looked different in every font. Umut: "bei
  alle Projekte bzw bei allen möchte ich dieses emoji icon weg haben".

- Own CSS with the same nested selector twice kept only the last rule.
  "& .post-card{border:0}" followed by "& .post-card{transform:none}" lost
  the border - silently, which brought the core's card frame back on the
  project grid. The parser now joins the declarations in source order, as
  CSS does; the same for "&{…}" next to the element's own properties.

- The eager image over the fold was not preloaded. The background image of
  the first section has had a preload link for a while; a plain <img> was
  found only after the stylesheet was parsed, and that round trip went
  straight into LCP. PageController::heroImage() now writes the same src,
  srcset and sizes the block template writes, and page.html.twig emits the
  link - unless a background image is already preloaded.

- Measured on the way: an entrance effect on the LCP element costs about
  900 ms of render delay. The EB Solar hero image therefore has none.

Tests: 581 green, the icon test accepts quoted keys, two cases for the CSS
cascade, the preload asserted in the big-picture test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 12:56:57 +02:00

115 lines
6.1 KiB
JavaScript
Raw Permalink 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.
/*
* Zeichnet die Bildkarten des Solarchecks von EB Solar und rastert sie zu PNG.
*
* Die Formularbausteine zeigen eine Auswahl als Bildkarten, sobald eine Option ein Bild aus der
* Mediathek hat - und die Mediathek nimmt JPEG, PNG und WebP, kein SVG. Deshalb entstehen die
* Symbole hier als SVG (leicht zu ändern, alles in einer Datei) und werden mit Chromium auf
* 800 × 600 gerastert, dem Seitenverhältnis 4:3 der Karte. Stil wie die übrigen Symbole der
* Website: dünne Linie in Tinte, runde Enden, ein roter Akzent, helle Fläche dahinter.
*
* node scripts/build-ebsolar-options.mjs → var/ebsolar-assets/options/*.png und *.svg
*/
import { chromium } from 'playwright';
import { mkdir, writeFile } from 'node:fs/promises';
const ziel = process.argv[2] || 'var/ebsolar-assets/options';
const TINTE = '#252b30', ROT = '#b52c2c', HELL = '#f5f6f6', PAPIER = '#ffffff', GRAU = '#c9ced0';
/** Ein Bild: 800 × 600, helle Fläche, Inhalt in der Mitte. */
const bild = (inhalt) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="800" height="600">
<rect width="800" height="600" fill="${HELL}"/>
<g fill="none" stroke="${TINTE}" stroke-width="14" stroke-linecap="round" stroke-linejoin="round">${inhalt}</g>
</svg>`;
/** Eine Person: Kopf und Schultern. */
const person = (x, y, s = 1, farbe = TINTE) => `
<circle cx="${x}" cy="${y - 120 * s}" r="${46 * s}" stroke="${farbe}"/>
<path d="M${x - 88 * s} ${y + 70 * s}v-${30 * s}a${88 * s} ${88 * s} 0 0 1 ${176 * s} 0v${30 * s}" stroke="${farbe}"/>`;
/** Ein Haus: Wände plus Dach nach Angabe; das Dach ist der Akzent. */
const haus = (dach) => `
<path d="M190 520V300M610 520V300M150 520h500"/>
<rect x="360" y="410" width="80" height="110" rx="6"/>
${dach}`;
/** Ein Dach als schräge Fläche mit Modulen im Raster. */
const module = (spalten, zeilen) => {
const x0 = 160, y0 = 150, b = 480, h = 300, scher = 90;
const zellen = [];
for (let z = 0; z < zeilen; z++) {
for (let s = 0; s < spalten; s++) {
const bx = b / spalten, bh = h / zeilen;
const x = x0 + s * bx + (z * bh) * (scher / h), y = y0 + z * bh;
zellen.push(`<path d="M${x + 8} ${y + 8}h${bx - 16}l${(bh - 16) * scher / h} ${bh - 16}h-${bx - 16}z" fill="${ROT}" fill-opacity=".14" stroke="${ROT}" stroke-width="8"/>`);
}
}
return `<path d="M${x0} ${y0}h${b}l${scher} ${h}h-${b}z"/>${zellen.join('')}`;
};
const fragezeichen = (x, y, s = 1) => `<path d="M${x - 40 * s} ${y - 50 * s}a${40 * s} ${40 * s} 0 1 1 ${58 * s} ${36 * s}c-${14 * s} ${9 * s}-${18 * s} ${18 * s}-${18 * s} ${34 * s}" stroke="${ROT}"/><circle cx="${x}" cy="${y + 65 * s}" r="9" fill="${ROT}" stroke="none"/>`;
const batterie = (inhalt, farbe = TINTE) => `
<rect x="190" y="200" width="380" height="200" rx="26" stroke="${farbe}"/>
<path d="M570 260h30a20 20 0 0 1 20 20v40a20 20 0 0 1-20 20h-30" stroke="${farbe}"/>
${inhalt}`;
const bilder = {
// Wie viele Personen leben im Haushalt?
'personen-1': person(400, 320, 1.3),
'personen-2': person(300, 320, 1.15) + person(500, 320, 1.15),
'personen-3-4': person(210, 330) + person(400, 330) + person(590, 330),
'personen-4-plus': person(160, 330, .85) + person(320, 330, .85) + person(480, 330, .85) + person(640, 330, .85)
+ `<path d="M715 140v70M680 175h70" stroke="${ROT}"/>`,
// Welche Form hat Ihr Dach?
'dach-flach': haus(`<path d="M150 300h500" stroke="${ROT}"/><path d="M200 300v-40h400v40" stroke="${ROT}"/>`),
'dach-sattel': haus(`<path d="M140 320 400 120l260 200" stroke="${ROT}"/>`),
'dach-pult': haus(`<path d="M150 330 650 190" stroke="${ROT}"/><path d="M610 300v-95" />`),
'dach-versetzt': `
<path d="M120 520h560M160 520V370M440 520V270M660 520V330"/>
<rect x="260" y="420" width="70" height="100" rx="6"/>
<path d="M120 380 290 250l150 120" stroke="${ROT}"/><path d="M440 290 560 180l130 140" stroke="${ROT}"/>`,
'dach-sonstiges': haus(`<path d="M140 320 400 120l260 200" stroke="${GRAU}"/>`) + fragezeichen(400, 250, 1.1),
// Wie groß ist Ihre Dachfläche?
'flaeche-50': module(2, 1),
'flaeche-100': module(3, 2),
'flaeche-200': module(5, 3),
'flaeche-200-plus': module(7, 4),
'flaeche-unsicher': `<path d="M160 150h480l90 300H160z" stroke="${GRAU}"/>` + fragezeichen(400, 300, 1.4),
// Aus welchem Material besteht die Dacheindeckung?
'eindeckung-ziegel': [0, 1, 2, 3].map(z => [0, 1, 2, 3, 4].map(s => {
if (z % 2 === 1 && s === 4) return '';
const x = 150 + s * 100 + (z % 2) * 50, y = 170 + z * 80;
return `<path d="M${x} ${y}a50 50 0 0 0 100 0" stroke="${z === 2 && s === 2 ? ROT : TINTE}"/>`;
}).join('')).join(''),
'eindeckung-bitumen': `
<path d="M130 440h540v50H130z"/>
<circle cx="250" cy="360" r="70"/><circle cx="250" cy="360" r="26"/>
<path d="M320 400h340" stroke="${ROT}" stroke-width="22"/>
<path d="M250 430h410" stroke="${ROT}" stroke-width="22" stroke-opacity=".35"/>`,
'eindeckung-blech': `
<path d="M120 340h50l30-70h60l30 70h60l30-70h60l30 70h60l30-70h60l30 70h30"/>
<path d="M120 420h50l30-70h60l30 70h60l30-70h60l30 70h60l30-70h60l30 70h30" stroke="${GRAU}"/>
<path d="M120 340v80M680 340v80" stroke="${GRAU}"/>
<path d="M350 340l30-70h60l30 70" stroke="${ROT}"/>`,
'eindeckung-anderes': `<rect x="200" y="150" width="400" height="300" rx="20" stroke="${GRAU}"/>` + fragezeichen(400, 300, 1.4),
// Wünschen Sie einen Stromspeicher?
'speicher-ja': batterie(`<path d="M420 230 350 310h60l-30 70 70-80h-60z" fill="${ROT}" stroke="${ROT}" stroke-width="10"/>`),
'speicher-nein': batterie('', GRAU) + `<path d="M190 470 610 130" stroke="${ROT}"/>`,
};
await mkdir(ziel, { recursive: true });
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 800, height: 600 }, deviceScaleFactor: 1 });
for (const [name, inhalt] of Object.entries(bilder)) {
const svg = bild(inhalt);
await writeFile(`${ziel}/${name}.svg`, svg);
await page.setContent(`<!doctype html><body style="margin:0;background:${PAPIER}">${svg}</body>`);
await page.locator('svg').screenshot({ path: `${ziel}/${name}.png`, omitBackground: false });
console.log('gezeichnet:', name);
}
await browser.close();