Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
648b25de7f
|
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
.DS_Store
|
||||
+13
@@ -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>
|
||||
Generated
+1364
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "demo-ecmascript-vue",
|
||||
"version": "1.0.0",
|
||||
"description": "LIBRETECH logo generator — Vue + 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"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<script setup>
|
||||
import { reactive, computed, ref, watchEffect } from 'vue';
|
||||
import { defaults, fonts, logoStyle, cssOutput } from './logo.js';
|
||||
|
||||
const FONT_LABELS = { Impact: 'Impact (System)' };
|
||||
|
||||
const s = reactive({ ...defaults });
|
||||
const css = computed(() => cssOutput(s));
|
||||
const logoCss = computed(() => logoStyle(s));
|
||||
const copied = ref(false);
|
||||
|
||||
watchEffect(() => {
|
||||
document.body.style.background = s.bg;
|
||||
});
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(css.value);
|
||||
copied.value = true;
|
||||
setTimeout(() => (copied.value = false), 1500);
|
||||
} catch {
|
||||
alert('Kopieren fehlgeschlagen — bitte manuell aus dem Code-Feld kopieren.');
|
||||
}
|
||||
}
|
||||
|
||||
// v-model bindings own the values, so restore defaults explicitly on reset.
|
||||
function reset() {
|
||||
Object.assign(s, defaults);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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" @submit.prevent @reset.prevent="reset">
|
||||
<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" v-model="s.text"></textarea>
|
||||
|
||||
<h2>Schrift</h2>
|
||||
<select id="font" aria-label="Schriftart" v-model="s.font">
|
||||
<option v-for="f in fonts" :key="f" :value="f">{{ FONT_LABELS[f] || f }}</option>
|
||||
</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" v-model.number="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" v-model.number="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" v-model.number="s.ls" />
|
||||
|
||||
<h2>Farben</h2>
|
||||
<div class="color-row">
|
||||
<input type="color" id="c-bg" v-model="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" v-model="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" v-model="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" v-model="s.shadow" aria-labelledby="lbl-shadow" />
|
||||
<span class="swatch-label" id="lbl-shadow">Schatten & 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" v-model.number="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" v-model.number="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" v-model.number="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" v-model.number="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" v-model.number="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" v-model.number="s.skew" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Generiertes CSS</h2>
|
||||
<code id="cssout">{{ css }}</code>
|
||||
<button type="button" class="copy-btn" :class="{ copied }" @click="copy">
|
||||
{{ copied ? 'Kopiert!' : 'CSS kopieren' }}
|
||||
</button>
|
||||
<button type="reset" class="reset-btn">Zurücksetzen</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
+99
@@ -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')};
|
||||
}`;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import './style.css';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
+169
@@ -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; }
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
});
|
||||
Reference in New Issue
Block a user