33fae37838
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>
269 lines
12 KiB
HTML
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>
|