rotate360 - Drehansicht aus Einzelbildern

Standard - 24 Frames, Buttons, Zoom per Mausrad
Multi-Row - 5 Zeilen, senkrecht ziehen wechselt die Blickhöhe
Hotspots - Sprechblase mit Führungslinie; Klick heftet sie an, zweiter Klick oder „Detailbild" öffnet das Bild (Esc schließt)
Frame 1, 6 oder 13 ansteuern - Marker mit Sprechblase erscheinen. Die Knöpfe drehen per goToPoi() hin.
Autoplay rückwärts, ohne Leiste - Steuerung über die API
Gekoppelt - zwei Viewer in einer Sync-Gruppe, Zoom ebenfalls gekoppelt

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.

rotate360.js JavaScript, Icons inklusive rotate360.css Tokens

Quick Start

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.

Bilder als Liste

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.

Bedienung

Data-Attribute

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).

AttributWertBeschreibung
Bildquelle
data-rotate-srcMusterBild-URL mit {i} (Frame) und optional {r} (Zeile).
data-rotate-countZahlFrames pro Zeile.
data-rotate-rowsZahlZeilen für Multi-Row. Default 1.
data-rotate-padZahlStellen, auf die {i} mit Nullen aufgefüllt wird.
data-rotate-index-startZahlErste Nummer im Dateinamen. Default 1.
data-rotate-framesJSON-Array oder a|b|cExplizite Bildliste statt Muster.
data-rotate-src-large, -frames-largeMuster, ListeZweite 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-preloadall | lazyall lädt alle Bilder vom Startbild nach außen; lazy lädt nur das aktuelle Bild plus zwei Nachbarn je Richtung.
data-rotate-when-visibleboolBilder 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-rowZahlStartposition (0-basiert).
data-rotate-reverseboolDrehrichtung umkehren.
data-rotate-reverse-rowsboolKipprichtung bei Multi-Row umkehren. Default: nach oben ziehen zeigt das Objekt von unten (Zeile 0 = unterste Blickhöhe).
data-rotate-loopboolNach dem letzten Frame wieder beim ersten. Default true.
data-rotate-loop-rowsboolZeilen ebenfalls endlos. Default false.
Ziehen und Nachlauf
data-rotate-sensitivitypxMausweg pro Frame, absolut. Default 8. Kleiner = schneller.
data-rotate-sensitivity-ypxMausweg pro Zeile, absolut. Default 40.
data-rotate-sweepZahlAnteil 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-yZahlDasselbe 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-refpxBezugsbreite 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, -frictionbool, 0–1Nachlauf nach dem Loslassen und dessen Abbremsung pro 16 ms. Default true, 0.92.
data-rotate-snap, -snap-rangebool, ZahlEndet ein Nachlauf höchstens snap-range (Default 1) Frames neben einem Frame mit auto-Sprechblase, dreht der Viewer sanft dorthin. Default true.
data-rotate-keyboardboolTastatursteuerung; setzt tabindex=0. Default true.
Autoplay
data-rotate-autoplay, -fpsbool, ZahlAutomatisch drehen, Frames pro Sekunde (Default 12).
data-rotate-autostartboolfalse unterdrückt den Autoplay-Start beim Init.
data-rotate-resume-delaymsAutoplay nach einer Interaktion (auch nach dem Schließen eines Detailbilds) fortsetzen. 0 = nie.
Zoom
data-rotate-zoomboolZoom erlauben. Default true.
data-rotate-zoom-min, -zoom-max, -zoom-stepZahlGrenzen und Schrittweite. Default 1, 4, 0.5.
data-rotate-zoom-wheel, -zoom-dblclickboolGesten für den Zoom an/aus.
data-rotate-zoom-dragrotate | panZiehen mit links, wenn gezoomt ist. Default rotate; verschoben wird dann mit rechter Maustaste, Strg/Cmd oder zwei Fingern.
Oberfläche
data-rotate-controlsboolButton-Leiste. Default true.
data-rotate-controls-listListeAuswahl und Reihenfolge: prev next zoomin zoomout play fullscreen.
data-rotate-fullscreenboolVollbild-Button und Taste F. Default true.
data-rotate-hint, -hint-textbool, TextHinweis-Overlay nach dem Laden. Default „Ziehen zum Drehen".
data-rotate-hover-delaymsKarenz, bevor eine hover-Sprechblase nach dem Verlassen von Marker/Box schließt. Default 250.
data-rotate-detail-labelTextBeschriftung des Buttons in der Sprechblase, wenn ein Detailbild da ist. Default „Detailbild", leer = kein Button.
data-rotate-altTextARIA-Label des Hosts.
data-rotate-debugboolKleine 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, -hotspotsJSONMarker und Sprechblasen, siehe nächster Abschnitt. Meist bequemer als Inline-JSON im Host.
Gekoppelte Drehung
data-rotate-syncNameAlle 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-propsListeWas gekoppelt wird: frame (Frame und Zeile, Default) und/oder zoom (Zoom und Ausschnitt, an die jeweilige Viewergröße angepasst).
data-rotate-sync-roleboth | followfollow macht einen Viewer zum reinen Folger: eigenes Drehen und Kippen wird ignoriert, Zoom bleibt möglich.
Kopplung, Randfälle: Weichen die Frame-Zahlen ab (24 ↔ 36), wird über den Drehwinkel gekoppelt - mit einer Warnung in der Konsole. Zeilen werden nur bei gleicher Zeilenzahl übernommen. Ein Mitglied, das noch lädt oder außerhalb des Viewports liegt, merkt sich den Zielframe und steht richtig, sobald seine Bilder da sind; es gibt keine Fehler, höchstens übersprungene Zwischenbilder. Startet ein Mitglied Autoplay oder wird angefasst, halten die anderen ihr Autoplay an, damit nur eine Uhr läuft.

Points of Interest und Hotspots

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" }
  ]
}

Detailbild

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([ … ]);

JavaScript-API

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

Events

Alle Events sind CustomEvents am Host, bubbeln und tragen detail.instance.

EventdetailWann
rotate360:progressloaded, totalpro geladenem Bild
rotate360:ready-alle Bilder geladen (bei lazy: das Startbild)
rotate360:frameframe, row, poisnach jedem Bildwechsel
rotate360:zoomzoom, panX, panYbei Zoom oder Pan
rotate360:play / :pause-Autoplay gestartet / angehalten
rotate360:fullscreenactiveVollbild betreten / verlassen
rotate360:poipoi, frame, rowKlick auf einen Marker
rotate360:poienter / :poileavepoi, frame, rowMarker wird sichtbar / unsichtbar
rotate360:detailopen / :detailclosepoi, detailDetailbild geöffnet / geschlossen
rotate360:poifocuspoi, frame, rowgoToPoi() ist angekommen

Buttons

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.

Größe und Responsive

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.

Anpassen per CSS-Variablen

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.

Editor

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.

Hinweise

Frame-
Zeile-
Zoom-
Viewer-Breite rechts verkleinern und den Unterschied vergleichen.
8
Ist die Bühne schmaler, wird die Sensitivity im selben Verhältnis kleiner.
0.92
Näher an 1 = längeres Ausrollen.
Bei „drehen" verschiebt die rechte Maustaste, Strg/Cmd oder zwei Finger.
Keine Auswahl = Leiste aus.
100
0
Beides wirkt sofort, ohne den Viewer neu zu bauen.
1
Für Bilder, die schon auf einem Server liegen: JSON-Array (1D oder 2D), ein Objekt mit 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.
Bilder werden über den Dateinamen zugeordnet und in die Reihenfolge der Config gebracht; POIs, Hotspots, Zeilen und Startframe werden übernommen. Vorher die Bilder laden (Detailbilder bei Hotspots bzw. POIs nachladen).
-
Klick ins Bild setzt einen POI für dieses Bild. Marker ziehen verschiebt, Klick auf einen Marker wählt ihn aus; die Sprechblase eines Hotspots lässt sich ebenfalls ziehen. Rechtsklick (Touch: langes Drücken) öffnet ein Menü - auf einem Marker mit Beschriftung, Hotspot-Zuweisung, Kopieren und Löschen, auf freier Fläche mit „POI mit Hotspot hier" und „alle POIs dieses Bildes löschen". Pfeiltasten wechseln das Bild, N / P kopieren den gewählten POI ins nächste bzw. vorherige Bild und springen hin, H legt ihm einen Hotspot an, Entf löscht ihn, O schaltet Onion-Skin, Strg+Z / Strg+Y machen rückgängig bzw. wiederholen. Kleine graue Marker gehören zum vorherigen bzw. nächsten Bild - Klick springt dorthin.
Points of Interest (dieses Bild)
Hotspots ·
Sprechblase mit Text, Anker und Detailbild - von beliebig vielen POIs über mehrere Frames gemeinsam genutzt. Ein Detailbild direkt am POI hat Vorrang. Einem POI über sein Auswahlfeld zuweisen, dann mit „→ nächstes Bild" oder „Bereich" mitführen. Goldener Balken: im aktuellen Bild sichtbar; Rahmen: zum gewählten POI gehörig.
Konfiguration (JSON)
HTML-Schnipsel
Die Bilder selbst müssen unter dem Basis-Pfad auf dem Server liegen - der Editor exportiert nur Namen, POIs und Hotspots.
Live-Vorschau