Add Svelte + Vite port of the logo generator

Svelte 5 (runes) single-component port over the shared src/logo.js
core. $state holds the model, $derived produces the generated CSS and
the logo's style string, $effect syncs the body background. bind:value
drives the controls (range/number inputs coerce to numbers). Keeps the
accessibility markup (labels, aria, <details>/<summary>, focus rings);
reset restores defaults via the form's onreset handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 12:25:17 +02:00
co-authored by Claude Opus 4.7
parent f495c52401
commit c147843cb5
10 changed files with 1919 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
*.log
.DS_Store
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Logo-Generator</title>
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Bowlby+One&family=Bebas+Neue&family=Bungee&family=Passion+One:wght@900&family=Ultra&display=swap" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+1462
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "demo-ecmascript-svelte",
"version": "1.0.0",
"description": "LIBRETECH logo generator — Svelte + Vite",
"repository": {
"type": "git",
"url": "ssh://tengo@git.librete.ch:41240/public/demo-ecmascript.git"
},
"license": "WTFPL",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"svelte": "^5.16.0",
"vite": "^6.0.0"
}
}
+133
View File
@@ -0,0 +1,133 @@
<script>
import { defaults, fonts, logoStyle, cssOutput } from './logo.js';
const FONT_LABELS = { Impact: 'Impact (System)' };
let s = $state({ ...defaults });
let copied = $state(false);
const css = $derived(cssOutput(s));
const logoCss = $derived(styleString(logoStyle(s)));
$effect(() => {
document.body.style.background = s.bg;
});
// logoStyle() returns a camelCase object; the style attribute needs a string.
function styleString(obj) {
return Object.entries(obj)
.map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())}:${v}`)
.join(';');
}
async function copy() {
try {
await navigator.clipboard.writeText(css);
copied = true;
setTimeout(() => (copied = false), 1500);
} catch {
alert('Kopieren fehlgeschlagen — bitte manuell aus dem Code-Feld kopieren.');
}
}
function onSubmit(e) {
e.preventDefault();
}
// bind:value owns the values, so restore defaults explicitly on reset.
function onReset(e) {
e.preventDefault();
s = { ...defaults };
}
</script>
<div class="logo" style={logoCss}>{s.text || ' '}</div>
<details class="disclosure" id="panel" open>
<summary class="toggle-panel">Panel ein/aus</summary>
<div class="panel">
<h1>Logo-Generator</h1>
<form id="logoForm" onsubmit={onSubmit} onreset={onReset}>
<h2>Inhalt</h2>
<p class="hint" id="hint-text">Mehrere Zeilen mit Enter trennen.</p>
<textarea id="text" rows="2" aria-label="Logo-Text" aria-describedby="hint-text" bind:value={s.text}></textarea>
<h2>Schrift</h2>
<select id="font" aria-label="Schriftart" bind:value={s.font}>
{#each fonts as f}
<option value={f}>{FONT_LABELS[f] || f}</option>
{/each}
</select>
<div class="row-2col">
<div>
<label for="fs">Größe: <span class="val">{s.fs}</span>vw</label>
<input type="range" id="fs" min="6" max="30" step="0.5" bind:value={s.fs} />
</div>
<div>
<label for="lh">Zeilenabstand: <span class="val">{s.lh.toFixed(2)}</span></label>
<input type="range" id="lh" min="0.5" max="2" step="0.05" bind:value={s.lh} />
</div>
</div>
<label for="ls">Buchstabenabstand: <span class="val">{s.ls.toFixed(3)}</span>em</label>
<input type="range" id="ls" min="-0.15" max="0.3" step="0.005" bind:value={s.ls} />
<h2>Farben</h2>
<div class="color-row">
<input type="color" id="c-bg" bind:value={s.bg} aria-labelledby="lbl-bg" />
<span class="swatch-label" id="lbl-bg">Hintergrund</span>
<span class="hex">{s.bg}</span>
</div>
<div class="color-row">
<input type="color" id="c-face" bind:value={s.face} aria-labelledby="lbl-face" />
<span class="swatch-label" id="lbl-face">Face</span>
<span class="hex">{s.face}</span>
</div>
<div class="color-row">
<input type="color" id="c-depth" bind:value={s.depth} aria-labelledby="lbl-depth" />
<span class="swatch-label" id="lbl-depth">Tiefe</span>
<span class="hex">{s.depth}</span>
</div>
<div class="color-row">
<input type="color" id="c-shadow" bind:value={s.shadow} aria-labelledby="lbl-shadow" />
<span class="swatch-label" id="lbl-shadow">Schatten &amp; Kontur</span>
<span class="hex">{s.shadow}</span>
</div>
<h2>Lichtrichtung</h2>
<p class="hint">Steuert alle Versätze gleichzeitig — Schatten geht in Lichtrichtung, Tiefe entgegen.</p>
<label for="angle">Winkel: <span class="val">{s.angle}</span>° (0=oben, 90=rechts, 180=unten)</label>
<input type="range" id="angle" min="0" max="359" bind:value={s.angle} />
<h2>Schichten</h2>
<p class="hint">Hinten nach vorn: Schatten · Tiefe · Kontur · Face</p>
<label for="shadow-len">Schatten-Länge: <span class="val">{s.shadowLen}</span>px</label>
<input type="range" id="shadow-len" min="0" max="80" bind:value={s.shadowLen} />
<label for="depth-len">Tiefe (in Gegenrichtung): <span class="val">{s.depthLen}</span>px</label>
<input type="range" id="depth-len" min="0" max="30" bind:value={s.depthLen} />
<label for="contour-len">Kontur (vor der Tiefe): <span class="val">{s.contourLen}</span>px</label>
<input type="range" id="contour-len" min="0" max="20" bind:value={s.contourLen} />
<h2>Komposition</h2>
<div class="row-2col">
<div>
<label for="rot">Drehung: <span class="val">{s.rot}</span>°</label>
<input type="range" id="rot" min="-25" max="25" bind:value={s.rot} />
</div>
<div>
<label for="skew">Schräge: <span class="val">{s.skew}</span>°</label>
<input type="range" id="skew" min="-20" max="20" bind:value={s.skew} />
</div>
</div>
<h2>Generiertes CSS</h2>
<code id="cssout">{css}</code>
<button type="button" class="copy-btn" class:copied onclick={copy}>
{copied ? 'Kopiert!' : 'CSS kopieren'}
</button>
<button type="reset" class="reset-btn">Zurücksetzen</button>
</form>
</div>
</details>
+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')};
}`;
}
+7
View File
@@ -0,0 +1,7 @@
import { mount } from 'svelte';
import App from './App.svelte';
import './style.css';
const app = mount(App, { target: document.getElementById('app') });
export default app;
+169
View File
@@ -0,0 +1,169 @@
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
background: #362582;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Anton', 'Bebas Neue', Impact, sans-serif;
overflow: hidden;
transition: background 0.15s;
}
#app { display: contents; }
.logo {
font-size: 18vw;
font-weight: 400;
color: #f6d939;
line-height: 1.05;
letter-spacing: 0.01em;
transform: rotate(-10deg) skewX(-8deg);
transform-origin: center;
text-align: center;
user-select: none;
transition: color 0.15s;
white-space: pre-line;
}
.panel {
position: fixed;
top: 12px;
right: 12px;
background: rgba(0,0,0,0.72);
color: #eee;
padding: 14px 18px;
border-radius: 10px;
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
font-size: 12px;
width: 320px;
backdrop-filter: blur(10px);
max-height: 96vh;
overflow-y: auto;
box-shadow: 0 6px 30px rgba(0,0,0,0.4);
}
.panel h1 { margin: 0 0 10px; font-size: 14px; color: #fff; }
.panel h2 {
margin: 14px 0 6px;
font-size: 11px;
color: #f6d939;
text-transform: uppercase;
letter-spacing: 0.08em;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 4px;
}
.panel .hint {
font-size: 10px;
color: #aaa;
margin: 0 0 8px;
font-style: italic;
}
.panel label {
display: block;
margin: 6px 0 3px;
font-size: 11px;
color: #ccc;
}
.panel input[type=range] { width: 100%; margin: 0; }
.panel input[type=text],
.panel textarea,
.panel select {
width: 100%;
padding: 5px 8px;
background: rgba(255,255,255,0.08);
color: white;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
font-family: inherit;
font-size: 12px;
}
.panel textarea {
resize: vertical;
min-height: 60px;
}
.panel input:focus, .panel textarea:focus, .panel select:focus {
outline: 2px solid #f6d939;
outline-offset: 1px;
border-color: rgba(246,217,57,0.6);
}
.toggle-panel:focus-visible,
.copy-btn:focus-visible,
.reset-btn:focus-visible {
outline: 2px solid #f6d939;
outline-offset: 2px;
}
.color-row {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 8px;
align-items: center;
margin: 4px 0;
}
.color-row input[type=color] {
width: 36px;
height: 28px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
background: transparent;
cursor: pointer;
padding: 2px;
}
.color-row .swatch-label { font-size: 11px; color: #ccc; }
.color-row .hex { font-size: 10px; color: #aaa; text-align: right; width: 60px; }
.val { color: #f6d939; font-weight: bold; }
.row-2col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.panel code {
display: block;
background: rgba(0,0,0,0.5);
padding: 8px;
margin-top: 10px;
border-radius: 4px;
font-size: 10px;
max-height: 220px;
overflow-y: auto;
white-space: pre;
color: #aaa;
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
.copy-btn, .reset-btn {
width: 100%;
padding: 6px;
margin-top: 6px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 11px;
}
.copy-btn {
background: rgba(246,217,57,0.15);
color: #f6d939;
border: 1px solid rgba(246,217,57,0.4);
}
.copy-btn:hover { background: rgba(246,217,57,0.25); }
.copy-btn.copied { background: rgba(120,200,120,0.25); color: #cfc; border-color: rgba(120,200,120,0.5); }
.reset-btn {
background: rgba(255,255,255,0.05);
color: #ccc;
border: 1px solid rgba(255,255,255,0.15);
}
.reset-btn:hover { background: rgba(255,255,255,0.1); }
.toggle-panel {
position: fixed;
top: 12px;
left: 12px;
background: rgba(0,0,0,0.6);
color: white;
border: 1px solid rgba(255,255,255,0.2);
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 11px;
z-index: 10;
}
summary.toggle-panel { list-style: none; }
summary.toggle-panel::-webkit-details-marker { display: none; }
.disclosure { position: static; }
+5
View File
@@ -0,0 +1,5 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
};
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
});