Upload via GUI (123 Dateien)
This commit is contained in:
@@ -0,0 +1,983 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* skineditor.js – Pixel-Editor für Minecraft-Skins
|
||||
* -------------------------------------------------
|
||||
* Ein Skin ist eine einzige 64×64-Textur. Jedes Körperteil hat darin zwei
|
||||
* Bereiche: die Grundschicht (liegt direkt auf der Figur) und die 2. Schicht
|
||||
* (eine etwas größere Hülle darüber – Haare, Jacke, Ärmel …). Die 2. Schicht
|
||||
* darf durchsichtig sein und lässt die Figur dadurch plastischer wirken.
|
||||
*
|
||||
* Gemalt wird wahlweise auf der aufgeklappten Vorlage (2D) oder direkt auf der
|
||||
* drehbaren Figur (3D). Beide Ansichten sind immer da – die gerade nicht
|
||||
* gewählte steht klein rechts und ist ebenfalls bemalbar.
|
||||
*
|
||||
* Läuft im Popup-Fenster "skineditor" und nutzt die Helfer aus renderer.js
|
||||
* (el, show, toast, askConfirm, POPOUT, SKIN_POSES).
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const N = 64;
|
||||
|
||||
// Lage der Körperteile in der Textur. w/h/d = Breite/Höhe/Tiefe in Pixeln,
|
||||
// base/over = linke obere Ecke des Bereichs der Grund- bzw. 2. Schicht.
|
||||
const TEILE = [
|
||||
{ key: 'head', name: 'Kopf', w: 8, h: 8, d: 8, base: [0, 0], over: [32, 0], spiegel: 'head' },
|
||||
{ key: 'body', name: 'Körper', w: 8, h: 12, d: 4, base: [16, 16], over: [16, 32], spiegel: 'body' },
|
||||
{ key: 'rightArm', name: 'Rechter Arm', w: 4, h: 12, d: 4, base: [40, 16], over: [40, 32], spiegel: 'leftArm', arm: true },
|
||||
{ key: 'leftArm', name: 'Linker Arm', w: 4, h: 12, d: 4, base: [32, 48], over: [48, 48], spiegel: 'rightArm', arm: true },
|
||||
{ key: 'rightLeg', name: 'Rechtes Bein', w: 4, h: 12, d: 4, base: [0, 16], over: [0, 32], spiegel: 'leftLeg' },
|
||||
{ key: 'leftLeg', name: 'Linkes Bein', w: 4, h: 12, d: 4, base: [16, 48], over: [0, 48], spiegel: 'rightLeg' },
|
||||
];
|
||||
const TEIL = Object.fromEntries(TEILE.map((t) => [t.key, t]));
|
||||
const SEITE_NAME = { oben: 'oben', unten: 'unten', rechts: 'rechte Seite', vorne: 'vorne', links: 'linke Seite', hinten: 'hinten' };
|
||||
const SEITE_GEGENSTUECK = { rechts: 'links', links: 'rechts' };
|
||||
const SCHICHT_NAME = ['Grundschicht', '2. Schicht'];
|
||||
|
||||
// Rechtecke [x, y, breite, höhe] der sechs Seiten eines Teils (Minecraft-Aufteilung:
|
||||
// oben/unten in der ersten Reihe, darunter rechts | vorne | links | hinten)
|
||||
function seiten(teil, schicht, slim) {
|
||||
const w = teil.arm && slim ? 3 : teil.w;
|
||||
const { h, d } = teil;
|
||||
const [u, v] = schicht ? teil.over : teil.base;
|
||||
return {
|
||||
oben: [u + d, v, w, d],
|
||||
unten: [u + d + w, v, w, d],
|
||||
rechts: [u, v + d, d, h],
|
||||
vorne: [u + d, v + d, w, h],
|
||||
links: [u + d + w, v + d, d, h],
|
||||
hinten: [u + 2 * d + w, v + d, w, h],
|
||||
};
|
||||
}
|
||||
|
||||
// Für jede Armform: Liste aller Flächen und eine Pixel→Fläche-Karte
|
||||
const AUFTEILUNG = [false, true].map((slim) => {
|
||||
const flaechen = [];
|
||||
const karte = new Array(N * N).fill(null);
|
||||
for (const teil of TEILE) {
|
||||
for (const schicht of [0, 1]) {
|
||||
for (const [seite, r] of Object.entries(seiten(teil, schicht, slim))) {
|
||||
const f = { teil, schicht, seite, x: r[0], y: r[1], w: r[2], h: r[3] };
|
||||
flaechen.push(f);
|
||||
for (let yy = 0; yy < f.h; yy++) for (let xx = 0; xx < f.w; xx++) karte[(f.y + yy) * N + f.x + xx] = f;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { flaechen, karte };
|
||||
});
|
||||
|
||||
// ---------- Zustand ----------
|
||||
const ed = {
|
||||
daten: new Uint8ClampedArray(N * N * 4),
|
||||
slim: false,
|
||||
werkzeug: 'stift',
|
||||
vorPipette: 'stift',
|
||||
farbe: '#3a6ea5',
|
||||
schicht: 0,
|
||||
zeigeAussen: true,
|
||||
durchscheinen: true,
|
||||
spiegeln: false,
|
||||
modus: '2d',
|
||||
undo: [],
|
||||
redo: [],
|
||||
geaendert: false,
|
||||
hover: null,
|
||||
strich: null, // { snap, letzter: [x,y] | null, letzterBild: [cx,cy] | null, geaendert }
|
||||
viewer: null,
|
||||
bereit: false,
|
||||
zeichnenGeplant: false,
|
||||
};
|
||||
const textur = document.createElement('canvas');
|
||||
textur.width = textur.height = N;
|
||||
const texturCtx = textur.getContext('2d', { willReadFrequently: true });
|
||||
// Grundschicht als Schatten unter die 2. Schicht legen (Option "durchscheinen")
|
||||
const schatten = document.createElement('canvas');
|
||||
schatten.width = schatten.height = N;
|
||||
const schattenCtx = schatten.getContext('2d');
|
||||
// Hintergrund (Karomuster in den Flächen) je Armform einmal vorberechnet
|
||||
const hintergrund = [null, null];
|
||||
|
||||
const aufteilung = () => AUFTEILUNG[ed.slim ? 1 : 0];
|
||||
const flaecheBei = (x, y) => (x < 0 || y < 0 || x >= N || y >= N ? null : aufteilung().karte[y * N + x]);
|
||||
const dunkel = () => document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
|
||||
function hexZuRgb(hex) {
|
||||
const m = /^#?([0-9a-f]{6})$/i.exec(hex || '');
|
||||
if (!m) return [0, 0, 0];
|
||||
const n = parseInt(m[1], 16);
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
}
|
||||
const rgbZuHex = (r, g, b) => '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
// Pixel auf der gespiegelten Körperseite (rechter ↔ linker Arm/Bein,
|
||||
// rechte ↔ linke Seite, waagerecht umgedreht)
|
||||
function spiegelPixel(x, y) {
|
||||
const f = flaecheBei(x, y);
|
||||
if (!f) return null;
|
||||
const partner = TEIL[f.teil.spiegel];
|
||||
const seite = SEITE_GEGENSTUECK[f.seite] || f.seite;
|
||||
const r = seiten(partner, f.schicht, ed.slim)[seite];
|
||||
const fx = x - f.x;
|
||||
const fy = y - f.y;
|
||||
return [r[0] + (r[2] - 1 - fx), r[1] + fy];
|
||||
}
|
||||
|
||||
// ---------- Daten ändern ----------
|
||||
function setzePixel(x, y, rgba) {
|
||||
const i = (y * N + x) * 4;
|
||||
const d = ed.daten;
|
||||
if (d[i] === rgba[0] && d[i + 1] === rgba[1] && d[i + 2] === rgba[2] && d[i + 3] === rgba[3]) return false;
|
||||
d[i] = rgba[0]; d[i + 1] = rgba[1]; d[i + 2] = rgba[2]; d[i + 3] = rgba[3];
|
||||
return true;
|
||||
}
|
||||
|
||||
function zielPixel(x, y) {
|
||||
const liste = [[x, y]];
|
||||
if (ed.spiegeln) {
|
||||
const s = spiegelPixel(x, y);
|
||||
if (s && (s[0] !== x || s[1] !== y)) liste.push(s);
|
||||
}
|
||||
return liste;
|
||||
}
|
||||
|
||||
function malFarbe() {
|
||||
if (ed.werkzeug === 'radierer') return [0, 0, 0, 0];
|
||||
return [...hexZuRgb(ed.farbe), 255];
|
||||
}
|
||||
|
||||
// Einen Pixel mit dem aktuellen Werkzeug bearbeiten (Stift/Radierer)
|
||||
function malePixel(x, y) {
|
||||
const f = flaecheBei(x, y);
|
||||
if (!f || f.schicht !== ed.schicht) return false;
|
||||
let geaendert = false;
|
||||
const rgba = malFarbe();
|
||||
for (const [px, py] of zielPixel(x, y)) if (setzePixel(px, py, rgba)) geaendert = true;
|
||||
return geaendert;
|
||||
}
|
||||
|
||||
// Füllen: zusammenhängende gleichfarbige Pixel innerhalb derselben Fläche
|
||||
function fuelle(x, y) {
|
||||
const f = flaecheBei(x, y);
|
||||
if (!f || f.schicht !== ed.schicht) return false;
|
||||
const d = ed.daten;
|
||||
const i0 = (y * N + x) * 4;
|
||||
const alt = [d[i0], d[i0 + 1], d[i0 + 2], d[i0 + 3]];
|
||||
const neu = malFarbe();
|
||||
if (alt.every((v, k) => v === neu[k])) return false;
|
||||
const gleich = (px, py) => {
|
||||
const i = (py * N + px) * 4;
|
||||
return d[i] === alt[0] && d[i + 1] === alt[1] && d[i + 2] === alt[2] && d[i + 3] === alt[3];
|
||||
};
|
||||
const gesehen = new Set();
|
||||
const stapel = [[x, y]];
|
||||
const treffer = [];
|
||||
while (stapel.length) {
|
||||
const [px, py] = stapel.pop();
|
||||
const k = py * N + px;
|
||||
if (gesehen.has(k)) continue;
|
||||
if (px < f.x || py < f.y || px >= f.x + f.w || py >= f.y + f.h) continue;
|
||||
gesehen.add(k);
|
||||
if (!gleich(px, py)) continue;
|
||||
treffer.push([px, py]);
|
||||
stapel.push([px + 1, py], [px - 1, py], [px, py + 1], [px, py - 1]);
|
||||
}
|
||||
let geaendert = false;
|
||||
for (const [px, py] of treffer) for (const [zx, zy] of zielPixel(px, py)) if (setzePixel(zx, zy, neu)) geaendert = true;
|
||||
return geaendert;
|
||||
}
|
||||
|
||||
function nimmFarbe(x, y) {
|
||||
const i = (y * N + x) * 4;
|
||||
const d = ed.daten;
|
||||
if (d[i + 3] === 0) return false;
|
||||
setzeFarbe(rgbZuHex(d[i], d[i + 1], d[i + 2]));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- Striche, Rückgängig ----------
|
||||
function beginneStrich() {
|
||||
ed.strich = { snap: ed.daten.slice(), letzter: null, letzterBild: null, geaendert: false };
|
||||
}
|
||||
|
||||
function beendeStrich() {
|
||||
const s = ed.strich;
|
||||
ed.strich = null;
|
||||
if (!s || !s.geaendert) return;
|
||||
ed.undo.push(s.snap);
|
||||
if (ed.undo.length > 100) ed.undo.shift();
|
||||
ed.redo = [];
|
||||
ed.geaendert = true;
|
||||
if (ed.werkzeug !== 'radierer') merkeFarbe(ed.farbe);
|
||||
aktualisiereKnoepfe();
|
||||
}
|
||||
|
||||
// Werkzeug auf einen Texturpixel anwenden (während eines Strichs)
|
||||
function wendeAn(x, y) {
|
||||
const s = ed.strich;
|
||||
if (!s) return;
|
||||
if (ed.werkzeug === 'pipette') {
|
||||
nimmFarbe(x, y);
|
||||
return;
|
||||
}
|
||||
if (ed.werkzeug === 'fuellen') {
|
||||
if (s.letzter) return; // ein Klick = eine Füllung
|
||||
s.letzter = [x, y];
|
||||
if (fuelle(x, y)) { s.geaendert = true; texturGeaendert(); }
|
||||
return;
|
||||
}
|
||||
let geaendert = false;
|
||||
// Lücken zwischen zwei Mausereignissen schließen (nur innerhalb der Vorlage
|
||||
// sinnvoll, auf der Figur liegen benachbarte Pixel nicht nebeneinander)
|
||||
const punkte = s.letzter && s.zweiD ? linie(s.letzter[0], s.letzter[1], x, y) : [[x, y]];
|
||||
for (const [px, py] of punkte) if (malePixel(px, py)) geaendert = true;
|
||||
s.letzter = [x, y];
|
||||
if (geaendert) { s.geaendert = true; texturGeaendert(); }
|
||||
}
|
||||
|
||||
function linie(x0, y0, x1, y1) {
|
||||
const punkte = [];
|
||||
const dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
|
||||
const dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
|
||||
let err = dx + dy;
|
||||
for (;;) {
|
||||
punkte.push([x0, y0]);
|
||||
if (x0 === x1 && y0 === y1) break;
|
||||
const e2 = 2 * err;
|
||||
if (e2 >= dy) { err += dy; x0 += sx; }
|
||||
if (e2 <= dx) { err += dx; y0 += sy; }
|
||||
}
|
||||
return punkte;
|
||||
}
|
||||
|
||||
function rueckgaengig() {
|
||||
if (!ed.undo.length) return;
|
||||
ed.redo.push(ed.daten.slice());
|
||||
ed.daten = ed.undo.pop();
|
||||
ed.geaendert = true;
|
||||
texturGeaendert();
|
||||
aktualisiereKnoepfe();
|
||||
}
|
||||
|
||||
function wiederholen() {
|
||||
if (!ed.redo.length) return;
|
||||
ed.undo.push(ed.daten.slice());
|
||||
ed.daten = ed.redo.pop();
|
||||
ed.geaendert = true;
|
||||
texturGeaendert();
|
||||
aktualisiereKnoepfe();
|
||||
}
|
||||
|
||||
// ganze Änderung (Laden, Leeren …) als ein Rückgängig-Schritt
|
||||
function aenderung(fn) {
|
||||
const snap = ed.daten.slice();
|
||||
fn();
|
||||
if (snap.every((v, i) => v === ed.daten[i])) return false;
|
||||
ed.undo.push(snap);
|
||||
if (ed.undo.length > 100) ed.undo.shift();
|
||||
ed.redo = [];
|
||||
ed.geaendert = true;
|
||||
texturGeaendert();
|
||||
aktualisiereKnoepfe();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- Farben ----------
|
||||
const FARB_SCHLUESSEL = 'aeromc-skin-farben';
|
||||
const STANDARD_FARBEN = ['#000000', '#ffffff', '#c69c6d', '#a87b4f', '#6b4a2f', '#3b2416', '#d32f2f', '#f9a825',
|
||||
'#43a047', '#1e88e5', '#3949ab', '#8e24aa', '#00897b', '#757575', '#424242', '#ff8a65'];
|
||||
let zuletzt = [];
|
||||
try { zuletzt = JSON.parse(localStorage.getItem(FARB_SCHLUESSEL) || '[]'); } catch { zuletzt = []; }
|
||||
if (!Array.isArray(zuletzt) || !zuletzt.length) zuletzt = STANDARD_FARBEN.slice();
|
||||
|
||||
function merkeFarbe(hex) {
|
||||
zuletzt = [hex, ...zuletzt.filter((f) => f !== hex)].slice(0, 16);
|
||||
try { localStorage.setItem(FARB_SCHLUESSEL, JSON.stringify(zuletzt)); } catch { /* egal */ }
|
||||
zeigeZuletzt();
|
||||
}
|
||||
|
||||
function zeigeZuletzt() {
|
||||
const box = el('se-zuletzt');
|
||||
box.innerHTML = '';
|
||||
for (const f of zuletzt) {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'se-farbfeld' + (f === ed.farbe ? ' on' : '');
|
||||
b.style.background = f;
|
||||
b.title = f;
|
||||
b.addEventListener('click', () => setzeFarbe(f));
|
||||
box.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
function setzeFarbe(hex) {
|
||||
ed.farbe = hex.toLowerCase();
|
||||
el('se-farbe').value = ed.farbe;
|
||||
el('se-hex').value = ed.farbe;
|
||||
// nach dem Aufnehmen zurück zum vorherigen Werkzeug; mit dem Radierer
|
||||
// ergibt eine neue Farbe keinen Sinn -> Stift
|
||||
if (ed.werkzeug === 'pipette') waehleWerkzeug(ed.vorPipette === 'radierer' ? 'stift' : ed.vorPipette);
|
||||
else if (ed.werkzeug === 'radierer') waehleWerkzeug('stift');
|
||||
zeigeZuletzt();
|
||||
}
|
||||
|
||||
// ---------- Bedienelemente ----------
|
||||
function waehleWerkzeug(w) {
|
||||
if (w === 'radierer' && ed.schicht === 0) w = 'stift';
|
||||
if (w === 'pipette' && ed.werkzeug !== 'pipette') ed.vorPipette = ed.werkzeug;
|
||||
ed.werkzeug = w;
|
||||
document.querySelectorAll('[data-se-tool]').forEach((b) => b.classList.toggle('on', b.dataset.seTool === w));
|
||||
for (const h of [el('se-2d'), el('se-3d')]) h.dataset.werkzeug = w;
|
||||
}
|
||||
|
||||
function waehleSchicht(s) {
|
||||
ed.schicht = s;
|
||||
document.querySelectorAll('[data-se-schicht]').forEach((b) => b.classList.toggle('on', Number(b.dataset.seSchicht) === s));
|
||||
// die 2. Schicht muss sichtbar sein, wenn man darauf malen will
|
||||
if (s === 1 && !ed.zeigeAussen) { ed.zeigeAussen = true; el('se-aussen').checked = true; }
|
||||
const radierer = document.querySelector('[data-se-tool="radierer"]');
|
||||
radierer.disabled = s === 0;
|
||||
radierer.title = s === 0
|
||||
? 'Radierer – nur in der 2. Schicht (die Grundschicht darf keine Löcher haben)'
|
||||
: 'Radierer (E) – macht Pixel durchsichtig';
|
||||
if (s === 0 && ed.werkzeug === 'radierer') waehleWerkzeug('stift');
|
||||
el('se-durch').disabled = s === 0;
|
||||
ed.hover = null;
|
||||
texturGeaendert();
|
||||
}
|
||||
|
||||
function waehleModus(m) {
|
||||
ed.modus = m;
|
||||
document.querySelectorAll('[data-se-modus]').forEach((b) => b.classList.toggle('on', b.dataset.seModus === m));
|
||||
const gross = el('se-buehne');
|
||||
const klein = el('se-mini');
|
||||
gross.appendChild(m === '2d' ? el('se-2d') : el('se-3d'));
|
||||
klein.appendChild(m === '2d' ? el('se-3d') : el('se-2d'));
|
||||
el('se-mini-titel').textContent = m === '2d' ? '3D-Vorschau' : 'Vorlage';
|
||||
zeigeInfo();
|
||||
passeGroessenAn();
|
||||
}
|
||||
|
||||
function aktualisiereKnoepfe() {
|
||||
el('se-undo').disabled = !ed.undo.length;
|
||||
el('se-redo').disabled = !ed.redo.length;
|
||||
}
|
||||
|
||||
function zeigeInfo() {
|
||||
const info = el('se-info');
|
||||
const h = ed.hover;
|
||||
const f = h && flaecheBei(h[0], h[1]);
|
||||
if (f) {
|
||||
info.textContent = `${f.teil.name} · ${SEITE_NAME[f.seite]} · ${SCHICHT_NAME[f.schicht]} · Pixel ${h[0]}, ${h[1]}`;
|
||||
return;
|
||||
}
|
||||
info.textContent = ed.modus === '3d'
|
||||
? 'Linke Maustaste malt · rechte Maustaste dreht · Mausrad zoomt · Alt+Klick nimmt Farbe auf'
|
||||
: 'Linke Maustaste malt · Alt+Klick nimmt eine Farbe auf · abgedunkelte Bereiche gehören zur anderen Schicht';
|
||||
}
|
||||
|
||||
function status(text) { el('se-status').textContent = text || ''; el('se-status').title = text || ''; }
|
||||
|
||||
// ---------- Zeichnen ----------
|
||||
function texturGeaendert() {
|
||||
texturCtx.putImageData(new ImageData(ed.daten, N, N), 0, 0);
|
||||
// Schatten: Grundschicht-Pixel an die Stelle der zugehörigen 2. Schicht legen
|
||||
const s = schattenCtx.createImageData(N, N);
|
||||
for (const f of aufteilung().flaechen) {
|
||||
if (f.schicht !== 1) continue;
|
||||
const r = seiten(f.teil, 0, ed.slim)[f.seite];
|
||||
for (let yy = 0; yy < f.h; yy++) {
|
||||
for (let xx = 0; xx < f.w; xx++) {
|
||||
const q = ((r[1] + yy) * N + r[0] + xx) * 4;
|
||||
const z = ((f.y + yy) * N + f.x + xx) * 4;
|
||||
s.data[z] = ed.daten[q]; s.data[z + 1] = ed.daten[q + 1]; s.data[z + 2] = ed.daten[q + 2]; s.data[z + 3] = ed.daten[q + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
schattenCtx.putImageData(s, 0, 0);
|
||||
planeZeichnen();
|
||||
}
|
||||
|
||||
function planeZeichnen() {
|
||||
if (ed.zeichnenGeplant) return;
|
||||
ed.zeichnenGeplant = true;
|
||||
requestAnimationFrame(() => {
|
||||
ed.zeichnenGeplant = false;
|
||||
zeichne2D();
|
||||
zeichne3D();
|
||||
zeigeInfo();
|
||||
});
|
||||
}
|
||||
|
||||
function hintergrundFuer(slim) {
|
||||
const k = slim ? 1 : 0;
|
||||
if (hintergrund[k] && hintergrund[k].dunkel === dunkel()) return hintergrund[k].canvas;
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = N;
|
||||
const g = c.getContext('2d');
|
||||
const img = g.createImageData(N, N);
|
||||
const [a, b] = dunkel() ? [[58, 58, 64], [48, 48, 54]] : [[236, 236, 238], [218, 219, 223]];
|
||||
for (const f of AUFTEILUNG[k].flaechen) {
|
||||
for (let yy = f.y; yy < f.y + f.h; yy++) {
|
||||
for (let xx = f.x; xx < f.x + f.w; xx++) {
|
||||
const farbe = (xx + yy) % 2 ? a : b;
|
||||
const i = (yy * N + xx) * 4;
|
||||
img.data[i] = farbe[0]; img.data[i + 1] = farbe[1]; img.data[i + 2] = farbe[2]; img.data[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.putImageData(img, 0, 0);
|
||||
hintergrund[k] = { canvas: c, dunkel: dunkel() };
|
||||
return c;
|
||||
}
|
||||
|
||||
// sichtbare Textur für die 2D-Ansicht: ohne ungenutzte Bereiche und, falls
|
||||
// ausgeblendet, ohne 2. Schicht
|
||||
const sicht = document.createElement('canvas');
|
||||
sicht.width = sicht.height = N;
|
||||
const sichtCtx = sicht.getContext('2d');
|
||||
|
||||
function zeichne2D() {
|
||||
const canvas = el('se-2d-canvas');
|
||||
const k = Number(canvas.dataset.k) || 0;
|
||||
if (!k) return;
|
||||
const g = canvas.getContext('2d');
|
||||
g.setTransform(1, 0, 0, 1, 0, 0);
|
||||
g.clearRect(0, 0, canvas.width, canvas.height);
|
||||
g.imageSmoothingEnabled = false;
|
||||
g.setTransform(k, 0, 0, k, 0, 0);
|
||||
g.fillStyle = dunkel() ? '#1c1c1f' : '#c9ccd1';
|
||||
g.fillRect(0, 0, N, N);
|
||||
g.drawImage(hintergrundFuer(ed.slim), 0, 0);
|
||||
|
||||
if (ed.schicht === 1 && ed.durchscheinen) {
|
||||
g.globalAlpha = 0.35;
|
||||
g.drawImage(schatten, 0, 0);
|
||||
g.globalAlpha = 1;
|
||||
}
|
||||
sichtCtx.clearRect(0, 0, N, N);
|
||||
const img = sichtCtx.createImageData(N, N);
|
||||
const karte = aufteilung().karte;
|
||||
for (let i = 0; i < N * N; i++) {
|
||||
const f = karte[i];
|
||||
if (!f || (f.schicht === 1 && !ed.zeigeAussen)) continue;
|
||||
const q = i * 4;
|
||||
img.data[q] = ed.daten[q]; img.data[q + 1] = ed.daten[q + 1]; img.data[q + 2] = ed.daten[q + 2]; img.data[q + 3] = ed.daten[q + 3];
|
||||
}
|
||||
sichtCtx.putImageData(img, 0, 0);
|
||||
g.drawImage(sicht, 0, 0);
|
||||
|
||||
// andere Schicht abdunkeln
|
||||
g.fillStyle = dunkel() ? 'rgba(20,20,22,.62)' : 'rgba(40,44,52,.45)';
|
||||
for (const f of aufteilung().flaechen) if (f.schicht !== ed.schicht) g.fillRect(f.x, f.y, f.w, f.h);
|
||||
|
||||
const px = 1 / k;
|
||||
// Pixelraster
|
||||
if (k >= 6) {
|
||||
g.strokeStyle = dunkel() ? 'rgba(255,255,255,.07)' : 'rgba(0,0,0,.08)';
|
||||
g.lineWidth = px;
|
||||
g.beginPath();
|
||||
for (let i = 1; i < N; i++) { g.moveTo(i, 0); g.lineTo(i, N); g.moveTo(0, i); g.lineTo(N, i); }
|
||||
g.stroke();
|
||||
}
|
||||
// Flächenränder
|
||||
g.lineWidth = 1.5 * px;
|
||||
for (const f of aufteilung().flaechen) {
|
||||
g.strokeStyle = f.schicht === ed.schicht
|
||||
? (dunkel() ? 'rgba(255,255,255,.45)' : 'rgba(0,0,0,.4)')
|
||||
: (dunkel() ? 'rgba(255,255,255,.12)' : 'rgba(0,0,0,.12)');
|
||||
g.strokeRect(f.x, f.y, f.w, f.h);
|
||||
}
|
||||
// Mauszeiger und gespiegelter Partner
|
||||
if (ed.hover) {
|
||||
const [hx, hy] = ed.hover;
|
||||
g.lineWidth = 2 * px;
|
||||
g.strokeStyle = dunkel() ? '#ffffff' : '#000000';
|
||||
g.strokeRect(hx + px, hy + px, 1 - 2 * px, 1 - 2 * px);
|
||||
if (ed.spiegeln) {
|
||||
const s = spiegelPixel(hx, hy);
|
||||
if (s && (s[0] !== hx || s[1] !== hy)) {
|
||||
g.setLineDash([2 * px, 2 * px]);
|
||||
g.strokeRect(s[0] + px, s[1] + px, 1 - 2 * px, 1 - 2 * px);
|
||||
g.setLineDash([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function zeichne3D() {
|
||||
const v = ed.viewer;
|
||||
if (!v || !v.skinTexture) return;
|
||||
const g = v.skinCanvas.getContext('2d');
|
||||
if (v.skinCanvas.width !== N || v.skinCanvas.height !== N) { v.skinCanvas.width = N; v.skinCanvas.height = N; }
|
||||
g.clearRect(0, 0, N, N);
|
||||
g.drawImage(textur, 0, 0);
|
||||
// Pixel unter der Maus hervorheben
|
||||
if (ed.hover && ed.werkzeug !== 'pipette') {
|
||||
const ziele = ed.spiegeln ? zielPixel(ed.hover[0], ed.hover[1]) : [ed.hover];
|
||||
for (const [hx, hy] of ziele) {
|
||||
const i = (hy * N + hx) * 4;
|
||||
const hell = ed.daten[i + 3] === 0 || (ed.daten[i] + ed.daten[i + 1] + ed.daten[i + 2]) < 380;
|
||||
g.fillStyle = hell ? 'rgba(255,255,255,.75)' : 'rgba(0,0,0,.55)';
|
||||
g.fillRect(hx, hy, 1, 1);
|
||||
}
|
||||
}
|
||||
v.skinTexture.needsUpdate = true;
|
||||
v.playerObject.skin.setOuterLayerVisible(ed.zeigeAussen);
|
||||
}
|
||||
|
||||
// ---------- Größen ----------
|
||||
function passeGroessenAn() {
|
||||
const h2 = el('se-2d');
|
||||
const c = el('se-2d-canvas');
|
||||
const platz = Math.min(h2.clientWidth, h2.clientHeight) - 8;
|
||||
const s = Math.max(1, Math.floor(platz / N));
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
c.style.width = c.style.height = (s * N) + 'px';
|
||||
c.width = c.height = Math.round(s * N * dpr);
|
||||
c.dataset.k = String((s * N * dpr) / N);
|
||||
const h3 = el('se-3d');
|
||||
if (ed.viewer && h3.clientWidth && h3.clientHeight) ed.viewer.setSize(h3.clientWidth, h3.clientHeight);
|
||||
planeZeichnen();
|
||||
}
|
||||
|
||||
// ---------- 2D-Eingabe ----------
|
||||
function texelAus2D(e) {
|
||||
const c = el('se-2d-canvas');
|
||||
const r = c.getBoundingClientRect();
|
||||
const x = Math.floor(((e.clientX - r.left) / r.width) * N);
|
||||
const y = Math.floor(((e.clientY - r.top) / r.height) * N);
|
||||
return x >= 0 && y >= 0 && x < N && y < N ? [x, y] : null;
|
||||
}
|
||||
|
||||
function verbinde2D() {
|
||||
const c = el('se-2d-canvas');
|
||||
c.addEventListener('pointerdown', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
const t = texelAus2D(e);
|
||||
if (!t) return;
|
||||
e.preventDefault();
|
||||
try { c.setPointerCapture(e.pointerId); } catch { /* egal */ }
|
||||
if (e.altKey) { nimmFarbe(t[0], t[1]); return; }
|
||||
beginneStrich();
|
||||
ed.strich.zweiD = true;
|
||||
wendeAn(t[0], t[1]);
|
||||
});
|
||||
c.addEventListener('pointermove', (e) => {
|
||||
const t = texelAus2D(e);
|
||||
if (ed.strich && t) wendeAn(t[0], t[1]);
|
||||
const neu = t && flaecheBei(t[0], t[1]) ? t : null;
|
||||
if (String(neu) !== String(ed.hover)) { ed.hover = neu; planeZeichnen(); }
|
||||
});
|
||||
const ende = () => beendeStrich();
|
||||
c.addEventListener('pointerup', ende);
|
||||
c.addEventListener('pointercancel', ende);
|
||||
c.addEventListener('pointerleave', () => { if (!ed.strich && ed.hover) { ed.hover = null; planeZeichnen(); } });
|
||||
}
|
||||
|
||||
// ---------- 3D: Strahl gegen die Figur ----------
|
||||
// three.js' Raycaster wird von skinview3d nicht mitgeliefert – daher eigene
|
||||
// Schnittprüfung Strahl/Dreieck (Möller–Trumbore) gegen die Quader der Figur.
|
||||
function schnitt(o, d, a, b, c) {
|
||||
const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
|
||||
const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
|
||||
const px = d.y * e2z - d.z * e2y, py = d.z * e2x - d.x * e2z, pz = d.x * e2y - d.y * e2x;
|
||||
const det = e1x * px + e1y * py + e1z * pz;
|
||||
if (Math.abs(det) < 1e-9) return null;
|
||||
const inv = 1 / det;
|
||||
const tx = o.x - a.x, ty = o.y - a.y, tz = o.z - a.z;
|
||||
const u = (tx * px + ty * py + tz * pz) * inv;
|
||||
if (u < 0 || u > 1) return null;
|
||||
const qx = ty * e1z - tz * e1y, qy = tz * e1x - tx * e1z, qz = tx * e1y - ty * e1x;
|
||||
const v = (d.x * qx + d.y * qy + d.z * qz) * inv;
|
||||
if (v < 0 || u + v > 1) return null;
|
||||
const t = (e2x * qx + e2y * qy + e2z * qz) * inv;
|
||||
return t > 1e-6 ? { t, u, v } : null;
|
||||
}
|
||||
|
||||
// liefert den Texturpixel der getroffenen Schicht (0/1) oder null
|
||||
function strahl(clientX, clientY, schicht) {
|
||||
const v = ed.viewer;
|
||||
if (!v) return null;
|
||||
const r = v.canvas.getBoundingClientRect();
|
||||
if (!r.width || !r.height) return null;
|
||||
const cam = v.camera;
|
||||
const V3 = cam.position.constructor;
|
||||
cam.updateMatrixWorld();
|
||||
const o = new V3().setFromMatrixPosition(cam.matrixWorld);
|
||||
const nx = ((clientX - r.left) / r.width) * 2 - 1;
|
||||
const ny = -((clientY - r.top) / r.height) * 2 + 1;
|
||||
const d = new V3(nx, ny, 0.5).unproject(cam).sub(o).normalize();
|
||||
v.playerObject.updateMatrixWorld(true);
|
||||
const ecken = [new V3(), new V3(), new V3()];
|
||||
let best = null;
|
||||
for (const teil of v.playerObject.skin.getBodyParts()) {
|
||||
if (!teil.visible) continue;
|
||||
const mesh = schicht ? teil.outerLayer : teil.innerLayer;
|
||||
if (!mesh || !mesh.visible) continue;
|
||||
const geo = mesh.geometry;
|
||||
const pos = geo.attributes.position;
|
||||
const idx = geo.index;
|
||||
const anzahl = idx ? idx.count : pos.count;
|
||||
for (let i = 0; i < anzahl; i += 3) {
|
||||
const ia = idx ? [idx.getX(i), idx.getX(i + 1), idx.getX(i + 2)] : [i, i + 1, i + 2];
|
||||
for (let k = 0; k < 3; k++) ecken[k].fromBufferAttribute(pos, ia[k]).applyMatrix4(mesh.matrixWorld);
|
||||
const h = schnitt(o, d, ecken[0], ecken[1], ecken[2]);
|
||||
if (h && (!best || h.t < best.t)) best = { t: h.t, b: [1 - h.u - h.v, h.u, h.v], ia, uv: geo.attributes.uv };
|
||||
}
|
||||
}
|
||||
if (!best) return null;
|
||||
let U = 0, W = 0;
|
||||
let uMin = 1, uMax = 0, vMin = 1, vMax = 0;
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const u = best.uv.getX(best.ia[k]);
|
||||
const w = best.uv.getY(best.ia[k]);
|
||||
U += best.b[k] * u; W += best.b[k] * w;
|
||||
uMin = Math.min(uMin, u); uMax = Math.max(uMax, u); vMin = Math.min(vMin, w); vMax = Math.max(vMax, w);
|
||||
}
|
||||
// Dreieck gehört zu genau einer Seitenfläche – Rundung an der Kante darf
|
||||
// nicht in die Nachbarfläche rutschen
|
||||
const x0 = Math.round(uMin * N), x1 = Math.round(uMax * N) - 1;
|
||||
const y0 = Math.round((1 - vMax) * N), y1 = Math.round((1 - vMin) * N) - 1;
|
||||
const x = Math.min(x1, Math.max(x0, Math.floor(U * N)));
|
||||
const y = Math.min(y1, Math.max(y0, Math.floor((1 - W) * N)));
|
||||
return { t: best.t, x, y };
|
||||
}
|
||||
|
||||
// sichtbarer Pixel für die Pipette: erst die 2. Schicht (falls dort Farbe ist)
|
||||
function sichtbarerPixel(cx, cy) {
|
||||
if (ed.zeigeAussen) {
|
||||
const a = strahl(cx, cy, 1);
|
||||
if (a && ed.daten[(a.y * N + a.x) * 4 + 3] > 0) return [a.x, a.y];
|
||||
}
|
||||
const b = strahl(cx, cy, 0);
|
||||
return b ? [b.x, b.y] : null;
|
||||
}
|
||||
|
||||
function verbinde3D() {
|
||||
const huelle = el('se-3d');
|
||||
const canvas = el('se-3d-canvas');
|
||||
// in der Einfangphase: trifft die Maus die Figur, malen wir und die
|
||||
// Drehsteuerung von skinview3d bekommt das Ereignis gar nicht erst
|
||||
huelle.addEventListener('pointerdown', (e) => {
|
||||
if (e.button !== 0 || !ed.viewer) return;
|
||||
if (e.altKey || ed.werkzeug === 'pipette') {
|
||||
const t = sichtbarerPixel(e.clientX, e.clientY);
|
||||
if (!t) return;
|
||||
e.stopPropagation(); e.preventDefault();
|
||||
nimmFarbe(t[0], t[1]);
|
||||
return;
|
||||
}
|
||||
const t = strahl(e.clientX, e.clientY, ed.schicht);
|
||||
if (!t) return;
|
||||
e.stopPropagation(); e.preventDefault();
|
||||
try { canvas.setPointerCapture(e.pointerId); } catch { /* egal */ }
|
||||
beginneStrich();
|
||||
ed.strich.letzterBild = [e.clientX, e.clientY];
|
||||
wendeAn(t.x, t.y);
|
||||
}, true);
|
||||
canvas.addEventListener('pointermove', (e) => {
|
||||
if (ed.strich) {
|
||||
// Strecke seit dem letzten Ereignis in kleinen Schritten abtasten
|
||||
const [ax, ay] = ed.strich.letzterBild || [e.clientX, e.clientY];
|
||||
const schritte = Math.max(1, Math.ceil(Math.hypot(e.clientX - ax, e.clientY - ay) / 2));
|
||||
for (let i = 1; i <= schritte; i++) {
|
||||
const t = strahl(ax + ((e.clientX - ax) * i) / schritte, ay + ((e.clientY - ay) * i) / schritte, ed.schicht);
|
||||
if (t) wendeAn(t.x, t.y);
|
||||
}
|
||||
ed.strich.letzterBild = [e.clientX, e.clientY];
|
||||
}
|
||||
if (e.buttons && !ed.strich) return; // dreht gerade
|
||||
const t = strahl(e.clientX, e.clientY, ed.schicht);
|
||||
const neu = t ? [t.x, t.y] : null;
|
||||
if (String(neu) !== String(ed.hover)) { ed.hover = neu; planeZeichnen(); }
|
||||
});
|
||||
const ende = () => beendeStrich();
|
||||
canvas.addEventListener('pointerup', ende);
|
||||
canvas.addEventListener('pointercancel', ende);
|
||||
canvas.addEventListener('pointerleave', () => { if (!ed.strich && ed.hover) { ed.hover = null; planeZeichnen(); } });
|
||||
}
|
||||
|
||||
async function erzeugeViewer() {
|
||||
if (ed.viewer || typeof skinview3d === 'undefined') {
|
||||
if (typeof skinview3d === 'undefined') el('se-3d-leer').textContent = '3D-Ansicht nicht verfügbar';
|
||||
return;
|
||||
}
|
||||
const h = el('se-3d');
|
||||
const viewer = new skinview3d.SkinViewer({
|
||||
canvas: el('se-3d-canvas'),
|
||||
width: h.clientWidth || 300,
|
||||
height: h.clientHeight || 300,
|
||||
skin: textur,
|
||||
model: ed.slim ? 'slim' : 'default',
|
||||
});
|
||||
await new Promise((resolve) => {
|
||||
const check = (n) => {
|
||||
if ((viewer.playerObject && viewer.playerObject.skin && viewer.playerObject.skin.map) || n > 150) resolve();
|
||||
else setTimeout(() => check(n + 1), 40);
|
||||
};
|
||||
check(0);
|
||||
});
|
||||
viewer.fov = 36;
|
||||
viewer.zoom = 0.78;
|
||||
viewer.autoRotate = false;
|
||||
viewer.animation = null;
|
||||
// leicht abgespreizte Arme/Beine, damit man auch an die Innenseiten kommt
|
||||
const p = viewer.playerObject;
|
||||
p.rotation.set(0, -0.35, 0);
|
||||
p.skin.leftArm.rotation.set(0, 0, 0.2);
|
||||
p.skin.rightArm.rotation.set(0, 0, -0.2);
|
||||
p.skin.leftLeg.rotation.set(0, 0, 0.06);
|
||||
p.skin.rightLeg.rotation.set(0, 0, -0.06);
|
||||
viewer.controls.enableRotate = true;
|
||||
viewer.controls.enableZoom = true;
|
||||
viewer.controls.enablePan = false;
|
||||
// rechte Maustaste dreht immer – auch wenn man gerade auf die Figur zeigt
|
||||
viewer.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 0 };
|
||||
ed.viewer = viewer;
|
||||
el('se-3d-leer').classList.add('hidden');
|
||||
passeGroessenAn();
|
||||
}
|
||||
|
||||
async function setzeArmform(slim) {
|
||||
ed.slim = !!slim;
|
||||
el('se-variante').value = ed.slim ? 'slim' : 'classic';
|
||||
ed.hover = null;
|
||||
if (ed.viewer) {
|
||||
await ed.viewer.loadSkin(textur, { model: ed.slim ? 'slim' : 'default' });
|
||||
}
|
||||
texturGeaendert();
|
||||
}
|
||||
|
||||
// ---------- Laden / Vorlage ----------
|
||||
function vorlage() {
|
||||
const d = new Uint8ClampedArray(N * N * 4);
|
||||
const farben = { head: '#c69c6d', body: '#2f9e9e', rightArm: '#c69c6d', leftArm: '#c69c6d', rightLeg: '#3b3f9e', leftLeg: '#3b3f9e' };
|
||||
for (const f of AUFTEILUNG[ed.slim ? 1 : 0].flaechen) {
|
||||
if (f.schicht !== 0) continue;
|
||||
const [r, g, b] = hexZuRgb(farben[f.teil.key]);
|
||||
for (let yy = f.y; yy < f.y + f.h; yy++) {
|
||||
for (let xx = f.x; xx < f.x + f.w; xx++) {
|
||||
const i = (yy * N + xx) * 4;
|
||||
d[i] = r; d[i + 1] = g; d[i + 2] = b; d[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
function ladeBild(dataUri) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (img.width !== N || (img.height !== N && img.height !== N / 2)) {
|
||||
reject(new Error(`Ein Skin muss 64×64 oder 64×32 Pixel groß sein (diese Datei: ${img.width}×${img.height}).`));
|
||||
return;
|
||||
}
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = N;
|
||||
const g = c.getContext('2d', { willReadFrequently: true });
|
||||
g.drawImage(img, 0, 0);
|
||||
const daten = g.getImageData(0, 0, N, N).data;
|
||||
if (img.height === N / 2) {
|
||||
// alte 64×32-Skins: linker Arm und linkes Bein sind dort gespiegelte
|
||||
// Kopien der rechten – wie Minecraft es selbst macht
|
||||
const karte = AUFTEILUNG[0].karte;
|
||||
for (let i = 0; i < N * N / 2; i++) {
|
||||
const f = karte[i];
|
||||
if (!f || f.schicht !== 0 || (f.teil.key !== 'rightArm' && f.teil.key !== 'rightLeg')) continue;
|
||||
const partner = TEIL[f.teil.spiegel];
|
||||
const seite = SEITE_GEGENSTUECK[f.seite] || f.seite;
|
||||
const r = seiten(partner, 0, false)[seite];
|
||||
const x = i % N, y = Math.floor(i / N);
|
||||
const z = ((r[1] + (y - f.y)) * N + r[0] + (r[2] - 1 - (x - f.x))) * 4;
|
||||
for (let k = 0; k < 4; k++) daten[z + k] = daten[i * 4 + k];
|
||||
}
|
||||
}
|
||||
resolve(daten);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Bild konnte nicht gelesen werden.'));
|
||||
img.src = dataUri;
|
||||
});
|
||||
}
|
||||
|
||||
async function uebernimmDaten(daten, variante, text) {
|
||||
if (variante) await setzeArmform(variante === 'slim');
|
||||
aenderung(() => { ed.daten = new Uint8ClampedArray(daten); });
|
||||
ed.geaendert = false;
|
||||
texturGeaendert();
|
||||
status(text || '');
|
||||
}
|
||||
|
||||
function wennVerwerfenOk(aktion) {
|
||||
if (!ed.geaendert) { aktion(); return; }
|
||||
askConfirm('Änderungen verwerfen?', 'Der Skin im Editor hat ungespeicherte Änderungen. Trotzdem fortfahren?', 'Verwerfen', aktion);
|
||||
}
|
||||
|
||||
async function ladeAktuellen() {
|
||||
status('Aktueller Skin wird geladen …');
|
||||
const res = await window.api.skinCurrentData();
|
||||
if (!res.ok) {
|
||||
if (res.standard) {
|
||||
await uebernimmDaten(vorlage(), res.variante, 'Dein Konto nutzt den Standard-Skin – hier ist eine leere Vorlage zum Loslegen.');
|
||||
} else {
|
||||
status('✖ ' + (res.message || 'Skin konnte nicht geladen werden.'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await uebernimmDaten(await ladeBild(res.dataUri), res.variante, `Skin von ${res.name || 'deinem Konto'} geladen.`);
|
||||
} catch (err) { status('✖ ' + err.message); }
|
||||
}
|
||||
|
||||
async function ladeDatei(pfad, variante) {
|
||||
const res = pfad ? await window.api.skinReadData(pfad) : await window.api.skinChoose();
|
||||
if (res.canceled) return;
|
||||
if (!res.ok) { status('✖ ' + (res.message || 'Datei konnte nicht geladen werden.')); return; }
|
||||
try {
|
||||
await uebernimmDaten(await ladeBild(res.dataUri), variante, `${(res.pfad || pfad || '').split(/[\\/]/).pop()} geladen.`);
|
||||
} catch (err) { status('✖ ' + err.message); }
|
||||
}
|
||||
|
||||
// ---------- Speichern / Übernehmen ----------
|
||||
function alsBase64() {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = N;
|
||||
c.getContext('2d').putImageData(new ImageData(ed.daten, N, N), 0, 0);
|
||||
return c.toDataURL('image/png').split(',')[1];
|
||||
}
|
||||
|
||||
// Minecraft zeigt durchsichtige Pixel der Grundschicht schwarz an
|
||||
function loecherInGrundschicht() {
|
||||
let n = 0;
|
||||
for (const f of aufteilung().flaechen) {
|
||||
if (f.schicht !== 0) continue;
|
||||
for (let yy = f.y; yy < f.y + f.h; yy++) for (let xx = f.x; xx < f.x + f.w; xx++) if (ed.daten[(yy * N + xx) * 4 + 3] < 255) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function loecherHinweis() {
|
||||
const n = loecherInGrundschicht();
|
||||
return n ? ` Hinweis: ${n} durchsichtige Pixel in der Grundschicht erscheinen im Spiel schwarz.` : '';
|
||||
}
|
||||
|
||||
async function speichere() {
|
||||
const res = await window.api.skinSaveData(alsBase64());
|
||||
if (res.canceled) return;
|
||||
if (!res.ok) { status('✖ ' + (res.message || 'Speichern fehlgeschlagen.')); return; }
|
||||
ed.geaendert = false;
|
||||
status('✔ Gespeichert: ' + res.pfad.split(/[\\/]/).pop() + '.' + loecherHinweis());
|
||||
}
|
||||
|
||||
async function uebernimm() {
|
||||
const btn = el('se-uebernehmen');
|
||||
btn.disabled = true;
|
||||
status('Wird hochgeladen …');
|
||||
try {
|
||||
const res = await window.api.skinApplyData(alsBase64(), ed.slim ? 'slim' : 'classic');
|
||||
if (res.ok) {
|
||||
ed.geaendert = false;
|
||||
status('✔ Skin gesetzt. Im Spiel kann es einen Moment dauern.' + loecherHinweis());
|
||||
toast('Skin geändert.');
|
||||
} else {
|
||||
status('✖ ' + (res.message || 'Fehlgeschlagen.'));
|
||||
}
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Öffnen ----------
|
||||
let verdrahtet = false;
|
||||
|
||||
function verdrahte() {
|
||||
if (verdrahtet) return;
|
||||
verdrahtet = true;
|
||||
document.querySelectorAll('[data-se-tool]').forEach((b) => b.addEventListener('click', () => waehleWerkzeug(b.dataset.seTool)));
|
||||
document.querySelectorAll('[data-se-schicht]').forEach((b) => b.addEventListener('click', () => waehleSchicht(Number(b.dataset.seSchicht))));
|
||||
document.querySelectorAll('[data-se-modus]').forEach((b) => b.addEventListener('click', () => waehleModus(b.dataset.seModus)));
|
||||
el('se-farbe').addEventListener('input', () => setzeFarbe(el('se-farbe').value));
|
||||
el('se-hex').addEventListener('change', () => {
|
||||
let v = el('se-hex').value.trim();
|
||||
if (!v.startsWith('#')) v = '#' + v;
|
||||
if (/^#[0-9a-f]{6}$/i.test(v)) setzeFarbe(v);
|
||||
else el('se-hex').value = ed.farbe;
|
||||
});
|
||||
el('se-aussen').addEventListener('change', () => {
|
||||
ed.zeigeAussen = el('se-aussen').checked;
|
||||
if (!ed.zeigeAussen && ed.schicht === 1) waehleSchicht(0);
|
||||
planeZeichnen();
|
||||
});
|
||||
el('se-durch').addEventListener('change', () => { ed.durchscheinen = el('se-durch').checked; planeZeichnen(); });
|
||||
el('se-spiegeln').addEventListener('change', () => { ed.spiegeln = el('se-spiegeln').checked; planeZeichnen(); });
|
||||
el('se-leeren').addEventListener('click', () => {
|
||||
askConfirm('2. Schicht leeren?', 'Alle Pixel der 2. Schicht werden durchsichtig. Mit Strg+Z lässt sich das rückgängig machen.', 'Leeren', () => {
|
||||
const geaendert = aenderung(() => {
|
||||
for (const f of aufteilung().flaechen) {
|
||||
if (f.schicht !== 1) continue;
|
||||
for (let yy = f.y; yy < f.y + f.h; yy++) for (let xx = f.x; xx < f.x + f.w; xx++) setzePixel(xx, yy, [0, 0, 0, 0]);
|
||||
}
|
||||
});
|
||||
status(geaendert ? '2. Schicht geleert.' : 'Die 2. Schicht war schon leer.');
|
||||
});
|
||||
});
|
||||
el('se-variante').addEventListener('change', () => setzeArmform(el('se-variante').value === 'slim'));
|
||||
el('se-undo').addEventListener('click', rueckgaengig);
|
||||
el('se-redo').addEventListener('click', wiederholen);
|
||||
el('se-neu').addEventListener('click', () => wennVerwerfenOk(() => uebernimmDaten(vorlage(), null, 'Neue Vorlage – leg los!')));
|
||||
el('se-laden').addEventListener('click', () => wennVerwerfenOk(ladeAktuellen));
|
||||
el('se-oeffnen').addEventListener('click', () => wennVerwerfenOk(() => ladeDatei(null, null)));
|
||||
el('se-speichern').addEventListener('click', speichere);
|
||||
el('se-uebernehmen').addEventListener('click', uebernimm);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (el('modal-skineditor').classList.contains('hidden')) return;
|
||||
if (!el('modal-confirm').classList.contains('hidden')) return;
|
||||
const a = document.activeElement;
|
||||
if (a && (['INPUT', 'TEXTAREA', 'SELECT'].includes(a.tagName) && a.type !== 'checkbox')) return;
|
||||
const strg = e.ctrlKey || e.metaKey;
|
||||
const k = e.key.toLowerCase();
|
||||
if (strg && k === 'z' && !e.shiftKey) { e.preventDefault(); rueckgaengig(); return; }
|
||||
if (strg && (k === 'y' || (k === 'z' && e.shiftKey))) { e.preventDefault(); wiederholen(); return; }
|
||||
if (strg || e.altKey) return;
|
||||
const werkzeug = { b: 'stift', e: 'radierer', i: 'pipette', g: 'fuellen' }[k];
|
||||
if (werkzeug) { e.preventDefault(); waehleWerkzeug(werkzeug); return; }
|
||||
if (k === 'm') { e.preventDefault(); el('se-spiegeln').click(); return; }
|
||||
if (k === 'l') { e.preventDefault(); waehleSchicht(ed.schicht ? 0 : 1); }
|
||||
});
|
||||
|
||||
verbinde2D();
|
||||
verbinde3D();
|
||||
new ResizeObserver(() => passeGroessenAn()).observe(el('se-buehne'));
|
||||
new ResizeObserver(() => passeGroessenAn()).observe(el('se-mini'));
|
||||
// Hell/Dunkel gewechselt -> Hintergrund neu
|
||||
new MutationObserver(() => planeZeichnen()).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
}
|
||||
|
||||
async function openSkinEditor(p = {}) {
|
||||
if (POPOUT !== 'skineditor') { window.api.popoutOpen('skineditor', p); return; }
|
||||
verdrahte();
|
||||
show('modal-skineditor');
|
||||
if (!ed.bereit) {
|
||||
ed.bereit = true;
|
||||
el('se-farbe').value = ed.farbe;
|
||||
el('se-hex').value = ed.farbe;
|
||||
zeigeZuletzt();
|
||||
waehleWerkzeug('stift');
|
||||
waehleSchicht(0);
|
||||
aktualisiereKnoepfe();
|
||||
ed.daten = vorlage();
|
||||
texturGeaendert();
|
||||
waehleModus('2d');
|
||||
await erzeugeViewer();
|
||||
texturGeaendert();
|
||||
}
|
||||
const variante = p.variante === 'slim' || p.variante === 'classic' ? p.variante : null;
|
||||
if (p.pfad) wennVerwerfenOk(() => ladeDatei(p.pfad, variante));
|
||||
else if (p.quelle === 'aktuell') wennVerwerfenOk(ladeAktuellen);
|
||||
else if (variante && !ed.geaendert) await setzeArmform(variante === 'slim');
|
||||
}
|
||||
|
||||
window.openSkinEditor = openSkinEditor;
|
||||
// für Tests
|
||||
window.__skinEditor = { ed, strahl, spiegelPixel, seiten, AUFTEILUNG };
|
||||
})();
|
||||
Reference in New Issue
Block a user