178 lines
8.8 KiB
JavaScript
178 lines
8.8 KiB
JavaScript
/* ============================================================
|
||
TROX – Gemeinsames Karten-Zeichenmodul
|
||
------------------------------------------------------------
|
||
Eine einzige Quelle für das Kartendesign.
|
||
Wird von generate_cards.js (ganze Karten) UND
|
||
generate_box.js (skalierte Mini-Karten im Fächer) genutzt,
|
||
damit die Box-Karten exakt den echten Karten entsprechen.
|
||
|
||
Bezugssystem: Alle Zeichenfunktionen arbeiten in den
|
||
Koordinaten der ORIGINAL-Karte (694 x 1069). Für andere
|
||
Maße/Skalierungen wird außen herum transformiert.
|
||
============================================================ */
|
||
|
||
// Referenzmaße der Original-Karte (Designraster)
|
||
const REF_W = 694;
|
||
const REF_H = 1069;
|
||
|
||
// Farben + Codes (Y = Yellow, G = Green – englische Logik)
|
||
const SUITS = {
|
||
Rot: { hex: '#E53935', code: 'R', name: 'Red' }, // Trumpf
|
||
Gelb: { hex: '#FDD835', code: 'Y', name: 'Yellow' },
|
||
Gruen: { hex: '#43A047', code: 'G', name: 'Green' },
|
||
Schwarz: { hex: '#212121', code: 'B', name: 'Black' },
|
||
};
|
||
|
||
// ------------------------------------------------------------
|
||
// Symbolzentrum (rotationssymmetrisches Paar) – Koordinaten
|
||
// relativ zum Kartenmittelpunkt (0,0).
|
||
// ------------------------------------------------------------
|
||
function centerIcons(colorName, hex) {
|
||
if (colorName === 'Rot') {
|
||
const crown = `
|
||
<path d="M -20 12 L -24 -8 L -11 2 L 0 -15 L 11 2 L 24 -8 L 20 12 Z" fill="${hex}" />
|
||
<rect x="-20" y="14" width="40" height="4" fill="${hex}" rx="1" />`;
|
||
return `
|
||
<g transform="translate(0, -22)">${crown}</g>
|
||
<g transform="rotate(180) translate(0, -22)">${crown}</g>`;
|
||
}
|
||
if (colorName === 'Gruen') {
|
||
const leaf = `
|
||
<path d="M 0 12 Q -16 2 -14 -12 Q -9 -22 0 -25 Q 9 -22 14 -12 Q 16 2 0 12" fill="${hex}" />
|
||
<path d="M 0 12 Q -5 16 -10 20" fill="none" stroke="${hex}" stroke-width="3" stroke-linecap="round" />`;
|
||
return `
|
||
<g transform="translate(0, -22)">${leaf}</g>
|
||
<g transform="rotate(180) translate(0, -22)">${leaf}</g>`;
|
||
}
|
||
if (colorName === 'Gelb') {
|
||
const sunRay = `<path d="M 0 -12 L 2.5 -19 L 0 -22 L -2.5 -19 Z" fill="${hex}" />`;
|
||
const singleSun = `
|
||
<circle cx="0" cy="0" r="8" fill="${hex}" />
|
||
${sunRay}
|
||
<g transform="rotate(45)">${sunRay}</g>
|
||
<g transform="rotate(90)">${sunRay}</g>
|
||
<g transform="rotate(135)">${sunRay}</g>
|
||
<g transform="rotate(180)">${sunRay}</g>
|
||
<g transform="rotate(225)">${sunRay}</g>
|
||
<g transform="rotate(270)">${sunRay}</g>
|
||
<g transform="rotate(315)">${sunRay}</g>`;
|
||
return `
|
||
<g transform="translate(0, -22)">${singleSun}</g>
|
||
<g transform="rotate(180) translate(0, -22)">${singleSun}</g>`;
|
||
}
|
||
// Schwarz – Zahnrad-Paar
|
||
const singleGear = `
|
||
<rect x="-2.5" y="-14" width="5" height="28" fill="${hex}" rx="1"/>
|
||
<rect x="-2.5" y="-14" width="5" height="28" fill="${hex}" rx="1" transform="rotate(60)"/>
|
||
<rect x="-2.5" y="-14" width="5" height="28" fill="${hex}" rx="1" transform="rotate(120)"/>
|
||
<circle cx="0" cy="0" r="10" fill="${hex}"/>
|
||
<circle cx="0" cy="0" r="5" fill="#ffffff"/>
|
||
<circle cx="0" cy="0" r="1.5" fill="${hex}"/>`;
|
||
return `
|
||
<g transform="translate(0, -22)">${singleGear}</g>
|
||
<g transform="rotate(180) translate(0, -22)">${singleGear}</g>`;
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// cardArtwork: der gesamte Karteninhalt OHNE den weißen
|
||
// Kartenkörper – als <g>, gezeichnet im Raster width x height.
|
||
// safe = Abstand für Eckelemente & Zierrand
|
||
// drawBorder = Zierrand zeichnen (ja bei echten Karten)
|
||
// ------------------------------------------------------------
|
||
function cardArtwork(colorName, num, { width = REF_W, height = REF_H, safe = 35, drawBorder = true, cornerRadius = 30 } = {}) {
|
||
const suit = SUITS[colorName];
|
||
const hex = suit.hex;
|
||
const cardCode = `TK-${suit.code}${num}`;
|
||
|
||
const cornerX = safe + 20;
|
||
const cornerNumY = safe + 55;
|
||
const cornerCodeY = cornerNumY + 22;
|
||
const centerGap = 70;
|
||
|
||
const border = drawBorder
|
||
? `<rect x="${safe}" y="${safe}" width="${width - 2 * safe}" height="${height - 2 * safe}" rx="${Math.max(0, cornerRadius - 5)}" fill="none" stroke="${hex}" stroke-width="2" opacity="0.4" />`
|
||
: '';
|
||
|
||
return `
|
||
${border}
|
||
|
||
<!-- Ecken oben -->
|
||
<text x="${cornerX}" y="${cornerNumY}" class="trox-cnum" fill="${hex}">${num}</text>
|
||
<text x="${cornerX}" y="${cornerCodeY}" class="trox-code">${cardCode}</text>
|
||
<text x="${width - cornerX}" y="${cornerNumY}" class="trox-cnum" fill="${hex}" text-anchor="end">${num}</text>
|
||
<text x="${width - cornerX}" y="${cornerCodeY}" class="trox-code" text-anchor="end">${cardCode}</text>
|
||
|
||
<!-- Ecken unten (180°) -->
|
||
<g transform="translate(${width}, ${height}) rotate(180)">
|
||
<text x="${cornerX}" y="${cornerNumY}" class="trox-cnum" fill="${hex}">${num}</text>
|
||
<text x="${cornerX}" y="${cornerCodeY}" class="trox-code">${cardCode}</text>
|
||
<text x="${width - cornerX}" y="${cornerNumY}" class="trox-cnum" fill="${hex}" text-anchor="end">${num}</text>
|
||
<text x="${width - cornerX}" y="${cornerCodeY}" class="trox-code" text-anchor="end">${cardCode}</text>
|
||
</g>
|
||
|
||
<!-- Große Mittelzahl OBEN -->
|
||
<text x="${width / 2}" y="${height / 2 - centerGap}" class="trox-bignum" fill="${hex}" text-anchor="middle">${num}</text>
|
||
|
||
<!-- Mittellinie + Symbolzentrum -->
|
||
<line x1="${safe + 5}" y1="${height / 2}" x2="${width / 2 - 50}" y2="${height / 2}" stroke="${hex}" stroke-width="4" stroke-linecap="round" />
|
||
<line x1="${width / 2 + 50}" y1="${height / 2}" x2="${width - safe - 5}" y2="${height / 2}" stroke="${hex}" stroke-width="4" stroke-linecap="round" />
|
||
<g transform="translate(${width / 2}, ${height / 2})">
|
||
${centerIcons(colorName, hex)}
|
||
</g>
|
||
|
||
<!-- Große Mittelzahl UNTEN (180°) -->
|
||
<g transform="translate(${width}, ${height}) rotate(180)">
|
||
<text x="${width / 2}" y="${height / 2 - centerGap}" class="trox-bignum" fill="${hex}" text-anchor="middle">${num}</text>
|
||
</g>`;
|
||
}
|
||
|
||
// CSS-Klassen, die cardArtwork voraussetzt (font-size SKALIERBAR über fontScale)
|
||
function cardStyles(hex, fontScale = 1) {
|
||
return `
|
||
.trox-cnum { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: bold; font-size: ${65 * fontScale}px; }
|
||
.trox-code { font-family: 'Helvetica Neue', Arial, sans-serif; font-weight: bold; font-size: ${14 * fontScale}px; fill: #888888; letter-spacing: 1px; }
|
||
.trox-bignum { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: bold; font-size: ${320 * fontScale}px; letter-spacing: -10px; }`;
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// buildCardSVG: vollständige, eigenständige Karten-SVG.
|
||
// ------------------------------------------------------------
|
||
function buildCardSVG(colorName, num, { width = REF_W, height = REF_H, safe = 35, cornerRadius = 35 } = {}) {
|
||
const suit = SUITS[colorName];
|
||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
|
||
<defs>
|
||
<style>${cardStyles(suit.hex, 1)}</style>
|
||
</defs>
|
||
<rect width="${width}" height="${height}" ${cornerRadius ? `rx="${cornerRadius}"` : ''} fill="#ffffff" />
|
||
${cardArtwork(colorName, num, { width, height, safe, drawBorder: true, cornerRadius })}
|
||
</svg>`;
|
||
}
|
||
|
||
// ------------------------------------------------------------
|
||
// miniCardGroup: eine VOLLSTÄNDIGE Karte als skalierte <g>,
|
||
// zentriert auf (cx,cy), gedreht um rot Grad. Für den Box-Fächer.
|
||
// Zeichnet Schatten + weißen Körper + komplettes Artwork,
|
||
// alles im Referenzraster und dann sauber herunterskaliert.
|
||
// ------------------------------------------------------------
|
||
function miniCardGroup(colorName, num, cx, cy, targetWidth, rot = 0) {
|
||
const suit = SUITS[colorName];
|
||
const scale = targetWidth / REF_W; // ein einziger Skalenfaktor → 1:1 Optik
|
||
const w = REF_W, h = REF_H;
|
||
|
||
return `
|
||
<g transform="translate(${cx}, ${cy}) rotate(${rot}) scale(${scale}) translate(${-w / 2}, ${-h / 2})">
|
||
<style>${cardStyles(suit.hex, 1)}</style>
|
||
<!-- Schlagschatten -->
|
||
<rect x="14" y="20" width="${w}" height="${h}" rx="30" fill="#000000" opacity="0.28" />
|
||
<!-- weißer Kartenkörper -->
|
||
<rect x="0" y="0" width="${w}" height="${h}" rx="30" fill="#ffffff" stroke="${suit.hex}" stroke-width="3" />
|
||
<!-- exakt dasselbe Artwork wie auf der echten Karte -->
|
||
${cardArtwork(colorName, num, { width: w, height: h, safe: 35, drawBorder: true, cornerRadius: 30 })}
|
||
</g>`;
|
||
}
|
||
|
||
module.exports = {
|
||
REF_W, REF_H, SUITS,
|
||
centerIcons, cardArtwork, cardStyles, buildCardSVG, miniCardGroup,
|
||
};
|