Private
Public Access
1
0
Files
it232Abschied/secure/obstacle_editor.html
Sebastian Unterschütz 669c783a06
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
add music, better sync, particles
2025-11-29 23:37:57 +01:00

355 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Obstacle Tuner (Realtime)</title>
<style>
body { background: #1a1a1a; color: #ddd; font-family: 'Segoe UI', monospace; display: flex; height: 100vh; margin: 0; overflow: hidden; }
/* Sidebar */
#controls {
width: 360px; background: #2a2a2a; padding: 15px; border-right: 2px solid #444;
display: flex; flex-direction: column; gap: 10px; overflow-y: auto;
box-shadow: 5px 0 15px rgba(0,0,0,0.5);
}
/* Canvas Area */
#preview {
flex: 1; background-color: #333;
/* Schachbrettmuster */
background-image: linear-gradient(45deg, #252525 25%, transparent 25%), linear-gradient(-45deg, #252525 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #252525 75%), linear-gradient(-45deg, transparent 75%, #252525 75%);
background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
display: flex; align-items: center; justify-content: center; position: relative;
}
canvas { border: 2px solid #555; background: rgba(0,0,0,0.3); box-shadow: 0 0 20px rgba(0,0,0,0.8); cursor: grab; }
canvas:active { cursor: grabbing; }
/* Controls */
.group { background: #333; padding: 8px; border-radius: 6px; border: 1px solid #444; }
.group-title { font-size: 10px; color: #ffcc00; font-weight: bold; margin-bottom: 5px; text-transform: uppercase; border-bottom: 1px solid #444; padding-bottom: 2px; display:flex; justify-content:space-between;}
label { display: block; color: #888; font-size: 10px; margin-bottom: 1px; }
input, select, textarea {
width: 100%; box-sizing: border-box; background: #1a1a1a; border: 1px solid #555;
color: #fff; padding: 4px 6px; border-radius: 4px; font-family: monospace; font-size: 12px;
}
input:focus { outline: none; border-color: #ffcc00; }
input[type=range] { padding: 0; margin: 5px 0; cursor: pointer; }
.row { display: flex; gap: 8px; align-items: center; }
h2 { margin: 0 0 10px 0; color: #ffcc00; border-bottom: 1px solid #555; padding-bottom: 10px; font-size: 18px; }
#output { height: 60px; font-size: 10px; color: #aaddff; resize: vertical; margin-top: auto;}
button { background: #4caf50; color: white; border: none; padding: 8px; font-weight: bold; cursor: pointer; border-radius: 4px; width: 100%; font-size: 12px; }
button:hover { background: #45a049; }
.btn-copy { background: #2196F3; margin-top: 5px; }
/* Highlight wenn Dragging */
.drag-active { border: 1px solid #2196F3 !important; background: #2a3a4a !important; }
</style>
</head>
<body>
<div id="controls">
<h2>🛠 OBSTACLE TUNER</h2>
<div class="group">
<div class="group-title">Basis</div>
<div class="row">
<div style="flex:2"><label>ID</label><input type="text" id="inp-id" value="new_item" oninput="update()"></div>
<div style="flex:1"><label>Type</label>
<select id="inp-type" oninput="update()">
<option value="obstacle">Obstacle</option>
<option value="teacher">Teacher</option>
<option value="coin">Coin</option>
<option value="powerup">PowerUp</option>
</select>
</div>
</div>
<label style="margin-top:5px">Bild Datei (../../assets/)</label>
<input type="text" id="inp-image" value="teacher1.png" oninput="loadImage()">
</div>
<div class="group">
<div class="group-title">🔴 Hitbox (Physik)</div>
<div class="row">
<div style="flex:1"><label>Breite</label><input type="number" id="inp-w" value="30" oninput="update()"></div>
<div style="flex:1"><label>Höhe</label><input type="number" id="inp-h" value="60" oninput="update()"></div>
</div>
<input type="range" min="10" max="200" value="30" id="slider-w" oninput="syncSlider('w')">
<input type="range" min="10" max="200" value="60" id="slider-h" oninput="syncSlider('h')">
<label>Y-Offset (Schweben)</label>
<div class="row">
<input type="number" id="inp-yoff" value="0" oninput="update()">
<input type="range" min="-50" max="100" value="0" step="5" id="slider-yoff" oninput="syncSlider('yoff')">
</div>
</div>
<div class="group" style="border-color: #2196F3;" id="grp-visuals">
<div class="group-title" style="color:#2196F3">
<span>🖼️ Optik (Textur)</span>
<small style="font-weight:normal; font-size:9px; color:#aaa;">Drag Canvas to Move</small>
</div>
<label>Scale (Größe)</label>
<div class="row">
<input type="number" id="inp-scale" value="1.0" step="0.1" oninput="syncInput('scale')">
<input type="range" min="0.5" max="3.0" step="0.1" value="1.0" id="slider-scale" oninput="syncSlider('scale')">
</div>
<div class="row" style="margin-top:5px;">
<div style="flex:1"><label>Offset X</label><input type="number" id="inp-imgx" value="0" oninput="update()"></div>
<div style="flex:1"><label>Offset Y</label><input type="number" id="inp-imgy" value="0" oninput="update()"></div>
</div>
<button onclick="resetVisuals()" style="background:#555; margin-top:5px; padding:4px; font-size:10px;">Reset Optik</button>
</div>
<div class="group">
<label>Fallback Farbe</label>
<input type="color" id="inp-color" value="#ff0000" style="height:25px; padding:0;" oninput="update()">
</div>
<textarea id="output" readonly onclick="this.select()"></textarea>
<button class="btn-copy" onclick="copyToClipboard()">📋 GO STRING KOPIEREN</button>
</div>
<div id="preview">
<canvas id="canvas" width="600" height="400"></canvas>
<div style="position:absolute; bottom: 10px; right: 10px; color:white; font-size:10px; text-align:right; pointer-events:none;">
<span style="color:#2196F3; font-weight:bold;">Maus Drag: Bild verschieben</span><br>
Grün = Spieler (Referenz)<br>Rot = Hitbox
</div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Konstanten
const GROUND_Y = 300;
const PLAYER = { w: 30, h: 50, color: '#33cc33' };
// Bild Cache
let imgObj = new Image();
let imgLoaded = false;
// Dragging Status
let isDragging = false;
let dragStart = {x:0, y:0};
let startOffset = {x:0, y:0};
// --- INIT ---
loadImage(); // Lädt Bild und startet Draw Loop
// --- EVENTS & LOGIC ---
// 1. Slider <-> Input Sync (Bi-Direktional)
function syncSlider(id) {
document.getElementById('inp-'+id).value = document.getElementById('slider-'+id).value;
update();
}
function syncInput(id) {
document.getElementById('slider-'+id).value = document.getElementById('inp-'+id).value;
update();
}
// 2. Reset Button
function resetVisuals() {
document.getElementById('inp-scale').value = 1.0;
document.getElementById('slider-scale').value = 1.0;
document.getElementById('inp-imgx').value = 0;
document.getElementById('inp-imgy').value = 0;
update();
}
// 3. Bild Laden (Nur wenn Pfad sich ändert!)
function loadImage() {
const path = document.getElementById('inp-image').value;
imgLoaded = false;
imgObj = new Image();
// Pfad-Logik für Local vs Server
imgObj.src = "../../assets/" + path;
imgObj.onload = () => {
imgLoaded = true;
draw(); // Sofort neu zeichnen
};
imgObj.onerror = () => {
// Fallback Pfad probieren
imgObj.src = "../../assets/" + path;
};
draw();
}
// 4. Zentrales Update (Alles außer Bildpfad)
function update() {
// Hier wird NICHT das Bild neu geladen, nur Parameter gelesen
draw();
generateString();
}
// --- DRAG & DROP LOGIK ---
canvas.addEventListener('mousedown', e => {
isDragging = true;
dragStart = { x: e.clientX, y: e.clientY };
// Aktuelle Werte holen
startOffset = {
x: parseFloat(document.getElementById('inp-imgx').value) || 0,
y: parseFloat(document.getElementById('inp-imgy').value) || 0
};
document.getElementById('grp-visuals').classList.add('drag-active');
canvas.style.cursor = "grabbing";
});
window.addEventListener('mousemove', e => {
if (!isDragging) return;
const dx = e.clientX - dragStart.x;
const dy = e.clientY - dragStart.y;
// Neue Werte berechnen (Integer reichen meist für Pixel Art)
const newX = Math.round(startOffset.x + dx);
const newY = Math.round(startOffset.y + dy);
// Inputs updaten (ohne draw aufzurufen, das machen wir direkt)
document.getElementById('inp-imgx').value = newX;
document.getElementById('inp-imgy').value = newY;
update(); // Löst Draw & String Gen aus
});
window.addEventListener('mouseup', () => {
if(isDragging) {
isDragging = false;
document.getElementById('grp-visuals').classList.remove('drag-active');
canvas.style.cursor = "grab";
}
});
// --- RENDERING ---
function draw() {
// Canvas leeren
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 1. BODEN
ctx.fillStyle = "#222";
ctx.fillRect(0, GROUND_Y, canvas.width, 2);
// 2. SPIELER (DUMMY)
const pX = 200;
const pY = GROUND_Y - PLAYER.h;
ctx.fillStyle = PLAYER.color;
ctx.fillRect(pX, pY, PLAYER.w, PLAYER.h);
ctx.fillStyle = "white"; ctx.font = "10px sans-serif"; ctx.fillText("PLAYER", pX, pY - 5);
// 3. PARAMETER LESEN
const w = parseFloat(document.getElementById('inp-w').value) || 30;
const h = parseFloat(document.getElementById('inp-h').value) || 30;
const yOff = parseFloat(document.getElementById('inp-yoff').value) || 0;
const scale = parseFloat(document.getElementById('inp-scale').value) || 1.0;
const imgX = parseFloat(document.getElementById('inp-imgx').value) || 0;
const imgY = parseFloat(document.getElementById('inp-imgy').value) || 0;
const color = document.getElementById('inp-color').value;
const id = document.getElementById('inp-id').value;
// Position des Objekts (300px = Rechts vom Spieler)
const oX = 300;
const oY = GROUND_Y - h - yOff;
// A. HITBOX ZEICHNEN (Unter dem Bild)
ctx.fillStyle = "rgba(255, 0, 0, 0.25)"; // Halbtransparent
ctx.fillRect(oX, oY, w, h);
ctx.strokeStyle = "#ff0000"; ctx.lineWidth = 2; ctx.setLineDash([]);
ctx.strokeRect(oX, oY, w, h);
// Label
ctx.fillStyle = "#ff5555";
ctx.fillText(`HITBOX ${w}x${h}`, oX, oY - 5);
// B. BILD ZEICHNEN
if (imgLoaded) {
const drawW = w * scale;
const drawH = h * scale;
// Logik wie im Spiel:
// 1. Zentrieren auf Hitbox
const centerX = oX + (w - drawW) / 2;
// 2. Unten Bündig
const bottomY = oY + (h - drawH);
// 3. Offsets anwenden
const finalX = centerX + imgX;
const finalY = bottomY + imgY;
// Zeichnen
ctx.drawImage(imgObj, finalX, finalY, drawW, drawH);
// Blauer Rahmen um Textur
ctx.strokeStyle = "#2196F3"; ctx.lineWidth = 1; ctx.setLineDash([4, 2]);
ctx.strokeRect(finalX, finalY, drawW, drawH);
// Verbindungslinie (Center Hitbox -> Center Bild)
ctx.beginPath();
ctx.moveTo(oX + w/2, oY + h/2);
ctx.lineTo(finalX + drawW/2, finalY + drawH/2);
ctx.strokeStyle = "rgba(33, 150, 243, 0.4)";
ctx.stroke();
} else {
// Fallback Grafik
ctx.fillStyle = color;
ctx.fillRect(oX, oY, w, h);
ctx.fillStyle = "white";
ctx.fillText("IMG FEHLT", oX+2, oY + h/2);
}
}
// --- EXPORT ---
function generateString() {
const id = document.getElementById('inp-id').value;
const type = document.getElementById('inp-type').value;
const w = document.getElementById('inp-w').value;
const h = document.getElementById('inp-h').value;
const color = document.getElementById('inp-color').value;
const img = document.getElementById('inp-image').value;
const yOff = parseFloat(document.getElementById('inp-yoff').value);
const scale = parseFloat(document.getElementById('inp-scale').value);
const imgX = parseFloat(document.getElementById('inp-imgx').value);
const imgY = parseFloat(document.getElementById('inp-imgy').value);
// String bauen
let str = `{ID: "${id}", Type: "${type}", Width: ${w}, Height: ${h}, Color: "${color}", Image: "${img}"`;
if (yOff !== 0) str += `, YOffset: ${yOff}`;
if (scale !== 1.0) str += `, ImgScale: ${scale}`;
if (imgX !== 0) str += `, ImgOffsetX: ${imgX}`;
if (imgY !== 0) str += `, ImgOffsetY: ${imgY}`;
str += `}, // ${id}`;
document.getElementById('output').value = str;
}
function copyToClipboard() {
const copyText = document.getElementById("output");
copyText.select();
navigator.clipboard.writeText(copyText.value);
const btn = document.querySelector('.btn-copy');
const oldText = btn.innerText;
btn.innerText = "✅ KOPIERT!";
btn.style.background = "#2e7d32";
setTimeout(() => {
btn.innerText = oldText;
btn.style.background = "#2196F3";
}, 1000);
}
</script>
</body>
</html>