Add Vue + Vite port of the logo generator

Single-file component (App.vue) over the shared src/logo.js core.
Reactive state via reactive(); v-model drives controls, computed
properties produce the live logo style and generated CSS. Keeps the
accessibility markup (labels, aria, <details>/<summary>, focus
rings); reset restores defaults through the form's @reset handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 12:22:28 +02:00
co-authored by Claude Opus 4.7
parent f495c52401
commit 648b25de7f
9 changed files with 1804 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
// Framework-agnostic core for the logo generator.
// State shape (see `defaults`) is shared by every port.
export const defaults = {
text: 'LIBRE\nTECH',
font: 'Anton',
fs: 18,
lh: 1.05,
ls: 0.01,
bg: '#362582',
face: '#f6d939',
depth: '#ea741e',
shadow: '#221953',
angle: 133,
shadowLen: 30,
depthLen: 15,
contourLen: 11,
rot: -10,
skew: -8,
};
export const fonts = [
'Anton',
'Bowlby One',
'Bebas Neue',
'Bungee',
'Passion One',
'Ultra',
'Impact',
];
const round = (n) => Math.round(n * 100) / 100;
// Layered text-shadow offsets. `depthColor`/`shadowColor` are substituted
// so the same geometry serves both the live render (literal hex) and the
// generated snippet (CSS var()).
export function shadowLayers(s, depthColor, shadowColor) {
const rad = (s.angle * Math.PI) / 180;
const dx = Math.sin(rad);
const dy = -Math.cos(rad);
const parts = [];
// Kontur (front): steps along the light direction, shadow colour.
for (let i = 1; i <= s.contourLen; i++) {
parts.push(`${round(dx * i)}px ${round(dy * i)}px 0 ${shadowColor}`);
}
// Tiefe (middle): opposite direction.
for (let i = 1; i <= s.depthLen; i++) {
parts.push(`${round(-dx * i)}px ${round(-dy * i)}px 0 ${depthColor}`);
}
// Schatten (back): long, same direction as Kontur; continue past
// contourLen so steps do not overlap.
for (let i = s.contourLen + 1; i <= s.contourLen + s.shadowLen; i++) {
parts.push(`${round(dx * i)}px ${round(dy * i)}px 0 ${shadowColor}`);
}
return parts;
}
export function inlineTextShadow(s) {
const parts = shadowLayers(s, s.depth, s.shadow);
return parts.length ? parts.join(', ') : 'none';
}
export function logoStyle(s) {
return {
fontFamily: `'${s.font}', 'Bebas Neue', Impact, sans-serif`,
fontSize: `${s.fs}vw`,
lineHeight: String(s.lh),
letterSpacing: `${s.ls}em`,
color: s.face,
transform: `rotate(${s.rot}deg) skewX(${s.skew}deg)`,
textShadow: inlineTextShadow(s),
};
}
export function cssOutput(s) {
const lines = shadowLayers(s, 'var(--depth)', 'var(--shadow)').map((l) => ' ' + l);
return `:root {
--bg: ${s.bg};
--face: ${s.face};
--depth: ${s.depth};
--shadow: ${s.shadow};
}
body {
background: var(--bg);
}
.logo {
font-family: '${s.font}', sans-serif;
font-size: ${s.fs}vw;
line-height: ${s.lh};
letter-spacing: ${s.ls}em;
color: var(--face);
transform: rotate(${s.rot}deg) skewX(${s.skew}deg);
white-space: pre-line;
text-shadow:
${lines.join(',\n')};
}`;
}