rotate360 blättert eine Bildserie (z.B. 24 oder 36 Fotos vom Drehteller) per Maus, Touch,
Tastatur oder Autoplay so schnell durch, dass sich das Objekt scheinbar dreht. Dazu kommen
Multi-Row (Blick von oben/unten), Zoom mit Pan, Vollbild, Points of Interest mit
Sprechblasen und Detailbild sowie eine kleine Button-Leiste. Keine Abhängigkeiten,
Konfiguration über data-*-Attribute, Inline-JSON oder JavaScript. Auto-Init beim DOM-ready inklusive.
CSS und JS einbinden, ein Element mit Klasse .rotate360 anlegen, Bildmuster angeben.
<link rel="stylesheet" href="rotate360.css">
<script src="rotate360.js" defer></script>
<div class="rotate360"
data-rotate-src="img/produkt_{i}.jpg"
data-rotate-count="24"
data-rotate-pad="2"></div>
{i} wird durch die Frame-Nummer ersetzt (Start bei data-rotate-index-start,
Default 1), data-rotate-pad="2" füllt auf 01, 02, … auf. Bei mehreren
Zeilen steht {r} für die Zeilennummer ab 0. Die Breite bestimmt das umgebende
CSS, die Höhe folgt dem Seitenverhältnis des ersten Bildes - siehe
Größe und Responsive.
Statt eines Musters kann die Liste direkt angegeben werden - als Attribut oder als Inline-JSON im Host:
<div class="rotate360" data-rotate-frames='["a.jpg","b.jpg","c.jpg"]'></div>
<div class="rotate360">
<script type="application/json" data-rotate-config>
{
"frames": [["r0_01.jpg", "r0_02.jpg"], ["r1_01.jpg", "r1_02.jpg"]],
"pois": [ { "frame": 0, "x": 50, "y": 40, "label": "Logo" } ]
}
</script>
</div>
Ein zweidimensionales Array ergibt Multi-Row ([zeile][frame]). Alle Optionen aus dem Inline-JSON überschreiben die Data-Attribute; JavaScript-Optionen überschreiben beides.
zoomDrag: 'pan'. Der Ausschnitt bleibt beim Bildwechsel identisch, weil Zoom und Pan auf einer Ebene über allen Frames liegen.Nach Themen gruppiert - was in einer Gruppe steht, wird meist zusammen gesetzt. Boolesche Attribute akzeptieren true, false oder das leere Attribut (data-rotate-reverse = an).
| Attribut | Wert | Beschreibung |
|---|---|---|
| Bildquelle | ||
data-rotate-src | Muster | Bild-URL mit {i} (Frame) und optional {r} (Zeile). |
data-rotate-count | Zahl | Frames pro Zeile. |
data-rotate-rows | Zahl | Zeilen für Multi-Row. Default 1. |
data-rotate-pad | Zahl | Stellen, auf die {i} mit Nullen aufgefüllt wird. |
data-rotate-index-start | Zahl | Erste Nummer im Dateinamen. Default 1. |
data-rotate-frames | JSON-Array oder a|b|c | Explizite Bildliste statt Muster. |
data-rotate-src-large, -frames-large | Muster, Liste | Zweite Serie in hoher Auflösung, gleiche Anzahl und Form. Wird ab data-rotate-large-at (Default 1.25) für den aktuellen Frame nachgeladen und angezeigt, Nachbarn werden vorgeladen; darunter läuft alles über die kleine Serie. |
| Laden | ||
data-rotate-preload | all | lazy | all lädt alle Bilder vom Startbild nach außen; lazy lädt nur das aktuelle Bild plus zwei Nachbarn je Richtung. |
data-rotate-when-visible | bool | Bilder erst laden, wenn der Viewer (mit 200 px Vorlauf) in den Viewport kommt. Default true. Wichtig bei mehreren Viewern pro Seite. |
| Startposition und Drehrichtung | ||
data-rotate-start-frame, -start-row | Zahl | Startposition (0-basiert). |
data-rotate-reverse | bool | Drehrichtung umkehren. |
data-rotate-reverse-rows | bool | Kipprichtung bei Multi-Row umkehren. Default: nach oben ziehen zeigt das Objekt von unten (Zeile 0 = unterste Blickhöhe). |
data-rotate-loop | bool | Nach dem letzten Frame wieder beim ersten. Default true. |
data-rotate-loop-rows | bool | Zeilen ebenfalls endlos. Default false. |
| Ziehen und Nachlauf | ||
data-rotate-sensitivity | px | Mausweg pro Frame, absolut. Default 8. Kleiner = schneller. |
data-rotate-sensitivity-y | px | Mausweg pro Zeile, absolut. Default 40. |
data-rotate-sweep | Zahl | Anteil der Bühnenbreite für eine volle Umdrehung: 1 = einmal quer ziehen dreht das Objekt einmal herum, 0.5 = doppelt so flink. Rechnet relativ, fühlt sich also auf jeder Breite gleich an, und hat Vorrang vor sensitivity. Default 0 (aus). |
data-rotate-sweep-y | Zahl | Dasselbe für die Zeilen, bezogen auf die Bühnenhöhe: 1 = von ganz oben nach ganz unten ziehen durchläuft alle Zeilen. Default 0 (aus). |
data-rotate-sensitivity-ref | px | Bezugsbreite für die absoluten Werte. Ist die Bühne schmaler, werden sensitivity und sensitivity-y im selben Verhältnis verkleinert; oberhalb bleibt alles wie eingestellt. Für Seiten, die schon einen absoluten Wert eingestellt haben. Default 0 (aus). |
data-rotate-inertia, -friction | bool, 0–1 | Nachlauf nach dem Loslassen und dessen Abbremsung pro 16 ms. Default true, 0.92. |
data-rotate-snap, -snap-range | bool, Zahl | Endet ein Nachlauf höchstens snap-range (Default 1) Frames neben einem Frame mit auto-Sprechblase, dreht der Viewer sanft dorthin. Default true. |
data-rotate-keyboard | bool | Tastatursteuerung; setzt tabindex=0. Default true. |
| Autoplay | ||
data-rotate-autoplay, -fps | bool, Zahl | Automatisch drehen, Frames pro Sekunde (Default 12). |
data-rotate-autostart | bool | false unterdrückt den Autoplay-Start beim Init. |
data-rotate-resume-delay | ms | Autoplay nach einer Interaktion (auch nach dem Schließen eines Detailbilds) fortsetzen. 0 = nie. |
| Zoom | ||
data-rotate-zoom | bool | Zoom erlauben. Default true. |
data-rotate-zoom-min, -zoom-max, -zoom-step | Zahl | Grenzen und Schrittweite. Default 1, 4, 0.5. |
data-rotate-zoom-wheel, -zoom-dblclick | bool | Gesten für den Zoom an/aus. |
data-rotate-zoom-drag | rotate | pan | Ziehen mit links, wenn gezoomt ist. Default rotate; verschoben wird dann mit rechter Maustaste, Strg/Cmd oder zwei Fingern. |
| Oberfläche | ||
data-rotate-controls | bool | Button-Leiste. Default true. |
data-rotate-controls-list | Liste | Auswahl und Reihenfolge: prev next zoomin zoomout play fullscreen. |
data-rotate-fullscreen | bool | Vollbild-Button und Taste F. Default true. |
data-rotate-hint, -hint-text | bool, Text | Hinweis-Overlay nach dem Laden. Default „Ziehen zum Drehen". |
data-rotate-hover-delay | ms | Karenz, bevor eine hover-Sprechblase nach dem Verlassen von Marker/Box schließt. Default 250. |
data-rotate-detail-label | Text | Beschriftung des Buttons in der Sprechblase, wenn ein Detailbild da ist. Default „Detailbild", leer = kein Button. |
data-rotate-alt | Text | ARIA-Label des Hosts. |
data-rotate-debug | bool | Kleine Einblendung unten links: Zeile, Frame, Dateiname, Zoom - fest 11 px (--r360-debug-size), unabhängig vom Bild. Per r.setDebug(on) auch zur Laufzeit; frameNames liefert Anzeigenamen, wenn die Frames Blob-URLs sind. |
| Points of Interest und Hotspots | ||
data-rotate-pois, -hotspots | JSON | Marker und Sprechblasen, siehe nächster Abschnitt. Meist bequemer als Inline-JSON im Host. |
| Gekoppelte Drehung | ||
data-rotate-sync | Name | Alle Viewer mit demselben Namen drehen gemeinsam. Wer bewegt wird (Ziehen, Tastatur, Nachlauf, Autoplay, goToPoi), führt; die anderen folgen. Ein Neuling übernimmt beim Init den Stand der Gruppe. |
data-rotate-sync-props | Liste | Was gekoppelt wird: frame (Frame und Zeile, Default) und/oder zoom (Zoom und Ausschnitt, an die jeweilige Viewergröße angepasst). |
data-rotate-sync-role | both | follow | follow macht einen Viewer zum reinen Folger: eigenes Drehen und Kippen wird ignoriert, Zoom bleibt möglich. |
Ein POI ist ein Marker an einer Bildposition, sichtbar in bestimmten Frames. Positionen sind Prozent der Bildfläche (0–100), damit sie unabhängig von Anzeigegröße, Zoom und Vollbild stimmen. Marker wandern mit Zoom und Pan mit, behalten aber ihre Größe. Weil sich die Bildposition mit jeder Drehung verschiebt, gehört ein POI in der Regel zu genau einem Frame.
Ein Hotspot ist die Sprechblase dazu: Text (HTML erlaubt), fester Ankerpunkt der Box,
optional ein Detailbild. Ein Hotspot kann von beliebig vielen POIs genutzt werden - so wandert der
Marker von Frame zu Frame, während Text und Detailbild nur einmal gepflegt werden. Eine Führungslinie
verbindet Marker und Box; die Box liegt links oder rechts vom Anker, je nachdem, auf welcher Seite
des Markers er sitzt - side: "left"|"right" legt sie fest -, und wird am
Bildrand hineingeschoben statt umgeklappt. Ihr Anker ist Prozent des Bildes in
seiner ungezoomten Lage: sie wandert also nicht mit Zoom und Pan, bleibt aber auch im
Vollbild über dem Bild statt im Letterbox-Rand. Teilen sich mehrere POIs eines Frames einen Hotspot, liegen ihre
Sprechblasen exakt übereinander - der erste POI entscheidet die Seite.
{
"hotspots": [
{ "id": "flansch",
"x": 14, "y": 40, // Anker der Sprechblase (Prozent)
"html": "Text mit <a href=…>Link</a>", // oder "text": "…" (wird escaped)
"open": "hover", // "hover" (Default) | "auto" | "always"
"side": "left", // Box fest links/rechts vom Anker (sonst automatisch)
"detail": { "image": "img/flansch_gross.jpg", "caption": "Flanschdichtung" } }
],
"pois": [
{ "id": "flansch_5", "frame": 4, "x": 41, "y": 82, "hotspot": "flansch" },
{ "id": "flansch_6", "frame": 5, "x": 44, "y": 81, "hotspot": "flansch" },
{ "id": "logo", "frames": [0, 1, 23], "x": 50, "y": 40, "label": "Logo" }, // nur Tooltip
{ "id": "inline", "frame": 12, "x": 60, "y": 30,
"callout": { "x": 85, "y": 20, "text": "Inline-Sprechblase ohne Hotspot" },
"detail": "img/inline.jpg" }
]
}
open: "hover" - Sprechblase bei Hover/Fokus. Verlassen von Marker oder Box schließt sie erst nach data-rotate-hover-delay ms (Default 250), damit der Weg in die Box zu Links oder zum Detail-Button nicht abreißt. Ein Klick auf den Marker heftet sie an - sie bleibt bis Framewechsel, Esc oder Klick/Ziehen auf der Bühne. Ein zweiter Klick auf den angehefteten Marker öffnet das Detailbild, wenn eines da ist, sonst löst er die Blase.open: "auto" - öffnet sich, sobald der POI ins Bild kommt; während Autoplay bleibt sie zu. Klick öffnet das Detailbild.open: "always" - immer offen. Klick öffnet das Detailbild.side hält die Box auf einer Seite des Ankers, unabhängig davon, wo der Marker gerade liegt. Der Editor setzt es beim Verschieben der Sprechblase automatisch.label ohne Hotspot ergibt den kleinen Tooltip wie bisher. Mit Hotspot dient label nur noch als Aria-Label.callout direkt am POI ist die Inline-Variante bzw. überschreibt einzelne Felder des Hotspots (z.B. den Anker in einem bestimmten Frame).
detail darf am POI und/oder am Hotspot stehen - das POI gewinnt.
So kann ein Hotspot, der über viele Frames wandert, ein gemeinsames Bild tragen, und ein einzelner POI
trotzdem sein eigenes. Ist ein Detailbild wirksam, hängt die Sprechblase unten den Button
„Detailbild" an (Beschriftung data-rotate-detail-label, leer = kein Button). Das Bild
öffnet sich über diesen Button, über den Marker (bei hover mit dem zweiten Klick, sonst direkt)
oder bei POIs ohne Sprechblase mit dem ersten Klick.
Es erscheint als Overlay in Viewergröße - im Vollbild also bildschirmfüllend. X-Button und
Bildunterschrift sitzen dabei am eingepassten Bild, nicht am Overlay-Rand - bei einem schmalen Bild
auf breitem Monitor also an dessen Ecke bzw. Unterkante. Geschlossen wird mit
Esc, dem X-Button oder Klick auf den Hintergrund. Autoplay pausiert solange und läuft nach
resumeDelay weiter. detail kann ein String (URL) oder
{ image, caption, alt } sein.
Das Overlay wächst aus dem Element heraus, über das es geöffnet wurde: rund aus dem
Marker, rechteckig aus der Sprechblase (Button „Detailbild"). Beim Schließen schrumpft es dorthin
zurück; ist das Element dann nicht mehr sichtbar, in den Marker bzw. in die Mitte.
openDetail() wächst aus dem Marker, wenn der POI im aktuellen Frame zu sehen ist, sonst
aus der Mitte. Wer das Bild aus einer hover-Blase heraus öffnet, heftet sie damit an - so
kehrt das Bild in eine sichtbare Box zurück. Mit prefers-reduced-motion wird nur
überblendet.
el.addEventListener('rotate360:poi', (e) => {
const { poi, frame, row } = e.detail;
console.log('POI geklickt:', poi.id, poi.hotspot);
});
el.addEventListener('rotate360:detailopen', (e) => { /* e.detail.poi, e.detail.detail */ });
el.addEventListener('rotate360:detailclose', (e) => { /* e.detail.poi */ });
r.openDetail('flansch_5'); // per id oder POI-Objekt
r.closeDetail();
r.setPois([ … ]); // Listen zur Laufzeit ersetzen
r.setHotspots([ … ]);
const r = new Rotate360(element, {
src: 'img/produkt_{i}.jpg',
count: 36,
pad: 2,
sensitivity: 6,
autoplay: true,
fps: 15,
resumeDelay: 3000,
pois: [ … ],
hotspots: [ … ]
});
r.next(); r.prev(); r.next(5); // drehen
r.goTo(12); r.goTo(12, 2); // Frame, optional Zeile
r.goTo(12, 0, { animate: true }); // sanft auf kürzestem Weg (goToDuration ms je Frame)
r.goToPoi('flansch_5').then(…); // hindrehen + Sprechblase öffnen, Event rotate360:poifocus
// Kopplung ohne Attribute: opts.sync = 'gruppe', syncProps: 'frame zoom', syncRole: 'follow'
r.rowUp(); r.rowDown(); // Multi-Row
r.play(); r.pause(); r.isPlaying; // Autoplay
r.zoomIn(); r.zoomOut(); r.resetZoom();
r.setZoom(2.5, x, y); // Zoom, Fixpunkt in Bühnen-Pixeln
r.fullscreen(); r.exitFullscreen(); r.toggleFullscreen(); r.isFullscreen;
r.openDetail(poiOrId); r.closeDetail(); r.isDetailOpen;
r.frame; r.row; r.zoom; // aktueller Zustand
r.destroy();
Rotate360.init(); // alle .rotate360 nachträglich (idempotent)
element._rotate360; // Instanz-Backref
$('.rotate360').rotate360({ … }); // jQuery-Bridge, falls jQuery geladen
Alle Events sind CustomEvents am Host, bubbeln und tragen detail.instance.
| Event | detail | Wann |
|---|---|---|
rotate360:progress | loaded, total | pro geladenem Bild |
rotate360:ready | - | alle Bilder geladen (bei lazy: das Startbild) |
rotate360:frame | frame, row, pois | nach jedem Bildwechsel |
rotate360:zoom | zoom, panX, panY | bei Zoom oder Pan |
rotate360:play / :pause | - | Autoplay gestartet / angehalten |
rotate360:fullscreen | active | Vollbild betreten / verlassen |
rotate360:poi | poi, frame, row | Klick auf einen Marker |
rotate360:poienter / :poileave | poi, frame, row | Marker wird sichtbar / unsichtbar |
rotate360:detailopen / :detailclose | poi, detail | Detailbild geöffnet / geschlossen |
rotate360:poifocus | poi, frame, row | goToPoi() ist angekommen |
Die Leiste bringt acht Icons als PNG mit (Base64, in rotate360.js). Sie werden als
CSS-Maske gezeichnet, die Farbe kommt aus --r360-btn-fg. Wer eigene Buttons
außerhalb des Widgets bauen will, setzt data-rotate-controls="false" und ruft die
API auf - so wie in Demo 4. Die Icons stehen unter Rotate360.ICONS auch für eigene
Buttons bereit.
Die Breite kommt immer vom umgebenden CSS. Die Höhe folgt dem Seitenverhältnis des ersten
Bildes; --r360-aspect setzt stattdessen ein festes Verhältnis. Eine
height direkt am Host wirkt nicht - der Host schneidet dann
nur unten ab, weil die Bühne ihre Seitenverhältnis-Höhe behält. Dafür gibt es zwei eigene
Tokens:
/* Deckel: Breite bleibt fluid, Höhe wird begrenzt, Bild wird eingepasst */
.rotate360.produkt { --r360-max-height: 70dvh; }
/* feste Höhe: das Seitenverhältnis fällt weg */
.rotate360.karte { --r360-height: 320px; }
/* Höhe kommt vom Host (Grid-/Flex-Zelle, Modal, Splitview) */
.viewer-zelle { height: 100%; }
.viewer-zelle .rotate360 { height: 100%; --r360-height: 100%; }
Das Bild wird in jedem Fall per contain in die Bühne gelegt - freie Fläche zeigt
--r360-bg, genau wie im Vollbild. POIs, Sprechblasen und der Zoom-Ausschnitt
rechnen sich über einen ResizeObserver sofort neu, auch in Tabs und
Accordions. dvh ist vh vorzuziehen, sonst springt die Höhe auf
iOS beim Ein- und Ausblenden der Browserleiste.
Wer den Viewer schmal werden lässt, sollte auch das Ziehen mitskalieren:
data-rotate-sensitivity ist ein absoluter Pixelwert, ein auf dem Desktop
eingestellter Wert lässt einen auf dem Handy also mehrfach absetzen und neu wischen.
data-rotate-sweep="1" rechnet den Mausweg stattdessen relativ zur Bühne
(einmal quer = eine Umdrehung) und fühlt sich auf jeder Größe gleich an;
data-rotate-sensitivity-ref ist der Weg für Seiten, die ihren absoluten Wert
behalten wollen. Beides lässt sich in der Spielwiese zusammen mit der Viewer-Breite
ausprobieren.
Das Painting ist gekapselt; angepasst wird ausschließlich über die --r360-*-Tokens am Host:
.rotate360.mein-viewer {
--r360-bg: #fff;
--r360-radius: 12px;
--r360-aspect: 4 / 3; /* statt Bild-Seitenverhältnis */
--r360-max-height: 70dvh; /* Höhendeckel, Bild wird eingepasst */
/* --r360-height: 320px; */ /* feste Höhe statt Seitenverhältnis */
--r360-btn-bg: rgba(0,0,0,.6);
--r360-btn-fg: #fff;
--r360-btn-opacity: 0.4; /* erst bei Hover voll sichtbar */
--r360-poi-bg: #ff5500;
--r360-loader-color: #ff5500;
/* Hotspots */
--r360-leader-color: rgba(0,0,0,.55); --r360-leader-width: 1.5px; --r360-leader-dash: none;
--r360-callout-bg: #fff; --r360-callout-fg: #222; --r360-callout-width: 240px;
--r360-callout-radius: 6px; --r360-callout-size: .85rem; --r360-callout-link: #c0392b;
--r360-callout-border: 1px solid rgba(0,0,0,.12); --r360-callout-shadow: 0 2px 12px rgba(0,0,0,.18);
--r360-callout-padding: .6em .85em;
--r360-callout-detail-fg: #c0392b; --r360-callout-detail-size: .92em; /* Button „Detailbild" */
--r360-callout-pinned-shadow: 0 0 0 2px rgba(59,130,246,.5); /* angeheftet, optional */
/* Detailbild */
--r360-detail-bg: rgba(18,18,20,.94);
--r360-detail-caption-bg: rgba(0,0,0,.55); --r360-detail-caption-fg: #fff; --r360-detail-caption-size: .9rem;
}
Alle Tokens mit Defaults stehen in rotate360.css; die Hotspot- und Detail-Tokens haben zusätzlich Fallbacks in der Library, funktionieren also auch mit einer älteren rotate360.css.
Der Tab Editor baut die Konfiguration, ohne dass JSON von Hand geschrieben werden muss. Der Arbeitsablauf: Bilder hineinziehen (oder „Bilder per URL"), die Reihenfolge im Filmstreifen prüfen, im großen Bild POIs setzen, ihnen einen Hotspot geben und sie mit „→ nächstes Bild" bzw. „Bereich" über die Frames mitführen. JSON und HTML-Schnipsel entstehen dabei live, die Vorschau darunter ist die echte Library. Der Knopf Rundgang zeigt das einmal an einem Beispiel; Hinweise setzt Nummern an die Bedienelemente zum Nachschlagen.
el.shadowRoot; für delegierte Listener am Host das echte Ziel über Rotate360.realTarget(e) ermitteln.prefers-reduced-motion abgeschaltet.height am Host bleibt wirkungslos; dafür sind --r360-height und --r360-max-height da (siehe Größe und Responsive).ResizeObserver misst den Bildrahmen neu, sobald das Element sichtbar wird (Tabs, Accordions, Modals).goToPoi() das Objekt hindreht, ist mit wenigen Zeilen gebaut - siehe Demo 3.frames oder einfach eine URL pro Zeile. Die Reihenfolge bleibt wie angegeben, bei einem 2D-Array ergibt sich die Zeilenzahl von selbst. Die URLs wandern unverändert in den Export - der Basis-Pfad wird nur für Detailbilder benutzt. Ein ZIP enthält diese Bilder nicht.