1404 lines
67 KiB
HTML
1404 lines
67 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>JavaScript ≠ Browser · Web Engineering DHBW</title>
|
||
<meta name="description" content="Eine Sprache, zwei Laufzeiten: JS/ECMAScript im Browser vs. Node.js auf dem Server.">
|
||
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Bebas+Neue&family=Bungee&family=Bricolage+Grotesque:opsz,wght@12..96,300..800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||
|
||
<style>
|
||
/* ============================================================
|
||
Design-Tokens — LibreTech-Markenwelt
|
||
============================================================ */
|
||
:root {
|
||
--bg: #14102b; /* Canvas */
|
||
--bg-2: #1b1540; /* Leiste / Panels */
|
||
--surface: #221953; /* Karten (= LibreTech "shadow") */
|
||
--surface-2: #2c2068;
|
||
--line: #3a2b7a; /* Konturen */
|
||
--line-soft: #2a1f5f;
|
||
|
||
--brand: #f6d939; /* Gelb (Face) */
|
||
--brand-2: #ea741e; /* Orange (Tiefe) */
|
||
--brand-3: #d63384; /* Pink (Akzent / aus den Folien) */
|
||
|
||
--text: #f3f0ff;
|
||
--dim: #c4bce6;
|
||
--faint:#8e84bf;
|
||
|
||
--shadow-col: #221953;
|
||
|
||
/* Welt-Kodierung (immer zusätzlich mit Text/Position kommuniziert) */
|
||
--c-browser: var(--brand);
|
||
--c-node: var(--brand-2);
|
||
--c-shared: var(--brand-3);
|
||
|
||
--r: 12px;
|
||
--maxw: 1180px;
|
||
--ease: cubic-bezier(.2,.7,.2,1);
|
||
|
||
--f-display: 'Anton', 'Arial Black', Impact, sans-serif;
|
||
--f-body: 'Bricolage Grotesque', system-ui, -apple-system, Segoe UI, sans-serif;
|
||
--f-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||
}
|
||
|
||
/* ============================================================
|
||
Reset & Grundlayout
|
||
============================================================ */
|
||
*, *::before, *::after { box-sizing: border-box; }
|
||
html, body { margin: 0; }
|
||
body {
|
||
font-family: var(--f-body);
|
||
color: var(--text);
|
||
background:
|
||
radial-gradient(1200px 700px at 85% -10%, rgba(214,51,132,.18), transparent 60%),
|
||
radial-gradient(1100px 700px at -10% 110%, rgba(234,116,30,.15), transparent 55%),
|
||
var(--bg);
|
||
height: 100vh; height: 100dvh;
|
||
display: grid;
|
||
grid-template-rows: auto 1fr auto;
|
||
overflow: hidden;
|
||
line-height: 1.55;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
/* dezente Grid-Textur für Atmosphäre */
|
||
body::before {
|
||
content: "";
|
||
position: fixed; inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba(255,255,255,.022) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(255,255,255,.022) 1px, transparent 1px);
|
||
background-size: 44px 44px;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
a { color: var(--brand); text-underline-offset: 3px; }
|
||
|
||
.skip-link {
|
||
position: absolute; left: 8px; top: -60px;
|
||
background: var(--brand); color: #1a1230;
|
||
padding: 8px 14px; border-radius: 8px; font-weight: 700;
|
||
z-index: 100; transition: top .15s var(--ease);
|
||
}
|
||
.skip-link:focus { top: 8px; }
|
||
|
||
:focus-visible {
|
||
outline: 3px solid var(--brand);
|
||
outline-offset: 2px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* ============================================================
|
||
Kopfleiste + Fortschritt
|
||
============================================================ */
|
||
.topbar {
|
||
position: relative; z-index: 5;
|
||
display: flex; align-items: center; gap: 16px;
|
||
padding: 10px clamp(14px, 3vw, 30px);
|
||
background: linear-gradient(180deg, rgba(27,21,64,.95), rgba(27,21,64,.78));
|
||
border-bottom: 1px solid var(--line);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.wordmark {
|
||
font-family: var(--f-display);
|
||
font-size: 20px; letter-spacing: .02em;
|
||
color: var(--brand);
|
||
transform: skewX(-7deg);
|
||
text-shadow: 2px 2px 0 var(--brand-2), 3px 3px 0 var(--shadow-col);
|
||
white-space: nowrap; user-select: none;
|
||
}
|
||
.wordmark span { color: var(--text); }
|
||
.topbar .meta {
|
||
margin-left: auto; display: flex; align-items: center; gap: 14px;
|
||
font-family: var(--f-mono); font-size: 12px; color: var(--dim);
|
||
}
|
||
.screen-title {
|
||
font-family: var(--f-mono); font-size: 12px;
|
||
color: var(--text); text-transform: uppercase; letter-spacing: .14em;
|
||
}
|
||
.counter { color: var(--brand); font-weight: 600; }
|
||
.progress {
|
||
position: absolute; left: 0; bottom: -1px; height: 3px;
|
||
background: linear-gradient(90deg, var(--brand), var(--brand-3));
|
||
width: 0; transition: width .35s var(--ease);
|
||
}
|
||
|
||
/* ============================================================
|
||
Hauptbereich + Screens
|
||
============================================================ */
|
||
main {
|
||
position: relative; z-index: 1;
|
||
overflow-y: auto;
|
||
padding: clamp(18px, 3.4vw, 46px) clamp(14px, 4vw, 40px) 48px;
|
||
scroll-behavior: smooth;
|
||
}
|
||
.screen { max-width: var(--maxw); margin: 0 auto; }
|
||
.screen[hidden] { display: none; }
|
||
|
||
.eyebrow {
|
||
font-family: var(--f-mono); font-size: 12px;
|
||
letter-spacing: .22em; text-transform: uppercase;
|
||
color: var(--brand-3); margin: 0 0 10px;
|
||
}
|
||
h1, h2 { font-family: var(--f-display); font-weight: 400; line-height: 1.02; margin: 0; }
|
||
h2 {
|
||
font-size: clamp(28px, 4.6vw, 50px);
|
||
letter-spacing: .005em; color: var(--text);
|
||
}
|
||
h2 .y { color: var(--brand); }
|
||
.lead {
|
||
font-size: clamp(15px, 1.5vw, 18px); color: var(--dim);
|
||
max-width: 66ch; margin: 14px 0 0;
|
||
}
|
||
h3 {
|
||
font-family: var(--f-mono); font-weight: 600;
|
||
font-size: 13px; letter-spacing: .12em; text-transform: uppercase;
|
||
color: var(--brand); margin: 0 0 10px;
|
||
}
|
||
.screen :focus-visible { scroll-margin-top: 20px; }
|
||
h2[tabindex] { outline: none; }
|
||
|
||
/* gestaffelter Auftritt */
|
||
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
|
||
.is-entering > * { animation: rise .5s var(--ease) backwards; }
|
||
.is-entering > *:nth-child(1) { animation-delay: .02s; }
|
||
.is-entering > *:nth-child(2) { animation-delay: .08s; }
|
||
.is-entering > *:nth-child(3) { animation-delay: .14s; }
|
||
.is-entering > *:nth-child(4) { animation-delay: .20s; }
|
||
.is-entering > *:nth-child(5) { animation-delay: .26s; }
|
||
|
||
.card {
|
||
background: linear-gradient(180deg, var(--surface), var(--surface) 60%, var(--bg-2));
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--r);
|
||
padding: clamp(16px, 2.4vw, 26px);
|
||
}
|
||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
|
||
.grid-aside { display: grid; grid-template-columns: 1.15fr .85fr; gap: 18px; align-items: start; }
|
||
@media (max-width: 860px) {
|
||
.grid-2, .grid-aside { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
.tag {
|
||
display: inline-block; font-family: var(--f-mono); font-size: 11px;
|
||
padding: 3px 9px; border-radius: 999px; letter-spacing: .04em;
|
||
border: 1px solid var(--line);
|
||
}
|
||
.tag.browser { color: #1a1230; background: var(--c-browser); border-color: var(--c-browser); }
|
||
.tag.node { color: #1a1230; background: var(--c-node); border-color: var(--c-node); }
|
||
.tag.shared { color: #fff; background: var(--c-shared); border-color: var(--c-shared); }
|
||
|
||
/* ============================================================
|
||
Code-Darstellung
|
||
============================================================ */
|
||
pre, .code {
|
||
font-family: var(--f-mono); font-size: 13px; line-height: 1.6;
|
||
background: #0f0b22; color: #e9e5ff;
|
||
border: 1px solid var(--line-soft);
|
||
border-left: 3px solid var(--brand-3);
|
||
border-radius: 10px;
|
||
padding: 14px 16px; margin: 0;
|
||
overflow: auto; white-space: pre;
|
||
tab-size: 2;
|
||
}
|
||
.code { display: block; }
|
||
.tok-com { color: #7c74a8; font-style: italic; }
|
||
.tok-str { color: #f6d939; }
|
||
.tok-kw { color: #ff8fc4; }
|
||
.tok-num { color: #ffb56b; }
|
||
|
||
.note {
|
||
margin-top: 12px; padding: 12px 14px;
|
||
background: rgba(214,51,132,.10);
|
||
border: 1px solid rgba(214,51,132,.35);
|
||
border-radius: 10px; font-size: 14px; color: var(--text);
|
||
}
|
||
.note strong { color: var(--brand); }
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
font-family: var(--f-mono); font-size: 13px;
|
||
padding: 9px 14px; border-radius: 9px; cursor: pointer;
|
||
background: var(--surface-2); color: var(--text);
|
||
border: 1px solid var(--line);
|
||
transition: transform .12s var(--ease), background .15s, border-color .15s;
|
||
}
|
||
.btn:hover { background: #372571; transform: translateY(-1px); border-color: var(--faint); }
|
||
.btn:active { transform: translateY(0); }
|
||
.btn.primary {
|
||
background: var(--brand); color: #1a1230; border-color: var(--brand); font-weight: 700;
|
||
}
|
||
.btn.primary:hover { background: #ffe65a; }
|
||
.btn.ghost { background: transparent; }
|
||
.btn-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
||
|
||
/* ============================================================
|
||
Screen 0 — Hero
|
||
============================================================ */
|
||
.hero { min-height: 62vh; display: flex; flex-direction: column; justify-content: center; gap: 6px; }
|
||
.hero h1 {
|
||
font-size: clamp(36px, 6.5vw, 78px);
|
||
color: var(--text); line-height: 1.06;
|
||
margin: 12px 0 0; max-width: 16ch;
|
||
}
|
||
.hero h1 .y { color: var(--brand); }
|
||
.hero-vs {
|
||
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
||
font-family: var(--f-mono); font-size: 14px; color: var(--dim); margin-top: 10px;
|
||
}
|
||
.pill-world {
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
padding: 8px 14px; border-radius: 999px; border: 1px solid var(--line);
|
||
background: var(--surface);
|
||
}
|
||
.pill-world.b { border-color: var(--c-browser); }
|
||
.pill-world.n { border-color: var(--c-node); }
|
||
.dot { width: 9px; height: 9px; border-radius: 50%; }
|
||
.dot.b { background: var(--c-browser); }
|
||
.dot.n { background: var(--c-node); }
|
||
.kbd-hint { font-family: var(--f-mono); font-size: 12.5px; color: var(--faint); margin-top: 26px; }
|
||
kbd {
|
||
font-family: var(--f-mono); font-size: 12px;
|
||
background: var(--surface-2); border: 1px solid var(--line);
|
||
border-bottom-width: 2px; border-radius: 6px; padding: 2px 7px; color: var(--text);
|
||
}
|
||
|
||
/* ============================================================
|
||
Screen 1 — Venn
|
||
============================================================ */
|
||
.venn-wrap { width: 100%; }
|
||
.venn { width: 100%; height: auto; display: block; }
|
||
.venn .circ { transition: fill-opacity .25s, stroke .2s; }
|
||
.venn .circ.on { stroke-width: 4; }
|
||
.venn text { font-family: var(--f-display); fill: var(--text); }
|
||
.venn .hint-api { font-family: var(--f-mono); font-size: 13px; fill: var(--faint); }
|
||
|
||
.chip-grid { display: flex; flex-wrap: wrap; gap: 8px; }
|
||
.chip {
|
||
font-family: var(--f-mono); font-size: 12.5px; cursor: pointer;
|
||
padding: 7px 11px; border-radius: 8px; color: var(--text);
|
||
background: var(--surface); border: 1px solid var(--line);
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
transition: transform .12s var(--ease), background .15s;
|
||
}
|
||
.chip:hover { transform: translateY(-1px); }
|
||
.chip[aria-pressed="true"] { background: var(--surface-2); border-color: var(--faint); }
|
||
.chip .where {
|
||
font-size: 9.5px; text-transform: uppercase; letter-spacing: .06em;
|
||
padding: 2px 6px; border-radius: 999px; font-weight: 700;
|
||
}
|
||
.chip.browser .where { background: var(--c-browser); color: #1a1230; }
|
||
.chip.node .where { background: var(--c-node); color: #1a1230; }
|
||
.chip.shared .where { background: var(--c-shared); color: #fff; }
|
||
.chip[aria-pressed="true"].browser { border-color: var(--c-browser); }
|
||
.chip[aria-pressed="true"].node { border-color: var(--c-node); }
|
||
.chip[aria-pressed="true"].shared { border-color: var(--c-shared); }
|
||
|
||
#venn-note { min-height: 3.4em; }
|
||
|
||
/* ============================================================
|
||
Screen 2 — Zwei Konsolen
|
||
============================================================ */
|
||
.console-head {
|
||
display: flex; align-items: center; gap: 8px;
|
||
font-family: var(--f-mono); font-size: 12px; letter-spacing: .08em;
|
||
text-transform: uppercase; margin-bottom: 8px;
|
||
}
|
||
.console-head .lamp { width: 10px; height: 10px; border-radius: 50%; }
|
||
.console.browser .lamp { background: var(--c-browser); }
|
||
.console.node .lamp { background: var(--c-node); }
|
||
.console pre {
|
||
min-height: 96px; border-left-color: var(--line-soft);
|
||
white-space: pre-wrap; word-break: break-word;
|
||
}
|
||
.console.browser pre { border-left-color: var(--c-browser); }
|
||
.console.node pre { border-left-color: var(--c-node); }
|
||
.prompt { color: var(--faint); }
|
||
.dim { color: var(--dim); }
|
||
.out-ok { color: #b7f5c2; }
|
||
.out-err { color: #ff9aa2; }
|
||
.ex-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
||
.ex-btn {
|
||
font-family: var(--f-mono); font-size: 12.5px; cursor: pointer;
|
||
padding: 7px 11px; border-radius: 8px; color: var(--text);
|
||
background: #0f0b22; border: 1px solid var(--line);
|
||
transition: background .15s, border-color .15s, transform .12s var(--ease);
|
||
}
|
||
.ex-btn:hover { transform: translateY(-1px); border-color: var(--faint); }
|
||
.ex-btn[aria-pressed="true"] { border-color: var(--brand); color: var(--brand); }
|
||
|
||
/* ============================================================
|
||
Screen 3 — DOM Playground
|
||
============================================================ */
|
||
#dom-demo { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
|
||
#dom-demo li {
|
||
background: var(--surface-2); border: 1px solid var(--line);
|
||
border-radius: 9px; padding: 12px 14px;
|
||
font-family: var(--f-mono); font-size: 13.5px;
|
||
display: flex; align-items: center; gap: 10px;
|
||
transition: background .2s, border-color .2s, transform .2s var(--ease);
|
||
}
|
||
#dom-demo li .lbl::before { content: "<li>"; color: var(--faint); margin-right: 8px; }
|
||
#dom-demo li.hit { background: var(--brand); color: #1a1230; border-color: var(--brand); }
|
||
#dom-demo li.hit .lbl::before { color: #5a4a00; }
|
||
@keyframes pop { 0% { transform: scale(1); } 50% { transform: scale(1.03); } 100% { transform: scale(1); } }
|
||
.dom-echo { min-height: 64px; }
|
||
|
||
/* ============================================================
|
||
Tabs (Node / Syntax / Frameworks)
|
||
============================================================ */
|
||
.tablist {
|
||
display: inline-flex; flex-wrap: wrap; gap: 4px; margin-bottom: 18px;
|
||
background: #0f0b22; border: 1px solid var(--line);
|
||
border-radius: 12px; padding: 5px; max-width: 100%;
|
||
}
|
||
[role="tab"] {
|
||
font-family: var(--f-mono); font-size: 12.5px; cursor: pointer;
|
||
padding: 8px 14px; border-radius: 8px; color: var(--dim);
|
||
background: transparent; border: none;
|
||
transition: color .15s, background .15s;
|
||
}
|
||
[role="tab"]:hover { color: var(--text); background: rgba(255,255,255,.05); }
|
||
[role="tab"][aria-selected="true"] {
|
||
color: #1a1230; background: var(--brand); font-weight: 700;
|
||
}
|
||
[role="tabpanel"][hidden] { display: none; }
|
||
[data-run] { margin-top: 22px; }
|
||
.panel-note { font-family: var(--f-mono); font-size: 12px; color: var(--dim); margin: 0 0 10px; }
|
||
|
||
.fields { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
|
||
.field-btn {
|
||
font-family: var(--f-mono); font-size: 12px; cursor: pointer;
|
||
padding: 6px 11px; border-radius: 999px; color: var(--brand);
|
||
background: rgba(246,217,57,.08); border: 1px solid rgba(246,217,57,.4);
|
||
transition: background .15s, transform .12s var(--ease);
|
||
}
|
||
.field-btn:hover { background: rgba(246,217,57,.18); transform: translateY(-1px); }
|
||
.field-btn[aria-pressed="true"] { background: var(--brand); color: #1a1230; }
|
||
#pkg-note { min-height: 3em; }
|
||
|
||
.run-out { min-height: 70px; margin-top: 12px; }
|
||
.kernidee {
|
||
font-family: var(--f-mono); font-size: 12.5px; color: var(--dim);
|
||
margin: 0 0 12px; padding: 8px 12px;
|
||
border-left: 3px solid var(--brand-3); background: rgba(255,255,255,.03); border-radius: 0 8px 8px 0;
|
||
}
|
||
|
||
/* ============================================================
|
||
Screen 6 — LibreTech Mini
|
||
============================================================ */
|
||
.lt-stage {
|
||
background: #362582; border-radius: var(--r); border: 1px solid var(--line);
|
||
min-height: 280px; display: flex; align-items: center; justify-content: center;
|
||
overflow: hidden; padding: 20px; transition: background .15s;
|
||
}
|
||
.lt-logo {
|
||
font-family: 'Anton', 'Bebas Neue', Impact, sans-serif;
|
||
font-size: clamp(48px, 9vw, 104px); color: #f6d939; line-height: 1.04;
|
||
letter-spacing: .01em; text-align: center; white-space: pre-line;
|
||
transform: rotate(-10deg) skewX(-8deg); user-select: none;
|
||
transition: color .15s;
|
||
}
|
||
.controls label { display: block; font-size: 12px; color: var(--dim); margin: 12px 0 4px; }
|
||
.controls .val { color: var(--brand); font-weight: 700; font-family: var(--f-mono); }
|
||
.controls input[type=range] { width: 100%; accent-color: var(--brand-3); }
|
||
.controls input[type=text],
|
||
.controls textarea,
|
||
.controls select {
|
||
width: 100%; padding: 7px 10px; font-family: var(--f-mono); font-size: 13px;
|
||
background: var(--bg); color: var(--text);
|
||
border: 1px solid var(--line); border-radius: 8px;
|
||
}
|
||
.controls textarea { resize: vertical; min-height: 52px; }
|
||
.controls .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||
.controls input[type=color] {
|
||
width: 100%; height: 34px; padding: 2px; border-radius: 8px;
|
||
border: 1px solid var(--line); background: var(--bg); cursor: pointer;
|
||
}
|
||
.uses-item { padding: 14px 0; border-top: 1px solid var(--line-soft); }
|
||
.uses-item:first-child { border-top: none; padding-top: 2px; }
|
||
.uses-code {
|
||
display: block; font-family: var(--f-mono); font-size: 12.5px; color: #e9e5ff;
|
||
background: #0f0b22; border: 1px solid var(--line-soft);
|
||
border-left: 3px solid var(--brand);
|
||
border-radius: 8px; padding: 9px 12px; overflow-x: auto; white-space: pre;
|
||
}
|
||
.uses-desc { margin: 9px 2px 0; font-size: 13.5px; color: var(--dim); line-height: 1.5; }
|
||
.uses-desc strong { color: var(--brand); font-weight: 600; }
|
||
|
||
.lt-css-head {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
gap: 12px; margin-bottom: 10px; flex-wrap: wrap;
|
||
}
|
||
.lt-css { color: #e9e5ff; white-space: pre; }
|
||
.copy-status { font-family: var(--f-mono); font-size: 12px; color: var(--brand); min-height: 1.2em; }
|
||
|
||
/* ============================================================
|
||
Fußleiste / Navigation
|
||
============================================================ */
|
||
.navbar {
|
||
position: relative; z-index: 5;
|
||
display: flex; align-items: center; gap: 14px;
|
||
padding: 10px clamp(14px, 3vw, 30px);
|
||
background: linear-gradient(0deg, rgba(27,21,64,.96), rgba(27,21,64,.78));
|
||
border-top: 1px solid var(--line);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.dots { display: flex; gap: 8px; margin: 0 auto; }
|
||
.dots button {
|
||
width: 11px; height: 11px; padding: 0; border-radius: 50%; cursor: pointer;
|
||
background: var(--surface-2); border: 1px solid var(--line);
|
||
transition: background .2s, transform .15s var(--ease), width .2s;
|
||
}
|
||
.dots button:hover { transform: scale(1.25); }
|
||
.dots button[aria-current="true"] { background: var(--brand); border-color: var(--brand); width: 26px; border-radius: 6px; }
|
||
.nav-btn {
|
||
font-family: var(--f-mono); font-size: 13px; cursor: pointer;
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
padding: 9px 15px; border-radius: 9px; color: var(--text);
|
||
background: var(--surface); border: 1px solid var(--line);
|
||
transition: background .15s, transform .12s var(--ease), opacity .15s;
|
||
}
|
||
.nav-btn:hover:not(:disabled) { background: var(--surface-2); transform: translateY(-1px); }
|
||
.nav-btn:disabled { opacity: .35; cursor: not-allowed; }
|
||
.nav-btn.next { background: var(--brand); color: #1a1230; font-weight: 700; border-color: var(--brand); }
|
||
.nav-btn.next:hover:not(:disabled) { background: #ffe65a; }
|
||
|
||
.visually-hidden {
|
||
position: absolute; width: 1px; height: 1px; margin: -1px;
|
||
padding: 0; border: 0; clip: rect(0 0 0 0); overflow: hidden; white-space: nowrap;
|
||
}
|
||
|
||
/* ============================================================
|
||
Reduzierte Bewegung
|
||
============================================================ */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<a class="skip-link" href="#screen-host">Zum Inhalt springen</a>
|
||
|
||
<!-- Kopfleiste -->
|
||
<header class="topbar">
|
||
<div class="wordmark" aria-hidden="true">LIBRE<span>TECH</span></div>
|
||
<div class="meta">
|
||
<span class="screen-title" id="screen-title">Einführung</span>
|
||
<span class="counter"><span id="counter-cur">01</span> / 08</span>
|
||
</div>
|
||
<div class="progress" id="progress" aria-hidden="true"></div>
|
||
</header>
|
||
|
||
<!-- Hauptbereich -->
|
||
<main id="screen-host">
|
||
|
||
<!-- ===================== SCREEN 0 ===================== -->
|
||
<section class="screen hero" id="screen-0" data-title="Einführung" aria-labelledby="h-0">
|
||
<p class="eyebrow">Web Engineering · DHBW Stuttgart</p>
|
||
<h1 id="h-0">JavaScript im <span class="y">Browser</span> und in <span class="y">Node.js</span></h1>
|
||
<p class="lead">JavaScript ist eine Programmiersprache, definiert durch den ECMAScript-Standard. Ausgeführt wird sie von einer Laufzeitumgebung: im Browser oder durch Node.js auf dem Server. Beide verwenden dieselbe Sprach-Engine, stellen aber unterschiedliche Schnittstellen bereit — der Browser das DOM, Node.js den Zugriff auf Dateisystem, Netzwerk und Prozess. Diese Sitzung grenzt ab, was zur Sprache selbst gehört und was zur jeweiligen Umgebung.</p>
|
||
<div class="hero-vs" aria-hidden="true">
|
||
<span class="pill-world b"><span class="dot b"></span> Browser</span>
|
||
<span class="pill-world n"><span class="dot n"></span> Node.js</span>
|
||
<span class="pill-world"><span class="dot" style="background:var(--brand-3)"></span> gemeinsam: ECMAScript</span>
|
||
</div>
|
||
<p class="kbd-hint">Navigation: <kbd>←</kbd> <kbd>→</kbd> · <kbd>Leertaste</kbd> weiter · <kbd>1</kbd>–<kbd>8</kbd> direkt · <kbd>Pos 1</kbd>/<kbd>Ende</kbd></p>
|
||
</section>
|
||
|
||
<!-- ===================== SCREEN 1 ===================== -->
|
||
<section class="screen" id="screen-1" data-title="Modell" aria-labelledby="h-1" hidden>
|
||
<p class="eyebrow">Sprache und Umgebung</p>
|
||
<h2 id="h-1" tabindex="-1">ECMAScript-Kern und <span class="y">umgebungsspezifische APIs</span></h2>
|
||
<p class="lead">Eine Laufzeitumgebung besteht aus der Sprach-Engine (etwa V8) und den von ihr bereitgestellten Schnittstellen. Der ECMAScript-Kern ist in Browser und Node.js identisch; die übrigen APIs unterscheiden sich. Die folgende Übersicht ordnet einzelne APIs der jeweiligen Umgebung zu.</p>
|
||
|
||
<div class="grid-aside" style="margin-top:20px;">
|
||
<div class="card venn-wrap">
|
||
<svg class="venn" viewBox="0 0 640 360" role="img" aria-label="Venn-Diagramm: Browser und Node.js teilen sich den ECMAScript-Sprachkern in der Mitte.">
|
||
<circle id="region-browser" class="circ" cx="250" cy="185" r="155" fill="#f6d939" fill-opacity="0.12" stroke="#f6d939" stroke-width="2"></circle>
|
||
<circle id="region-node" class="circ" cx="390" cy="185" r="155" fill="#ea741e" fill-opacity="0.12" stroke="#ea741e" stroke-width="2"></circle>
|
||
<ellipse id="region-shared" class="circ" cx="320" cy="185" rx="74" ry="128" fill="#d63384" fill-opacity="0.12" stroke="#d63384" stroke-width="2"></ellipse>
|
||
<text x="168" y="62" font-size="26" text-anchor="middle">Browser</text>
|
||
<text x="472" y="62" font-size="26" text-anchor="middle">Node.js</text>
|
||
<text x="320" y="150" font-size="19" text-anchor="middle" fill="#d63384">geteilt</text>
|
||
<text class="hint-api" x="150" y="210" text-anchor="middle">document</text>
|
||
<text class="hint-api" x="150" y="232" text-anchor="middle">window</text>
|
||
<text class="hint-api" x="490" y="210" text-anchor="middle">node:fs</text>
|
||
<text class="hint-api" x="490" y="232" text-anchor="middle">node:http</text>
|
||
<text class="hint-api" x="320" y="190" text-anchor="middle">const · fetch</text>
|
||
<text class="hint-api" x="320" y="210" text-anchor="middle">Promise</text>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<h3>APIs einordnen</h3>
|
||
<div class="chip-grid" id="chip-grid" role="group" aria-label="APIs auswählen, um die Zuordnung anzuzeigen"></div>
|
||
<div class="note" id="venn-note" role="status" aria-live="polite">Eine API auswählen, um zu sehen, in welcher Umgebung sie verfügbar ist.</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== SCREEN 2 ===================== -->
|
||
<section class="screen" id="screen-2" data-title="Konsole" aria-labelledby="h-2" hidden>
|
||
<p class="eyebrow">Browser-Konsole und Node-REPL</p>
|
||
<h2 id="h-2" tabindex="-1">Denselben Ausdruck in <span class="y">beiden Umgebungen</span> auswerten</h2>
|
||
<p class="lead">Links wird der Ausdruck direkt in dieser Seite ausgewertet, also im Browser. Rechts steht die Ausgabe, die das Node-REPL liefert. Der Vergleich zeigt, welche Objekte in welcher Umgebung definiert sind.</p>
|
||
|
||
<div class="ex-row" id="ex-row" role="group" aria-label="Ausdruck auswählen" style="margin-top:18px;"></div>
|
||
|
||
<div class="grid-2">
|
||
<div class="console browser card">
|
||
<div class="console-head"><span class="lamp"></span> Browser-Konsole</div>
|
||
<pre id="out-browser" aria-live="polite" aria-label="Browser-Ausgabe"><span class="prompt">></span> <span class="dim">Ausdruck wählen …</span></pre>
|
||
</div>
|
||
<div class="console node card">
|
||
<div class="console-head"><span class="lamp"></span> Node REPL <span style="color:var(--faint)">($ node)</span></div>
|
||
<pre id="out-node" aria-live="polite" aria-label="Node-Ausgabe"><span class="prompt">></span> <span class="dim">Ausdruck wählen …</span></pre>
|
||
</div>
|
||
</div>
|
||
<div class="note"><strong>Beobachtung:</strong> Der Browser stellt DOM- und Fensterobjekte wie <code>document</code> und <code>window</code> bereit, Node den Zugriff auf Prozess und Dateisystem. Was eine Umgebung nicht definiert, führt dort zu einem <code>ReferenceError</code>.</div>
|
||
</section>
|
||
|
||
<!-- ===================== SCREEN 3 ===================== -->
|
||
<section class="screen" id="screen-3" data-title="DOM" aria-labelledby="h-3" hidden>
|
||
<p class="eyebrow">Browser-Umgebung</p>
|
||
<h2 id="h-3" tabindex="-1">Das <span class="y">Document Object Model</span></h2>
|
||
<p class="lead">Das Objekt <code>document</code> ist der Einstiegspunkt in das DOM — die Objektrepräsentation der geladenen Seite. Die folgenden Operationen verändern die Liste direkt; rechts steht der jeweils ausgeführte Aufruf. In Node existiert <code>document</code> nicht.</p>
|
||
|
||
<div class="grid-2" style="margin-top:18px;">
|
||
<div class="card">
|
||
<h3>Live-Liste</h3>
|
||
<ul id="dom-demo" aria-label="Demo-Liste zum Manipulieren">
|
||
<li><span class="lbl">Komponente A</span></li>
|
||
<li><span class="lbl">Komponente B</span></li>
|
||
<li><span class="lbl">Komponente C</span></li>
|
||
<li><span class="lbl">Komponente D</span></li>
|
||
</ul>
|
||
<div class="btn-row" style="margin-top:14px;">
|
||
<button class="btn" data-dom="count">querySelectorAll · zählen</button>
|
||
<button class="btn" data-dom="highlight">Erstes hervorheben</button>
|
||
<button class="btn" data-dom="text">Letzten Text ändern</button>
|
||
<button class="btn" data-dom="remove">Letztes entfernen</button>
|
||
<button class="btn ghost" data-dom="reset">Zurücksetzen</button>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>Konsolen-Echo</h3>
|
||
<pre class="dom-echo" id="dom-echo" aria-live="polite"><span class="prompt">></span> <span class="dim">Befehl ausführen …</span></pre>
|
||
<div class="note">In Node ist <code>document</code> nicht definiert; dieselben Aufrufe würden dort mit <code>ReferenceError: document is not defined</code> abbrechen.</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== SCREEN 4 ===================== -->
|
||
<section class="screen" id="screen-4" data-title="Node.js" aria-labelledby="h-4" hidden>
|
||
<p class="eyebrow">Serverseitiges JavaScript</p>
|
||
<h2 id="h-4" tabindex="-1">Node.js: <span class="y">Dateisystem, Server und Prozess</span></h2>
|
||
<p class="lead">Node.js führt JavaScript außerhalb des Browsers aus — mit derselben V8-Engine, aber serverseitigen Modulen. <code>npm init</code> erzeugt die <code>package.json</code>; darunter folgen die wichtigsten eingebauten Module im ESM-Format mit <code>node:</code>-Präfix.</p>
|
||
|
||
<div class="card" style="margin-top:18px;">
|
||
<div class="tablist" role="tablist" aria-label="Node.js-Themen">
|
||
<button role="tab" id="nt-1" aria-controls="np-1" aria-selected="true" tabindex="0">package.json</button>
|
||
<button role="tab" id="nt-2" aria-controls="np-2" aria-selected="false" tabindex="-1">node:http</button>
|
||
<button role="tab" id="nt-3" aria-controls="np-3" aria-selected="false" tabindex="-1">node:fs</button>
|
||
<button role="tab" id="nt-4" aria-controls="np-4" aria-selected="false" tabindex="-1">process</button>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="np-1" aria-labelledby="nt-1" tabindex="0">
|
||
<p class="panel-note">$ npm init -y erzeugt die <code>package.json</code>. Ein Feld auswählen für die Erläuterung:</p>
|
||
<div class="grid-aside">
|
||
<pre class="code" data-lang="json">{
|
||
"name": "mein-projekt",
|
||
"version": "1.0.0",
|
||
"type": "module",
|
||
"main": "index.js",
|
||
"scripts": {
|
||
"start": "node index.js",
|
||
"dev": "node --watch index.js",
|
||
"test": "node --test"
|
||
},
|
||
"dependencies": {
|
||
"express": "^4.18.0"
|
||
}
|
||
}</pre>
|
||
<div>
|
||
<div class="fields" id="pkg-fields" role="group" aria-label="package.json-Felder erklären">
|
||
<button class="field-btn" data-pkg="type">type</button>
|
||
<button class="field-btn" data-pkg="main">main</button>
|
||
<button class="field-btn" data-pkg="scripts">scripts</button>
|
||
<button class="field-btn" data-pkg="dependencies">dependencies</button>
|
||
<button class="field-btn" data-pkg="version">version</button>
|
||
</div>
|
||
<div class="note" id="pkg-note" role="status" aria-live="polite">Wähle ein Feld für die Erklärung.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="np-2" aria-labelledby="nt-2" tabindex="0" hidden>
|
||
<p class="kernidee">Ein HTTP-Server kann nur serverseitig laufen; ein Browser kann selbst keinen Server bereitstellen.</p>
|
||
<pre class="code" data-lang="js">import { createServer } from "node:http";
|
||
|
||
const server = createServer((req, res) => {
|
||
res.writeHead(200, { "Content-Type": "text/html" });
|
||
res.end("<h1>Hallo Welt!</h1>");
|
||
});
|
||
|
||
server.listen(3000, () => {
|
||
console.log("Server läuft auf Port 3000");
|
||
});</pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="np-3" aria-labelledby="nt-3" tabindex="0" hidden>
|
||
<p class="kernidee">Direkter Dateizugriff ist im Browser aus Sicherheitsgründen nicht möglich, in Node hingegen Standard.</p>
|
||
<pre class="code" data-lang="js">import { readFile } from "node:fs/promises";
|
||
|
||
// Promise-API (modern, empfohlen)
|
||
const data = await readFile("data.json", "utf8");
|
||
console.log(JSON.parse(data));</pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="np-4" aria-labelledby="nt-4" tabindex="0" hidden>
|
||
<p class="kernidee">Das <code>process</code>-Objekt existiert nur in Node und steuert u. a. Signale, Argumente, Umgebungsvariablen und Exit-Codes.</p>
|
||
<pre class="code" data-lang="js">process.on("SIGINT", () => {
|
||
console.log("Received SIGINT. Closing server...");
|
||
server.close(() => process.exit(0));
|
||
});
|
||
|
||
process.exit(0); // 0 = Erfolg, 1 = Fehler</pre>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== SCREEN 5 ===================== -->
|
||
<section class="screen" id="screen-5" data-title="ECMAScript" aria-labelledby="h-5" hidden>
|
||
<p class="eyebrow">Der ECMAScript-Sprachkern</p>
|
||
<h2 id="h-5" tabindex="-1">Moderne <span class="y">JavaScript-Syntax</span> (ES2015+)</h2>
|
||
<p class="lead">Diese Sprachmerkmale gehören zu ECMAScript und verhalten sich in Browser und Node identisch. Durchgängig wird modernes ES verwendet, ohne <code>var</code>. Ein Thema auswählen und den Code ausführen.</p>
|
||
|
||
<div class="card" style="margin-top:18px;">
|
||
<div class="tablist" role="tablist" aria-label="Syntax-Themen">
|
||
<button role="tab" id="st-1" aria-controls="sp-1" aria-selected="true" tabindex="0">const / let</button>
|
||
<button role="tab" id="st-fn" aria-controls="sp-fn" aria-selected="false" tabindex="-1">Funktionen</button>
|
||
<button role="tab" id="st-2" aria-controls="sp-2" aria-selected="false" tabindex="-1">Arrow Functions</button>
|
||
<button role="tab" id="st-3" aria-controls="sp-3" aria-selected="false" tabindex="-1">Destructuring</button>
|
||
<button role="tab" id="st-4" aria-controls="sp-4" aria-selected="false" tabindex="-1">Template Strings</button>
|
||
<button role="tab" id="st-5" aria-controls="sp-5" aria-selected="false" tabindex="-1">async / await</button>
|
||
<button role="tab" id="st-6" aria-controls="sp-6" aria-selected="false" tabindex="-1">IIFE</button>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="sp-1" aria-labelledby="st-1" tabindex="0">
|
||
<pre class="code" data-lang="js">const TAX = 0.19; // konstant
|
||
let warenkorb = 100;
|
||
warenkorb += 50; // ✓ let ist veränderbar
|
||
// TAX = 0.07; // ✗ TypeError — const
|
||
|
||
{
|
||
let rabatt = 10; // lebt nur in diesem Block
|
||
}
|
||
// rabatt — außerhalb: ReferenceError</pre>
|
||
<button class="btn primary" data-run="constlet">▶ Ausführen</button>
|
||
<pre class="run-out" data-out="constlet" aria-live="polite"></pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="sp-fn" aria-labelledby="st-fn" tabindex="0" hidden>
|
||
<pre class="code" data-lang="js">// Funktionsdeklaration mit Parametern, Rückgabewert
|
||
// und einem Standard-Parameter (satz)
|
||
function bruttoPreis(netto, satz = 0.19) {
|
||
return netto * (1 + satz);
|
||
}
|
||
|
||
bruttoPreis(100); // 119
|
||
bruttoPreis(100, 0.07); // 107 (satz überschrieben)</pre>
|
||
<button class="btn primary" data-run="funktionen">▶ Ausführen</button>
|
||
<pre class="run-out" data-out="funktionen" aria-live="polite"></pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="sp-2" aria-labelledby="st-2" tabindex="0" hidden>
|
||
<pre class="code" data-lang="js">const preise = [19.99, 5, 12.5];
|
||
|
||
const summe = preise.reduce((a, b) => a + b, 0);
|
||
const teuer = preise.filter(p => p > 10);
|
||
const inklMwSt = preise.map(p => p * 1.19);</pre>
|
||
<button class="btn primary" data-run="arrow">▶ Ausführen</button>
|
||
<pre class="run-out" data-out="arrow" aria-live="polite"></pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="sp-3" aria-labelledby="st-3" tabindex="0" hidden>
|
||
<pre class="code" data-lang="js">const user = { name: "Mara", rolle: "Admin", aktiv: true };
|
||
|
||
const { name, rolle } = user; // herauslösen
|
||
const kopie = { ...user, aktiv: false }; // kopieren + ändern
|
||
|
||
const [erster, ...rest] = [1, 2, 3, 4]; // Rest-Elemente</pre>
|
||
<button class="btn primary" data-run="destructuring">▶ Ausführen</button>
|
||
<pre class="run-out" data-out="destructuring" aria-live="polite"></pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="sp-4" aria-labelledby="st-4" tabindex="0" hidden>
|
||
<pre class="code" data-lang="js">const name = "Mara";
|
||
const punkte = 42;
|
||
|
||
const text = `Hallo ${name}, du hast ${punkte} Punkte.`;
|
||
|
||
// Ausdrücke direkt im String — z. B. für CSS:
|
||
const css = `transform: rotate(${punkte / 2}deg);`;</pre>
|
||
<button class="btn primary" data-run="template">▶ Ausführen</button>
|
||
<pre class="run-out" data-out="template" aria-live="polite"></pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="sp-5" aria-labelledby="st-5" tabindex="0" hidden>
|
||
<pre class="code" data-lang="js">// "Daten laden" liefert ein Promise
|
||
const ladeUser = () =>
|
||
new Promise(resolve =>
|
||
setTimeout(() => resolve({ name: "Mara" }), 400)
|
||
);
|
||
|
||
const user = await ladeUser(); // wartet, ohne zu blockieren
|
||
console.log(user.name);</pre>
|
||
<button class="btn primary" data-run="asyncawait">▶ Ausführen</button>
|
||
<pre class="run-out" data-out="asyncawait" aria-live="polite"></pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="sp-6" aria-labelledby="st-6" tabindex="0" hidden>
|
||
<pre class="code" data-lang="js">// IIFE: erzeugt einen eigenen, privaten Gültigkeitsbereich
|
||
const counter = (() => {
|
||
let n = 0; // von außen nicht zugänglich
|
||
return { tick: () => ++n }; // tick() ist die einzige Schnittstelle
|
||
})();
|
||
|
||
counter.tick(); // 1
|
||
counter.tick(); // 2</pre>
|
||
<button class="btn primary" data-run="iife">▶ Ausführen</button>
|
||
<pre class="run-out" data-out="iife" aria-live="polite"></pre>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== SCREEN 6 ===================== -->
|
||
<section class="screen" id="screen-6" data-title="Beispiel" aria-labelledby="h-6" hidden>
|
||
<p class="eyebrow">Anwendungsbeispiel: Vanilla JavaScript</p>
|
||
<h2 id="h-6" tabindex="-1"><span class="y">Logo</span>-Generator</h2>
|
||
<p class="lead">Diese Anwendung kombiniert die behandelten Konzepte: DOM-Zugriff, Event-Handling und moderne Syntax — ohne Framework. Die Regler verändern die Vorschau; das erzeugte CSS unten aktualisiert sich entsprechend und lässt sich kopieren.</p>
|
||
|
||
<div class="grid-aside" style="margin-top:18px;">
|
||
<div class="lt-stage"><div class="lt-logo" id="lt-logo">LIBRE
|
||
TECH</div></div>
|
||
|
||
<div class="card controls">
|
||
<h3>Regler</h3>
|
||
<label for="lt-text">Text (mehrzeilig mit Enter)</label>
|
||
<textarea id="lt-text" rows="2">LIBRE
|
||
TECH</textarea>
|
||
|
||
<label for="lt-font">Schrift</label>
|
||
<select id="lt-font">
|
||
<option value="Anton">Anton</option>
|
||
<option value="Bungee">Bungee</option>
|
||
<option value="Bebas Neue">Bebas Neue</option>
|
||
<option value="Impact">Impact (System)</option>
|
||
</select>
|
||
|
||
<div class="row2">
|
||
<div>
|
||
<label for="lt-face">Face-Farbe</label>
|
||
<input type="color" id="lt-face" value="#f6d939">
|
||
</div>
|
||
<div>
|
||
<label for="lt-bg">Hintergrund</label>
|
||
<input type="color" id="lt-bg" value="#362582">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row2">
|
||
<div>
|
||
<label for="lt-depth">Tiefen-Farbe</label>
|
||
<input type="color" id="lt-depth" value="#ea741e">
|
||
</div>
|
||
<div>
|
||
<label for="lt-shadowcol">Schatten-Farbe</label>
|
||
<input type="color" id="lt-shadowcol" value="#221953">
|
||
</div>
|
||
</div>
|
||
|
||
<label for="lt-angle">Lichtwinkel: <span class="val" id="lt-angle-v">133</span>°</label>
|
||
<input type="range" id="lt-angle" min="0" max="359" value="133">
|
||
|
||
<label for="lt-shadow">Schatten-Länge: <span class="val" id="lt-shadow-v">22</span>px</label>
|
||
<input type="range" id="lt-shadow" min="0" max="60" value="22">
|
||
|
||
<label for="lt-depthlen">Tiefe: <span class="val" id="lt-depthlen-v">12</span>px</label>
|
||
<input type="range" id="lt-depthlen" min="0" max="30" value="12">
|
||
|
||
<label for="lt-contour">Kontur: <span class="val" id="lt-contour-v">7</span>px</label>
|
||
<input type="range" id="lt-contour" min="0" max="20" value="7">
|
||
|
||
<label for="lt-rot">Drehung: <span class="val" id="lt-rot-v">-10</span>°</label>
|
||
<input type="range" id="lt-rot" min="-25" max="25" value="-10">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top:18px;">
|
||
<div class="lt-css-head">
|
||
<h3 style="margin:0;">Generiertes CSS</h3>
|
||
<button class="btn" id="lt-copy">CSS kopieren</button>
|
||
</div>
|
||
<pre class="lt-css" id="lt-css" aria-label="Generiertes CSS für das Logo"></pre>
|
||
<p class="copy-status" id="lt-copy-status" role="status" aria-live="polite"></p>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top:18px;">
|
||
<h3>Verwendete Konzepte</h3>
|
||
<div class="uses-item">
|
||
<code class="uses-code">const $ = id => document.getElementById(id)</code>
|
||
<p class="uses-desc"><strong>Arrow Function</strong> und DOM-Zugriff: holt ein Element über seine ID.</p>
|
||
</div>
|
||
<div class="uses-item">
|
||
<code class="uses-code">$$("input, select, textarea").forEach(el => el.addEventListener("input", update))</code>
|
||
<p class="uses-desc"><strong>Event-Handling</strong>: bindet alle Regler gemeinsam; bei jeder Eingabe wird <code>update</code> aufgerufen.</p>
|
||
</div>
|
||
<div class="uses-item">
|
||
<code class="uses-code">await navigator.clipboard.writeText(css)</code>
|
||
<p class="uses-desc"><strong>Browser-API</strong> zum Kopieren in die Zwischenablage; in Node nicht verfügbar.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===================== SCREEN 7 ===================== -->
|
||
<section class="screen" id="screen-7" data-title="Ausblick" aria-labelledby="h-7" hidden>
|
||
<p class="eyebrow">Ausblick auf die nächste Sitzung</p>
|
||
<h2 id="h-7" tabindex="-1">Derselbe Zähler in <span class="y">vier Frameworks</span></h2>
|
||
<p class="lead">Dasselbe Verhalten — ein klickbarer Zähler — umgesetzt in vier Ansätzen, vom Web-Standard bis zum Compiler. In der nächsten Sitzung wird diese Logik mit einem Framework nachgebaut.</p>
|
||
|
||
<div class="card" style="margin-top:18px;">
|
||
<div class="tablist" role="tablist" aria-label="Framework-Beispiele">
|
||
<button role="tab" id="ft-1" aria-controls="fp-1" aria-selected="true" tabindex="0">Web Components</button>
|
||
<button role="tab" id="ft-2" aria-controls="fp-2" aria-selected="false" tabindex="-1">React</button>
|
||
<button role="tab" id="ft-3" aria-controls="fp-3" aria-selected="false" tabindex="-1">Vue</button>
|
||
<button role="tab" id="ft-4" aria-controls="fp-4" aria-selected="false" tabindex="-1">Svelte</button>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="fp-1" aria-labelledby="ft-1" tabindex="0">
|
||
<p class="kernidee">Web-Standard, kein Framework — läuft nativ im Browser.</p>
|
||
<pre class="code" data-lang="js">class MyCounter extends HTMLElement {
|
||
constructor() {
|
||
super();
|
||
this.count = 0;
|
||
}
|
||
connectedCallback() {
|
||
this.innerHTML = `<button>Clicked 0 times</button>`;
|
||
const btn = this.querySelector("button");
|
||
btn.addEventListener("click", () => {
|
||
this.count++;
|
||
btn.textContent = `Clicked ${this.count} times`;
|
||
});
|
||
}
|
||
}
|
||
customElements.define("my-counter", MyCounter);</pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="fp-2" aria-labelledby="ft-2" tabindex="0" hidden>
|
||
<p class="kernidee">Virtual DOM · JSX · Hooks (useState).</p>
|
||
<pre class="code" data-lang="jsx">import { useState } from "react";
|
||
|
||
function MyButton() {
|
||
const [count, setCount] = useState(0);
|
||
|
||
return (
|
||
<button onClick={() => setCount(count + 1)}>
|
||
Clicked {count} times
|
||
</button>
|
||
);
|
||
}</pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="fp-3" aria-labelledby="ft-3" tabindex="0" hidden>
|
||
<p class="kernidee">Reaktive refs · Single-File Components (.vue).</p>
|
||
<pre class="code" data-lang="vue"><script setup>
|
||
import { ref } from "vue";
|
||
const count = ref(0);
|
||
</script>
|
||
|
||
<template>
|
||
<button @click="count++">
|
||
Clicked {{ count }} times
|
||
</button>
|
||
</template></pre>
|
||
</div>
|
||
|
||
<div role="tabpanel" id="fp-4" aria-labelledby="ft-4" tabindex="0" hidden>
|
||
<p class="kernidee">Compiler — kein Runtime-Framework, reaktive Zuweisungen.</p>
|
||
<pre class="code" data-lang="svelte"><script>
|
||
let count = 0;
|
||
</script>
|
||
|
||
<button on:click={() => count += 1}>
|
||
Clicked {count} {count === 1 ? 'time' : 'times'}
|
||
</button></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="note" style="margin-top:18px;">
|
||
<strong>Zusammenfassung:</strong>
|
||
Sprachkern (<span class="tag shared">ECMAScript</span>) läuft überall ·
|
||
<span class="tag browser">document/window</span> nur im Browser ·
|
||
<span class="tag node">node:fs · node:http · process</span> nur in Node. Frameworks bauen auf dem Browser-Teil auf.
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<!-- Fußleiste / Navigation -->
|
||
<nav class="navbar" aria-label="Bildschirm-Navigation">
|
||
<button class="nav-btn prev" id="btn-prev">← Zurück</button>
|
||
<div class="dots" id="dots" role="group" aria-label="Direkt zu Bildschirm"></div>
|
||
<button class="nav-btn next" id="btn-next">Weiter →</button>
|
||
</nav>
|
||
|
||
<div class="visually-hidden" id="live-screen" role="status" aria-live="polite"></div>
|
||
|
||
<script>
|
||
/* IIFE — eigener Scope, sofort ausgeführt (genau das Pattern aus Screen 5). */
|
||
(() => {
|
||
"use strict";
|
||
const $ = (sel, root = document) => root.querySelector(sel);
|
||
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
|
||
|
||
/* ========================================================
|
||
Syntax-Highlighter (ein Durchlauf, sicher)
|
||
======================================================== */
|
||
const KEYWORDS = /\b(const|let|var|function|return|import|export|from|await|async|new|class|extends|super|if|else|for|of|in|try|catch|typeof|default|this)\b/;
|
||
function highlight(src) {
|
||
const esc = s => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
const re = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|\b(const|let|var|function|return|import|export|from|await|async|new|class|extends|super|if|else|for|of|in|try|catch|typeof|default|this)\b|\b(\d+(?:\.\d+)?)\b/g;
|
||
let out = "", last = 0, m;
|
||
while ((m = re.exec(src))) {
|
||
out += esc(src.slice(last, m.index));
|
||
if (m[1]) out += '<span class="tok-com">' + esc(m[1]) + "</span>";
|
||
else if (m[2]) out += '<span class="tok-str">' + esc(m[2]) + "</span>";
|
||
else if (m[3]) out += '<span class="tok-kw">' + esc(m[3]) + "</span>";
|
||
else if (m[4]) out += '<span class="tok-num">' + esc(m[4]) + "</span>";
|
||
last = re.lastIndex;
|
||
}
|
||
out += esc(src.slice(last));
|
||
return out;
|
||
}
|
||
$$(".code[data-lang]").forEach(el => { el.innerHTML = highlight(el.textContent); });
|
||
|
||
/* ========================================================
|
||
Screen-Navigation
|
||
======================================================== */
|
||
const screens = $$(".screen");
|
||
const total = screens.length;
|
||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||
let current = 0;
|
||
|
||
const elPrev = $("#btn-prev");
|
||
const elNext = $("#btn-next");
|
||
const elDots = $("#dots");
|
||
const elTitle = $("#screen-title");
|
||
const elCounter = $("#counter-cur");
|
||
const elProgress = $("#progress");
|
||
const elLive = $("#live-screen");
|
||
const pad2 = n => String(n).padStart(2, "0");
|
||
|
||
// Punkte erzeugen
|
||
screens.forEach((s, i) => {
|
||
const b = document.createElement("button");
|
||
b.type = "button";
|
||
b.setAttribute("aria-label", `Bildschirm ${i + 1}: ${s.dataset.title}`);
|
||
b.addEventListener("click", () => goTo(i));
|
||
elDots.appendChild(b);
|
||
});
|
||
const dots = $$("button", elDots);
|
||
|
||
function goTo(i) {
|
||
i = Math.max(0, Math.min(total - 1, i));
|
||
const target = screens[i];
|
||
screens.forEach((s, k) => {
|
||
if (k === i) {
|
||
s.hidden = false;
|
||
if (!reduceMotion) {
|
||
s.classList.add("is-entering");
|
||
s.addEventListener("animationend", function done() {
|
||
s.classList.remove("is-entering");
|
||
s.removeEventListener("animationend", done);
|
||
}, { once: true });
|
||
}
|
||
} else {
|
||
s.hidden = true;
|
||
s.classList.remove("is-entering");
|
||
}
|
||
});
|
||
current = i;
|
||
elTitle.textContent = target.dataset.title;
|
||
elCounter.textContent = pad2(i + 1);
|
||
elProgress.style.width = ((i + 1) / total * 100) + "%";
|
||
dots.forEach((d, k) => d.setAttribute("aria-current", k === i ? "true" : "false"));
|
||
elPrev.disabled = i === 0;
|
||
elNext.disabled = i === total - 1;
|
||
elLive.textContent = `Bildschirm ${i + 1} von ${total}: ${target.dataset.title}`;
|
||
// Fokus auf die Überschrift (außer ganz am Anfang)
|
||
const h = target.querySelector("h2[tabindex], h1");
|
||
if (h && h.hasAttribute("tabindex")) h.focus();
|
||
$("#screen-host").scrollTop = 0;
|
||
}
|
||
|
||
elPrev.addEventListener("click", () => goTo(current - 1));
|
||
elNext.addEventListener("click", () => goTo(current + 1));
|
||
|
||
document.addEventListener("keydown", e => {
|
||
const t = e.target;
|
||
const typing = t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT");
|
||
if (typing) return;
|
||
if (["ArrowRight", "PageDown", " "].includes(e.key)) { e.preventDefault(); goTo(current + 1); }
|
||
else if (["ArrowLeft", "PageUp"].includes(e.key)) { e.preventDefault(); goTo(current - 1); }
|
||
else if (e.key === "Home") { e.preventDefault(); goTo(0); }
|
||
else if (e.key === "End") { e.preventDefault(); goTo(total - 1); }
|
||
else if (/^[1-8]$/.test(e.key)) { e.preventDefault(); goTo(Number(e.key) - 1); }
|
||
});
|
||
|
||
/* ========================================================
|
||
Wiederverwendbare ARIA-Tabs (Pfeiltasten + roving tabindex)
|
||
======================================================== */
|
||
function initTabs(root) {
|
||
const tabs = $$('[role="tab"]', root);
|
||
const activate = (tab, focus = true) => {
|
||
tabs.forEach(t => {
|
||
const sel = t === tab;
|
||
t.setAttribute("aria-selected", String(sel));
|
||
t.tabIndex = sel ? 0 : -1;
|
||
const p = $("#" + t.getAttribute("aria-controls"), root);
|
||
if (p) p.hidden = !sel;
|
||
});
|
||
if (focus) tab.focus();
|
||
};
|
||
tabs.forEach((t, i) => {
|
||
t.addEventListener("click", () => activate(t, false));
|
||
t.addEventListener("keydown", e => {
|
||
let ni = null;
|
||
if (e.key === "ArrowRight" || e.key === "ArrowDown") ni = (i + 1) % tabs.length;
|
||
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") ni = (i - 1 + tabs.length) % tabs.length;
|
||
else if (e.key === "Home") ni = 0;
|
||
else if (e.key === "End") ni = tabs.length - 1;
|
||
if (ni !== null) { e.preventDefault(); activate(tabs[ni]); }
|
||
});
|
||
});
|
||
}
|
||
$$('[role="tablist"]').forEach(list => initTabs(list.parentElement));
|
||
|
||
/* ========================================================
|
||
Screen 1 — Venn / API-Chips
|
||
======================================================== */
|
||
const apis = [
|
||
{ id: "document", where: "browser", note: "Einstieg ins DOM. Existiert nur im Browser — im Node-REPL: ReferenceError." },
|
||
{ id: "window", where: "browser", note: "Das globale Objekt im Browser. In Node heißt das Pendant globalThis." },
|
||
{ id: "navigator", where: "browser", note: "Infos zu Browser/Gerät (z. B. navigator.language). Kein Node-Äquivalent." },
|
||
{ id: "localStorage", where: "browser", note: "Key-Value-Speicher im Browser. In Node: Dateisystem oder Datenbank." },
|
||
{ id: "alert", where: "browser", note: "Blockierender Dialog — reine Browser-UI." },
|
||
{ id: "fetch", where: "shared", note: "Web-API aus dem Fetch-Standard. Im Browser seit Langem verfügbar, in Node seit v18 (über undici). Kein Sprachmerkmal, sondern eine Host-API." },
|
||
{ id: "console", where: "shared", note: "console.log & Co. bieten beide Runtimes — aber als Host-API, nicht als ECMAScript." },
|
||
{ id: "setTimeout", where: "shared", note: "Timer-API; von beiden Runtimes bereitgestellt, nicht Teil des Sprachkerns." },
|
||
{ id: "const / let", where: "shared", note: "ECMAScript-Sprachkern. Läuft in jeder konformen Engine — Browser wie Node." },
|
||
{ id: "Array.map", where: "shared", note: "Array-Methode aus dem Sprachkern. Überall verfügbar." },
|
||
{ id: "Promise", where: "shared", note: "ECMAScript-Sprachkern (ES2015). Basis für async/await." },
|
||
{ id: "async/await", where: "shared", note: "Sprachsyntax (ES2017). In beiden Umgebungen identisch." },
|
||
{ id: "node:fs", where: "node", note: "Dateisystem-Zugriff. Nur in Node — ein Browser darf das aus Sicherheitsgründen nicht." },
|
||
{ id: "node:http", where: "node", note: "Server starten & HTTP sprechen. Browser können keinen Server hosten." },
|
||
{ id: "process", where: "node", note: "Der laufende Node-Prozess (argv, env, platform, exit …) — ein Global, kein Modul. Im Browser nicht vorhanden." },
|
||
{ id: "node:path", where: "node", note: "Pfade plattformübergreifend bauen. Node-only." },
|
||
{ id: "__dirname", where: "node", note: "Verzeichnis der aktuellen Datei (CJS). In ESM via import.meta.url rekonstruieren." },
|
||
{ id: "require", where: "node", note: "CommonJS-Modulladen. Browser nutzen native ES-Module (import)." },
|
||
];
|
||
const whereLabel = { browser: "nur im Browser", node: "nur in Node.js", shared: "in beiden Umgebungen" };
|
||
const whereTag = { browser: "Browser", node: "Node", shared: "beide" };
|
||
const regionEl = { browser: $("#region-browser"), node: $("#region-node"), shared: $("#region-shared") };
|
||
const chipGrid = $("#chip-grid");
|
||
const vennNote = $("#venn-note");
|
||
|
||
apis.forEach(api => {
|
||
const b = document.createElement("button");
|
||
b.type = "button";
|
||
b.className = "chip " + api.where;
|
||
b.setAttribute("aria-pressed", "false");
|
||
b.innerHTML = `<span class="code-id">${api.id}</span> <span class="where">${whereTag[api.where]}</span>`;
|
||
b.addEventListener("click", () => {
|
||
$$(".chip", chipGrid).forEach(c => c.setAttribute("aria-pressed", "false"));
|
||
b.setAttribute("aria-pressed", "true");
|
||
Object.values(regionEl).forEach(r => { r.classList.remove("on"); r.setAttribute("fill-opacity", "0.12"); });
|
||
const r = regionEl[api.where];
|
||
r.classList.add("on");
|
||
r.setAttribute("fill-opacity", "0.4");
|
||
vennNote.innerHTML = `<strong>${api.id}</strong> — ${whereLabel[api.where]}. ${api.note}`;
|
||
});
|
||
chipGrid.appendChild(b);
|
||
});
|
||
|
||
/* ========================================================
|
||
Screen 2 — Zwei Konsolen
|
||
======================================================== */
|
||
const examples = [
|
||
{ expr: 'document.querySelector("h2").textContent', browser: () => document.querySelector("h2").textContent, node: "ReferenceError: document is not defined", nodeErr: true },
|
||
{ expr: "navigator.language", browser: () => navigator.language, node: "ReferenceError: navigator is not defined", nodeErr: true },
|
||
{ expr: "location.href", browser: () => location.href, node: "ReferenceError: location is not defined", nodeErr: true },
|
||
{ expr: "window.innerWidth", browser: () => window.innerWidth, node: "ReferenceError: window is not defined", nodeErr: true },
|
||
{ expr: "process.platform", browser: () => process.platform, node: "'linux'" },
|
||
{ expr: "process.cwd()", browser: () => process.cwd(), node: "'/home/mara/projekt'" },
|
||
];
|
||
const exRow = $("#ex-row");
|
||
const outBrowser = $("#out-browser");
|
||
const outNode = $("#out-node");
|
||
|
||
const fmt = v => {
|
||
if (typeof v === "string") return "'" + v + "'";
|
||
if (Array.isArray(v)) return "[ " + v.join(", ") + " ]";
|
||
return String(v);
|
||
};
|
||
const renderConsole = (el, expr, output, isErr) =>
|
||
el.innerHTML = `<span class="prompt">></span> ${expr}\n<span class="${isErr ? "out-err" : "out-ok"}">${output}</span>`;
|
||
|
||
examples.forEach((ex, i) => {
|
||
const b = document.createElement("button");
|
||
b.type = "button";
|
||
b.className = "ex-btn";
|
||
b.textContent = ex.expr;
|
||
b.setAttribute("aria-pressed", "false");
|
||
b.addEventListener("click", () => {
|
||
$$(".ex-btn", exRow).forEach(x => x.setAttribute("aria-pressed", "false"));
|
||
b.setAttribute("aria-pressed", "true");
|
||
let bOut, bErr = false;
|
||
try { bOut = fmt(ex.browser()); }
|
||
catch (err) { bOut = err.name + ": " + err.message; bErr = true; }
|
||
let nOut, nErr = !!ex.nodeErr;
|
||
if (typeof ex.node === "function") {
|
||
try { nOut = fmt(ex.node()); }
|
||
catch (err) { nOut = err.name + ": " + err.message; nErr = true; }
|
||
} else {
|
||
nOut = ex.node;
|
||
}
|
||
renderConsole(outBrowser, ex.expr, bOut, bErr);
|
||
renderConsole(outNode, ex.expr, nOut, nErr);
|
||
});
|
||
exRow.appendChild(b);
|
||
});
|
||
|
||
/* ========================================================
|
||
Screen 3 — DOM Playground
|
||
======================================================== */
|
||
const demo = $("#dom-demo");
|
||
const demoOriginal = demo.innerHTML;
|
||
const domEcho = $("#dom-echo");
|
||
const echo = (cmd, res) =>
|
||
domEcho.innerHTML = `<span class="prompt">></span> ${cmd}` + (res !== undefined ? `\n<span class="out-ok">${res}</span>` : "");
|
||
|
||
$$("[data-dom]").forEach(btn => {
|
||
btn.addEventListener("click", () => {
|
||
const action = btn.dataset.dom;
|
||
if (action === "count") {
|
||
const n = demo.querySelectorAll("li").length;
|
||
echo('document.querySelectorAll("#dom-demo li").length', n);
|
||
} else if (action === "highlight") {
|
||
const first = demo.querySelector("li");
|
||
if (first) {
|
||
first.classList.toggle("hit");
|
||
if (!reduceMotion) { first.style.animation = "pop .3s"; setTimeout(() => first.style.animation = "", 320); }
|
||
}
|
||
echo('document.querySelector("#dom-demo li").classList.toggle("hit")', first ? "✓" : "— leer");
|
||
} else if (action === "text") {
|
||
const last = demo.querySelector("li:last-child .lbl");
|
||
if (last) last.textContent = "Geändert";
|
||
echo('document.querySelector("#dom-demo li:last-child .lbl").textContent = "Geändert"', last ? "✓" : "— leer");
|
||
} else if (action === "remove") {
|
||
const last = demo.querySelector("li:last-child");
|
||
if (last) last.remove();
|
||
echo('document.querySelector("#dom-demo li:last-child")?.remove()', "verbleibend: " + demo.querySelectorAll("li").length);
|
||
} else if (action === "reset") {
|
||
demo.innerHTML = demoOriginal;
|
||
echo("// Liste auf den Ausgangszustand zurückgesetzt", demo.querySelectorAll("li").length + " Elemente");
|
||
}
|
||
});
|
||
});
|
||
|
||
/* ========================================================
|
||
Screen 4 — package.json-Felder
|
||
======================================================== */
|
||
const pkgNotes = {
|
||
type: "\"type\": \"module\" → ESM aktivieren. Dann gilt import/export statt require.",
|
||
main: "Einstiegspunkt des Pakets — die Datei, die beim Import geladen wird.",
|
||
scripts: "Benannte Kommandos: npm run dev, npm test … --watch startet bei Änderungen neu.",
|
||
dependencies: "Laufzeit-Abhängigkeiten. ^4.18.0 = kompatible Minor/Patch-Updates erlaubt.",
|
||
version: "Semantic Versioning: MAJOR.MINOR.PATCH.",
|
||
};
|
||
const pkgNote = $("#pkg-note");
|
||
$$("[data-pkg]").forEach(btn => {
|
||
btn.addEventListener("click", () => {
|
||
$$("[data-pkg]").forEach(b => b.setAttribute("aria-pressed", "false"));
|
||
btn.setAttribute("aria-pressed", "true");
|
||
pkgNote.innerHTML = `<strong>${btn.dataset.pkg}</strong> — ${pkgNotes[btn.dataset.pkg]}`;
|
||
});
|
||
});
|
||
|
||
/* ========================================================
|
||
Screen 5 — Syntax wirklich ausführen (kein eval)
|
||
======================================================== */
|
||
const runners = {
|
||
constlet() {
|
||
const TAX = 0.19;
|
||
let warenkorb = 100;
|
||
warenkorb += 50;
|
||
return [
|
||
"warenkorb → " + warenkorb + " (let: neu zugewiesen ✓)",
|
||
"TAX → " + TAX + " (const: gebunden)",
|
||
"inkl. MwSt → " + (warenkorb * (1 + TAX)).toFixed(2) + " €",
|
||
];
|
||
},
|
||
funktionen() {
|
||
function bruttoPreis(netto, satz = 0.19) {
|
||
return netto * (1 + satz);
|
||
}
|
||
return [
|
||
"bruttoPreis(100) → " + bruttoPreis(100).toFixed(2) + " €",
|
||
"bruttoPreis(100, 0.07) → " + bruttoPreis(100, 0.07).toFixed(2) + " €",
|
||
"Standard-Parameter satz = 0.19, falls nicht angegeben",
|
||
];
|
||
},
|
||
arrow() {
|
||
const preise = [19.99, 5, 12.5];
|
||
const summe = preise.reduce((a, b) => a + b, 0);
|
||
const teuer = preise.filter(p => p > 10);
|
||
const inklMwSt = preise.map(p => +(p * 1.19).toFixed(2));
|
||
return [
|
||
"summe → " + summe.toFixed(2) + " €",
|
||
"teuer → " + JSON.stringify(teuer),
|
||
"inklMwSt → " + JSON.stringify(inklMwSt),
|
||
];
|
||
},
|
||
destructuring() {
|
||
const user = { name: "Mara", rolle: "Admin", aktiv: true };
|
||
const { name, rolle } = user;
|
||
const kopie = { ...user, aktiv: false };
|
||
const [erster, ...rest] = [1, 2, 3, 4];
|
||
return [
|
||
"name → '" + name + "', rolle → '" + rolle + "'",
|
||
"kopie → " + JSON.stringify(kopie),
|
||
"erster → " + erster + ", rest → " + JSON.stringify(rest),
|
||
];
|
||
},
|
||
template() {
|
||
const name = "Mara", punkte = 42;
|
||
const text = `Hallo ${name}, du hast ${punkte} Punkte.`;
|
||
const css = `transform: rotate(${punkte / 2}deg);`;
|
||
return ["text → '" + text + "'", "css → '" + css + "'"];
|
||
},
|
||
async asyncawait() {
|
||
const ladeUser = () => new Promise(resolve => setTimeout(() => resolve({ name: "Mara" }), 400));
|
||
const user = await ladeUser();
|
||
return [
|
||
"await ladeUser() … 400 ms gewartet",
|
||
"user.name → '" + user.name + "'",
|
||
"→ nicht-blockierend: die Oberfläche bleibt flüssig",
|
||
];
|
||
},
|
||
iife() {
|
||
const counter = (() => { let n = 0; return { tick: () => ++n }; })();
|
||
const a = counter.tick(), b = counter.tick();
|
||
return [
|
||
"counter.tick() → " + a,
|
||
"counter.tick() → " + b,
|
||
"n bleibt verborgen — erreichbar nur über tick()",
|
||
];
|
||
},
|
||
};
|
||
$$("[data-run]").forEach(btn => {
|
||
btn.addEventListener("click", async () => {
|
||
const key = btn.dataset.run;
|
||
const out = $('[data-out="' + key + '"]');
|
||
out.innerHTML = `<span class="prompt">></span> <span class="dim">läuft …</span>`;
|
||
const result = runners[key]();
|
||
const lines = result instanceof Promise ? await result : result;
|
||
out.innerHTML = lines.map(l => `<span class="prompt">></span> <span class="out-ok">${l}</span>`).join("\n");
|
||
});
|
||
});
|
||
|
||
/* ========================================================
|
||
Screen 6 — Logo-Generator
|
||
======================================================== */
|
||
function buildShadow(angle, shadowLen, depthLen, contourLen, depthCol, shadowCol) {
|
||
const rad = angle * Math.PI / 180;
|
||
const dx = Math.sin(rad), dy = -Math.cos(rad);
|
||
const r = n => Math.round(n * 100) / 100;
|
||
const parts = [];
|
||
for (let i = 1; i <= contourLen; i++) parts.push(`${r(dx*i)}px ${r(dy*i)}px 0 ${shadowCol}`);
|
||
for (let i = 1; i <= depthLen; i++) parts.push(`${r(-dx*i)}px ${r(-dy*i)}px 0 ${depthCol}`);
|
||
for (let i = contourLen + 1; i <= contourLen + shadowLen; i++) parts.push(`${r(dx*i)}px ${r(dy*i)}px 0 ${shadowCol}`);
|
||
return parts.length ? parts.join(", ") : "none";
|
||
}
|
||
|
||
const ltLogo = $("#lt-logo");
|
||
const ltCss = $("#lt-css");
|
||
const ltCopy = $("#lt-copy");
|
||
const ltCopyStatus = $("#lt-copy-status");
|
||
const lt = {
|
||
text: $("#lt-text"), font: $("#lt-font"), face: $("#lt-face"), bg: $("#lt-bg"),
|
||
depth: $("#lt-depth"), shadowcol: $("#lt-shadowcol"),
|
||
angle: $("#lt-angle"), shadow: $("#lt-shadow"),
|
||
depthlen: $("#lt-depthlen"), contour: $("#lt-contour"), rot: $("#lt-rot"),
|
||
};
|
||
function ltUpdate() {
|
||
$("#lt-angle-v").textContent = lt.angle.value;
|
||
$("#lt-shadow-v").textContent = lt.shadow.value;
|
||
$("#lt-depthlen-v").textContent = lt.depthlen.value;
|
||
$("#lt-contour-v").textContent = lt.contour.value;
|
||
$("#lt-rot-v").textContent = lt.rot.value;
|
||
const shadow = buildShadow(
|
||
+lt.angle.value, +lt.shadow.value, +lt.depthlen.value,
|
||
+lt.contour.value, lt.depth.value, lt.shadowcol.value
|
||
);
|
||
ltLogo.textContent = lt.text.value || " ";
|
||
ltLogo.style.fontFamily = `'${lt.font.value}', 'Bebas Neue', Impact, sans-serif`;
|
||
ltLogo.style.color = lt.face.value;
|
||
ltLogo.style.transform = `rotate(${lt.rot.value}deg) skewX(-8deg)`;
|
||
ltLogo.style.textShadow = shadow;
|
||
$(".lt-stage").style.background = lt.bg.value;
|
||
const shadowOut = shadow === "none"
|
||
? " none"
|
||
: "\n " + shadow.split(", ").join(",\n ");
|
||
ltCss.textContent =
|
||
".logo {\n" +
|
||
` font-family: '${lt.font.value}', sans-serif;\n` +
|
||
` color: ${lt.face.value};\n` +
|
||
` transform: rotate(${lt.rot.value}deg) skewX(-8deg);\n` +
|
||
` text-shadow:${shadowOut};\n` +
|
||
"}";
|
||
}
|
||
Object.values(lt).forEach(el => {
|
||
el.addEventListener("input", ltUpdate);
|
||
el.addEventListener("change", ltUpdate);
|
||
});
|
||
|
||
ltCopy.addEventListener("click", async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(ltCss.textContent);
|
||
ltCopyStatus.textContent = "In die Zwischenablage kopiert ✓";
|
||
} catch {
|
||
const range = document.createRange();
|
||
range.selectNodeContents(ltCss);
|
||
const sel = window.getSelection();
|
||
sel.removeAllRanges();
|
||
sel.addRange(range);
|
||
ltCopyStatus.textContent = "Markiert — mit Strg/Cmd + C kopieren";
|
||
}
|
||
setTimeout(() => { ltCopyStatus.textContent = ""; }, 3000);
|
||
});
|
||
|
||
/* Start */
|
||
ltUpdate();
|
||
goTo(0);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|