This is an old revision of the document!
<!doctype html> <html lang=“de”> <head> <meta charset=“utf-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1”> <title>Wireframe-Ideen-Generator</title> <style>
:root {
--k: #111111; --o: #FF6A13; --b: #2B5BFF;
--g1: #F3F3F3; --g2: #D4D4D4; --g3: #5D5D5D;
color-scheme: light;
}
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: var(--k); font: 16px/1.4 Arial, Helvetica, sans-serif; }
#app { max-width: 1000px; margin: 0 auto; padding: 16px; }
h1 { font-size: 22px; margin: 0 0 4px; }
.hint { margin: 0 0 14px; color: var(--g3); font-size: 14px; }
.bar { display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: center; margin-bottom: 14px; }
.grp { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.grp > span { font-size: 13px; font-weight: bold; color: var(--g3); text-transform: uppercase; }
.seg { display: inline-flex; }
button {
font: inherit; font-weight: bold; font-size: 15px; color: var(--k); background: #fff;
border: 2px solid var(--k); border-radius: 8px; padding: 8px 14px; cursor: pointer;
}
button:hover { background: var(--g1); }
button:focus-visible { outline: 3px solid var(--b); outline-offset: 2px; }
.seg button { border-radius: 0; margin-left: -2px; padding: 8px 12px; }
.seg button:first-child { border-radius: 8px 0 0 8px; margin-left: 0; }
.seg button:last-child { border-radius: 0 8px 8px 0; }
.seg button[aria-pressed="true"] { background: var(--k); color: #fff; }
button.primary { background: var(--o); border-color: var(--o); font-size: 17px; padding: 10px 20px; }
button.primary:hover { background: #ff7f33; border-color: #ff7f33; }
#stage { width: 100%; }
#stage canvas { display: block; }
.panel { border: 2px solid var(--k); border-radius: 14px; padding: 14px 18px; margin-top: 14px; }
.panel h2 { margin: 0 0 8px; font-size: 13px; color: var(--g3); text-transform: uppercase; }
#brief { margin: 0; white-space: pre-wrap; font: 16px/1.6 Arial, Helvetica, sans-serif; }
.panel { position: relative; }
.iconbtn { position: absolute; top: 6px; right: 11px; width: 36px; height: 36px; padding: 0; border: 0; background: transparent;
color: var(--k); display: flex; align-items: center; justify-content: center; border-radius: 8px; }
.iconbtn:hover { background: var(--g1); }
.timer { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
#time { font-size: 40px; font-weight: bold; min-width: 120px; font-variant-numeric: tabular-nums; }
#time.done { color: var(--o); }
.note { color: var(--b); font-size: 14px; font-weight: bold; }
@media (max-width: 480px) { #app { padding: 12px; } #time { font-size: 32px; min-width: 96px; } }
</style> </head> <body>
<h1>Wireframe-Ideen-Generator</h1> <p class="hint">Klick auf eine Karte würfelt sie neu. Das Schloss hält eine Karte fest.</p>
<div class="bar">
<button class="primary" id="rollAll" type="button">Alles würfeln</button>
<div class="grp" role="group" aria-label="Anzahl der Karten">
<span>Karten</span>
<div class="seg">
<button type="button" data-n="3" aria-pressed="false">3 · Basis</button>
<button type="button" data-n="6" aria-pressed="true">6 · Voll</button>
</div>
</div>
<div class="grp" role="group" aria-label="Chaos-Level">
<span>Chaos</span>
<div class="seg">
<button type="button" data-l="1" aria-pressed="false">realistisch</button>
<button type="button" data-l="2" aria-pressed="false">schräg</button>
<button type="button" data-l="3" aria-pressed="true">absurd</button>
</div>
</div>
</div>
<div id="stage" role="img" aria-label="Zufällig gewürfelte Karten: Produkt, Kundin, Thema, Gerät, Hauptaktion, Twist. Die Auswahl steht auch als Text unter den Karten."></div>
<div class="panel" id="briefPanel">
<h2>Dein Briefing</h2>
<button type="button" class="iconbtn" id="copyTxt" aria-label="Briefing kopieren" title="Briefing kopieren">
<svg id="icCopy" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="8" y="8" width="12" height="12" rx="3"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>
</svg>
<svg id="icDone" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#2B5BFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none">
<path d="M5 12.5l4.5 4.5L19 7.5"/>
</svg>
</button>
<pre id="brief" aria-live="polite"></pre>
</div>
<div class="panel">
<h2>Timer</h2>
<div class="timer">
<div id="time" aria-live="off">10:00</div>
<button type="button" id="tStart">Start</button>
<button type="button" id="tReset">Zurück</button>
<div class="seg" role="group" aria-label="Zeit wählen">
<button type="button" data-min="10" aria-pressed="true">10 Min · Skizzen</button>
<button type="button" data-min="25" aria-pressed="false">25 Min · Mid-Fi</button>
</div>
</div>
</div>
<script src=“https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js”
onerror="document.getElementById('stage').textContent='p5.js konnte nicht geladen werden. Bitte Internetverbindung prüfen.'"></script>
<script> /* ———- Farben (wie in den Praktikumsfolien) ———- */ const K = '#111111', O = '#FF6A13', B = '#2B5BFF', G1 = '#F3F3F3', G2 = '#D4D4D4', G3 = '#5D5D5D';
/* ———- Inhalte: [Text, Chaos-Stufe 1–3, (Breite, Höhe nur bei Gerät)] ———- */ const POOLS = [
{ name: 'Produkt', label: 'PRODUKT', icon: 'produkt', items: [
['Mobile App', 1], ['Website mit Landingpage', 1], ['Onlineshop', 1], ['Buchungs-Plattform', 1],
['Dashboard', 1], ['Lern-Plattform', 1], ['Karten- und Navigations-App', 1], ['Mitglieder-Portal', 1],
['Bezahlvorgang (Checkout)', 1], ['Onboarding-Strecke', 1], ['Chat-Oberfläche', 1],
['Dating-App', 2], ['Tauschbörse', 2], ['Bewertungsportal', 2], ['Podcast-App', 2],
['Rezept-Plattform', 2], ['Smart-Home-Steuerung', 2], ['Crowdfunding-Seite', 2], ['Kiosk-Terminal', 2],
['Social Network für genau zwei Personen', 3], ['Kontrollzentrum mit zu vielen Knöpfen', 3],
['Beschwerde-Hotline als App', 3], ['Wahl-O-Mat für Alltagsentscheidungen', 3], ['Notfall-Knopf-Portal', 3]
]},
{ name: 'Kundin', label: 'KUNDIN', icon: 'kundin', items: [
['Studentin im Prüfungsstress', 1], ['Landwirtin mit schlechtem Netz', 1], ['Busfahrerin im Schichtdienst', 1],
['Hebamme auf Nachtschicht', 1], ['Hochzeitsplanerin mit 14 Terminen', 1], ['Bibliothekarin, die alles sortiert', 1],
['Ärztin mit 3 Minuten Zeit', 1], ['Pilotin und alleinerziehend', 1],
['Gärtnerin mit 300 Zimmerpflanzen', 2], ['Marathonläuferin ohne Orientierungssinn', 2],
['Kapitänin eines Fischkutters ohne Empfang', 2], ['Zirkusdirektorin mit Lampenfieber', 2],
['Schornsteinfegerin mit Höhenangst', 2], ['Influencerin, die privat offline lebt', 2],
['Detektivin, die ständig etwas verlegt', 2], ['Köchin, die nie Rezepte liest', 2],
['Zeitreisende aus dem Jahr 1850', 3], ['Drachenbesitzerin mit Kleinunternehmen', 3], ['Hexe im Homeoffice', 3],
['Außerirdische Austauschstudentin', 3], ['Piratenkapitänin im Ruhestand', 3], ['Fee mit Burnout', 3],
['Waschbär-Anwältin', 3], ['Königin eines sehr kleinen Landes', 3], ['Yeti-Vorsitzende des Bergvereins', 3],
['Roboter-Oma mit Sprachfehler', 3], ['Einhorn-Züchterin mit Pollenallergie', 3]
]},
{ name: 'Thema', label: 'THEMA', icon: 'thema', items: [
['Nachbarschaftshilfe', 1], ['Zimmerpflanzen-Pflege', 1], ['Mittagspause', 1], ['Geburtstagsüberraschung', 1],
['Mülltrennung', 1], ['Mitfahrgelegenheiten', 1], ['Kaffee', 1], ['Urlaubsplanung', 1], ['Wocheneinkauf', 1], ['Umzug', 1],
['Socken, die einzeln verschwinden', 2], ['Yoga für Katzen', 2], ['Kompost mit Charakter', 2],
['Gerüchteküche im Büro', 2], ['Wolkenbeobachtung', 2], ['Schneckenrennen', 2], ['Lieblingsausreden', 2],
['Hochzeit mit Hindernissen', 2], ['Mittagsschlaf-Optimierung', 2], ['Verleih von Regenschirmen', 2],
['Weltherrschaft für Anfänger', 3], ['Schlafwandel-Navigation', 3], ['Zeitreise-Tickets', 3],
['Rückruf-Aktion für Glück', 3], ['Einhorn-Verleih', 3], ['Tauschbörse für Geheimnisse', 3],
['Die Kunst des Zuspätkommens', 3], ['Keks-Notfalldienst', 3], ['Wetterbeeinflussung im Kleingarten', 3],
['Verwaltung von Superkräften', 3], ['Geisterhaus-Vermietung', 3], ['Streit mit dem Toaster', 3]
]},
{ name: 'Gerät', label: 'GERÄT / FORMAT', icon: 'geraet', items: [
['Smartphone, Hochformat (390 × 844)', 1, 390, 844], ['Tablet, Hochformat (768 × 1024)', 1, 768, 1024],
['Desktop (1440 × 900)', 1, 1440, 900],
['Smartphone, Querformat (844 × 390)', 2, 844, 390], ['Smartwatch (ca. 200 × 240)', 2, 200, 240],
['Kiosk-Display, Querformat (1920 × 1080)', 2, 1920, 1080], ['Tablet, Querformat (1024 × 768)', 2, 1024, 768],
['Sehr kleines Display (ca. 120 × 120)', 3, 120, 120], ['Riesen-Bildschirm (3840 × 1080)', 3, 3840, 1080]
]},
{ name: 'Hauptaktion', label: 'HAUPTAKTION (ORANGE!)', icon: 'aktion', items: [
['Einen Termin buchen', 1], ['Etwas kaufen', 1], ['Sich anmelden', 1], ['Etwas bewerten', 1], ['Etwas teilen', 1],
['Eine Nachricht senden', 1], ['Hilfe rufen', 1], ['Etwas zurückgeben', 1], ['Jemanden finden', 1], ['Etwas melden', 1],
['Eine Ausrede abschicken', 2], ['Eine Überraschung planen', 2], ['Etwas verleihen', 2],
['Einen Streit schlichten', 2], ['Eine Wette abschließen', 2],
['Den Alarm auslösen', 3], ['Wolken bestellen', 3], ['Eine Verschwörung melden', 3],
['Das Universum neu starten', 3], ['Einen Drachen adoptieren', 3]
]},
{ name: 'Twist', label: 'TWIST', icon: 'twist', items: [
['Nur 3 Elemente pro Screen', 1], ['Alles auf einem Screen, ohne Scrollen', 1], ['Ohne Text, nur Symbole', 1],
['Die Nutzerin hat nur eine Hand frei', 1], ['Schlechter Empfang: Offline-Zustand mitdenken', 1],
['Besonders große Touch-Ziele', 1], ['Hoher Kontrast und große Schrift (barrierearm)', 1],
['Erster Besuch, keine Anleitung', 1], ['Fehlerfall mitzeichnen: Was geht schief?', 1],
['Die Nutzerin ist wütend', 2], ['In 30 Sekunden bedienbar', 2], ['Navigation nur am unteren Rand', 2],
['Alle Inhalte in einer einzigen Liste', 2], ['Zwei wichtige Aktionen, aber nur eine darf orange sein', 2],
['Bedienung nur mit dem Ellbogen', 3], ['Das Display ist nass vom Regen', 3], ['Es gibt nur einen einzigen Button', 3],
['Die Nutzerin liest nur jedes zweite Wort', 3], ['Bedienung im Dunkeln', 3]
]}
];
/* ———- Zustand ———- */ const S = { sel: [0,0,0,0,0,0], locked: [false,false,false,false,false,false], until: [0,0,0,0,0,0], level: 3, n: 6 }; let hoverIdx = -1, hoverLock = false, host;
/* ———- Würfeln ———- */ function pickIndex(pi) {
const items = POOLS[pi].items;
let cands = [];
items.forEach((it, ix) => { if (it[1] <= S.level) cands.push([ix, it[1] === S.level ? 3 : 1]); });
if (cands.length > 1) cands = cands.filter(c => c[0] !== S.sel[pi]);
const total = cands.reduce((a, c) => a + c[1], 0);
let r = Math.random() * total;
for (const c of cands) { r -= c[1]; if (r <= 0) return c[0]; }
return cands[0][0];
} function rollCard(i, order) {
if (S.locked[i]) return; S.sel[i] = pickIndex(i); S.until[i] = millis() + 450 + (order || 0) * 120; loop();
} function rollAll() { for (let i = 0; i < S.n; i++) rollCard(i, i); updateBrief(); }
/* ———- Layout ———- */ function layout() {
const w = width, cols = w >= 720 ? 3 : (w >= 460 ? 2 : 1), gap = 14;
const cw = (w - gap * (cols - 1)) / cols, ch = cols === 3 ? 172 : (cols === 2 ? 164 : 144);
const rects = [], rows = Math.ceil(S.n / cols);
for (let i = 0; i < S.n; i++) {
rects.push({ x: (i % cols) * (cw + gap), y: Math.floor(i / cols) * (ch + gap), w: cw, h: ch });
}
return { rects, H: rows * ch + (rows - 1) * gap };
} function applyLayout() {
const w = Math.max(280, host.clientWidth); resizeCanvas(w, 10, true); resizeCanvas(w, layout().H);
}
/* ———- Zeichnen ———- */ function wrapLines(str, w) {
const words = str.split(' '), lines = []; let cur = '';
for (const wd of words) {
const t = cur ? cur + ' ' + wd : wd;
if (!cur || textWidth(t) <= w) cur = t; else { lines.push(cur); cur = wd; }
}
if (cur) lines.push(cur);
return lines;
} function fit(str, w, h) {
textStyle(BOLD);
for (let s = 28; s >= 14; s--) {
textSize(s);
const ls = wrapLines(str, w);
if (ls.length * s * 1.2 <= h && ls.every(l => textWidth(l) <= w)) return { s, ls };
}
textSize(14);
return { s: 14, ls: wrapLines(str, w) };
} function drawIcon(kind, x, y, item) {
noFill(); stroke(K); strokeWeight(2 / 0.75); // wirkt nach scale(0.75) wie 2 px, wie die Kästchen
if (kind === 'produkt') {
rect(x, y + 4, 44, 34, 4); line(x, y + 13, x + 44, y + 13);
noStroke(); fill(O); circle(x + 7, y + 8.5, 4);
} else if (kind === 'kundin') {
circle(x + 22, y + 13, 16); arc(x + 22, y + 44, 34, 30, PI, TWO_PI);
} else if (kind === 'thema') {
rect(x, y + 6, 44, 32, 3); line(x, y + 6, x + 44, y + 38); line(x + 44, y + 6, x, y + 38);
} else if (kind === 'geraet') {
const sc = Math.min(40 / item[2], 40 / item[3]), w = item[2] * sc, h = item[3] * sc;
rect(x + 22 - w / 2, y + 22 - h / 2, w, h, 3);
} else if (kind === 'aktion') {
noStroke(); fill(O); rect(x, y + 10, 44, 24, 7);
} else if (kind === 'twist') {
noStroke(); fill(B); push(); translate(x + 22, y + 22); rotate(PI / 4); rect(-13, -13, 26, 26, 3); pop();
}
} function drawLock(x, y, locked) {
const c = locked ? B : G3; stroke(c); strokeWeight(2); noFill(); arc(x + 9, y + (locked ? 8 : 4), 12, 14, PI, TWO_PI); fill(locked ? B : 255); rect(x, y + 8, 18, 13, 3);
} function drawCard(i, r, item, rolling) {
const p = POOLS[i];
// 2 px Kontur, 14 px Radius, 1 px nach innen versetzt: exakt wie das Kästchen „Dein Briefing“
stroke(hoverIdx === i ? O : K); strokeWeight(2); fill(255); rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2, 14);
noStroke(); fill(G3); textStyle(BOLD); textSize(13); textAlign(LEFT, TOP);
text(p.label, r.x + 20, r.y + 16);
push(); translate(r.x + r.w - 20 - 33, r.y + 14); scale(0.75); drawIcon(p.icon, 0, 0, item); pop();
const zone = { x: r.x + 20, y: r.y + 52, w: r.w - 40, h: r.h - 52 - 40 };
const f = fit(item[0], zone.w, zone.h);
textSize(f.s); textStyle(BOLD); noStroke(); fill(rolling ? G2 : K); textAlign(LEFT, TOP);
f.ls.forEach((l, k) => text(l, zone.x, zone.y + k * f.s * 1.2));
// unten: neu würfeln + Schloss
noFill(); stroke(G3); strokeWeight(2); arc(r.x + 26, r.y + r.h - 26, 11, 11, 0.5, TWO_PI - 0.6);
noStroke(); fill(G3); textSize(12); textStyle(NORMAL); textAlign(LEFT, CENTER);
text('neu würfeln', r.x + 40, r.y + r.h - 26);
drawLock(r.x + r.w - 20 - 18, r.y + r.h - 16 - 21, S.locked[i]);
} function setup() {
host = document.getElementById('stage');
const c = createCanvas(Math.max(280, host.clientWidth), 10);
c.parent(host); textFont('Arial'); frameRate(30);
const el = c.elt;
el.addEventListener('click', e => {
const h = hit(e.offsetX, e.offsetY); if (h.i < 0) return;
if (h.lock) { S.locked[h.i] = !S.locked[h.i]; redraw(); } else { rollCard(h.i, 0); updateBrief(); }
});
el.addEventListener('mousemove', e => {
const h = hit(e.offsetX, e.offsetY);
if (h.i !== hoverIdx || h.lock !== hoverLock) { hoverIdx = h.i; hoverLock = h.lock; el.style.cursor = h.i >= 0 ? 'pointer' : 'default'; redraw(); }
});
el.addEventListener('mouseleave', () => { hoverIdx = -1; hoverLock = false; redraw(); });
window.addEventListener('resize', () => { applyLayout(); });
initFromHash();
} function draw() {
background(255);
const L = layout(), now = millis(); let busy = false;
for (let i = 0; i < S.n; i++) {
const rolling = now < S.until[i]; if (rolling) busy = true;
const items = POOLS[i].items;
const idx = rolling ? Math.floor(Math.abs(Math.sin(Math.floor(now / 70) * 12.9898 + i * 78.233)) * 10000) % items.length : S.sel[i];
drawCard(i, L.rects[i], items[idx], rolling);
}
if (!busy) noLoop();
} function hit(mx, my) {
const L = layout();
for (let i = 0; i < S.n; i++) {
const r = L.rects[i];
if (mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h) {
return { i, lock: mx > r.x + r.w - 56 && my > r.y + r.h - 50 };
}
}
return { i: -1, lock: false };
}
/* ———- Briefing, Teilen ———- */ function briefText() {
const out = [];
for (let i = 0; i < S.n; i++) out.push(POOLS[i].name + ': ' + POOLS[i].items[S.sel[i]][0]);
return out.join('\n');
} function updateBrief() { document.getElementById('brief').textContent = briefText(); } function initFromHash() {
let ok = false;
try {
const m = location.hash.replace('#', '').split('.');
if (m.length === 3) {
const n = parseInt(m[0], 10), l = parseInt(m[1], 10), sel = m[2].split('-').map(x => parseInt(x, 10));
if ((n === 3 || n === 6) && l >= 1 && l <= 3 && sel.length === 6 &&
sel.every((v, i) => Number.isInteger(v) && v >= 0 && v < POOLS[i].items.length)) {
S.n = n; S.level = l; S.sel = sel; ok = true;
}
}
} catch (e) {}
syncSeg(); applyLayout();
if (ok) { updateBrief(); } else { rollAll(); }
}
/* ———- Bedienelemente ———- */ function syncSeg() {
document.querySelectorAll('[data-n]').forEach(b => b.setAttribute('aria-pressed', String(+b.dataset.n === S.n)));
document.querySelectorAll('[data-l]').forEach(b => b.setAttribute('aria-pressed', String(+b.dataset.l === S.level)));
} document.getElementById('rollAll').addEventListener('click', rollAll); document.querySelectorAll('[data-n]').forEach(b ⇒ b.addEventListener('click', () ⇒ {
S.n = +b.dataset.n; syncSeg(); applyLayout(); updateBrief();
})); document.querySelectorAll('[data-l]').forEach(b ⇒ b.addEventListener('click', () ⇒ {
S.level = +b.dataset.l; syncSeg(); rollAll();
})); async function copyText(text) {
try { await navigator.clipboard.writeText(text); }
catch (e) {
const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); } catch (_) {}
ta.remove();
}
} document.getElementById('copyTxt').addEventListener('click', async () ⇒ {
await copyText(briefText());
const a = document.getElementById('icCopy'), b = document.getElementById('icDone');
a.style.display = 'none'; b.style.display = 'block';
setTimeout(() => { a.style.display = 'block'; b.style.display = 'none'; }, 1400);
});
/* ———- Timer ———- */ let tTotal = 600, tLeft = 600, tEnd = 0, tTimer = null; const timeEl = document.getElementById('time'), startBtn = document.getElementById('tStart'); function fmt(s) { return String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0'); } function showTime() {
timeEl.textContent = tLeft > 0 ? fmt(tLeft) : 'Zeit!';
timeEl.classList.toggle('done', tLeft <= 0);
} function tick() {
tLeft = Math.max(0, Math.ceil((tEnd - Date.now()) / 1000)); showTime();
if (tLeft <= 0) { clearInterval(tTimer); tTimer = null; startBtn.textContent = 'Start'; }
} startBtn.addEventListener('click', () ⇒ {
if (tTimer) { clearInterval(tTimer); tTimer = null; startBtn.textContent = 'Weiter'; return; }
if (tLeft <= 0) tLeft = tTotal;
tEnd = Date.now() + tLeft * 1000; tTimer = setInterval(tick, 250); startBtn.textContent = 'Pause'; tick();
}); function resetTimer() {
if (tTimer) { clearInterval(tTimer); tTimer = null; }
tLeft = tTotal; startBtn.textContent = 'Start'; showTime();
} document.getElementById('tReset').addEventListener('click', resetTimer); document.querySelectorAll('[data-min]').forEach(b ⇒ b.addEventListener('click', () ⇒ {
tTotal = +b.dataset.min * 60;
document.querySelectorAll('[data-min]').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
resetTimer();
})); </script> </body> </html>