Canvas mit der Maus schieben und zoomen
Das blanke Rad zoomt jetzt, die linke Maustaste zieht die Flaeche. Eine gewoehnliche PC-Maus kam vorher nicht an den Ausschnitt heran: Zoomen ging nur mit Strg/Cmd, Schieben nur ueber Leertaste oder mittlere Taste. Die Radschritte laufen exponentiell ueber deltaY statt in festen Spruengen, mit Umrechnung der deltaMode-Einheiten -- ohne die zoomt Firefox pro Rastung um ein Vielfaches weniger als Chrome. Waagerechtes Wischen und Shift+Rad schieben weiter, damit sich ein Trackpad nicht wie eine Zoomwippe anfuehlt. Im Bearbeiten-Modus behaelt das Element unter dem Zeiger die linke Taste. Nach einem echten Zug wird der Klick geschluckt, sonst sprang die Auswahl auf die Slide, ueber der die Maus zufaellig losliess. Dabei zwei aeltere Fehler mitgenommen: setZoom rechnete den Zeigerpunkt per Dreisatz auf scrollLeft hoch und uebersah das Polster der Flaeche. Der Inhalt lief pro Schritt um 60 x (Faktor-1) px weg. Der Punkt wird jetzt gemessen statt gerechnet. Der Bearbeiten-Zug leitete seinen Massstab aus der Border-Box von .strip ab. Die misst Breite mal Massstab hoch zwei, weil .strip ein Flex-Kind der schon skalierten Huelle ist -- geteilt wurde also durch das Quadrat, das Element wanderte bei eingepasster Ansicht um das 5,6-fache der Mausbewegung. Geprueft mit echten Rad- und Zieh-Ereignissen ueber das DevTools-Protokoll, 15 Pruefungen gegen beide Dateien. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1359c6eba8
commit
94e1b9a84a
3 files changed
+148
-34
No files matched your search
@@ -59,6 +59,20 @@ aus dem Browser einmalig hineingeschrieben.
|
||||
Im Bearbeiten-Modus lassen sich Elemente ziehen, mit den Pfeiltasten
|
||||
pixelweise schieben (`⇧` = 10 px) und über `A−`/`A+` in der Größe ändern.
|
||||
|
||||
### Auf der Fläche bewegen
|
||||
|
||||
| Maus | Wirkung |
|
||||
|---|---|
|
||||
| Ziehen | Ausschnitt schieben |
|
||||
| Rad | Zoomen, auf den Zeiger zu |
|
||||
| `⇧` + Rad | Waagerecht schieben |
|
||||
| Leertaste-Ziehen, mittlere Taste | Schieben, auch im Bearbeiten-Modus |
|
||||
|
||||
Ein gewöhnliches Mausrad genügt, ohne Zusatztaste. Im Bearbeiten-Modus zieht
|
||||
die linke Taste das Element unter dem Zeiger — die Fläche schiebt man dort mit
|
||||
gedrückter Leertaste oder der mittleren Taste. `Fit` unten rechts setzt den
|
||||
Maßstab zurück.
|
||||
|
||||
## Aufbau
|
||||
|
||||
```
|
||||
|
||||
+67
-17
@@ -1161,14 +1161,22 @@
|
||||
function setZoom(k, anchor) {
|
||||
const scroller = document.getElementById("scroller");
|
||||
const before = zoom || autoFit();
|
||||
zoom = Math.min(2, Math.max(0.05, k));
|
||||
// Um den Mauszeiger zoomen, nicht um die linke obere Ecke.
|
||||
const ratio = zoom / before;
|
||||
const ax = anchor ? anchor.x : scroller.clientWidth / 2;
|
||||
const ay = anchor ? anchor.y : scroller.clientHeight / 2;
|
||||
/* Um den Mauszeiger zoomen, nicht um die linke obere Ecke — und dafür den
|
||||
Punkt unter dem Zeiger messen statt ihn aus scrollLeft hochzurechnen.
|
||||
Das Blatt sitzt in einer gepolsterten Fläche und wird zentriert, solange
|
||||
Platz übrig ist; beides verschiebt den Ursprung. Ein reiner Dreisatz
|
||||
ließ den Inhalt deshalb bei jedem Schritt um das Polster wegwandern. */
|
||||
const s0 = scroller.getBoundingClientRect();
|
||||
const r0 = deck.getBoundingClientRect();
|
||||
const u = (s0.left + ax - r0.left) / before;
|
||||
const v = (s0.top + ay - r0.top) / before;
|
||||
zoom = Math.min(2, Math.max(0.05, k));
|
||||
fitStrip();
|
||||
scroller.scrollLeft = (scroller.scrollLeft + ax) * ratio - ax;
|
||||
scroller.scrollTop = (scroller.scrollTop + ay) * ratio - ay;
|
||||
const r1 = deck.getBoundingClientRect();
|
||||
scroller.scrollLeft += (r1.left + u * zoom) - (s0.left + ax);
|
||||
scroller.scrollTop += (r1.top + v * zoom) - (s0.top + ay);
|
||||
}
|
||||
|
||||
const setStatus = (msg, kind) => {
|
||||
@@ -1880,7 +1888,12 @@
|
||||
show(Number(holder.dataset.n));
|
||||
pick(idx, key);
|
||||
|
||||
const k = deck.getBoundingClientRect().width / (1080 * (view === "streifen" ? SLIDES.length : 1));
|
||||
// Den Maßstab nehmen, mit dem fitStrip gerechnet hat, nicht das Rechteck
|
||||
// messen: .strip ist ein Flex-Kind der schon skalierten Hülle, seine
|
||||
// Border-Box misst deshalb Breite mal Maßstab hoch zwei. Geteilt wurde
|
||||
// damit durch das Quadrat — bei eingepasster Ansicht wanderte das Element
|
||||
// um ein Vielfaches der Mausbewegung.
|
||||
const k = zoom || autoFit();
|
||||
const set = (edits[idx][key] = edits[idx][key] || {});
|
||||
const startX = e.clientX, startY = e.clientY;
|
||||
const baseX = set.dx || 0, baseY = set.dy || 0;
|
||||
@@ -2031,21 +2044,40 @@
|
||||
});
|
||||
window.addEventListener("resize", fitStrip);
|
||||
|
||||
/* Strg/Cmd + Rad zoomt, blankes Rad scrollt. Trackpad-Pinch schickt
|
||||
ctrlKey mit, verhält sich also automatisch richtig. */
|
||||
/* deltaMode: 0 = Pixel, 1 = Zeilen (Firefox), 2 = Seiten. Ohne Umrechnung
|
||||
zoomt Firefox pro Rastung um ein Vielfaches weniger als Chrome. */
|
||||
function wheelPx(v, mode) { return v * (mode === 1 ? 16 : mode === 2 ? 400 : 1); }
|
||||
|
||||
/* Blankes Rad zoomt — eine gewöhnliche PC-Maus soll ohne Zusatztaste
|
||||
auskommen. Strg/Cmd + Rad tut dasselbe, damit der Trackpad-Pinch (der
|
||||
ctrlKey mitschickt) weiter greift. Waagerechtes Wischen und ⇧ + Rad
|
||||
schieben dagegen den Ausschnitt: ein Trackpad darf sich nicht wie eine
|
||||
Zoomwippe anfühlen. */
|
||||
document.getElementById("scroller").addEventListener("wheel", e => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
const sc = e.currentTarget;
|
||||
const dx = wheelPx(e.deltaX, e.deltaMode), dy = wheelPx(e.deltaY, e.deltaMode);
|
||||
if (e.shiftKey || (!dy && dx)) {
|
||||
e.preventDefault();
|
||||
sc.scrollLeft += (dx || dy);
|
||||
return;
|
||||
}
|
||||
if (!dy) return;
|
||||
e.preventDefault();
|
||||
const r = e.currentTarget.getBoundingClientRect();
|
||||
setZoom((zoom || autoFit()) * (e.deltaY < 0 ? 1.12 : 1 / 1.12),
|
||||
const r = sc.getBoundingClientRect();
|
||||
// Exponentiell gerechnet fühlen sich Rastung (~100 px auf einen Schlag)
|
||||
// und Trackpad (viele kleine Werte) gleich an. Der Deckel hält die
|
||||
// Schwung-Schübe im Zaum, die macOS nach dem Loslassen nachschiebt.
|
||||
const k = Math.min(3, Math.max(1 / 3, Math.exp(-dy * 0.0015)));
|
||||
setZoom((zoom || autoFit()) * k,
|
||||
{ x: e.clientX - r.left, y: e.clientY - r.top });
|
||||
}, { passive: false });
|
||||
|
||||
/* Panning als verlässlicher Weg, unabhängig von Trackpad-Gesten:
|
||||
Leertaste gedrückt halten und ziehen, oder mittlere Maustaste. */
|
||||
/* Panning: linke Maustaste auf der Fläche reicht. Leertaste und mittlere
|
||||
Taste bleiben als gewohnte Wege bestehen — im Bearbeiten-Modus sind sie
|
||||
der einzige, denn dort gehört der linke Zug dem Element unter dem Zeiger. */
|
||||
(function panning() {
|
||||
const sc = document.getElementById("scroller");
|
||||
let space = false, active = null;
|
||||
let space = false, active = null, dragged = false;
|
||||
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.code !== "Space" || e.target.matches("input, textarea")) return;
|
||||
@@ -2058,19 +2090,37 @@
|
||||
});
|
||||
|
||||
sc.addEventListener("mousedown", e => {
|
||||
if (!space && e.button !== 1) return;
|
||||
dragged = false;
|
||||
if (!space && e.button !== 1) {
|
||||
// Linke Taste zieht die Fläche, außer sie hat im Bearbeiten-Modus
|
||||
// ein Element erwischt: das verschiebt sich selbst.
|
||||
if (e.button !== 0) return;
|
||||
if (editMode === "an" && e.target.closest("[data-edit]")) return;
|
||||
}
|
||||
e.preventDefault();
|
||||
active = { x: e.clientX, y: e.clientY, l: sc.scrollLeft, t: sc.scrollTop };
|
||||
sc.classList.add("panning");
|
||||
});
|
||||
document.addEventListener("mousemove", e => {
|
||||
if (!active) return;
|
||||
sc.scrollLeft = active.l - (e.clientX - active.x);
|
||||
sc.scrollTop = active.t - (e.clientY - active.y);
|
||||
const dx = e.clientX - active.x, dy = e.clientY - active.y;
|
||||
// Erst ab ein paar Pixeln ist es ein Zug und kein Klick, sonst
|
||||
// verschluckt jede zittrige Hand die Slide-Auswahl.
|
||||
if (Math.abs(dx) + Math.abs(dy) > 3) dragged = true;
|
||||
sc.scrollLeft = active.l - dx;
|
||||
sc.scrollTop = active.t - dy;
|
||||
});
|
||||
document.addEventListener("mouseup", () => {
|
||||
active = null; sc.classList.remove("panning");
|
||||
});
|
||||
// Nach echtem Ziehen den Klick schlucken: sonst springt die Auswahl auf
|
||||
// die Slide, über der die Maus zufällig loslässt.
|
||||
sc.addEventListener("click", e => {
|
||||
if (!dragged) return;
|
||||
dragged = false;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}, true);
|
||||
})();
|
||||
|
||||
document.getElementById("zoom-in").addEventListener("click", () => setZoom((zoom || autoFit()) * 1.25));
|
||||
|
||||
+67
-17
@@ -1208,14 +1208,22 @@
|
||||
function setZoom(k, anchor) {
|
||||
const scroller = document.getElementById("scroller");
|
||||
const before = zoom || autoFit();
|
||||
zoom = Math.min(2, Math.max(0.05, k));
|
||||
// Um den Mauszeiger zoomen, nicht um die linke obere Ecke.
|
||||
const ratio = zoom / before;
|
||||
const ax = anchor ? anchor.x : scroller.clientWidth / 2;
|
||||
const ay = anchor ? anchor.y : scroller.clientHeight / 2;
|
||||
/* Um den Mauszeiger zoomen, nicht um die linke obere Ecke — und dafür den
|
||||
Punkt unter dem Zeiger messen statt ihn aus scrollLeft hochzurechnen.
|
||||
Das Blatt sitzt in einer gepolsterten Fläche und wird zentriert, solange
|
||||
Platz übrig ist; beides verschiebt den Ursprung. Ein reiner Dreisatz
|
||||
ließ den Inhalt deshalb bei jedem Schritt um das Polster wegwandern. */
|
||||
const s0 = scroller.getBoundingClientRect();
|
||||
const r0 = deck.getBoundingClientRect();
|
||||
const u = (s0.left + ax - r0.left) / before;
|
||||
const v = (s0.top + ay - r0.top) / before;
|
||||
zoom = Math.min(2, Math.max(0.05, k));
|
||||
fitStrip();
|
||||
scroller.scrollLeft = (scroller.scrollLeft + ax) * ratio - ax;
|
||||
scroller.scrollTop = (scroller.scrollTop + ay) * ratio - ay;
|
||||
const r1 = deck.getBoundingClientRect();
|
||||
scroller.scrollLeft += (r1.left + u * zoom) - (s0.left + ax);
|
||||
scroller.scrollTop += (r1.top + v * zoom) - (s0.top + ay);
|
||||
}
|
||||
|
||||
const setStatus = (msg, kind) => {
|
||||
@@ -1870,7 +1878,12 @@
|
||||
show(Number(holder.dataset.n));
|
||||
pick(idx, key);
|
||||
|
||||
const k = deck.getBoundingClientRect().width / (1080 * (view === "streifen" ? SLIDES.length : 1));
|
||||
// Den Maßstab nehmen, mit dem fitStrip gerechnet hat, nicht das Rechteck
|
||||
// messen: .strip ist ein Flex-Kind der schon skalierten Hülle, seine
|
||||
// Border-Box misst deshalb Breite mal Maßstab hoch zwei. Geteilt wurde
|
||||
// damit durch das Quadrat — bei eingepasster Ansicht wanderte das Element
|
||||
// um ein Vielfaches der Mausbewegung.
|
||||
const k = zoom || autoFit();
|
||||
const set = (edits[idx][key] = edits[idx][key] || {});
|
||||
const startX = e.clientX, startY = e.clientY;
|
||||
const baseX = set.dx || 0, baseY = set.dy || 0;
|
||||
@@ -2021,21 +2034,40 @@
|
||||
});
|
||||
window.addEventListener("resize", fitStrip);
|
||||
|
||||
/* Strg/Cmd + Rad zoomt, blankes Rad scrollt. Trackpad-Pinch schickt
|
||||
ctrlKey mit, verhält sich also automatisch richtig. */
|
||||
/* deltaMode: 0 = Pixel, 1 = Zeilen (Firefox), 2 = Seiten. Ohne Umrechnung
|
||||
zoomt Firefox pro Rastung um ein Vielfaches weniger als Chrome. */
|
||||
function wheelPx(v, mode) { return v * (mode === 1 ? 16 : mode === 2 ? 400 : 1); }
|
||||
|
||||
/* Blankes Rad zoomt — eine gewöhnliche PC-Maus soll ohne Zusatztaste
|
||||
auskommen. Strg/Cmd + Rad tut dasselbe, damit der Trackpad-Pinch (der
|
||||
ctrlKey mitschickt) weiter greift. Waagerechtes Wischen und ⇧ + Rad
|
||||
schieben dagegen den Ausschnitt: ein Trackpad darf sich nicht wie eine
|
||||
Zoomwippe anfühlen. */
|
||||
document.getElementById("scroller").addEventListener("wheel", e => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
const sc = e.currentTarget;
|
||||
const dx = wheelPx(e.deltaX, e.deltaMode), dy = wheelPx(e.deltaY, e.deltaMode);
|
||||
if (e.shiftKey || (!dy && dx)) {
|
||||
e.preventDefault();
|
||||
sc.scrollLeft += (dx || dy);
|
||||
return;
|
||||
}
|
||||
if (!dy) return;
|
||||
e.preventDefault();
|
||||
const r = e.currentTarget.getBoundingClientRect();
|
||||
setZoom((zoom || autoFit()) * (e.deltaY < 0 ? 1.12 : 1 / 1.12),
|
||||
const r = sc.getBoundingClientRect();
|
||||
// Exponentiell gerechnet fühlen sich Rastung (~100 px auf einen Schlag)
|
||||
// und Trackpad (viele kleine Werte) gleich an. Der Deckel hält die
|
||||
// Schwung-Schübe im Zaum, die macOS nach dem Loslassen nachschiebt.
|
||||
const k = Math.min(3, Math.max(1 / 3, Math.exp(-dy * 0.0015)));
|
||||
setZoom((zoom || autoFit()) * k,
|
||||
{ x: e.clientX - r.left, y: e.clientY - r.top });
|
||||
}, { passive: false });
|
||||
|
||||
/* Panning als verlässlicher Weg, unabhängig von Trackpad-Gesten:
|
||||
Leertaste gedrückt halten und ziehen, oder mittlere Maustaste. */
|
||||
/* Panning: linke Maustaste auf der Fläche reicht. Leertaste und mittlere
|
||||
Taste bleiben als gewohnte Wege bestehen — im Bearbeiten-Modus sind sie
|
||||
der einzige, denn dort gehört der linke Zug dem Element unter dem Zeiger. */
|
||||
(function panning() {
|
||||
const sc = document.getElementById("scroller");
|
||||
let space = false, active = null;
|
||||
let space = false, active = null, dragged = false;
|
||||
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.code !== "Space" || e.target.matches("input, textarea")) return;
|
||||
@@ -2048,19 +2080,37 @@
|
||||
});
|
||||
|
||||
sc.addEventListener("mousedown", e => {
|
||||
if (!space && e.button !== 1) return;
|
||||
dragged = false;
|
||||
if (!space && e.button !== 1) {
|
||||
// Linke Taste zieht die Fläche, außer sie hat im Bearbeiten-Modus
|
||||
// ein Element erwischt: das verschiebt sich selbst.
|
||||
if (e.button !== 0) return;
|
||||
if (editMode === "an" && e.target.closest("[data-edit]")) return;
|
||||
}
|
||||
e.preventDefault();
|
||||
active = { x: e.clientX, y: e.clientY, l: sc.scrollLeft, t: sc.scrollTop };
|
||||
sc.classList.add("panning");
|
||||
});
|
||||
document.addEventListener("mousemove", e => {
|
||||
if (!active) return;
|
||||
sc.scrollLeft = active.l - (e.clientX - active.x);
|
||||
sc.scrollTop = active.t - (e.clientY - active.y);
|
||||
const dx = e.clientX - active.x, dy = e.clientY - active.y;
|
||||
// Erst ab ein paar Pixeln ist es ein Zug und kein Klick, sonst
|
||||
// verschluckt jede zittrige Hand die Slide-Auswahl.
|
||||
if (Math.abs(dx) + Math.abs(dy) > 3) dragged = true;
|
||||
sc.scrollLeft = active.l - dx;
|
||||
sc.scrollTop = active.t - dy;
|
||||
});
|
||||
document.addEventListener("mouseup", () => {
|
||||
active = null; sc.classList.remove("panning");
|
||||
});
|
||||
// Nach echtem Ziehen den Klick schlucken: sonst springt die Auswahl auf
|
||||
// die Slide, über der die Maus zufällig loslässt.
|
||||
sc.addEventListener("click", e => {
|
||||
if (!dragged) return;
|
||||
dragged = false;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}, true);
|
||||
})();
|
||||
|
||||
document.getElementById("zoom-in").addEventListener("click", () => setZoom((zoom || autoFit()) * 1.25));
|
||||
|
||||
Reference in new issue
Block a user