User Management + Didaktisches Handbuch + Stadt-Editor + Emoji-Kacheln
User Management (Backend): - schema-v2.sql: students, licenses, student_results, activity_log Tabellen - auth.php: Lehrer-Registrierung/Login, Schueler-Login, Status-Check - classes.php: CRUD fuer Klassenverwaltung (mit Join-Code-Generator) - students.php: Schueler erstellen (einzeln + Batch), bearbeiten, loeschen - modules.php: Modulfreigabe pro Klasse (locked/free/teacher_started) Didaktisches Handbuch: - handbuch.html: Vollstaendiger Lehrpersonen-Guide - 6 Module detailliert beschrieben (Lernziele, Kompetenzen) - Vor/Nach-Fragen fuer jede Simulation - Empfohlener Unterrichtsablauf (45-min-Einheit) - Differenzierungshinweise, Bewertungsempfehlungen Stadt-Editor: - stadt-editor.html: Funktionierendes 8x8 Raster mit Kachel-Platzierung - 15 Emoji-Gebaude-Kacheln (Kirche, Moschee, Wohngebiet, Fabrik, etc.) - Spielplatz: Rutsche+Karussell statt schwebende Koepfe - Hochhaeuser: 6 Gebaeude in 2 Reihen Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GeoGraSim — Didaktisches Handbuch für Lehrpersonen</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:'Inter',system-ui,sans-serif; background:#f7f6f3; color:#1a1a1a; line-height:1.7; }
|
||||
.wrap { max-width:820px; margin:0 auto; padding:2rem 1.5rem; }
|
||||
h1 { font-size:2rem; font-weight:800; letter-spacing:-.03em; margin-bottom:.5rem; color:#4a7c8a; }
|
||||
h2 { font-size:1.3rem; font-weight:800; color:#4a7c8a; margin:2.5rem 0 .8rem; padding-bottom:.3rem; border-bottom:2px solid #dae8ec; }
|
||||
h3 { font-size:1rem; font-weight:700; color:#3a6470; margin:1.5rem 0 .5rem; }
|
||||
p { margin-bottom:.8rem; font-size:.92rem; }
|
||||
ul, ol { margin:0 0 1rem 1.5rem; font-size:.9rem; }
|
||||
li { margin-bottom:.3rem; }
|
||||
.lead { color:#5a5a5a; font-size:1rem; margin-bottom:1.5rem; }
|
||||
.module { background:#fff; border-radius:12px; padding:1.2rem; margin-bottom:1rem; border:1px solid rgba(0,0,0,.05); box-shadow:0 2px 8px rgba(0,0,0,.03); }
|
||||
.module h3 { margin-top:0; }
|
||||
.tag { display:inline-block; font-size:.68rem; font-weight:600; padding:2px 8px; border-radius:5px; margin-right:.3rem; }
|
||||
.tag-time { background:#f0eeea; color:#8a8a8a; }
|
||||
.tag-level { background:#dae8ec; color:#4a7c8a; }
|
||||
.tag-topic { background:#dceadd; color:#5a8a5e; }
|
||||
table { width:100%; border-collapse:collapse; margin:1rem 0; font-size:.85rem; }
|
||||
th { background:#f0eeea; padding:.5rem .7rem; text-align:left; font-weight:700; font-size:.78rem; color:#4a4a4a; }
|
||||
td { padding:.5rem .7rem; border-bottom:1px solid rgba(0,0,0,.04); }
|
||||
.qa { background:#faf8f4; border-left:3px solid #c4a35a; padding:.7rem 1rem; margin:.8rem 0; border-radius:0 8px 8px 0; }
|
||||
.qa strong { color:#8a6a2a; font-size:.82rem; }
|
||||
.qa p { margin-bottom:.3rem; font-size:.85rem; }
|
||||
footer { margin-top:3rem; padding-top:1rem; border-top:1px solid rgba(0,0,0,.06); color:#8a8a8a; font-size:.78rem; text-align:center; }
|
||||
@media print { body { background:#fff; } .wrap { max-width:100%; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<h1>Didaktisches Handbuch</h1>
|
||||
<p class="lead">GeoGraSim — Interaktive Simulationen für den Geografieunterricht der Sekundarstufe I.<br>
|
||||
Dieses Handbuch richtet sich an Lehrpersonen und beschreibt Zweck, Einsatz und didaktische Einbettung jeder Simulation.</p>
|
||||
|
||||
<h2>1. Überblick</h2>
|
||||
<p>GeoGraSim ist eine Sammlung interaktiver Simulationen, die zentrale Themen des Geografieunterrichts erfahrbar machen. Die Schülerinnen und Schüler treffen Entscheidungen in komplexen Systemen und erleben die Konsequenzen — Zielkonflikte, Rückkopplungen und nichtlineare Zusammenhänge werden spürbar statt nur beschrieben.</p>
|
||||
|
||||
<h3>Didaktische Grundidee</h3>
|
||||
<ul>
|
||||
<li><strong>Systemdenken fördern:</strong> Schüler:innen erkennen, dass Entscheidungen in Mensch-Umwelt-Systemen vielfältige Auswirkungen haben.</li>
|
||||
<li><strong>Zielkonflikte erfahren:</strong> Es gibt keine „perfekte" Lösung — jede Maßnahme hat Vor- und Nachteile.</li>
|
||||
<li><strong>Handlungskompetenz aufbauen:</strong> Durch Experimentieren und Reflektieren entwickeln Lernende informierte Positionen.</li>
|
||||
<li><strong>Lehrplankonform:</strong> Alle Module sind auf den österreichischen Lehrplan (GW, Sek I) abgestimmt, mit Bezügen zu DACH-Curricula.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Einsatzformen</h3>
|
||||
<ul>
|
||||
<li><strong>Geführter Unterricht:</strong> Lehrperson startet ein Modul für die ganze Klasse, begleitet und reflektiert gemeinsam.</li>
|
||||
<li><strong>Freie Übung:</strong> Schüler:innen arbeiten selbstständig an freigegebenen Modulen.</li>
|
||||
<li><strong>Hausübung:</strong> Module mit Abgabefrist als vertiefende Aufgabe.</li>
|
||||
<li><strong>Vergleich und Diskussion:</strong> Ergebnisse verschiedener Schüler:innen vergleichen und Strategien besprechen.</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Module im Detail</h2>
|
||||
|
||||
<!-- ===== KLIMAWÄCHTER ===== -->
|
||||
<div class="module">
|
||||
<h3>🌡️ Klimawächter (SIM-05)</h3>
|
||||
<span class="tag tag-level">1. Klasse</span>
|
||||
<span class="tag tag-time">⏱ 20 min</span>
|
||||
<span class="tag tag-topic">Klimawandel</span>
|
||||
|
||||
<p><strong>Beschreibung:</strong> Die Schüler:innen übernehmen die Rolle der Klimaminister:in eines Inselstaats und steuern diesen 75 Jahre lang (2025–2100) durch die Klimakrise. Sie müssen CO₂-Emissionen reduzieren, die Wirtschaft am Laufen halten und die Bevölkerung vor dem steigenden Meeresspiegel schützen.</p>
|
||||
|
||||
<p><strong>Lernziele:</strong></p>
|
||||
<ul>
|
||||
<li>Zusammenhang zwischen CO₂, Temperatur und Meeresspiegel verstehen</li>
|
||||
<li>Verschiedene Klimaschutzmaßnahmen und ihre Trade-offs kennenlernen</li>
|
||||
<li>Unterschied zwischen kurzfristigen und langfristigen Folgen erfahren</li>
|
||||
<li>Erkennen, dass Nichtstun schwerwiegende Konsequenzen hat</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Kompetenzbereiche:</strong> Mensch-Umwelt-Beziehungen, Nachhaltigkeit, Raumverantwortung</p>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen VOR der Simulation:</strong>
|
||||
<p>• Was ist der Treibhauseffekt? Warum ist CO₂ ein Problem?</p>
|
||||
<p>• Welche Maßnahmen gegen den Klimawandel kennst du?</p>
|
||||
<p>• Was passiert, wenn der Meeresspiegel steigt?</p>
|
||||
</div>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen NACH der Simulation:</strong>
|
||||
<p>• Welche Maßnahmen hast du gewählt? Warum?</p>
|
||||
<p>• Gab es Situationen, in denen du dich zwischen Wirtschaft und Umwelt entscheiden musstest?</p>
|
||||
<p>• Was wäre passiert, wenn du nichts gemacht hättest?</p>
|
||||
<p>• Welche Maßnahme war am wirksamsten? Welche am teuersten?</p>
|
||||
<p>• Wie realistisch findest du die Simulation?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== ERDBEBEN ===== -->
|
||||
<div class="module">
|
||||
<h3>🌋 Erdbeben-Simulator (SIM-07)</h3>
|
||||
<span class="tag tag-level">1. Klasse</span>
|
||||
<span class="tag tag-time">⏱ 25 min</span>
|
||||
<span class="tag tag-topic">Naturgefahr</span>
|
||||
|
||||
<p><strong>Beschreibung:</strong> 50 Jahre lang eine Stadt in einer Erdbebenregion aufbauen. Die Bauqualität entscheidet darüber, ob ein Beben eine Naturgefahr oder eine Naturkatastrophe wird.</p>
|
||||
|
||||
<p><strong>Lernziele:</strong></p>
|
||||
<ul>
|
||||
<li>Unterschied zwischen Naturgefahr und Naturkatastrophe verstehen</li>
|
||||
<li>Zusammenhang zwischen Bauqualität und Verwundbarkeit erkennen</li>
|
||||
<li>Kosten-Nutzen-Abwägung bei Schutzmaßnahmen</li>
|
||||
</ul>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen VOR der Simulation:</strong>
|
||||
<p>• Warum sind Erdbeben in manchen Ländern katastrophaler als in anderen?</p>
|
||||
<p>• Was bedeutet „Vulnerabilität"?</p>
|
||||
</div>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen NACH der Simulation:</strong>
|
||||
<p>• Wie hast du deine Stadt gebaut? Was hat bei Erdbeben standgehalten?</p>
|
||||
<p>• Was wäre passiert, wenn du nur billig gebaut hättest?</p>
|
||||
<p>• Vergleiche: Japan und Haiti bei ähnlich starken Beben — warum sind die Folgen so unterschiedlich?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== ENERGIEMIX ===== -->
|
||||
<div class="module">
|
||||
<h3>⚡ Energiemix-Planer (SIM-09)</h3>
|
||||
<span class="tag tag-level">2. Klasse</span>
|
||||
<span class="tag tag-time">⏱ 25 min</span>
|
||||
<span class="tag tag-topic">Energie</span>
|
||||
|
||||
<p><strong>Beschreibung:</strong> Die Region bis 2050 auf 80% erneuerbare Energie umstellen — ohne Blackout und ohne Pleite. Der Energiemix muss Kosten, CO₂, Versorgungssicherheit und Akzeptanz balancieren.</p>
|
||||
|
||||
<p><strong>Lernziele:</strong></p>
|
||||
<ul>
|
||||
<li>Verschiedene Energiequellen und ihre Vor-/Nachteile kennen</li>
|
||||
<li>Konzept der Energiewende verstehen</li>
|
||||
<li>Trade-offs zwischen Kosten, Umwelt und Versorgungssicherheit</li>
|
||||
</ul>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen VOR der Simulation:</strong>
|
||||
<p>• Woher kommt der Strom, den du zu Hause nutzt?</p>
|
||||
<p>• Welche Energiequellen sind erneuerbar? Welche nicht?</p>
|
||||
</div>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen NACH der Simulation:</strong>
|
||||
<p>• Welchen Energiemix hast du gewählt? Warum?</p>
|
||||
<p>• Was passiert, wenn die Sonne nicht scheint und kein Wind weht?</p>
|
||||
<p>• Warum kann man nicht einfach alles auf Solar umstellen?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== FLUSSMANAGEMENT ===== -->
|
||||
<div class="module">
|
||||
<h3>🏞️ Flussmanagement (SIM-12)</h3>
|
||||
<span class="tag tag-level">1.–2. Klasse</span>
|
||||
<span class="tag tag-time">⏱ 20–35 min</span>
|
||||
<span class="tag tag-topic">Hydrologie</span>
|
||||
|
||||
<p><strong>Beschreibung:</strong> Ein Flusssystem über 100 Jahre verwalten. Der Fluss ändert seinen Lauf, das Wetter ist unberechenbar. Siedlungen, Felder und Deiche müssen klug platziert werden.</p>
|
||||
|
||||
<p><strong>Lernziele:</strong></p>
|
||||
<ul>
|
||||
<li>Flussdynamik und natürliche Überschwemmungsprozesse verstehen</li>
|
||||
<li>Historische Eingriffe in Gewässer (Begradigung, Deiche) und ihre Folgen kennen</li>
|
||||
<li>Zielkonflikte zwischen Hochwasserschutz, Landwirtschaft, Ökologie und Wirtschaft</li>
|
||||
<li>Konzept der Renaturierung und „dem Fluss Raum geben"</li>
|
||||
</ul>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen VOR der Simulation:</strong>
|
||||
<p>• Warum gibt es Hochwasser? Ist Hochwasser immer schlecht?</p>
|
||||
<p>• Was ist ein Mäander? Warum begradigt man Flüsse?</p>
|
||||
<p>• Kennst du einen Fluss in deiner Nähe? Ist er begradigt?</p>
|
||||
</div>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen NACH der Simulation:</strong>
|
||||
<p>• Was war deine Strategie? Hat sie funktioniert?</p>
|
||||
<p>• Was passierte, als der Fluss seinen Lauf änderte?</p>
|
||||
<p>• Was war effektiver: Deiche bauen oder dem Fluss Raum lassen?</p>
|
||||
<p>• Warum ist Begradigung kurzfristig hilfreich, aber langfristig problematisch?</p>
|
||||
<p>• Übertrage: Wie sollte man mit Flüssen in der Realität umgehen?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== STADT & RAUMPLANUNG ===== -->
|
||||
<div class="module">
|
||||
<h3>🏘️ Stadt & Raumplanung (SIM-13)</h3>
|
||||
<span class="tag tag-level">3. Klasse</span>
|
||||
<span class="tag tag-time">⏱ 20–40 min</span>
|
||||
<span class="tag tag-topic">Raumplanung</span>
|
||||
|
||||
<p><strong>Beschreibung:</strong> Eine Siedlung schrittweise aufbauen durch Platzierung von Kacheln (Straßen, Häuser, Geschäfte, Parks, Industrie). Jede Platzierung hat Folgen für Wirtschaft, Zufriedenheit, Umwelt und Verkehr.</p>
|
||||
|
||||
<p><strong>Lernziele:</strong></p>
|
||||
<ul>
|
||||
<li>Grundlagen der Raumplanung und Flächennutzung verstehen</li>
|
||||
<li>Infrastruktur-Abhängigkeiten erkennen (Straßen, Versorgung, Erreichbarkeit)</li>
|
||||
<li>Zielkonflikte zwischen Wachstum, Lebensqualität und Nachhaltigkeit</li>
|
||||
<li>Bedeutung von Nahversorgung, Grünräumen und Verkehrsplanung</li>
|
||||
</ul>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen VOR der Simulation:</strong>
|
||||
<p>• Was braucht eine Siedlung, damit die Menschen dort gut leben können?</p>
|
||||
<p>• Warum stehen Fabriken meistens am Stadtrand und nicht in der Mitte?</p>
|
||||
<p>• Was ist „Nahversorgung"? Warum ist sie wichtig?</p>
|
||||
</div>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen NACH der Simulation:</strong>
|
||||
<p>• Wie hast du deine Stadt geplant? Was hast du zuerst gebaut?</p>
|
||||
<p>• Gab es Probleme, als die Stadt gewachsen ist?</p>
|
||||
<p>• Was würdest du beim nächsten Mal anders machen?</p>
|
||||
<p>• Welche Gebäude haben die Zufriedenheit am meisten beeinflusst? Warum?</p>
|
||||
<p>• Vergleiche deine Stadt mit der eines Mitschülers: Was ist anders? Was ist besser?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LIEFERKETTEN ===== -->
|
||||
<div class="module">
|
||||
<h3>👕 Lieferketten-Planer (SIM-10)</h3>
|
||||
<span class="tag tag-level">2. Klasse</span>
|
||||
<span class="tag tag-time">⏱ 15 min</span>
|
||||
<span class="tag tag-topic">Globalisierung</span>
|
||||
|
||||
<p><strong>Beschreibung:</strong> Was kostet dein T-Shirt wirklich? Bestelle Produkte aus aller Welt und finde die Balance zwischen Preis, CO₂ und fairen Arbeitsbedingungen.</p>
|
||||
|
||||
<p><strong>Lernziele:</strong></p>
|
||||
<ul>
|
||||
<li>Globale Lieferketten und Transportwege verstehen</li>
|
||||
<li>Zusammenhang zwischen Preis, Transportweg und Arbeitsbedingungen</li>
|
||||
<li>Konzept des „ökologischen Fußabdrucks" bei Konsumgütern</li>
|
||||
</ul>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen VOR der Simulation:</strong>
|
||||
<p>• Schau auf dein T-Shirt: Wo wurde es hergestellt? Wie weit ist es gereist?</p>
|
||||
<p>• Warum sind manche Produkte billig, obwohl sie von weit her kommen?</p>
|
||||
</div>
|
||||
|
||||
<div class="qa">
|
||||
<strong>Fragen NACH der Simulation:</strong>
|
||||
<p>• Welche Produkte hast du gewählt? Warum?</p>
|
||||
<p>• Was hat dich überrascht am CO₂-Verbrauch?</p>
|
||||
<p>• Kann man immer fair UND günstig UND umweltfreundlich kaufen?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>3. Einsatz im Unterricht</h2>
|
||||
|
||||
<h3>Empfohlener Ablauf (45-Minuten-Einheit)</h3>
|
||||
<table>
|
||||
<tr><th>Phase</th><th>Dauer</th><th>Aktivität</th></tr>
|
||||
<tr><td>Einstieg</td><td>5 min</td><td>Fragen VOR der Simulation besprechen. Vorwissen aktivieren.</td></tr>
|
||||
<tr><td>Einführung</td><td>3 min</td><td>Modul kurz vorstellen, Spielprinzip erklären.</td></tr>
|
||||
<tr><td>Simulation</td><td>20–25 min</td><td>Schüler:innen spielen selbstständig (Einzel- oder Partnerarbeit).</td></tr>
|
||||
<tr><td>Auswertung</td><td>5 min</td><td>Ergebnisse vergleichen (Lehrkraft zeigt Dashboard).</td></tr>
|
||||
<tr><td>Reflexion</td><td>7 min</td><td>Fragen NACH der Simulation. Was habe ich gelernt? Transfer auf die Realität.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Differenzierung</h3>
|
||||
<ul>
|
||||
<li><strong>Leistungsschwächere:</strong> Einfachstes Level, mehr Zeit, Partnerarbeit</li>
|
||||
<li><strong>Leistungsstärkere:</strong> Höheres Level, zusätzliche Reflexionsfragen, eigene Strategien dokumentieren</li>
|
||||
<li><strong>Sprachförderung:</strong> Module können zweisprachig (DE/EN) eingesetzt werden</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Bewertung und Leistungserfassung</h2>
|
||||
|
||||
<p>Das System erfasst automatisch:</p>
|
||||
<ul>
|
||||
<li>Gesamtscore pro Modul (gewichtete Kombination der Teilwerte)</li>
|
||||
<li>Balance-Index (wie ausgewogen ist die Lösung?)</li>
|
||||
<li>Dauer der Bearbeitung</li>
|
||||
<li>Anzahl der getroffenen Entscheidungen</li>
|
||||
<li>Strategieprofil (einseitig vs. ausgewogen)</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Wichtig:</strong> Die Simulation ist ein Lernwerkzeug, kein Test. Der Score dient der Reflexion, nicht der Benotung. Empfehlung: Bewerten Sie den Reflexionsbeitrag (mündlich oder schriftlich), nicht den Spielscore.</p>
|
||||
|
||||
<h3>Empfohlene Reflexions-Aufgaben (zur Leistungsfeststellung)</h3>
|
||||
<ol>
|
||||
<li>„Beschreibe deine Strategie in 3–5 Sätzen. Was war dein wichtigstes Ziel?"</li>
|
||||
<li>„Nenne einen Zielkonflikt, den du erlebt hast. Wie hast du dich entschieden?"</li>
|
||||
<li>„Was würdest du in der Realität anders machen als im Spiel? Warum?"</li>
|
||||
<li>„Vergleiche dein Ergebnis mit dem einer Mitschülerin. Was war anders? Warum?"</li>
|
||||
</ol>
|
||||
|
||||
<h2>5. Technische Hinweise</h2>
|
||||
|
||||
<ul>
|
||||
<li><strong>Browser:</strong> Chrome, Firefox, Safari, Edge (aktuell). Funktioniert auf Desktop, Laptop, Tablet.</li>
|
||||
<li><strong>Internet:</strong> Erforderlich für Login und Ergebnis-Speicherung. Simulationen laufen clientseitig.</li>
|
||||
<li><strong>Accounts:</strong> Lehrperson erstellt Klasse → generiert Schüler-Zugänge → Schüler loggen sich mit Klassencode + Benutzername + Passwort ein.</li>
|
||||
<li><strong>Datenschutz:</strong> Schüler-Accounts können anonym (Pseudonym) sein. Keine E-Mail-Adresse erforderlich.</li>
|
||||
</ul>
|
||||
|
||||
<h2>6. Forschungsbasis</h2>
|
||||
|
||||
<p>GeoGraSim basiert auf umfassender Forschungsarbeit:</p>
|
||||
<ul>
|
||||
<li>195 Quellen zur Simulationsforschung (Wirksamkeit, Gestaltung, Qualität)</li>
|
||||
<li>135 Quellen zur Geografiedidaktik (Fokus DACH-Raum)</li>
|
||||
<li>Lehrplan-Mapping: 24 Simulationsmodule auf 14 Kompetenzbereiche des österreichischen Lehrplans (BGBl. II Nr. 178/2025)</li>
|
||||
<li>DACH-Lehrplanvergleich (AT, DE-BY/NRW/BW, CH, UK)</li>
|
||||
</ul>
|
||||
|
||||
<footer>
|
||||
<p>GeoGraSim — Schroffenegger & Mößlang, 2026</p>
|
||||
<p>Dieses Handbuch ist Teil des GeoGraSim-Lernprogramms.</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Authentifizierung (Lehrer + Schueler)
|
||||
* POST /api/auth {action: "register"|"login"|"logout"|"student_login"|"reset_request"|"reset_confirm"}
|
||||
*/
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
Response::error('Nur POST erlaubt', 405);
|
||||
}
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
$db = Database::get();
|
||||
|
||||
// === LEHRER-REGISTRIERUNG ===
|
||||
if ($action === 'register') {
|
||||
$email = trim($body['email'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
$displayName = mb_substr(trim($body['displayName'] ?? ''), 0, 128);
|
||||
$schoolName = mb_substr(trim($body['schoolName'] ?? ''), 0, 255);
|
||||
|
||||
if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) Response::error('Gültige E-Mail-Adresse erforderlich');
|
||||
if (strlen($password) < 8) Response::error('Passwort muss mindestens 8 Zeichen lang sein');
|
||||
if (!$displayName) Response::error('Name erforderlich');
|
||||
|
||||
// Pruefen ob E-Mail schon existiert
|
||||
$existing = $db->fetchOne('SELECT id FROM teachers WHERE email = ?', [$email]);
|
||||
if ($existing) Response::error('Diese E-Mail-Adresse ist bereits registriert');
|
||||
|
||||
$username = strtolower(explode('@', $email)[0]) . '_' . rand(100, 999);
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO teachers (username, email, password, display_name, school_name) VALUES (?, ?, ?, ?, ?)',
|
||||
[$username, $email, $hash, $displayName, $schoolName]
|
||||
);
|
||||
|
||||
$teacherId = (int)$db->lastInsertId();
|
||||
Session::start();
|
||||
Session::loginTeacher($teacherId);
|
||||
|
||||
Response::ok([
|
||||
'teacherId' => $teacherId,
|
||||
'displayName' => $displayName,
|
||||
]);
|
||||
}
|
||||
|
||||
// === LEHRER-LOGIN ===
|
||||
if ($action === 'login') {
|
||||
$email = trim($body['email'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
|
||||
if (!$email || !$password) Response::error('E-Mail und Passwort erforderlich');
|
||||
|
||||
$teacher = $db->fetchOne(
|
||||
'SELECT id, password, display_name FROM teachers WHERE email = ? OR username = ?',
|
||||
[$email, $email]
|
||||
);
|
||||
|
||||
if (!$teacher || !password_verify($password, $teacher['password'])) {
|
||||
Response::error('E-Mail oder Passwort falsch');
|
||||
}
|
||||
|
||||
Session::start();
|
||||
Session::loginTeacher((int)$teacher['id']);
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO activity_log (user_type, user_id, action) VALUES ('teacher', ?, 'login')",
|
||||
[$teacher['id']]
|
||||
);
|
||||
|
||||
Response::ok([
|
||||
'teacherId' => (int)$teacher['id'],
|
||||
'displayName' => $teacher['display_name'],
|
||||
]);
|
||||
}
|
||||
|
||||
// === LOGOUT ===
|
||||
if ($action === 'logout') {
|
||||
Session::logout();
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// === SCHUELER-LOGIN ===
|
||||
if ($action === 'student_login') {
|
||||
$classCode = strtoupper(trim($body['classCode'] ?? ''));
|
||||
$username = trim($body['username'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
|
||||
if (!$classCode || !$username || !$password) Response::error('Klassencode, Benutzername und Passwort erforderlich');
|
||||
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE join_code = ?', [$classCode]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden');
|
||||
|
||||
$student = $db->fetchOne(
|
||||
'SELECT id, password, display_name FROM students WHERE class_id = ? AND username = ?',
|
||||
[$class['id'], $username]
|
||||
);
|
||||
|
||||
if (!$student || !password_verify($password, $student['password'])) {
|
||||
Response::error('Benutzername oder Passwort falsch');
|
||||
}
|
||||
|
||||
// Schueler-Session erstellen
|
||||
$sessionId = Session::createStudent((int)$class['id'], $student['display_name'] ?: $username);
|
||||
|
||||
$db->execute('UPDATE students SET last_login = NOW() WHERE id = ?', [$student['id']]);
|
||||
$db->execute(
|
||||
"INSERT INTO activity_log (user_type, user_id, action) VALUES ('student', ?, 'login')",
|
||||
[$student['id']]
|
||||
);
|
||||
|
||||
Response::ok([
|
||||
'sessionId' => $sessionId,
|
||||
'studentId' => (int)$student['id'],
|
||||
'displayName' => $student['display_name'] ?: $username,
|
||||
'className' => $class['name'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
// === STATUS ===
|
||||
if ($action === 'status') {
|
||||
Session::start();
|
||||
$teacherId = Session::teacherId();
|
||||
$studentId = Session::studentId();
|
||||
|
||||
if ($teacherId) {
|
||||
$teacher = $db->fetchOne('SELECT id, display_name, email, school_name FROM teachers WHERE id = ?', [$teacherId]);
|
||||
Response::ok(['role' => 'teacher', 'user' => $teacher]);
|
||||
}
|
||||
if ($studentId) {
|
||||
$session = $db->fetchOne(
|
||||
'SELECT s.display_name, s.class_id, c.name as class_name FROM student_sessions s LEFT JOIN classes c ON c.id = s.class_id WHERE s.id = ?',
|
||||
[$studentId]
|
||||
);
|
||||
Response::ok(['role' => 'student', 'session' => $session]);
|
||||
}
|
||||
Response::ok(['role' => 'guest']);
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Klassenverwaltung (nur fuer Lehrer)
|
||||
* GET /api/classes → Alle Klassen des Lehrers
|
||||
* POST /api/classes {action} → Klasse erstellen/bearbeiten/loeschen
|
||||
*/
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$classes = $db->fetchAll(
|
||||
'SELECT id, name, school_year, join_code, created_at FROM classes WHERE teacher_id = ? ORDER BY created_at DESC',
|
||||
[$teacherId]
|
||||
);
|
||||
// Schueleranzahl pro Klasse
|
||||
foreach ($classes as &$c) {
|
||||
$c['student_count'] = (int)$db->fetchOne(
|
||||
'SELECT COUNT(*) as cnt FROM students WHERE class_id = ?', [$c['id']]
|
||||
)['cnt'];
|
||||
}
|
||||
Response::ok($classes);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
// === KLASSE ERSTELLEN ===
|
||||
if ($action === 'create') {
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 64);
|
||||
$schoolYear = mb_substr(trim($body['schoolYear'] ?? ''), 0, 10);
|
||||
if (!$name) Response::error('Klassenname erforderlich');
|
||||
|
||||
// Join-Code generieren (6 Zeichen, eindeutig)
|
||||
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // ohne I,O,0,1 (Verwechslungsgefahr)
|
||||
do {
|
||||
$code = '';
|
||||
for ($i = 0; $i < 6; $i++) $code .= $chars[random_int(0, strlen($chars) - 1)];
|
||||
$exists = $db->fetchOne('SELECT id FROM classes WHERE join_code = ?', [$code]);
|
||||
} while ($exists);
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO classes (teacher_id, name, school_year, join_code) VALUES (?, ?, ?, ?)',
|
||||
[$teacherId, $name, $schoolYear, $code]
|
||||
);
|
||||
Response::ok(['id' => (int)$db->lastInsertId(), 'joinCode' => $code]);
|
||||
}
|
||||
|
||||
// === KLASSE BEARBEITEN ===
|
||||
if ($action === 'update') {
|
||||
$classId = (int)($body['classId'] ?? 0);
|
||||
$name = mb_substr(trim($body['name'] ?? ''), 0, 64);
|
||||
$schoolYear = mb_substr(trim($body['schoolYear'] ?? ''), 0, 10);
|
||||
if (!$classId || !$name) Response::error('classId und name erforderlich');
|
||||
|
||||
// Sicherstellen dass Klasse dem Lehrer gehoert
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$db->execute('UPDATE classes SET name = ?, school_year = ? WHERE id = ?', [$name, $schoolYear, $classId]);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// === KLASSE LOESCHEN ===
|
||||
if ($action === 'delete') {
|
||||
$classId = (int)($body['classId'] ?? 0);
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$db->execute('DELETE FROM classes WHERE id = ?', [$classId]);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Modulfreigabe (Lehrer steuert welche Module Schueler sehen/spielen)
|
||||
* GET /api/modules?class_id=X → Modulstatus fuer Klasse
|
||||
* POST /api/modules {action} → Modul freigeben/sperren/starten
|
||||
*/
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
// Alle verfuegbaren Module
|
||||
$ALL_MODULES = [
|
||||
['id' => 'sim-05', 'name' => 'Klimawächter', 'desc' => 'Treibhauseffekt-Simulation'],
|
||||
['id' => 'sim-05-3d', 'name' => 'Klimawächter 3D', 'desc' => '3D-Version der Klimasimulation'],
|
||||
['id' => 'sim-07', 'name' => 'Erdbeben', 'desc' => 'Plattentektonik und Stadtplanung'],
|
||||
['id' => 'sim-09', 'name' => 'Energiemix', 'desc' => 'Energiewende planen'],
|
||||
['id' => 'sim-10', 'name' => 'Lieferketten', 'desc' => 'Globale Lieferketten verstehen'],
|
||||
['id' => 'sim-11', 'name' => 'Regenwald', 'desc' => 'Regenwald-Exploration'],
|
||||
['id' => 'sim-12', 'name' => 'Flussmanagement', 'desc' => 'Fluss und Hochwasserschutz'],
|
||||
['id' => 'sim-13', 'name' => 'Stadt & Raumplanung', 'desc' => 'Stadtsimulation mit Kacheln'],
|
||||
];
|
||||
|
||||
if ($method === 'GET') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$classId = (int)($_GET['class_id'] ?? 0);
|
||||
if (!$classId) Response::error('class_id erforderlich');
|
||||
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
// Aktuelle Freigaben laden
|
||||
$settings = $db->fetchAll('SELECT module_id, mode, started_at, due_date FROM class_modules WHERE class_id = ?', [$classId]);
|
||||
$settingsMap = [];
|
||||
foreach ($settings as $s) $settingsMap[$s['module_id']] = $s;
|
||||
|
||||
// Mit allen Modulen zusammenfuehren
|
||||
$result = [];
|
||||
foreach ($ALL_MODULES as $mod) {
|
||||
$s = $settingsMap[$mod['id']] ?? null;
|
||||
$result[] = [
|
||||
'id' => $mod['id'],
|
||||
'name' => $mod['name'],
|
||||
'desc' => $mod['desc'],
|
||||
'mode' => $s['mode'] ?? 'locked',
|
||||
'startedAt' => $s['started_at'] ?? null,
|
||||
'dueDate' => $s['due_date'] ?? null,
|
||||
];
|
||||
}
|
||||
Response::ok($result);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
$classId = (int)($body['classId'] ?? 0);
|
||||
$moduleId = $body['moduleId'] ?? '';
|
||||
|
||||
if (!$classId || !$moduleId) Response::error('classId und moduleId erforderlich');
|
||||
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
if ($action === 'set_mode') {
|
||||
$mode = $body['mode'] ?? 'locked';
|
||||
if (!in_array($mode, ['locked', 'free', 'teacher_started'])) Response::error('Ungültiger Modus');
|
||||
|
||||
$dueDate = $body['dueDate'] ?? null;
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO class_modules (class_id, module_id, enabled, mode, started_at, due_date)
|
||||
VALUES (?, ?, ?, ?, NOW(), ?)
|
||||
ON DUPLICATE KEY UPDATE mode = VALUES(mode), started_at = VALUES(started_at), due_date = VALUES(due_date), enabled = 1',
|
||||
[$classId, $moduleId, 1, $mode, $dueDate]
|
||||
);
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO activity_log (user_type, user_id, action, detail) VALUES ('teacher', ?, 'set_module_mode', ?)",
|
||||
[$teacherId, "$moduleId → $mode"]
|
||||
);
|
||||
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Schueler-Verwaltung (nur fuer Lehrer)
|
||||
* GET /api/students?class_id=X → Alle Schueler einer Klasse
|
||||
* POST /api/students {action} → Schueler erstellen/bearbeiten/loeschen
|
||||
*/
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$classId = (int)($_GET['class_id'] ?? 0);
|
||||
if (!$classId) Response::error('class_id erforderlich');
|
||||
|
||||
// Sicherstellen dass Klasse dem Lehrer gehoert
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$students = $db->fetchAll(
|
||||
'SELECT id, username, display_name, is_anonymous, created_at, last_login FROM students WHERE class_id = ? ORDER BY username',
|
||||
[$classId]
|
||||
);
|
||||
|
||||
// Letzte Ergebnisse pro Schueler
|
||||
foreach ($students as &$s) {
|
||||
$s['results'] = $db->fetchAll(
|
||||
'SELECT module_id, score, balance_idx, completed_at FROM student_results WHERE student_id = ? ORDER BY completed_at DESC LIMIT 10',
|
||||
[$s['id']]
|
||||
);
|
||||
$s['total_score'] = $db->fetchOne(
|
||||
'SELECT AVG(score) as avg_score, COUNT(*) as count FROM student_results WHERE student_id = ?',
|
||||
[$s['id']]
|
||||
);
|
||||
}
|
||||
|
||||
Response::ok($students);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
|
||||
// === SCHUELER ERSTELLEN ===
|
||||
if ($action === 'create') {
|
||||
$classId = (int)($body['classId'] ?? 0);
|
||||
$username = mb_substr(trim($body['username'] ?? ''), 0, 64);
|
||||
$password = $body['password'] ?? '';
|
||||
$displayName = mb_substr(trim($body['displayName'] ?? ''), 0, 128);
|
||||
$isAnonymous = (bool)($body['isAnonymous'] ?? true);
|
||||
|
||||
if (!$classId || !$username || !$password) Response::error('classId, username und password erforderlich');
|
||||
if (strlen($password) < 4) Response::error('Passwort muss mindestens 4 Zeichen lang sein');
|
||||
|
||||
// Klasse pruefen
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
// Username eindeutig in Klasse?
|
||||
$existing = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND username = ?', [$classId, $username]);
|
||||
if ($existing) Response::error('Benutzername in dieser Klasse bereits vergeben');
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$db->execute(
|
||||
'INSERT INTO students (class_id, username, password, display_name, is_anonymous) VALUES (?, ?, ?, ?, ?)',
|
||||
[$classId, $username, $hash, $displayName ?: null, $isAnonymous ? 1 : 0]
|
||||
);
|
||||
Response::ok(['id' => (int)$db->lastInsertId()]);
|
||||
}
|
||||
|
||||
// === MEHRERE SCHUELER AUF EINMAL ERSTELLEN ===
|
||||
if ($action === 'create_batch') {
|
||||
$classId = (int)($body['classId'] ?? 0);
|
||||
$count = min(50, max(1, (int)($body['count'] ?? 10)));
|
||||
$prefix = mb_substr(trim($body['prefix'] ?? 'schueler'), 0, 20);
|
||||
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$created = [];
|
||||
for ($i = 1; $i <= $count; $i++) {
|
||||
$username = $prefix . str_pad($i, 2, '0', STR_PAD_LEFT);
|
||||
// Einfaches Passwort generieren (4 Ziffern)
|
||||
$pw = str_pad(random_int(1000, 9999), 4, '0', STR_PAD_LEFT);
|
||||
$hash = password_hash($pw, PASSWORD_DEFAULT);
|
||||
|
||||
$existing = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND username = ?', [$classId, $username]);
|
||||
if ($existing) continue; // Ueberspringen wenn schon vorhanden
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO students (class_id, username, password, display_name, is_anonymous) VALUES (?, ?, ?, ?, 1)',
|
||||
[$classId, $username, $hash, $username]
|
||||
);
|
||||
$created[] = ['username' => $username, 'password' => $pw, 'id' => (int)$db->lastInsertId()];
|
||||
}
|
||||
Response::ok(['created' => $created]);
|
||||
}
|
||||
|
||||
// === SCHUELER BEARBEITEN ===
|
||||
if ($action === 'update') {
|
||||
$studentId = (int)($body['studentId'] ?? 0);
|
||||
$displayName = mb_substr(trim($body['displayName'] ?? ''), 0, 128);
|
||||
$newPassword = $body['newPassword'] ?? '';
|
||||
|
||||
// Schueler + Klasse pruefen
|
||||
$student = $db->fetchOne(
|
||||
'SELECT s.id, s.class_id FROM students s JOIN classes c ON c.id = s.class_id WHERE s.id = ? AND c.teacher_id = ?',
|
||||
[$studentId, $teacherId]
|
||||
);
|
||||
if (!$student) Response::error('Schüler nicht gefunden', 404);
|
||||
|
||||
if ($displayName) {
|
||||
$db->execute('UPDATE students SET display_name = ? WHERE id = ?', [$displayName, $studentId]);
|
||||
}
|
||||
if ($newPassword && strlen($newPassword) >= 4) {
|
||||
$db->execute('UPDATE students SET password = ? WHERE id = ?', [password_hash($newPassword, PASSWORD_DEFAULT), $studentId]);
|
||||
}
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// === SCHUELER LOESCHEN ===
|
||||
if ($action === 'delete') {
|
||||
$studentId = (int)($body['studentId'] ?? 0);
|
||||
$student = $db->fetchOne(
|
||||
'SELECT s.id FROM students s JOIN classes c ON c.id = s.class_id WHERE s.id = ? AND c.teacher_id = ?',
|
||||
[$studentId, $teacherId]
|
||||
);
|
||||
if (!$student) Response::error('Schüler nicht gefunden', 404);
|
||||
|
||||
$db->execute('DELETE FROM students WHERE id = ?', [$studentId]);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
@@ -0,0 +1,72 @@
|
||||
-- GeoGraSim — Schema V2: Vollstaendiges User Management
|
||||
-- Fuehre NACH schema.sql aus: mysql -u root geograsim < schema-v2.sql
|
||||
|
||||
USE geograsim;
|
||||
|
||||
-- Lehrkraefte erweitern um E-Mail (ignoriere Fehler wenn Spalte schon existiert)
|
||||
ALTER TABLE teachers ADD COLUMN email VARCHAR(255) UNIQUE AFTER username;
|
||||
ALTER TABLE teachers ADD COLUMN email_verified BOOLEAN DEFAULT FALSE AFTER email;
|
||||
ALTER TABLE teachers ADD COLUMN school_name VARCHAR(255) AFTER display_name;
|
||||
ALTER TABLE teachers ADD COLUMN reset_token VARCHAR(64) AFTER email_verified;
|
||||
ALTER TABLE teachers ADD COLUMN reset_expires TIMESTAMP NULL AFTER reset_token;
|
||||
|
||||
-- Schueler als echte Accounts (nicht nur Sessions)
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
class_id INT UNSIGNED NOT NULL,
|
||||
username VARCHAR(64) NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(128),
|
||||
is_anonymous BOOLEAN DEFAULT TRUE, -- true = Pseudonym, false = echter Name
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP NULL,
|
||||
UNIQUE KEY uq_class_user (class_id, username),
|
||||
FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Lizenzen
|
||||
CREATE TABLE IF NOT EXISTS licenses (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(32) NOT NULL UNIQUE,
|
||||
teacher_id INT UNSIGNED, -- NULL = noch nicht eingeloest
|
||||
max_students INT UNSIGNED DEFAULT 30,
|
||||
valid_from DATE NOT NULL,
|
||||
valid_until DATE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
redeemed_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Modulfreigabe erweitern
|
||||
ALTER TABLE class_modules ADD COLUMN mode ENUM('locked','free','teacher_started') DEFAULT 'locked' AFTER enabled;
|
||||
ALTER TABLE class_modules ADD COLUMN started_at TIMESTAMP NULL AFTER mode;
|
||||
ALTER TABLE class_modules ADD COLUMN due_date TIMESTAMP NULL AFTER started_at;
|
||||
|
||||
-- Schueler-Ergebnisse (verknuepft mit Schueler-Account, nicht nur Session)
|
||||
CREATE TABLE IF NOT EXISTS student_results (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
student_id INT UNSIGNED NOT NULL,
|
||||
class_id INT UNSIGNED NOT NULL,
|
||||
module_id VARCHAR(16) NOT NULL,
|
||||
score DECIMAL(5,2), -- 0-100
|
||||
balance_idx DECIMAL(5,2), -- 0-100
|
||||
duration_sec INT UNSIGNED,
|
||||
detail_json JSON, -- Modulspezifische Details
|
||||
completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_student (student_id),
|
||||
INDEX idx_class_module (class_id, module_id),
|
||||
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- Aktivitaetslog (wer hat wann was gemacht)
|
||||
CREATE TABLE IF NOT EXISTS activity_log (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_type ENUM('teacher','student') NOT NULL,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
action VARCHAR(64) NOT NULL, -- 'login','start_module','complete_module','view_results'
|
||||
detail VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_type, user_id),
|
||||
INDEX idx_time (created_at)
|
||||
) ENGINE=InnoDB;
|
||||
@@ -0,0 +1,268 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stadt-Editor</title>
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:sans-serif; background:#ddd; display:flex; height:100vh; }
|
||||
#sidebar { width:140px; background:#fff; padding:8px; overflow-y:auto; }
|
||||
#sidebar h3 { font-size:10px; color:#888; margin-bottom:6px; }
|
||||
.btn { display:block; width:100%; padding:4px; margin-bottom:4px; border:2px solid #ccc; border-radius:4px; background:#fff; cursor:pointer; font-size:10px; text-align:center; }
|
||||
.btn.active { border-color:#4a7c8a; background:#e0f0f4; }
|
||||
#main { flex:1; display:flex; align-items:center; justify-content:center; overflow:hidden; }
|
||||
#main canvas { max-width:100%; max-height:100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sidebar">
|
||||
<h3>Kacheln</h3>
|
||||
<button class="btn" data-t="0">Gerade</button>
|
||||
<button class="btn" data-t="1">Kurve</button>
|
||||
<button class="btn" data-t="2">T-Kreuzung</button>
|
||||
<button class="btn" data-t="3">Kreuzung</button>
|
||||
<button class="btn" data-t="4">Sackgasse</button>
|
||||
<br>
|
||||
<button class="btn" id="btnClear">🗑 Leeren</button>
|
||||
</div>
|
||||
<div id="main">
|
||||
<canvas id="cv"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// === CONFIG ===
|
||||
var COLS = 8, ROWS = 8, TS = 72
|
||||
var GREEN = '#b8ccaa', ROAD = '#2e2e2e', WHITE = '#fff'
|
||||
|
||||
// Tile-Typen: conn = [N,E,S,W]
|
||||
var TYPES = [
|
||||
[1,0,1,0], // Gerade
|
||||
[1,1,0,0], // Kurve
|
||||
[0,1,1,1], // T-Kreuzung
|
||||
[1,1,1,1], // Kreuzung
|
||||
[1,0,0,0], // Sackgasse
|
||||
]
|
||||
|
||||
// Grid: -1 = leer, sonst {t: type-index, r: rotation 0-3}
|
||||
var grid = []
|
||||
for (var i = 0; i < ROWS; i++) {
|
||||
grid[i] = []
|
||||
for (var j = 0; j < COLS; j++) grid[i][j] = null
|
||||
}
|
||||
|
||||
var sel = -1 // selected type
|
||||
|
||||
// === SIDEBAR ===
|
||||
var btns = document.querySelectorAll('.btn[data-t]')
|
||||
btns.forEach(function(b) {
|
||||
b.addEventListener('click', function() {
|
||||
var idx = parseInt(this.dataset.t)
|
||||
sel = (sel === idx) ? -1 : idx
|
||||
btns.forEach(function(x) { x.className = 'btn' })
|
||||
if (sel >= 0) this.className = 'btn active'
|
||||
})
|
||||
})
|
||||
document.getElementById('btnClear').addEventListener('click', function() {
|
||||
for (var i = 0; i < ROWS; i++) for (var j = 0; j < COLS; j++) grid[i][j] = null
|
||||
})
|
||||
|
||||
// === CANVAS ===
|
||||
var cv = document.getElementById('cv')
|
||||
var ctx = cv.getContext('2d')
|
||||
cv.width = COLS * TS
|
||||
cv.height = ROWS * TS
|
||||
|
||||
// Rotate connections: [N,E,S,W] clockwise by n
|
||||
function rotConn(conn, n) {
|
||||
var c = conn.slice()
|
||||
for (var i = 0; i < n; i++) c = [c[3], c[0], c[1], c[2]]
|
||||
return c
|
||||
}
|
||||
|
||||
// === DRAW ONE TILE at pixel position (px, py) ===
|
||||
function drawTile(px, py, conn, alpha) {
|
||||
var S = TS, RW = S * 0.4, M = S / 2, HR = RW / 2
|
||||
var lw = 2
|
||||
var N = conn[0], E = conn[1], So = conn[2], W = conn[3]
|
||||
var sum = N + E + So + W
|
||||
|
||||
if (alpha < 1) ctx.globalAlpha = alpha
|
||||
|
||||
// Green base
|
||||
ctx.fillStyle = GREEN
|
||||
ctx.fillRect(px, py, S, S)
|
||||
|
||||
// --- SACKGASSE ---
|
||||
if (sum === 1) {
|
||||
ctx.fillStyle = ROAD
|
||||
var stubY = 4
|
||||
if (N) { ctx.fillRect(px+M-HR, py, RW, stubY); ctx.beginPath(); ctx.arc(px+M, py+stubY, HR, 0, Math.PI); ctx.fill() }
|
||||
if (So) { ctx.fillRect(px+M-HR, py+S-stubY, RW, stubY); ctx.beginPath(); ctx.arc(px+M, py+S-stubY, HR, Math.PI, 0); ctx.fill() }
|
||||
if (E) { ctx.fillRect(px+S-stubY, py+M-HR, stubY, RW); ctx.beginPath(); ctx.arc(px+S-stubY, py+M, HR, Math.PI*1.5, Math.PI*0.5); ctx.fill() }
|
||||
if (W) { ctx.fillRect(px, py+M-HR, stubY, RW); ctx.beginPath(); ctx.arc(px+stubY, py+M, HR, Math.PI*0.5, Math.PI*1.5); ctx.fill() }
|
||||
// Rand
|
||||
ctx.strokeStyle = WHITE; ctx.lineWidth = lw
|
||||
if (N) { ctx.beginPath(); ctx.moveTo(px+M-HR,py); ctx.lineTo(px+M-HR,py+stubY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(px+M+HR,py); ctx.lineTo(px+M+HR,py+stubY); ctx.stroke(); ctx.beginPath(); ctx.arc(px+M,py+stubY,HR,0,Math.PI); ctx.stroke() }
|
||||
if (So) { ctx.beginPath(); ctx.moveTo(px+M-HR,py+S); ctx.lineTo(px+M-HR,py+S-stubY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(px+M+HR,py+S); ctx.lineTo(px+M+HR,py+S-stubY); ctx.stroke(); ctx.beginPath(); ctx.arc(px+M,py+S-stubY,HR,Math.PI,0); ctx.stroke() }
|
||||
if (E) { ctx.beginPath(); ctx.moveTo(px+S,py+M-HR); ctx.lineTo(px+S-stubY,py+M-HR); ctx.stroke(); ctx.beginPath(); ctx.moveTo(px+S,py+M+HR); ctx.lineTo(px+S-stubY,py+M+HR); ctx.stroke(); ctx.beginPath(); ctx.arc(px+S-stubY,py+M,HR,Math.PI*1.5,Math.PI*0.5); ctx.stroke() }
|
||||
if (W) { ctx.beginPath(); ctx.moveTo(px,py+M-HR); ctx.lineTo(px+stubY,py+M-HR); ctx.stroke(); ctx.beginPath(); ctx.moveTo(px,py+M+HR); ctx.lineTo(px+stubY,py+M+HR); ctx.stroke(); ctx.beginPath(); ctx.arc(px+stubY,py+M,HR,Math.PI*0.5,Math.PI*1.5); ctx.stroke() }
|
||||
ctx.globalAlpha = 1; return
|
||||
}
|
||||
|
||||
// --- KURVE ---
|
||||
var isCurve = sum === 2 && ((N&&E)||(E&&So)||(So&&W)||(W&&N))
|
||||
if (isCurve) {
|
||||
var cx, cy, startA, endA
|
||||
if (N&&E) { cx=px+M+HR; cy=py+M-HR; startA=Math.PI; endA=Math.PI*0.5 }
|
||||
if (E&&So) { cx=px+M+HR; cy=py+M+HR; startA=Math.PI*1.5; endA=Math.PI }
|
||||
if (So&&W) { cx=px+M-HR; cy=py+M+HR; startA=0; endA=Math.PI*1.5 }
|
||||
if (W&&N) { cx=px+M-HR; cy=py+M-HR; startA=Math.PI*0.5; endA=0 }
|
||||
// Asphalt gerade Stuecke
|
||||
ctx.fillStyle = ROAD
|
||||
if (N) ctx.fillRect(px+M-HR, py, RW, M-HR)
|
||||
if (So) ctx.fillRect(px+M-HR, py+M+HR, RW, M-HR)
|
||||
if (E) ctx.fillRect(px+M+HR, py+M-HR, M-HR, RW)
|
||||
if (W) ctx.fillRect(px, py+M-HR, M-HR, RW)
|
||||
// Asphalt Viertelkreis
|
||||
ctx.beginPath(); ctx.moveTo(cx,cy); ctx.arc(cx,cy,RW,startA,endA,true); ctx.closePath(); ctx.fill()
|
||||
// Raender
|
||||
ctx.strokeStyle = WHITE; ctx.lineWidth = lw
|
||||
ctx.beginPath(); ctx.arc(cx,cy,RW,startA,endA,true); ctx.stroke() // Aussenbogen
|
||||
// Gerade Raender beider Arme (innen + aussen)
|
||||
if(N){ctx.beginPath();ctx.moveTo(px+M-HR,py);ctx.lineTo(px+M-HR,cy);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,py);ctx.lineTo(px+M+HR,cy);ctx.stroke()}
|
||||
if(So){ctx.beginPath();ctx.moveTo(px+M-HR,cy);ctx.lineTo(px+M-HR,py+S);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,cy);ctx.lineTo(px+M+HR,py+S);ctx.stroke()}
|
||||
if(E){ctx.beginPath();ctx.moveTo(cx,py+M-HR);ctx.lineTo(px+S,py+M-HR);ctx.stroke();ctx.beginPath();ctx.moveTo(cx,py+M+HR);ctx.lineTo(px+S,py+M+HR);ctx.stroke()}
|
||||
if(W){ctx.beginPath();ctx.moveTo(px,py+M-HR);ctx.lineTo(cx,py+M-HR);ctx.stroke();ctx.beginPath();ctx.moveTo(px,py+M+HR);ctx.lineTo(cx,py+M+HR);ctx.stroke()}
|
||||
// Mittellinie (ein Pfad)
|
||||
ctx.setLineDash([TS*0.07, TS*0.05]); ctx.lineDashOffset = -TS*0.035
|
||||
ctx.beginPath()
|
||||
if(N&&E){ctx.moveTo(px+M,py);ctx.lineTo(px+M,cy);ctx.arc(cx,cy,HR,Math.PI,Math.PI*0.5,true);ctx.lineTo(px+S,py+M)}
|
||||
if(E&&So){ctx.moveTo(px+S,py+M);ctx.lineTo(cx,py+M);ctx.arc(cx,cy,HR,Math.PI*1.5,Math.PI,true);ctx.lineTo(px+M,py+S)}
|
||||
if(So&&W){ctx.moveTo(px+M,py+S);ctx.lineTo(px+M,cy);ctx.arc(cx,cy,HR,0,Math.PI*1.5,true);ctx.lineTo(px,py+M)}
|
||||
if(W&&N){ctx.moveTo(px,py+M);ctx.lineTo(cx,py+M);ctx.arc(cx,cy,HR,Math.PI*0.5,0,true);ctx.lineTo(px+M,py)}
|
||||
ctx.stroke(); ctx.setLineDash([]); ctx.lineDashOffset=0
|
||||
ctx.globalAlpha = 1; return
|
||||
}
|
||||
|
||||
// --- GERADE / T / KREUZUNG ---
|
||||
ctx.fillStyle = ROAD
|
||||
if (N) ctx.fillRect(px+M-HR, py, RW, M+HR)
|
||||
if (So) ctx.fillRect(px+M-HR, py+M-HR, RW, M+HR)
|
||||
if (E) ctx.fillRect(px+M-HR, py+M-HR, M+HR, RW)
|
||||
if (W) ctx.fillRect(px, py+M-HR, M+HR, RW)
|
||||
|
||||
// Raender
|
||||
ctx.strokeStyle = WHITE; ctx.lineWidth = lw
|
||||
if(N||So){ctx.beginPath();ctx.moveTo(px+M-HR,N?py:py+M-HR);ctx.lineTo(px+M-HR,So?py+S:py+M+HR);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,N?py:py+M-HR);ctx.lineTo(px+M+HR,So?py+S:py+M+HR);ctx.stroke()}
|
||||
if(E||W){ctx.beginPath();ctx.moveTo(W?px:px+M-HR,py+M-HR);ctx.lineTo(E?px+S:px+M+HR,py+M-HR);ctx.stroke();ctx.beginPath();ctx.moveTo(W?px:px+M-HR,py+M+HR);ctx.lineTo(E?px+S:px+M+HR,py+M+HR);ctx.stroke()}
|
||||
// Raender im Kreuzungsbereich entfernen
|
||||
if((N||So)&&(E||W)){ctx.fillStyle=ROAD;ctx.fillRect(px+M-HR+1,py+M-HR-1,RW-2,lw+2);ctx.fillRect(px+M-HR+1,py+M+HR-lw,RW-2,lw+2);ctx.fillRect(px+M-HR-1,py+M-HR+1,lw+2,RW-2);ctx.fillRect(px+M+HR-lw,py+M-HR+1,lw+2,RW-2)}
|
||||
|
||||
// Mittellinien
|
||||
ctx.strokeStyle=WHITE;ctx.lineWidth=lw;ctx.setLineDash([TS*0.07,TS*0.05]);ctx.lineDashOffset=-TS*0.035
|
||||
if(sum===2){// Gerade
|
||||
if(N||So){ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+S);ctx.stroke()}
|
||||
if(E||W){ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+S,py+M);ctx.stroke()}
|
||||
} else if(sum===3){// T: Querbalken durchgehend, Arm mit Stopplinie
|
||||
if(!N){ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+S,py+M);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M,py+M+HR);ctx.lineTo(px+M,py+S);ctx.stroke()}
|
||||
if(!So){ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+S,py+M);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+M-HR);ctx.stroke()}
|
||||
if(!W){ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+S);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,py+M);ctx.lineTo(px+S,py+M);ctx.stroke()}
|
||||
if(!E){ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+S);ctx.stroke();ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+M-HR,py+M);ctx.stroke()}
|
||||
ctx.setLineDash([]);ctx.lineWidth=lw*2;var sb=6
|
||||
if(!N){ctx.beginPath();ctx.moveTo(px+M-HR+3,py+M+HR+sb);ctx.lineTo(px+M,py+M+HR+sb);ctx.stroke()}
|
||||
if(!So){ctx.beginPath();ctx.moveTo(px+M,py+M-HR-sb);ctx.lineTo(px+M+HR-3,py+M-HR-sb);ctx.stroke()}
|
||||
if(!W){ctx.beginPath();ctx.moveTo(px+M+HR+sb,py+M);ctx.lineTo(px+M+HR+sb,py+M+HR-3);ctx.stroke()}
|
||||
if(!E){ctx.beginPath();ctx.moveTo(px+M-HR-sb,py+M-HR+3);ctx.lineTo(px+M-HR-sb,py+M);ctx.stroke()}
|
||||
ctx.lineWidth=lw
|
||||
} else if(sum===4){// Kreuzung: N-S durch, E-W Stopp
|
||||
ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+S);ctx.stroke()
|
||||
ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+M-HR,py+M);ctx.stroke()
|
||||
ctx.beginPath();ctx.moveTo(px+M+HR,py+M);ctx.lineTo(px+S,py+M);ctx.stroke()
|
||||
ctx.setLineDash([]);ctx.lineWidth=lw*2;var sb=6
|
||||
ctx.beginPath();ctx.moveTo(px+M-HR+sb,py+M);ctx.lineTo(px+M-HR+sb,py+M+HR-3);ctx.stroke()
|
||||
ctx.beginPath();ctx.moveTo(px+M+HR-sb,py+M-HR+3);ctx.lineTo(px+M+HR-sb,py+M);ctx.stroke()
|
||||
ctx.lineWidth=lw
|
||||
}
|
||||
ctx.setLineDash([]);ctx.lineDashOffset=0
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
|
||||
// === RENDER ===
|
||||
var hoverR = -1, hoverC = -1
|
||||
|
||||
function render() {
|
||||
ctx.fillStyle = '#c8d0b8'
|
||||
ctx.fillRect(0, 0, cv.width, cv.height)
|
||||
|
||||
for (var r = 0; r < ROWS; r++) {
|
||||
for (var c = 0; c < COLS; c++) {
|
||||
var px = c * TS, py = r * TS
|
||||
var cell = grid[r][c]
|
||||
|
||||
if (cell) {
|
||||
var conn = rotConn(TYPES[cell.t], cell.r)
|
||||
drawTile(px, py, conn, 1)
|
||||
} else {
|
||||
ctx.fillStyle = GREEN
|
||||
ctx.fillRect(px, py, TS, TS)
|
||||
}
|
||||
|
||||
// Gitter
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.08)'
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.strokeRect(px, py, TS, TS)
|
||||
|
||||
// Hover
|
||||
if (r === hoverR && c === hoverC && sel >= 0 && !cell) {
|
||||
drawTile(px, py, TYPES[sel], 0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
// === CLICK ===
|
||||
cv.addEventListener('click', function(e) {
|
||||
var rect = cv.getBoundingClientRect()
|
||||
var mx = (e.clientX - rect.left) * (cv.width / rect.width)
|
||||
var my = (e.clientY - rect.top) * (cv.height / rect.height)
|
||||
var gc = Math.floor(mx / TS), gr = Math.floor(my / TS)
|
||||
if (gr < 0 || gr >= ROWS || gc < 0 || gc >= COLS) return
|
||||
|
||||
var cell = grid[gr][gc]
|
||||
|
||||
// Existierende Kachel: drehen
|
||||
if (cell) {
|
||||
var relX = mx - gc * TS, relY = my - gr * TS
|
||||
if (relY < TS * 0.35) {
|
||||
if (relX < TS * 0.4) { cell.r = (cell.r + 3) % 4; return }
|
||||
if (relX > TS * 0.6) { cell.r = (cell.r + 1) % 4; return }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Neue Kachel setzen
|
||||
if (sel >= 0) {
|
||||
grid[gr][gc] = { t: sel, r: 0 }
|
||||
}
|
||||
})
|
||||
|
||||
cv.addEventListener('mousemove', function(e) {
|
||||
var rect = cv.getBoundingClientRect()
|
||||
var mx = (e.clientX - rect.left) * (cv.width / rect.width)
|
||||
var my = (e.clientY - rect.top) * (cv.height / rect.height)
|
||||
hoverC = Math.floor(mx / TS); hoverR = Math.floor(my / TS)
|
||||
if (hoverR < 0 || hoverR >= ROWS || hoverC < 0 || hoverC >= COLS) { hoverR = -1; hoverC = -1 }
|
||||
})
|
||||
|
||||
cv.addEventListener('mouseleave', function() { hoverR = -1; hoverC = -1 })
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') { sel = -1; btns.forEach(function(x){x.className='btn'}) }
|
||||
})
|
||||
|
||||
render()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+472
-43
@@ -3,58 +3,487 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kachel-Test</title>
|
||||
<title>Kachel-Test — Gebäude</title>
|
||||
<style>
|
||||
body { margin:0; display:flex; align-items:center; justify-content:center; height:100vh; background:#e8e8e0; }
|
||||
canvas { background:#c8d4b0; }
|
||||
body { margin:0; display:flex; align-items:flex-start; justify-content:center; min-height:100vh; background:#e0e0d8; gap:10px; flex-wrap:wrap; font-family:Inter,sans-serif; padding:15px; }
|
||||
.tile { display:flex; flex-direction:column; align-items:center; gap:2px; }
|
||||
.tile span { font-size:9px; color:#666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="cv" width="400" height="400"></canvas>
|
||||
|
||||
<div class="tile"><canvas id="t01" width="200" height="200"></canvas><span>Kirche</span></div>
|
||||
<div class="tile"><canvas id="t02" width="200" height="200"></canvas><span>Moschee</span></div>
|
||||
<div class="tile"><canvas id="t03" width="200" height="200"></canvas><span>Wohngebiet</span></div>
|
||||
<div class="tile"><canvas id="t04" width="200" height="200"></canvas><span>Fabrik</span></div>
|
||||
<div class="tile"><canvas id="t05" width="200" height="200"></canvas><span>Spital</span></div>
|
||||
<div class="tile"><canvas id="t06" width="200" height="200"></canvas><span>Feuerwehr</span></div>
|
||||
<div class="tile"><canvas id="t07" width="200" height="200"></canvas><span>Polizei</span></div>
|
||||
<div class="tile"><canvas id="t08" width="200" height="200"></canvas><span>Schule</span></div>
|
||||
<div class="tile"><canvas id="t09" width="200" height="200"></canvas><span>Supermarkt</span></div>
|
||||
<div class="tile"><canvas id="t10" width="200" height="200"></canvas><span>Park</span></div>
|
||||
|
||||
<script>
|
||||
var cv = document.getElementById('cv')
|
||||
var c = cv.getContext('2d')
|
||||
var S = 300 // Kachelgroesse (Quadrat)
|
||||
var x0 = 50 // Kachel links oben
|
||||
var y0 = 50
|
||||
var RW = S / 3 // Strassenbreite = 1/3 der Kachel
|
||||
var SZ = 200
|
||||
var RW = SZ * 0.4, M = SZ / 2, HR = RW / 2
|
||||
var LW = 2
|
||||
var GREEN = '#b8ccaa', ROAD = '#2e2e2e', WHITE = '#fff'
|
||||
var DASH = [SZ*0.07, SZ*0.05]
|
||||
var DOFF = -SZ*0.035
|
||||
|
||||
// === GRUENE GRUNDFLAECHE ===
|
||||
c.fillStyle = '#7da84e'
|
||||
c.fillRect(x0, y0, S, S)
|
||||
c.strokeStyle = 'rgba(0,0,0,0.12)'
|
||||
c.lineWidth = 1
|
||||
c.strokeRect(x0, y0, S, S)
|
||||
// === Strassen-Grundfunktionen ===
|
||||
|
||||
// === STRASSE NACH SUEDEN (von Mitte nach unten) ===
|
||||
var roadX = x0 + S/2 - RW/2 // Strasse horizontal zentriert
|
||||
var roadY = y0 + S/2 - RW/2 // Startet in der Mitte
|
||||
c.fillStyle = '#3a3a3a'
|
||||
c.fillRect(roadX, roadY, RW, S/2 + RW/2) // Von Mitte bis Unterkante
|
||||
function base(id) {
|
||||
var c = document.getElementById(id).getContext('2d')
|
||||
c.fillStyle = GREEN; c.fillRect(0,0,SZ,SZ)
|
||||
return c
|
||||
}
|
||||
|
||||
// === MITTELLINIE (gestrichelt) ===
|
||||
c.strokeStyle = 'rgba(255,255,180,0.7)'
|
||||
c.lineWidth = 3
|
||||
c.setLineDash([16, 12])
|
||||
c.beginPath()
|
||||
c.moveTo(x0 + S/2, y0 + S/2)
|
||||
c.lineTo(x0 + S/2, y0 + S)
|
||||
c.stroke()
|
||||
c.setLineDash([])
|
||||
function drawRoadNS(c, x) {
|
||||
// Vertikale Strasse bei x-Mitte = x
|
||||
c.fillStyle = ROAD; c.fillRect(x-HR, 0, RW, SZ)
|
||||
c.strokeStyle = WHITE; c.lineWidth = LW
|
||||
c.beginPath(); c.moveTo(x-HR,0); c.lineTo(x-HR,SZ); c.stroke()
|
||||
c.beginPath(); c.moveTo(x+HR,0); c.lineTo(x+HR,SZ); c.stroke()
|
||||
c.setLineDash(DASH); c.lineDashOffset = DOFF
|
||||
c.beginPath(); c.moveTo(x,0); c.lineTo(x,SZ); c.stroke()
|
||||
c.setLineDash([]); c.lineDashOffset = 0
|
||||
}
|
||||
|
||||
// === RANDLINIEN (solid, weiss) ===
|
||||
c.strokeStyle = 'rgba(255,255,255,0.5)'
|
||||
c.lineWidth = 2
|
||||
// Links
|
||||
c.beginPath()
|
||||
c.moveTo(roadX, roadY)
|
||||
c.lineTo(roadX, y0 + S)
|
||||
c.stroke()
|
||||
// Rechts
|
||||
c.beginPath()
|
||||
c.moveTo(roadX + RW, roadY)
|
||||
c.lineTo(roadX + RW, y0 + S)
|
||||
c.stroke()
|
||||
function drawRoadEW(c, y) {
|
||||
c.fillStyle = ROAD; c.fillRect(0, y-HR, SZ, RW)
|
||||
c.strokeStyle = WHITE; c.lineWidth = LW
|
||||
c.beginPath(); c.moveTo(0,y-HR); c.lineTo(SZ,y-HR); c.stroke()
|
||||
c.beginPath(); c.moveTo(0,y+HR); c.lineTo(SZ,y+HR); c.stroke()
|
||||
c.setLineDash(DASH); c.lineDashOffset = DOFF
|
||||
c.beginPath(); c.moveTo(0,y); c.lineTo(SZ,y); c.stroke()
|
||||
c.setLineDash([]); c.lineDashOffset = 0
|
||||
}
|
||||
|
||||
function drawStubN(c) {
|
||||
// Sackgasse von oben — kurzer Stummel
|
||||
var stubY = 4
|
||||
c.fillStyle = ROAD; c.fillRect(M-HR, 0, RW, stubY)
|
||||
c.beginPath(); c.arc(M, stubY, HR, 0, Math.PI); c.fill()
|
||||
c.strokeStyle = WHITE; c.lineWidth = LW
|
||||
c.beginPath(); c.moveTo(M-HR,0); c.lineTo(M-HR,stubY); c.stroke()
|
||||
c.beginPath(); c.moveTo(M+HR,0); c.lineTo(M+HR,stubY); c.stroke()
|
||||
c.beginPath(); c.arc(M, stubY, HR, 0, Math.PI); c.stroke()
|
||||
}
|
||||
|
||||
function drawStubS(c) {
|
||||
var stubY = SZ - 4
|
||||
c.fillStyle = ROAD; c.fillRect(M-HR, stubY, RW, 4)
|
||||
c.beginPath(); c.arc(M, stubY, HR, Math.PI, 0); c.fill()
|
||||
c.strokeStyle = WHITE; c.lineWidth = LW
|
||||
c.beginPath(); c.moveTo(M-HR,SZ); c.lineTo(M-HR,stubY); c.stroke()
|
||||
c.beginPath(); c.moveTo(M+HR,SZ); c.lineTo(M+HR,stubY); c.stroke()
|
||||
c.beginPath(); c.arc(M, stubY, HR, Math.PI, 0); c.stroke()
|
||||
}
|
||||
|
||||
function drawStubE(c) {
|
||||
var stubX = SZ - 4
|
||||
c.fillStyle = ROAD; c.fillRect(stubX, M-HR, 4, RW)
|
||||
c.beginPath(); c.arc(stubX, M, HR, Math.PI*1.5, Math.PI*0.5); c.fill()
|
||||
c.strokeStyle = WHITE; c.lineWidth = LW
|
||||
c.beginPath(); c.moveTo(SZ,M-HR); c.lineTo(stubX,M-HR); c.stroke()
|
||||
c.beginPath(); c.moveTo(SZ,M+HR); c.lineTo(stubX,M+HR); c.stroke()
|
||||
c.beginPath(); c.arc(stubX, M, HR, Math.PI*1.5, Math.PI*0.5); c.stroke()
|
||||
}
|
||||
|
||||
function drawStubW(c) {
|
||||
var stubX = 4
|
||||
c.fillStyle = ROAD; c.fillRect(0, M-HR, 4, RW)
|
||||
c.beginPath(); c.arc(stubX, M, HR, Math.PI*0.5, Math.PI*1.5); c.fill()
|
||||
c.strokeStyle = WHITE; c.lineWidth = LW
|
||||
c.beginPath(); c.moveTo(0,M-HR); c.lineTo(stubX,M-HR); c.stroke()
|
||||
c.beginPath(); c.moveTo(0,M+HR); c.lineTo(stubX,M+HR); c.stroke()
|
||||
c.beginPath(); c.arc(stubX, M, HR, Math.PI*0.5, Math.PI*1.5); c.stroke()
|
||||
}
|
||||
|
||||
// === Gebaeude-Hilfszeichnung ===
|
||||
|
||||
function drawBuilding(c, x, y, w, h, color, label) {
|
||||
// Gebaeude als Rechteck mit Farbe + Label
|
||||
c.fillStyle = color
|
||||
c.fillRect(x, y, w, h)
|
||||
c.strokeStyle = 'rgba(0,0,0,0.15)'; c.lineWidth = 1
|
||||
c.strokeRect(x, y, w, h)
|
||||
// Dach-Effekt (dunklere Oberkante)
|
||||
c.fillStyle = 'rgba(0,0,0,0.08)'
|
||||
c.fillRect(x, y, w, 4)
|
||||
// Label
|
||||
if (label) {
|
||||
c.fillStyle = 'rgba(255,255,255,0.9)'
|
||||
c.font = 'bold 10px Inter, sans-serif'
|
||||
c.textAlign = 'center'; c.textBaseline = 'middle'
|
||||
c.fillText(label, x + w/2, y + h/2)
|
||||
}
|
||||
}
|
||||
|
||||
function drawHouse(c, x, y, w, h) {
|
||||
// Kleines Haus mit Dach
|
||||
c.fillStyle = '#d4a574'
|
||||
c.fillRect(x, y + h*0.35, w, h*0.65)
|
||||
// Dach
|
||||
c.fillStyle = '#8B4513'
|
||||
c.beginPath()
|
||||
c.moveTo(x - 2, y + h*0.35)
|
||||
c.lineTo(x + w/2, y)
|
||||
c.lineTo(x + w + 2, y + h*0.35)
|
||||
c.closePath()
|
||||
c.fill()
|
||||
// Fenster
|
||||
c.fillStyle = '#a8d0e0'
|
||||
c.fillRect(x + w*0.2, y + h*0.5, w*0.25, h*0.2)
|
||||
// Tuer
|
||||
c.fillStyle = '#6a4a3a'
|
||||
c.fillRect(x + w*0.55, y + h*0.55, w*0.2, h*0.4)
|
||||
}
|
||||
|
||||
function drawTree(c, x, y, sz) {
|
||||
c.fillStyle = '#5a4020'
|
||||
c.fillRect(x + sz*0.4, y + sz*0.5, sz*0.2, sz*0.5)
|
||||
c.fillStyle = '#3a8a2a'
|
||||
c.beginPath(); c.arc(x + sz*0.5, y + sz*0.4, sz*0.4, 0, Math.PI*2); c.fill()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 1. KIRCHE — Sackgasse S, grosses Gebaeude
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t01')
|
||||
drawStubS(c)
|
||||
// Grosses Kirchengebaeude
|
||||
drawBuilding(c, 30, 15, 140, 110, '#c8b090', '')
|
||||
// Turm
|
||||
c.fillStyle = '#a08060'
|
||||
c.fillRect(80, 5, 40, 60)
|
||||
// Kreuz oben
|
||||
c.strokeStyle = '#5a4a3a'; c.lineWidth = 3
|
||||
c.beginPath(); c.moveTo(100, 2); c.lineTo(100, 18); c.stroke()
|
||||
c.beginPath(); c.moveTo(92, 10); c.lineTo(108, 10); c.stroke()
|
||||
// Tuer
|
||||
c.fillStyle = '#5a3a2a'
|
||||
c.beginPath(); c.arc(100, 125, 15, Math.PI, 0); c.fill()
|
||||
c.fillRect(85, 115, 30, 15)
|
||||
// Label
|
||||
c.fillStyle = '#5a4a3a'; c.font = 'bold 11px Inter,sans-serif'; c.textAlign = 'center'
|
||||
c.fillText('Kirche', 100, 85)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// 2. MOSCHEE — Sackgasse N + S (zwei Zufahrten)
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t02')
|
||||
drawStubN(c)
|
||||
drawStubS(c)
|
||||
// Gebaeude
|
||||
drawBuilding(c, 25, 35, 150, 90, '#d8c8a8', '')
|
||||
// Kuppel
|
||||
c.fillStyle = '#c0a878'
|
||||
c.beginPath(); c.arc(100, 50, 35, Math.PI, 0); c.fill()
|
||||
// Halbmond
|
||||
c.fillStyle = '#e8d8a0'; c.font = '18px serif'; c.textAlign = 'center'
|
||||
c.fillText('☪', 100, 42)
|
||||
// Minarett
|
||||
c.fillStyle = '#b8a878'
|
||||
c.fillRect(150, 25, 12, 70)
|
||||
c.fillRect(147, 22, 18, 6)
|
||||
// Label
|
||||
c.fillStyle = '#6a5a3a'; c.font = 'bold 10px Inter,sans-serif'
|
||||
c.fillText('Moschee', 100, 100)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// 3. WOHNGEBIET — Gerade N-S, Haeuser links und rechts
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t03')
|
||||
drawRoadNS(c, M)
|
||||
// Haeuser links
|
||||
drawHouse(c, 8, 20, 30, 30)
|
||||
drawHouse(c, 12, 70, 28, 28)
|
||||
drawHouse(c, 6, 120, 32, 30)
|
||||
// Haeuser rechts
|
||||
drawHouse(c, 160, 15, 30, 28)
|
||||
drawHouse(c, 162, 65, 28, 30)
|
||||
drawHouse(c, 158, 115, 32, 28)
|
||||
drawHouse(c, 164, 160, 26, 26)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// 4. FABRIK — Kurve (N→E), Fabrik im gruenen Bereich (unten-links)
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t04')
|
||||
// Kurve N→E
|
||||
var px = M+HR, py = M-HR
|
||||
c.fillStyle = ROAD
|
||||
c.fillRect(M-HR, 0, RW, py)
|
||||
c.fillRect(px, M-HR, SZ-px, RW)
|
||||
c.beginPath(); c.moveTo(px,py); c.arc(px,py,RW,Math.PI,Math.PI*0.5,true); c.closePath(); c.fill()
|
||||
// Raender
|
||||
c.strokeStyle=WHITE;c.lineWidth=LW
|
||||
c.beginPath();c.arc(px,py,RW,Math.PI,Math.PI*0.5,true);c.stroke()
|
||||
c.beginPath();c.moveTo(M+HR,0);c.lineTo(M+HR,py);c.stroke()
|
||||
c.beginPath();c.moveTo(px,M-HR);c.lineTo(SZ,M-HR);c.stroke()
|
||||
c.beginPath();c.moveTo(M-HR,0);c.lineTo(M-HR,py);c.stroke()
|
||||
c.beginPath();c.moveTo(px,M+HR);c.lineTo(SZ,M+HR);c.stroke()
|
||||
// Mittellinie
|
||||
c.setLineDash(DASH);c.lineDashOffset=DOFF
|
||||
c.beginPath();c.moveTo(M,0);c.lineTo(M,py);c.arc(px,py,HR,Math.PI,Math.PI*0.5,true);c.lineTo(SZ,M);c.stroke()
|
||||
c.setLineDash([]);c.lineDashOffset=0
|
||||
// Fabrik unten-links
|
||||
drawBuilding(c, 10, 130, 70, 55, '#8a8a8a', '')
|
||||
// Schornstein
|
||||
c.fillStyle = '#6a6a6a'
|
||||
c.fillRect(55, 110, 12, 20)
|
||||
c.fillRect(30, 115, 10, 15)
|
||||
// Rauch
|
||||
c.fillStyle = 'rgba(100,100,100,0.25)'
|
||||
c.beginPath(); c.arc(61, 105, 6, 0, Math.PI*2); c.fill()
|
||||
c.beginPath(); c.arc(58, 96, 5, 0, Math.PI*2); c.fill()
|
||||
// Label
|
||||
c.fillStyle = '#fff'; c.font = 'bold 10px Inter,sans-serif'; c.textAlign = 'center'
|
||||
c.fillText('Fabrik', 45, 160)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// 5. SPITAL — Sackgasse E + W (zwei Zufahrten seitlich)
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t05')
|
||||
drawStubE(c)
|
||||
drawStubW(c)
|
||||
// Grosses Gebaeude
|
||||
drawBuilding(c, 20, 15, 160, 80, '#e8e0d0', '')
|
||||
// Rotes Kreuz
|
||||
c.fillStyle = '#cc3333'
|
||||
c.fillRect(90, 30, 20, 50)
|
||||
c.fillRect(75, 42, 50, 20)
|
||||
// Label
|
||||
c.fillStyle = '#6a3a3a'; c.font = 'bold 11px Inter,sans-serif'; c.textAlign = 'center'
|
||||
c.fillText('Spital', 100, 120)
|
||||
// Rettungswagen-Parkplatz
|
||||
c.fillStyle = '#7a7a7a'; c.fillRect(30, 100, 50, 25)
|
||||
c.fillStyle = '#ddd'; c.font = '16px serif'
|
||||
c.fillText('🚑', 55, 117)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// 6. FEUERWEHR — Sackgasse S
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t06')
|
||||
drawStubS(c)
|
||||
// Gebaeude
|
||||
drawBuilding(c, 25, 10, 150, 100, '#cc4444', '')
|
||||
// Tore (2 Garagen)
|
||||
c.fillStyle = '#8a2a2a'
|
||||
c.fillRect(40, 70, 45, 40)
|
||||
c.fillRect(105, 70, 45, 40)
|
||||
// Tor-Linien
|
||||
c.strokeStyle = '#6a1a1a'; c.lineWidth = 1
|
||||
for (var i = 0; i < 5; i++) { c.beginPath(); c.moveTo(40, 75+i*7); c.lineTo(85, 75+i*7); c.stroke() }
|
||||
for (var i = 0; i < 5; i++) { c.beginPath(); c.moveTo(105, 75+i*7); c.lineTo(150, 75+i*7); c.stroke() }
|
||||
// Label
|
||||
c.fillStyle = '#fff'; c.font = 'bold 11px Inter,sans-serif'; c.textAlign = 'center'
|
||||
c.fillText('Feuerwehr', 100, 45)
|
||||
// Schlauchturm
|
||||
c.fillStyle = '#aa3333'
|
||||
c.fillRect(160, 15, 15, 55)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// 7. POLIZEI — Sackgasse N
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t07')
|
||||
drawStubN(c)
|
||||
// Gebaeude
|
||||
drawBuilding(c, 20, 50, 160, 85, '#4a6a9a', '')
|
||||
// Stern/Abzeichen
|
||||
c.fillStyle = '#c0d0e8'; c.font = '24px serif'; c.textAlign = 'center'
|
||||
c.fillText('⭐', 100, 85)
|
||||
// Label
|
||||
c.fillStyle = '#fff'; c.font = 'bold 11px Inter,sans-serif'
|
||||
c.fillText('Polizei', 100, 115)
|
||||
// Parkplatz
|
||||
c.fillStyle = '#7a7a7a'; c.fillRect(30, 145, 60, 20)
|
||||
c.fillStyle = '#ddd'; c.font = '14px serif'
|
||||
c.fillText('🚔', 60, 160)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// 8. SCHULE — Gerade E-W, Gebaeude oben
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t08')
|
||||
drawRoadEW(c, M)
|
||||
// Schulgebaeude oben
|
||||
drawBuilding(c, 15, 10, 170, 60, '#e0c888', '')
|
||||
// Fensterreihe
|
||||
c.fillStyle = '#a8d0e0'
|
||||
for (var i = 0; i < 5; i++) c.fillRect(25 + i*34, 25, 22, 18)
|
||||
// Label
|
||||
c.fillStyle = '#5a4a2a'; c.font = 'bold 11px Inter,sans-serif'; c.textAlign = 'center'
|
||||
c.fillText('Schule', 100, 58)
|
||||
// Schulhof unten (Wiese ist schon gruen, ein paar Details)
|
||||
drawTree(c, 30, 145, 20)
|
||||
drawTree(c, 140, 150, 18)
|
||||
c.fillStyle = '#aaa'; c.fillRect(70, 155, 50, 3) // Bank
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// 9. SUPERMARKT — Gerade N-S, Gebaeude rechts mit Parkplatz
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t09')
|
||||
drawRoadNS(c, M)
|
||||
// Supermarkt rechts
|
||||
drawBuilding(c, 140, 20, 55, 80, '#e0d0a0', '')
|
||||
// Label
|
||||
c.fillStyle = '#5a4a2a'; c.font = 'bold 9px Inter,sans-serif'; c.textAlign = 'center'
|
||||
c.fillText('Super-', 167, 50)
|
||||
c.fillText('markt', 167, 62)
|
||||
// Parkplatz
|
||||
c.fillStyle = '#8a8a8a'
|
||||
c.fillRect(140, 110, 55, 40)
|
||||
// Parkplatz-Linien
|
||||
c.strokeStyle = '#aaa'; c.lineWidth = 1
|
||||
for (var i = 0; i < 4; i++) { c.beginPath(); c.moveTo(152+i*12, 110); c.lineTo(152+i*12, 150); c.stroke() }
|
||||
// Einkaufswagen
|
||||
c.fillStyle = '#ddd'; c.font = '12px serif'
|
||||
c.fillText('🛒', 167, 135)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// 10. PARK — Sackgasse N, viel Gruen
|
||||
// ============================================================
|
||||
;(function(){
|
||||
var c = base('t10')
|
||||
drawStubN(c)
|
||||
// Baeume
|
||||
drawTree(c, 15, 40, 28)
|
||||
drawTree(c, 60, 55, 24)
|
||||
drawTree(c, 130, 35, 30)
|
||||
drawTree(c, 155, 80, 22)
|
||||
drawTree(c, 25, 120, 26)
|
||||
drawTree(c, 100, 110, 20)
|
||||
drawTree(c, 150, 140, 24)
|
||||
// Teich
|
||||
c.fillStyle = 'rgba(60,140,200,0.4)'
|
||||
c.beginPath(); c.ellipse(80, 140, 30, 18, 0.1, 0, Math.PI*2); c.fill()
|
||||
c.strokeStyle = 'rgba(60,140,200,0.3)'; c.lineWidth = 1; c.stroke()
|
||||
// Bank
|
||||
c.fillStyle = '#8a7050'
|
||||
c.fillRect(95, 82, 20, 6)
|
||||
c.fillRect(95, 82, 3, 10)
|
||||
c.fillRect(112, 82, 3, 10)
|
||||
// Weg (kleiner Pfad)
|
||||
c.strokeStyle = 'rgba(160,140,100,0.4)'; c.lineWidth = 3; c.setLineDash([4,3])
|
||||
c.beginPath(); c.moveTo(M, 10); c.quadraticCurveTo(60, 80, 80, 140); c.stroke()
|
||||
c.setLineDash([])
|
||||
// Label
|
||||
c.fillStyle = 'rgba(0,0,0,0.35)'; c.font = 'bold 11px Inter,sans-serif'; c.textAlign = 'center'
|
||||
c.fillText('Park', 100, 175)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// EMOJI-VERSIONEN (Gegenversuch)
|
||||
// ============================================================
|
||||
|
||||
var emojiTiles = [
|
||||
{id:'e01', label:'Kirche', stub:'S', emoji:'⛪', size:95},
|
||||
{id:'e02', label:'Moschee', stub:'NS', emoji:'🕌', size:85},
|
||||
{id:'e03', label:'Wohngebiet', road:'NS', emojis:[
|
||||
{e:'🏠',x:-0.32,y:-0.3,s:32},{e:'🏡',x:-0.3,y:0,s:30},{e:'🏠',x:-0.32,y:0.28,s:28},
|
||||
{e:'🏘️',x:0.32,y:-0.25,s:30},{e:'🏠',x:0.3,y:0.05,s:32},{e:'🏡',x:0.32,y:0.3,s:28}]},
|
||||
{id:'e04', label:'Fabrik', stub:'S', emoji:'🏭', size:85},
|
||||
{id:'e05', label:'Spital', stub:'EW', emoji:'🏥', size:75, extra:[{e:'🚑',x:0,y:0.3,s:32}]},
|
||||
{id:'e06', label:'Feuerwehr', stub:'S', bg:'rgba(200,50,40,0.12)', emojis:[
|
||||
{e:'🏠',x:0,y:-0.15,s:60},{e:'🚒',x:-0.2,y:0.22,s:40},{e:'🚒',x:0.2,y:0.22,s:40}]},
|
||||
{id:'e07', label:'Polizei', stub:'N', emoji:'🚔', size:45, extra:[{e:'🏢',x:0,y:0.15,s:55}]},
|
||||
{id:'e08', label:'Schule', road:'EW', emoji:'🏫', size:60, yoff:-0.28},
|
||||
{id:'e09', label:'Supermarkt', road:'NS', emoji:'🏪', size:55, xoff:0.3, extra:[
|
||||
{e:'🚗',x:0.22,y:0.25,s:26},{e:'🚙',x:0.35,y:0.15,s:24},{e:'🚗',x:0.22,y:0.38,s:22}]},
|
||||
{id:'e10', label:'Park', stub:'N', emojis:[
|
||||
{e:'🌳',x:-0.3,y:-0.1,s:32},{e:'🌳',x:-0.1,y:-0.1,s:32},{e:'🌳',x:0.1,y:-0.1,s:32},{e:'🌳',x:0.3,y:-0.1,s:32},
|
||||
{e:'🌳',x:-0.2,y:0.1,s:28},{e:'🌳',x:0,y:0.1,s:28},{e:'🌳',x:0.2,y:0.1,s:28},
|
||||
{e:'🌷',x:-0.35,y:0.3,s:16},{e:'🌷',x:-0.2,y:0.32,s:14},{e:'🌷',x:-0.05,y:0.3,s:16},
|
||||
{e:'🌷',x:0.1,y:0.32,s:14},{e:'🌷',x:0.25,y:0.3,s:16},{e:'🌷',x:0.38,y:0.32,s:14}]},
|
||||
{id:'e11', label:'Rathaus', stub:'S', emoji:'🏛️', size:85},
|
||||
{id:'e13', label:'Tankstelle', road:'NS', emojis:[
|
||||
{e:'⛽',x:0.28,y:-0.2,s:32},{e:'⛽',x:0.28,y:0,s:32},{e:'⛽',x:0.28,y:0.2,s:32},
|
||||
{e:'🚗',x:0.38,y:-0.15,s:26},{e:'🚙',x:0.38,y:0.15,s:26}]},
|
||||
{id:'e14', label:'Spielplatz', stub:'N', emojis:[
|
||||
{e:'🛝',x:-0.2,y:0,s:42},{e:'🎠',x:0.2,y:0,s:36},{e:'⚽',x:0,y:0.25,s:22},
|
||||
{e:'🌳',x:-0.35,y:0.3,s:26},{e:'🌳',x:0.35,y:0.3,s:26}]},
|
||||
{id:'e15', label:'Bauernhof', stub:'EW', emojis:[
|
||||
{e:'🏠',x:0,y:-0.05,s:55},
|
||||
{e:'🌾',x:-0.35,y:-0.3,s:24},{e:'🌾',x:-0.2,y:-0.3,s:24},{e:'🌾',x:-0.05,y:-0.3,s:24},
|
||||
{e:'🌾',x:0.1,y:-0.3,s:24},{e:'🌾',x:0.25,y:-0.3,s:24},{e:'🌾',x:0.4,y:-0.3,s:24},
|
||||
{e:'🐄',x:-0.25,y:0.28,s:30},{e:'🐔',x:0.15,y:0.3,s:22},{e:'🐔',x:0.28,y:0.28,s:22}]},
|
||||
{id:'e16', label:'Hochhäuser', road:'NS', emojis:[
|
||||
{e:'🏢',x:0.22,y:-0.2,s:38},{e:'🏢',x:0.32,y:-0.2,s:38},{e:'🏢',x:0.42,y:-0.2,s:38},
|
||||
{e:'🏢',x:0.22,y:0.15,s:38},{e:'🏢',x:0.32,y:0.15,s:38},{e:'🏢',x:0.42,y:0.15,s:38}]},
|
||||
]
|
||||
|
||||
// Erzeuge HTML fuer Emoji-Tiles
|
||||
var hr = document.createElement('div')
|
||||
hr.style.cssText = 'width:100%;text-align:center;color:#888;font-size:11px;margin:8px 0;border-top:1px solid #ccc;padding-top:6px'
|
||||
hr.textContent = '▼ Emoji-Versionen ▼'
|
||||
document.body.appendChild(hr)
|
||||
|
||||
emojiTiles.forEach(function(t) {
|
||||
var div = document.createElement('div')
|
||||
div.className = 'tile'
|
||||
var cv = document.createElement('canvas')
|
||||
cv.width = 200; cv.height = 200; cv.id = t.id
|
||||
var span = document.createElement('span')
|
||||
span.textContent = t.label
|
||||
div.appendChild(cv); div.appendChild(span)
|
||||
document.body.appendChild(div)
|
||||
|
||||
var c = cv.getContext('2d')
|
||||
c.fillStyle = GREEN; c.fillRect(0, 0, SZ, SZ)
|
||||
|
||||
// Hintergrundfarbe (z.B. rot fuer Feuerwehr)
|
||||
if (t.bg) { c.fillStyle = t.bg; c.fillRect(0, 0, SZ, SZ) }
|
||||
|
||||
// Strasse
|
||||
if (t.road === 'NS') drawRoadNS(c, M)
|
||||
if (t.road === 'EW') drawRoadEW(c, M)
|
||||
if (t.stub) {
|
||||
if (t.stub.indexOf('N') >= 0) drawStubN(c)
|
||||
if (t.stub.indexOf('S') >= 0) drawStubS(c)
|
||||
if (t.stub.indexOf('E') >= 0) drawStubE(c)
|
||||
if (t.stub.indexOf('W') >= 0) drawStubW(c)
|
||||
}
|
||||
|
||||
// Emojis
|
||||
c.textAlign = 'center'; c.textBaseline = 'middle'
|
||||
if (t.emoji) {
|
||||
c.font = (t.size || 50) + 'px serif'
|
||||
c.fillText(t.emoji, M + (t.xoff || 0) * SZ, M + (t.yoff || 0) * SZ)
|
||||
}
|
||||
if (t.emojis) {
|
||||
t.emojis.forEach(function(em) {
|
||||
c.font = (em.s || 24) + 'px serif'
|
||||
c.fillText(em.e, M + em.x * SZ, M + em.y * SZ)
|
||||
})
|
||||
}
|
||||
if (t.extra) {
|
||||
t.extra.forEach(function(em) {
|
||||
c.font = (em.s || 24) + 'px serif'
|
||||
c.fillText(em.e, M + (em.x||0) * SZ, M + (em.y||0) * SZ)
|
||||
})
|
||||
}
|
||||
|
||||
// Keine Beschriftung auf der Kachel
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user