Files
geograsim/App/stadt-editor.html
T
Adminator 33fae37838 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>
2026-04-14 01:52:55 +02:00

269 lines
12 KiB
HTML

<!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>