Files
AeroMc-Launcher/src/skineditor.js
T
2026-10-03 14:20:38 +00:00

1533 lines
63 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/*
* skineditor.js – Pixel-Editor für Minecraft-Skins und Umhänge
* -------------------------------------------------------------
* 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.
*
* Ein Umhang ist eine Textur im Verhältnis 2:1 – normal 64×32, als HD bis
* 1024×512. Dieselbe Datei trägt auch die Elytra. Eigene Umhänge landen auf
* dem AeroMC-Umhang-Server (Mojang nimmt keine eigenen Umhänge an).
*
* Gemalt wird auf der aufgeklappten Vorlage (2D) oder direkt auf der drehbaren
* Figur (3D); die jeweils andere Ansicht steht klein rechts und ist ebenfalls
* bemalbar. Skin und Umhang sind getrennte "Dokumente" mit eigener Größe und
* eigenem Rückgängig-Verlauf – Werkzeuge, Farben und Spiegeln teilen sie sich.
*
* Läuft im Popup-Fenster "skineditor" und nutzt die Helfer aus renderer.js
* (el, show, toast, askConfirm, POPOUT).
*/
(() => {
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 };
});
// ---------- Umhang: Flächen in Einheiten der 64×32-Vorlage (×k für HD) ----------
const UMHANG_STUFEN = [64, 128, 256, 512, 1024];
const UMHANG_FLAECHEN = [
{ name: 'Außenseite', x: 1, y: 1, w: 10, h: 16 },
{ name: 'Innenseite', x: 12, y: 1, w: 10, h: 16 },
{ name: 'Oberkante', x: 1, y: 0, w: 10, h: 1 },
{ name: 'Unterkante', x: 11, y: 0, w: 10, h: 1 },
{ name: 'Rechte Kante', x: 0, y: 1, w: 1, h: 16, spiegel: 'Linke Kante' },
{ name: 'Linke Kante', x: 11, y: 1, w: 1, h: 16, spiegel: 'Rechte Kante' },
// die Elytra nutzt dieselbe Datei: ein Flügel als Quader 10×20×2 ab (22, 0)
{ name: 'Elytra oben', x: 24, y: 0, w: 10, h: 2, elytra: true },
{ name: 'Elytra unten', x: 34, y: 0, w: 10, h: 2, elytra: true },
{ name: 'Elytra rechte Kante', x: 22, y: 2, w: 2, h: 20, elytra: true, spiegel: 'Elytra linke Kante' },
{ name: 'Elytra außen', x: 24, y: 2, w: 10, h: 20, elytra: true },
{ name: 'Elytra linke Kante', x: 34, y: 2, w: 2, h: 20, elytra: true, spiegel: 'Elytra rechte Kante' },
{ name: 'Elytra innen', x: 36, y: 2, w: 10, h: 20, elytra: true },
];
const umhangAufteilungen = new Map(); // Breite -> { flaechen, karte }
function umhangLayout(w) {
if (umhangAufteilungen.has(w)) return umhangAufteilungen.get(w);
const k = w / 64;
const h = w / 2;
const flaechen = UMHANG_FLAECHEN.map((f) => ({ ...f, umhang: true, x: f.x * k, y: f.y * k, w: f.w * k, h: f.h * k }));
const karte = new Int16Array(w * h).fill(-1);
flaechen.forEach((f, i) => {
for (let yy = 0; yy < f.h; yy++) karte.fill(i, (f.y + yy) * w + f.x, (f.y + yy) * w + f.x + f.w);
});
const ergebnis = { flaechen, karte };
umhangAufteilungen.set(w, ergebnis);
return ergebnis;
}
// ---------- Dokumente ----------
function neuesDok(art, w, h) {
return { art, w, h, daten: new Uint8ClampedArray(w * h * 4), undo: [], redo: [], geaendert: false };
}
const skinDok = neuesDok('skin', N, N);
const umhangDok = neuesDok('umhang', 64, 32);
// Rückgängig-Verlauf je Dokument auf ~64 MB begrenzen (HD-Umhänge sind groß)
const maxUndo = (d) => Math.max(10, Math.min(100, Math.floor(64e6 / (d.w * d.h * 4))));
const schnappschuss = (d) => ({ w: d.w, h: d.h, daten: d.daten.slice() });
// ---------- Zustand ----------
const ed = {
slim: false,
werkzeug: 'stift',
vorPipette: 'stift',
farbe: '#3a6ea5',
schicht: 0,
zeigeAussen: true,
durchscheinen: true,
spiegeln: false,
pinsel: 1,
elytra: false,
modus: '2d',
hover: null,
strich: null, // { dok, snap, letzter: [x,y] | null, letzterBild: [cx,cy] | null, geaendert }
viewer: null,
bereit: false,
zeichnenGeplant: false,
};
const dok = () => (ed.modus === 'umhang' ? umhangDok : skinDok);
const textur = document.createElement('canvas');
textur.width = textur.height = N;
const texturCtx = textur.getContext('2d', { willReadFrequently: true });
const umhangTextur = document.createElement('canvas');
umhangTextur.width = 64;
umhangTextur.height = 32;
// 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 dunkel = () => document.documentElement.getAttribute('data-theme') === 'dark';
function flaecheBei(x, y) {
const d = dok();
if (x < 0 || y < 0 || x >= d.w || y >= d.h) return null;
if (d.art === 'umhang') {
const L = umhangLayout(d.w);
const i = L.karte[y * d.w + x];
return i < 0 ? null : L.flaechen[i];
}
return aufteilung().karte[y * N + x];
}
const darfMalen = (f) => !!f && (f.umhang || f.schicht === ed.schicht);
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 Seite: beim Skin rechter ↔ linker Arm/Bein und
// rechte ↔ linke Seite, beim Umhang innerhalb der Fläche bzw. Kante ↔ Kante
function spiegelPixel(x, y) {
const f = flaecheBei(x, y);
if (!f) return null;
const fx = x - f.x;
const fy = y - f.y;
if (f.umhang) {
const p = f.spiegel ? umhangLayout(dok().w).flaechen.find((g) => g.name === f.spiegel) : f;
return [p.x + (p.w - 1 - fx), p.y + fy];
}
const partner = TEIL[f.teil.spiegel];
const seite = SEITE_GEGENSTUECK[f.seite] || f.seite;
const r = seiten(partner, f.schicht, ed.slim)[seite];
return [r[0] + (r[2] - 1 - fx), r[1] + fy];
}
// ---------- Daten ändern ----------
function setzePixel(x, y, rgba) {
const d = dok();
const i = (y * d.w + x) * 4;
const p = d.daten;
if (p[i] === rgba[0] && p[i + 1] === rgba[1] && p[i + 2] === rgba[2] && p[i + 3] === rgba[3]) return false;
p[i] = rgba[0]; p[i + 1] = rgba[1]; p[i + 2] = rgba[2]; p[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;
}
// Pinsel (nur beim Umhang größer als 1): Quadrat um den Pixel, beschnitten auf die Fläche
function pinselPunkte(x, y, f) {
const p = dok().art === 'umhang' ? ed.pinsel : 1;
if (p <= 1) return [[x, y]];
const o = Math.floor((p - 1) / 2);
const punkte = [];
for (let yy = Math.max(f.y, y - o); yy < Math.min(f.y + f.h, y - o + p); yy++) {
for (let xx = Math.max(f.x, x - o); xx < Math.min(f.x + f.w, x - o + p); xx++) punkte.push([xx, yy]);
}
return punkte;
}
function malFarbe() {
if (ed.werkzeug === 'radierer') return [0, 0, 0, 0];
return [...hexZuRgb(ed.farbe), 255];
}
// Einen Pixel (samt Pinsel und Spiegelung) mit Stift oder Radierer bearbeiten
function malePixel(x, y) {
const f = flaecheBei(x, y);
if (!darfMalen(f)) return false;
let geaendert = false;
const rgba = malFarbe();
for (const [bx, by] of pinselPunkte(x, y, f)) {
for (const [px, py] of zielPixel(bx, by)) 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 (!darfMalen(f)) return false;
const d = dok();
const p = d.daten;
const i0 = (y * d.w + x) * 4;
const alt = [p[i0], p[i0 + 1], p[i0 + 2], p[i0 + 3]];
const neu = malFarbe();
if (alt.every((v, k) => v === neu[k])) return false;
const gleich = (px, py) => {
const i = (py * d.w + px) * 4;
return p[i] === alt[0] && p[i + 1] === alt[1] && p[i + 2] === alt[2] && p[i + 3] === alt[3];
};
const gesehen = new Uint8Array(d.w * d.h);
const stapel = [[x, y]];
const treffer = [];
while (stapel.length) {
const [px, py] = stapel.pop();
if (px < f.x || py < f.y || px >= f.x + f.w || py >= f.y + f.h) continue;
const k = py * d.w + px;
if (gesehen[k]) continue;
gesehen[k] = 1;
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 d = dok();
const i = (y * d.w + x) * 4;
if (d.daten[i + 3] === 0) return false;
setzeFarbe(rgbZuHex(d.daten[i], d.daten[i + 1], d.daten[i + 2]));
return true;
}
// ---------- Striche, Rückgängig ----------
function merkeRueckgaengig(d, vorher) {
d.undo.push(vorher);
while (d.undo.length > maxUndo(d)) d.undo.shift();
d.redo = [];
d.geaendert = true;
}
function beginneStrich() {
ed.strich = { dok: dok(), snap: schnappschuss(dok()), letzter: null, letzterBild: null, geaendert: false };
}
function beendeStrich() {
const s = ed.strich;
ed.strich = null;
if (!s || !s.geaendert) return;
merkeRueckgaengig(s.dok, s.snap);
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 || s.dok !== dok()) 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 stelleHer(d, sn) {
const andereGroesse = sn.w !== d.w || sn.h !== d.h;
d.w = sn.w; d.h = sn.h; d.daten = sn.daten;
d.geaendert = true;
if (andereGroesse) nachGroessenwechsel();
texturGeaendert();
aktualisiereKnoepfe();
}
function rueckgaengig() {
const d = dok();
if (!d.undo.length || ed.strich) return;
d.redo.push(schnappschuss(d));
stelleHer(d, d.undo.pop());
}
function wiederholen() {
const d = dok();
if (!d.redo.length || ed.strich) return;
d.undo.push(schnappschuss(d));
stelleHer(d, d.redo.pop());
}
// ganze Änderung (Laden, Leeren, Größe …) als ein Rückgängig-Schritt
function aenderung(fn) {
const d = dok();
const vorher = schnappschuss(d);
fn();
const gleich = vorher.w === d.w && vorher.h === d.h && vorher.daten.every((v, i) => v === d.daten[i]);
if (gleich) return false;
merkeRueckgaengig(d, vorher);
if (vorher.w !== d.w) nachGroessenwechsel();
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 ----------
const radiererGesperrt = () => dok().art === 'skin' && ed.schicht === 0;
function waehleWerkzeug(w) {
if (w === 'radierer' && radiererGesperrt()) 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'), el('se-u2d')]) h.dataset.werkzeug = w;
}
function aktualisiereRadierer() {
const radierer = document.querySelector('[data-se-tool="radierer"]');
const gesperrt = radiererGesperrt();
radierer.disabled = gesperrt;
radierer.title = gesperrt
? 'Radierer – nur in der 2. Schicht (die Grundschicht darf keine Löcher haben)'
: 'Radierer (E) – macht Pixel durchsichtig';
if (gesperrt && ed.werkzeug === 'radierer') waehleWerkzeug('stift');
}
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; }
aktualisiereRadierer();
el('se-durch').disabled = s === 0;
ed.hover = null;
texturGeaendert();
}
const MINI_TITEL = { '2d': '3D-Vorschau', '3d': 'Vorlage', umhang: '3D-Vorschau (Rücken)' };
const TIPP = {
skin: 'Beide Ansichten sind bemalbar. Die <b>2. Schicht</b> liegt wie eine Hülle über der Figur – lässt man Pixel darin frei, wirken Haare, Kapuzen oder Ärmel plastisch.',
umhang: 'Auch die Figur ist bemalbar. Eigene Umhänge liegen auf dem AeroMC-Umhang-Server – im Spiel sehen sie Spieler mit dem AeroMC-Umhang-Mod. Die Datei trägt auch die <b>Elytra</b>.',
};
function waehleModus(m) {
const warUmhang = ed.modus === 'umhang';
if (ed.strich) beendeStrich();
ed.modus = m;
ed.hover = null;
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');
const ablage = el('se-ablage');
if (m === 'umhang') {
gross.appendChild(el('se-u2d'));
klein.appendChild(el('se-3d'));
ablage.appendChild(el('se-2d'));
} else {
gross.appendChild(m === '2d' ? el('se-2d') : el('se-3d'));
klein.appendChild(m === '2d' ? el('se-3d') : el('se-2d'));
ablage.appendChild(el('se-u2d'));
}
el('se-mini-titel').textContent = MINI_TITEL[m];
const umhang = m === 'umhang';
el('se-gruppe-ebene').classList.toggle('hidden', umhang);
el('se-gruppe-umhang').classList.toggle('hidden', !umhang);
el('se-laden').textContent = umhang ? 'Aktuellen Umhang laden' : 'Aktuellen Skin laden';
el('se-laden').title = umhang ? 'Deinen AeroMC-Umhang bzw. den getragenen offiziellen Umhang laden' : 'Den Skin deines angemeldeten Kontos laden';
el('se-neu').title = umhang ? 'Mit einer einfachen Umhang-Vorlage beginnen' : 'Mit einer leeren Vorlage beginnen';
el('se-uebernehmen').textContent = umhang ? 'Als Umhang übernehmen' : 'Als Skin übernehmen';
el('se-tipp').innerHTML = umhang ? TIPP.umhang : TIPP.skin;
aktualisiereRadierer();
aktualisiereKnoepfe();
if (warUmhang !== umhang) zeigeUmhangAufFigur(umhang);
texturGeaendert();
passeGroessenAn();
}
function aktualisiereKnoepfe() {
el('se-undo').disabled = !dok().undo.length;
el('se-redo').disabled = !dok().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.umhang
? `Umhang · ${f.name} · Pixel ${h[0]}, ${h[1]}`
: `${f.teil.name} · ${SEITE_NAME[f.seite]} · ${SCHICHT_NAME[f.schicht]} · Pixel ${h[0]}, ${h[1]}`;
return;
}
if (ed.modus === 'umhang') {
info.textContent = `${umhangDok.w}×${umhangDok.h} · Linke Maustaste malt · Mausrad zoomt · rechte Maustaste verschiebt · Alt+Klick nimmt Farbe auf`;
} else {
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() {
if (dok().art === 'umhang') {
if (umhangTextur.width !== umhangDok.w || umhangTextur.height !== umhangDok.h) {
umhangTextur.width = umhangDok.w;
umhangTextur.height = umhangDok.h;
}
umhangTextur.getContext('2d').putImageData(new ImageData(umhangDok.daten, umhangDok.w, umhangDok.h), 0, 0);
planeZeichnen();
return;
}
texturCtx.putImageData(new ImageData(skinDok.daten, N, N), 0, 0);
// Schatten: Grundschicht-Pixel an die Stelle der zugehörigen 2. Schicht legen
const s = schattenCtx.createImageData(N, N);
const p = skinDok.daten;
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] = p[q]; s.data[z + 1] = p[q + 1]; s.data[z + 2] = p[q + 2]; s.data[z + 3] = p[q + 3];
}
}
}
schattenCtx.putImageData(s, 0, 0);
planeZeichnen();
}
function planeZeichnen() {
if (ed.zeichnenGeplant) return;
ed.zeichnenGeplant = true;
requestAnimationFrame(() => {
ed.zeichnenGeplant = false;
if (ed.modus === 'umhang') zeichneUmhang2D();
else 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;
const p = skinDok.daten;
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] = p[q]; img.data[q + 1] = p[q + 1]; img.data[q + 2] = p[q + 2]; img.data[q + 3] = p[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([]);
}
}
}
}
// ---------- Umhang-Vorlage (2D) mit Zoom und Verschieben ----------
const ansicht = { zoom: 1, mx: 0.5, my: 0.5 }; // Mittelpunkt als Anteil der Textur
let karoMuster = null;
let karoDunkel = null;
function umhangTransform() {
const c = el('se-u2d-canvas');
const d = umhangDok;
const basis = Math.min(c.width / d.w, c.height / d.h) * 0.96;
const s = basis * ansicht.zoom;
return { s, ox: c.width / 2 - ansicht.mx * d.w * s, oy: c.height / 2 - ansicht.my * d.h * s };
}
function einpassen() {
ansicht.zoom = 1; ansicht.mx = 0.5; ansicht.my = 0.5;
planeZeichnen();
}
function zeichneUmhang2D() {
const c = el('se-u2d-canvas');
if (!c.width || !c.height) return;
const g = c.getContext('2d');
const d = umhangDok;
const L = umhangLayout(d.w);
const dpr = window.devicePixelRatio || 1;
const { s, ox, oy } = umhangTransform();
g.setTransform(1, 0, 0, 1, 0, 0);
g.clearRect(0, 0, c.width, c.height);
g.fillStyle = dunkel() ? '#1c1c1f' : '#c9ccd1';
g.fillRect(ox, oy, d.w * s, d.h * s);
// Karomuster in den Flächen (Durchsichtiges sichtbar machen)
if (!karoMuster || karoDunkel !== dunkel()) {
const kc = document.createElement('canvas');
kc.width = kc.height = 16;
const kg = kc.getContext('2d');
const [a, b] = dunkel() ? ['#3a3a40', '#303036'] : ['#ececee', '#dadbdf'];
kg.fillStyle = a; kg.fillRect(0, 0, 16, 16);
kg.fillStyle = b; kg.fillRect(0, 0, 8, 8); kg.fillRect(8, 8, 8, 8);
karoMuster = g.createPattern(kc, 'repeat');
karoDunkel = dunkel();
}
g.fillStyle = karoMuster;
for (const f of L.flaechen) g.fillRect(ox + f.x * s, oy + f.y * s, f.w * s, f.h * s);
g.imageSmoothingEnabled = false;
g.drawImage(umhangTextur, ox, oy, d.w * s, d.h * s);
// Elytra-Bereich leicht abgedunkelt, solange der Umhang gezeigt wird
if (!ed.elytra) {
g.fillStyle = dunkel() ? 'rgba(20,20,22,.35)' : 'rgba(40,44,52,.2)';
for (const f of L.flaechen) if (f.elytra) g.fillRect(ox + f.x * s, oy + f.y * s, f.w * s, f.h * s);
}
// Pixelraster nur, wenn die Pixel groß genug sind (sichtbarer Ausschnitt)
if (s >= 6 * dpr) {
const x0 = Math.max(0, Math.floor(-ox / s)), x1 = Math.min(d.w, Math.ceil((c.width - ox) / s));
const y0 = Math.max(0, Math.floor(-oy / s)), y1 = Math.min(d.h, Math.ceil((c.height - oy) / s));
g.strokeStyle = dunkel() ? 'rgba(255,255,255,.07)' : 'rgba(0,0,0,.08)';
g.lineWidth = 1;
g.beginPath();
for (let i = x0; i <= x1; i++) { const x = Math.round(ox + i * s) + 0.5; g.moveTo(x, oy + y0 * s); g.lineTo(x, oy + y1 * s); }
for (let i = y0; i <= y1; i++) { const y = Math.round(oy + i * s) + 0.5; g.moveTo(ox + x0 * s, y); g.lineTo(ox + x1 * s, y); }
g.stroke();
}
// Flächenränder und Beschriftung
g.lineWidth = 1.5 * dpr;
g.font = `${Math.round(11 * dpr)}px "Segoe UI", sans-serif`;
g.textBaseline = 'top';
for (const f of L.flaechen) {
g.strokeStyle = dunkel() ? 'rgba(255,255,255,.45)' : 'rgba(0,0,0,.4)';
g.strokeRect(ox + f.x * s, oy + f.y * s, f.w * s, f.h * s);
if (f.w * s > 80 * dpr && f.h * s > 24 * dpr) {
g.fillStyle = dunkel() ? 'rgba(255,255,255,.55)' : 'rgba(0,0,0,.5)';
g.fillText(f.name, ox + f.x * s + 4 * dpr, oy + f.y * s + 3 * dpr);
}
}
// Pinsel unter der Maus (+ gespiegelt)
if (ed.hover) {
const f = flaecheBei(ed.hover[0], ed.hover[1]);
if (f) {
g.lineWidth = 2 * dpr;
g.strokeStyle = dunkel() ? '#ffffff' : '#000000';
const rahmen = (punkte) => {
let ax = Infinity, ay = Infinity, bx = -Infinity, by = -Infinity;
for (const [x, y] of punkte) { ax = Math.min(ax, x); ay = Math.min(ay, y); bx = Math.max(bx, x); by = Math.max(by, y); }
if (ax !== Infinity) g.strokeRect(ox + ax * s, oy + ay * s, (bx - ax + 1) * s, (by - ay + 1) * s);
};
const punkte = ed.werkzeug === 'pipette' || ed.werkzeug === 'fuellen' ? [ed.hover] : pinselPunkte(ed.hover[0], ed.hover[1], f);
rahmen(punkte);
if (ed.spiegeln) {
const gesp = punkte.map(([x, y]) => spiegelPixel(x, y)).filter(Boolean);
g.setLineDash([4 * dpr, 4 * dpr]);
rahmen(gesp);
g.setLineDash([]);
}
}
}
}
function zeichne3D() {
const v = ed.viewer;
if (!v) return;
if (ed.modus === 'umhang') {
if (!v.capeTexture || v.capeCanvas.width !== umhangDok.w || v.capeCanvas.height !== umhangDok.h) return;
const g = v.capeCanvas.getContext('2d');
g.clearRect(0, 0, umhangDok.w, umhangDok.h);
g.drawImage(umhangTextur, 0, 0);
if (ed.hover && ed.werkzeug !== 'pipette') {
const f = flaecheBei(ed.hover[0], ed.hover[1]);
if (f) {
const punkte = ed.werkzeug === 'fuellen' ? [ed.hover] : pinselPunkte(ed.hover[0], ed.hover[1], f);
g.fillStyle = 'rgba(255,255,255,.6)';
for (const [x, y] of punkte) for (const [px, py] of zielPixel(x, y)) g.fillRect(px, py, 1, 1);
}
}
v.capeTexture.needsUpdate = true;
return;
}
if (!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 p = skinDok.daten;
const hell = p[i + 3] === 0 || (p[i] + p[i + 1] + p[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 dpr = window.devicePixelRatio || 1;
const h2 = el('se-2d');
if (h2.clientWidth) {
const c = el('se-2d-canvas');
const platz = Math.min(h2.clientWidth, h2.clientHeight) - 8;
const s = Math.max(1, Math.floor(platz / N));
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 hu = el('se-u2d');
if (hu.clientWidth) {
const c = el('se-u2d-canvas');
c.style.width = hu.clientWidth + 'px';
c.style.height = hu.clientHeight + 'px';
c.width = Math.round(hu.clientWidth * dpr);
c.height = Math.round(hu.clientHeight * dpr);
}
const h3 = el('se-3d');
if (ed.viewer && h3.clientWidth && h3.clientHeight) ed.viewer.setSize(h3.clientWidth, h3.clientHeight);
planeZeichnen();
}
// ---------- 2D-Eingabe (Skin) ----------
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 || ed.modus === 'umhang') 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) => {
if (ed.modus === 'umhang') return;
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(); } });
}
// ---------- 2D-Eingabe (Umhang): malen, zoomen, verschieben ----------
function bildpunkt(e) {
const c = el('se-u2d-canvas');
const r = c.getBoundingClientRect();
const dpr = c.width / (r.width || 1);
return [(e.clientX - r.left) * dpr, (e.clientY - r.top) * dpr];
}
function texelAusUmhang(e) {
const [px, py] = bildpunkt(e);
const { s, ox, oy } = umhangTransform();
const x = Math.floor((px - ox) / s);
const y = Math.floor((py - oy) / s);
return x >= 0 && y >= 0 && x < umhangDok.w && y < umhangDok.h ? [x, y] : null;
}
function verbindeUmhang2D() {
const c = el('se-u2d-canvas');
let schieben = null; // { x, y, mx, my }
c.addEventListener('contextmenu', (e) => e.preventDefault());
c.addEventListener('pointerdown', (e) => {
e.preventDefault();
try { c.setPointerCapture(e.pointerId); } catch { /* egal */ }
if (e.button === 1 || e.button === 2) {
const [px, py] = bildpunkt(e);
schieben = { x: px, y: py, mx: ansicht.mx, my: ansicht.my };
return;
}
if (e.button !== 0) return;
const t = texelAusUmhang(e);
if (!t) return;
if (e.altKey) { nimmFarbe(t[0], t[1]); return; }
beginneStrich();
ed.strich.zweiD = true;
wendeAn(t[0], t[1]);
});
c.addEventListener('pointermove', (e) => {
if (schieben) {
const [px, py] = bildpunkt(e);
const { s } = umhangTransform();
ansicht.mx = Math.min(1, Math.max(0, schieben.mx - (px - schieben.x) / (umhangDok.w * s)));
ansicht.my = Math.min(1, Math.max(0, schieben.my - (py - schieben.y) / (umhangDok.h * s)));
planeZeichnen();
return;
}
const t = texelAusUmhang(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 = () => { schieben = null; beendeStrich(); };
c.addEventListener('pointerup', ende);
c.addEventListener('pointercancel', ende);
c.addEventListener('pointerleave', () => { if (!ed.strich && ed.hover) { ed.hover = null; planeZeichnen(); } });
// Mausrad zoomt um die Mausposition
c.addEventListener('wheel', (e) => {
e.preventDefault();
const [px, py] = bildpunkt(e);
const vorher = umhangTransform();
const tx = (px - vorher.ox) / vorher.s;
const ty = (py - vorher.oy) / vorher.s;
const groesster = Math.max(8, umhangDok.w / 8);
ansicht.zoom = Math.min(groesster, Math.max(1, ansicht.zoom * (e.deltaY < 0 ? 1.2 : 1 / 1.2)));
const { s } = umhangTransform();
ansicht.mx = Math.min(1, Math.max(0, (c.width / 2 + tx * s - px) / (umhangDok.w * s)));
ansicht.my = Math.min(1, Math.max(0, (c.height / 2 + ty * s - py) / (umhangDok.h * s)));
planeZeichnen();
}, { passive: false });
}
// ---------- 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;
}
function sichtbareMeshes(objekt) {
const liste = [];
objekt.traverse((m) => { if (m.isMesh) liste.push(m); });
return liste.filter((m) => { for (let o = m; o; o = o.parent) if (o.visible === false) return false; return true; });
}
/*
* Texturpixel unter dem Mauspunkt. ziel: 0/1 = Skin-Schicht, 'umhang' = Umhang
* bzw. Elytra (die Figur selbst verdeckt dabei, man malt nicht durch sie hindurch).
*/
function strahl(clientX, clientY, ziel) {
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 teile = v.playerObject.skin.getBodyParts().filter((t) => t.visible);
let ziele;
let verdecker = [];
if (ziel === 'umhang') {
const p = v.playerObject;
ziele = sichtbareMeshes(p.backEquipment === 'elytra' ? p.elytra : p.cape);
verdecker = teile.map((t) => t.innerLayer).filter((m) => m && m.visible);
} else {
ziele = teile.map((t) => (ziel ? t.outerLayer : t.innerLayer)).filter((m) => m && m.visible);
}
const ecken = [new V3(), new V3(), new V3()];
let best = null;
const pruefe = (mesh, istZiel) => {
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, istZiel };
}
};
for (const m of ziele) pruefe(m, true);
for (const m of verdecker) pruefe(m, false);
if (!best || !best.istZiel) return null;
const W_ = ziel === 'umhang' ? umhangDok.w : N;
const H_ = ziel === 'umhang' ? umhangDok.h : N;
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 * W_), x1 = Math.round(uMax * W_) - 1;
const y0 = Math.round((1 - vMax) * H_), y1 = Math.round((1 - vMin) * H_) - 1;
const x = Math.min(x1, Math.max(x0, Math.floor(U * W_)));
const y = Math.min(y1, Math.max(y0, Math.floor((1 - W) * H_)));
return { t: best.t, x, y };
}
const zielAktuell = () => (ed.modus === 'umhang' ? 'umhang' : ed.schicht);
// sichtbarer Pixel für die Pipette: erst die 2. Schicht (falls dort Farbe ist)
function sichtbarerPixel(cx, cy) {
if (ed.modus === 'umhang') {
const u = strahl(cx, cy, 'umhang');
return u ? [u.x, u.y] : null;
}
if (ed.zeigeAussen) {
const a = strahl(cx, cy, 1);
if (a && skinDok.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, zielAktuell());
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, zielAktuell());
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, zielAktuell());
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');
if (ed.modus === 'umhang') await zeigeUmhangAufFigur(true);
passeGroessenAn();
}
// Umhang auf der Figur zeigen (Rücken zur Kamera) bzw. wieder abnehmen
async function zeigeUmhangAufFigur(an) {
const v = ed.viewer;
if (!v) return;
const p = v.playerObject;
if (an) {
texturGeaendert();
await v.loadCape(umhangTextur, { backEquipment: ed.elytra ? 'elytra' : 'cape' });
p.rotation.set(0, Math.PI - 0.5, 0);
} else {
v.resetCape();
p.rotation.set(0, -0.35, 0);
}
planeZeichnen();
}
// nach einer Größenänderung des Umhangs: Auswahl, Ansicht und Figur anpassen
function nachGroessenwechsel() {
el('se-u-groesse').value = String(umhangDok.w);
einpassen();
if (ed.viewer && ed.modus === 'umhang') zeigeUmhangAufFigur(true);
}
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();
}
// ---------- Vorlagen ----------
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 umhangVorlage(w) {
const h = w / 2;
const d = new Uint8ClampedArray(w * h * 4);
const farbe = (f) => (f.name.includes('außen') || f.name === 'Außenseite' ? [176, 46, 38]
: f.name.includes('innen') || f.name === 'Innenseite' ? [122, 31, 26] : [142, 37, 31]);
for (const f of umhangLayout(w).flaechen) {
const [r, g, b] = farbe(f);
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 * w + xx) * 4;
d[i] = r; d[i + 1] = g; d[i + 2] = b; d[i + 3] = 255;
}
}
}
return d;
}
// ---------- Laden ----------
function ladeImg(dataUri) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Bild konnte nicht gelesen werden.'));
img.src = dataUri;
});
}
async function ladeBild(dataUri) {
const img = await ladeImg(dataUri);
if (img.width !== N || (img.height !== N && img.height !== N / 2)) {
throw new Error(`Ein Skin muss 64×64 oder 64×32 Pixel groß sein (diese Datei: ${img.width}×${img.height}).`);
}
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];
}
}
return daten;
}
/*
* Umhang-Datei einlesen. Erkannt werden das übliche Format 2:1 (64×32, HD …)
* sowie die alten Formate 22:17 und 46:22. Ergebnis immer im 2:1-Format mit
* einer der Größen 64×32 … 1024×512 (größere Dateien werden verkleinert).
*/
async function ladeUmhangBild(dataUri) {
const img = await ladeImg(dataUri);
let k;
if (img.width === 2 * img.height) k = img.width / 64;
else if (17 * img.width === 22 * img.height) k = img.width / 22;
else if (11 * img.width === 23 * img.height) k = img.width / 46;
else throw new Error(`Das ist kein Umhang-Format (Seitenverhältnis 2:1 nötig, diese Datei: ${img.width}×${img.height}).`);
const ziel = UMHANG_STUFEN.reduce((best, w) => (Math.abs(w / 64 - k) < Math.abs(best / 64 - k) ? w : best), 64);
const kz = ziel / 64;
const c = document.createElement('canvas');
c.width = ziel;
c.height = ziel / 2;
const g = c.getContext('2d', { willReadFrequently: true });
g.imageSmoothingEnabled = kz < k;
g.imageSmoothingQuality = 'high';
g.drawImage(img, 0, 0, img.width * (kz / k), img.height * (kz / k));
return { w: ziel, h: ziel / 2, daten: g.getImageData(0, 0, ziel, ziel / 2).data };
}
async function uebernimmDaten(daten, variante, text) {
if (variante) await setzeArmform(variante === 'slim');
const d = skinDok;
if (dok() !== d) return;
aenderung(() => { d.daten = new Uint8ClampedArray(daten); });
d.geaendert = false;
texturGeaendert();
status(text || '');
}
function uebernimmUmhang(neu, text) {
const d = umhangDok;
if (dok() !== d) return;
aenderung(() => { d.w = neu.w; d.h = neu.h; d.daten = new Uint8ClampedArray(neu.daten); });
d.geaendert = false;
texturGeaendert();
status(text || '');
}
function wennVerwerfenOk(aktion) {
if (!dok().geaendert) { aktion(); return; }
const was = dok().art === 'umhang' ? 'Der Umhang' : 'Der Skin';
askConfirm('Änderungen verwerfen?', `${was} 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 ladeAktuellenUmhang() {
status('Aktueller Umhang wird geladen …');
const res = await window.api.capeCurrentData();
if (!res.ok) {
if (res.keiner) uebernimmUmhang({ w: umhangDok.w, h: umhangDok.h, daten: umhangVorlage(umhangDok.w) }, 'Dein Konto hat noch keinen Umhang – hier ist eine Vorlage zum Loslegen.');
else status('✖ ' + (res.message || 'Umhang konnte nicht geladen werden.'));
return;
}
try {
const neu = await ladeUmhangBild(res.dataUri);
uebernimmUmhang(neu, res.quelle === 'aeromc'
? `Dein AeroMC-Umhang geladen (${neu.w}×${neu.h}).`
: `Dein offizieller Umhang geladen (${neu.w}×${neu.h}) – als Vorlage für einen eigenen.`);
} 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); }
}
async function ladeUmhangDatei() {
const res = await window.api.capeChooseFile('umhang');
if (res.canceled) return;
if (!res.ok) { status('✖ ' + (res.message || 'Datei konnte nicht geladen werden.')); return; }
try {
const neu = await ladeUmhangBild(res.dataUri);
uebernimmUmhang(neu, `${res.pfad.split(/[\\/]/).pop()} geladen (${neu.w}×${neu.h}).`);
} catch (err) { status('✖ ' + err.message); }
}
// beliebiges Motiv auf die Außenseite setzen (füllt sie ganz, Ränder werden beschnitten)
async function bildEinsetzen() {
const res = await window.api.capeChooseFile('bild');
if (res.canceled) return;
if (!res.ok) { status('✖ ' + (res.message || 'Bild konnte nicht geladen werden.')); return; }
try {
const img = await ladeImg(res.dataUri);
const d = umhangDok;
const f = umhangLayout(d.w).flaechen.find((x) => x.name === 'Außenseite');
const c = document.createElement('canvas');
c.width = f.w;
c.height = f.h;
const g = c.getContext('2d', { willReadFrequently: true });
g.imageSmoothingEnabled = true;
g.imageSmoothingQuality = 'high';
const faktor = Math.max(f.w / img.width, f.h / img.height);
const bw = img.width * faktor, bh = img.height * faktor;
g.drawImage(img, (f.w - bw) / 2, (f.h - bh) / 2, bw, bh);
const px = g.getImageData(0, 0, f.w, f.h).data;
aenderung(() => {
for (let yy = 0; yy < f.h; yy++) {
d.daten.set(px.subarray(yy * f.w * 4, (yy + 1) * f.w * 4), ((f.y + yy) * d.w + f.x) * 4);
}
});
status(`Bild auf die Außenseite gesetzt (${f.w}×${f.h} Pixel).` + (d.w < 256 ? ' Tipp: Für feine Motive oben eine HD-Größe wählen und das Bild erneut einsetzen.' : ''));
} catch (err) { status('✖ ' + err.message); }
}
// Umhang auf eine andere Größe umrechnen (größer: Pixel bleiben scharf)
function skaliereUmhang(neuW) {
const d = umhangDok;
if (neuW === d.w) return;
const quelle = document.createElement('canvas');
quelle.width = d.w;
quelle.height = d.h;
quelle.getContext('2d').putImageData(new ImageData(d.daten, d.w, d.h), 0, 0);
const ziel = document.createElement('canvas');
ziel.width = neuW;
ziel.height = neuW / 2;
const g = ziel.getContext('2d', { willReadFrequently: true });
g.imageSmoothingEnabled = neuW < d.w;
g.imageSmoothingQuality = 'high';
g.drawImage(quelle, 0, 0, neuW, neuW / 2);
const daten = new Uint8ClampedArray(g.getImageData(0, 0, neuW, neuW / 2).data);
aenderung(() => { d.w = neuW; d.h = neuW / 2; d.daten = daten; });
status(`Umhang auf ${neuW}×${neuW / 2} umgestellt.` + (neuW > 64 ? ' Mit Mausrad hineinzoomen, rechte Maustaste verschiebt.' : ''));
}
// ---------- Speichern / Übernehmen ----------
function alsBase64() {
const d = dok();
const c = document.createElement('canvas');
c.width = d.w;
c.height = d.h;
c.getContext('2d').putImageData(new ImageData(d.daten, d.w, d.h), 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 (skinDok.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 umhang = dok().art === 'umhang';
const res = umhang ? await window.api.capeSaveData(alsBase64()) : await window.api.skinSaveData(alsBase64());
if (res.canceled) return;
if (!res.ok) { status('✖ ' + (res.message || 'Speichern fehlgeschlagen.')); return; }
dok().geaendert = false;
status('✔ Gespeichert: ' + res.pfad.split(/[\\/]/).pop() + '.' + (umhang ? '' : loecherHinweis()));
}
async function uebernimm() {
const btn = el('se-uebernehmen');
const umhang = dok().art === 'umhang';
btn.disabled = true;
status(umhang ? 'Umhang wird hochgeladen …' : 'Wird hochgeladen …');
try {
const res = umhang
? await window.api.capeUpload(alsBase64())
: await window.api.skinApplyData(alsBase64(), ed.slim ? 'slim' : 'classic');
if (res.ok) {
dok().geaendert = false;
if (umhang) {
status('✔ Umhang hochgeladen. Im Launcher sofort sichtbar – im Spiel bei Spielern mit dem AeroMC-Umhang-Mod.');
toast('Umhang gespeichert.');
} else {
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'));
// Umhang
el('se-u-groesse').addEventListener('change', () => skaliereUmhang(Number(el('se-u-groesse').value)));
el('se-u-pinsel').addEventListener('change', () => { ed.pinsel = Number(el('se-u-pinsel').value) || 1; planeZeichnen(); });
el('se-u-bild').addEventListener('click', bildEinsetzen);
el('se-u-einpassen').addEventListener('click', einpassen);
el('se-u-elytra').addEventListener('change', () => {
ed.elytra = el('se-u-elytra').checked;
if (ed.viewer && ed.modus === 'umhang') ed.viewer.playerObject.backEquipment = ed.elytra ? 'elytra' : 'cape';
planeZeichnen();
});
el('se-undo').addEventListener('click', rueckgaengig);
el('se-redo').addEventListener('click', wiederholen);
el('se-neu').addEventListener('click', () => wennVerwerfenOk(() => {
if (dok().art === 'umhang') uebernimmUmhang({ w: umhangDok.w, h: umhangDok.h, daten: umhangVorlage(umhangDok.w) }, 'Neue Umhang-Vorlage – leg los!');
else uebernimmDaten(vorlage(), null, 'Neue Vorlage – leg los!');
}));
el('se-laden').addEventListener('click', () => wennVerwerfenOk(dok().art === 'umhang' ? ladeAktuellenUmhang : ladeAktuellen));
el('se-oeffnen').addEventListener('click', () => wennVerwerfenOk(() => (dok().art === 'umhang' ? ladeUmhangDatei() : 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' && ed.modus !== 'umhang') { e.preventDefault(); waehleSchicht(ed.schicht ? 0 : 1); return; }
if (k === '0' && ed.modus === 'umhang') { e.preventDefault(); einpassen(); }
});
verbinde2D();
verbindeUmhang2D();
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);
skinDok.daten = vorlage();
umhangDok.daten = umhangVorlage(64);
waehleModus('2d');
await erzeugeViewer();
texturGeaendert();
}
const variante = p.variante === 'slim' || p.variante === 'classic' ? p.variante : null;
if (p.modus === 'umhang') {
if (ed.modus !== 'umhang') waehleModus('umhang');
if (p.quelle === 'aktuell') wennVerwerfenOk(ladeAktuellenUmhang);
return;
}
if (ed.modus === 'umhang' && (p.pfad || p.quelle)) waehleModus('2d');
if (p.pfad) wennVerwerfenOk(() => ladeDatei(p.pfad, variante));
else if (p.quelle === 'aktuell') wennVerwerfenOk(ladeAktuellen);
else if (variante && !skinDok.geaendert) await setzeArmform(variante === 'slim');
}
window.openSkinEditor = openSkinEditor;
// für Tests
window.__skinEditor = { ed, skinDok, umhangDok, strahl, spiegelPixel, seiten, AUFTEILUNG, umhangLayout, waehleModus, skaliereUmhang, ansicht };
})();