AnalogClock - Zifferblatt-Komponente

Zifferblätter

12 · lateinisch
12 · römisch
24 · Vollkreis, Stundenzeiger einmal pro Tag
12+24 · Innenring 13-24

Teilung

both · Minuten und Stunden
hour · nur Stundenstriche
quarter · vier Marken
minute · nur der Minutenkranz
off · Ziffern rücken nach außen

Beschriftung

Minuten in Fünferschritten
nur Viertelstunden
alle 60 Minuten, ohne Stundenziffern

Ziffernsysteme

arab · arabisch-indisch
deva · Devanagari
hanidec · chinesisch
eigenes Array · Wochentags-Blatt

Zeigerformen

stick · Standard
bar
taper
leaf
arrow
outline
je Zeiger einzeln gesetzt

Sektoren

Nachtstunden · 20 bis 7 auf dem 24er-Blatt
zwei Ringstücke mit eigener Farbe
Fortschritt der Stunde · Minutenskala, live

Zeiger ziehen

stufenlos · Minuten- und Stundenzeiger greifbar
rastet auf Viertelstunden
rastet auf 5 Minuten, Sekunden auf ganze Sekunden
ungekoppelt · jeder Zeiger einzeln, kein Übertrag

Lernaufgabe

Stelle die Uhr auf

--:--

Das Prüfen steckt im Seitenskript, nicht in der Bibliothek · „Lösung zeigen" ist ein Zielzeiger

Zeigerbewegung

sweep · stufenlos
tick · springt sekündlich
stop-to-go · 58,5 s Umlauf, dann Wartepause

Zeitzonen

Add-on: Mondphase

Standard · Scheibe bei 6 Uhr
mit Namen und Prozent, größere Scheibe
24er-Blatt mit Nachtsektor und Mond

Themes

Default · Porzellan
paper
mono
slate
neon
terminal · 24h
auto · folgt hell/dunkel des Systems

Ziffernkranz ausdünnen

24h · römisch · auto - jede zweite Stunde
dieselbe Uhr mit numeral-step="1"
12h · numeral-step="3"

Kontrast-Themes

Drei Blätter für Sehbeeinträchtigung und schwierige Ansichtsbedingungen: größer, fetter, ohne Schlagschatten, alle Farbpaare über 7:1. Die Zeiger sind an Länge und Dicke zu unterscheiden, nicht an der Farbe.

contrast · Schwarz auf Weiß, 21:1
contrast-dark · bei Blendempfindlichkeit
contrast-yellow · Gelb auf Schwarz

AnalogClock ist ein Zifferblatt für Uhrzeit, Zeitzonen und Lernsituationen: 12- und 24-Stunden-Blätter, steuerbare Teilung, lateinische, römische und beliebige andere Ziffern, sechs Zeigerformen, farbige Sektoren, ein Zielzeiger für Aufgaben, ein- und ausblendbare Zeiger, optional mit der Maus verstellbar - und eine Add-on-Schnittstelle für alles Weitere. Konfiguration entweder vollständig deklarativ über data-*-Attribute oder programmatisch über JavaScript. Auto-Init beim DOM-ready inklusive.

analogclock.js JavaScript analogclock.css Theme-Tokens

Quick Start

Zwei Dateien einbinden, ein Container-Element setzen - fertig. Alle .analogclock-Elemente werden beim Laden automatisch initialisiert.

<link rel="stylesheet" href="analogclock.css">
<script src="analogclock.js" defer></script>

<div class="analogclock"></div>

Der Container muss ein generisches Element sein (div, section) - Elemente wie input oder img erlauben keinen Shadow Root.

Konfiguration per Attribut

<div class="analogclock"
     data-clock-dial="12+24"
     data-clock-numerals="roman"
     data-clock-ticks="hour"
     data-clock-hand-shape="leaf"
     data-clock-minute-labels="5"
     data-clock-drag="hm"
     data-clock-snap="15"></div>

Konfiguration per JavaScript

const clock = new AnalogClock(document.querySelector('#uhr'), {
    dial: '24',
    numerals: 'roman',
    timezone: 'Asia/Tokyo',
    motion: 'stop-to-go'
});

Reihenfolge der Auswertung: Defaults < data-Attribute < opts-Objekt. Die Instanz hängt am Element als el._analogClock; ein zweiter new AnalogClock(el) gibt dieselbe Instanz zurück.

Shadow-DOM-Kapselung

Wie FlipArt rendert AnalogClock in einen Shadow Root pro Instanz. Das gesamte Painting steckt als SHADOW_CSS in der JS-Datei, analogclock.css enthält ausschließlich Theme-Tokens. Rohe Tag- oder Nachfahren-Selektoren der einbettenden Seite greifen nicht ins Widget. Styling erfolgt allein über die --ac-*-Tokens und data-clock-theme am Host.

Der Stil wird als geteiltes Constructable Stylesheet eingehängt - einmal geparst, von allen Instanzen benutzt. Für jedes Add-on kommt ein weiteres geteiltes Sheet dazu. Wo die Engine das nicht kennt, wandert derselbe Text wie früher als <style> in den Shadow Root.

Für eigene delegierte Listener: im Shadow wird event.target beim Bubbling auf den Host retargetet. Das echte Ziel liefert AnalogClock.realTarget(e).

Zifferblätter

WertAufbauStundenzeiger
121-12 außeneine Umdrehung je 12 h
241-24 auf dem Vollkreis, 24 obeneine Umdrehung je 24 h
12+241-12 außen, 13-24 auf einem inneren Ringeine Umdrehung je 12 h

Auf dem 24-Blatt fallen Stunden- und Minutenteilung nicht zusammen (15° gegen 6°). Die Stundenmarken rücken deshalb nach innen, damit beide Ringe lesbar bleiben. Der Minutenzeiger läuft in allen Varianten unverändert einmal pro Stunde herum.

Teilung

ticks bestimmt, welche Striche das Blatt trägt:

Wertzeigt
both60 Minutenstriche und die Stundenmarken (Standard)
minutenur den Minutenkranz
hournur die Stundenmarken
quarternur vier Marken bei 12, 3, 6 und 9
offkeine Teilung

Der Ziffernkranz reagiert mit: fällt der Stundenring weg, gibt es nichts mehr, dem die Ziffern ausweichen müssten - sie rücken automatisch nach außen und werden dadurch größer wirksam. Auf dem 24er-Blatt entfällt außerdem das Einrücken der Stundenmarken, sobald der Minutenkranz aus ist.

<!-- reduziertes Blatt: vier Marken, keine Ziffern -->
<div class="analogclock" data-clock-ticks="quarter" data-clock-numerals="off"></div>

clock.setTicks('hour');

Ziffern und Beschriftung

numerals kennt latin, roman und off. Römisch wird in der kurzen Schreibweise gesetzt - IV, nicht IIII.

Breite Ziffern rücken automatisch nach innen. Ein VIII baut siebenmal so breit wie ein I und würde auf einem festen Ziffernkreis in den Stundenring laufen - deshalb wird für jede Ziffer ausgerechnet, wie weit sie nach außen reicht, und nur die zu breiten werden zurückgesetzt. Schmale bleiben, wo sie sind. Der Kranz wird dadurch leicht unrund; das fällt deutlich weniger auf als eine Ziffer, die in die Teilung läuft.

Andere Ziffernsysteme

Jeder Name, den Intl als Ziffernsystem kennt, ist erlaubt - die Umsetzung übernimmt Intl.NumberFormat, es kommt keine Tabelle in die Bibliothek:

<div class="analogclock" data-clock-numerals="arab"></div>     <!-- ١ ٢ ٣ … -->
<div class="analogclock" data-clock-numerals="deva"></div>     <!-- १ २ ३ … -->
<div class="analogclock" data-clock-numerals="hanidec"></div>  <!-- 一 二 三 … -->

Gängig sind außerdem beng, thai, guru, khmr, mymr und fullwide. Kennt die Engine ein System nicht, meldet die Uhr das auf der Konsole und setzt lateinische Ziffern. Die Sprache für die Umsetzung steuert locale.

Ganz eigene Beschriftungen gehen als Array (Position 1 bis 12 bzw. 24) oder als Funktion:

new AnalogClock(el, {
    numerals: ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So', '', '', '', '', '']
});

new AnalogClock(el, {
    numerals: (n) => (n % 3 === 0 ? String(n) : '·')
});

Ein leerer Text lässt die Stelle frei. Für CSS-Selektoren tragen Array und Funktion am Host data-clock-numerals="custom".

Ziffernkranz ausdünnen

numeralStep beschriftet nur jede n-te Stunde - die Stundenstriche bleiben vollzählig. Der Standard auto dünnt genau dort aus, wo es sonst übereinander läuft: römisch auf dem 24er-Blatt. Dort stehen die Positionen 8,4 Einheiten auseinander, ein XVIII baut aber doppelt so breit - kein Schriftgrad rettet das, nur weniger Ziffern. Mit numeralStep: 1 lässt sich die Ausdünnung abschalten, mit 3 oder 6 kommt man zum sehr reduzierten Blatt.

<!-- II, IV, VI … XXIV - das macht auto von selbst -->
<div class="analogclock" data-clock-dial="24" data-clock-numerals="roman"></div>

<!-- nur 3, 6, 9, 12 -->
<div class="analogclock" data-clock-numeral-step="3"></div>

Minutenbeschriftung

minuteLabels legt einen zweiten Ring außerhalb der Stundenziffern an:

Wertzeigt
offnichts (Standard)
505, 10, 15 … 60
quarter15, 30, 45, 60
all1 bis 60 in Kleinstschrift

Schrift und Größe hängen an Tokens, die auf die Konfiguration reagieren: römische Ziffern holen sich automatisch eine Serifenschrift und werden etwas kleiner gesetzt, das 24er-Blatt reduziert die Größe nochmals, ostasiatische und indische Systeme ebenfalls. Alles überschreibbar:

/* Global */
.analogclock {
    --ac-numeral-font: "Cormorant Garamond", serif;
    --ac-numeral-size: 8.5px;
}

Größenangaben in px beziehen sich auf die SVG-viewBox: das Blatt ist 100 Einheiten breit, 9px sind also 9 % des Durchmessers. Dadurch skaliert alles mit --ac-size mit. Für eine einzelne Instanz gehen auch die Optionen numeralFont und numeralSize bzw. data-clock-numeral-font und data-clock-numeral-size.

Zeigerformen

handShape ändert den Umriss der Zeiger. Alle Formen sind rein geometrisch beschrieben:

WertForm
stickStrich mit runden Enden (Standard, als <line> gezeichnet)
barRechteck mit flachen Enden
taperTrapez, zur Spitze zulaufend
leafLanzette
arrowSchaft mit Pfeilspitze
outlineLanzette als offene Kontur

Eine Zeichenkette gilt für Stunden- und Minutenzeiger; der Sekundenzeiger bleibt dann bewusst ein Strich, weil gefüllte Formen bei dieser Breite nur unruhig wirken. Ein Objekt setzt jeden Zeiger einzeln:

<div class="analogclock" data-clock-hand-shape="leaf"></div>

<div class="analogclock"
     data-clock-hand-shape='{"h":"leaf","m":"taper","s":"stick"}'></div>

clock.setHandShape({ h: 'arrow', m: 'arrow', s: 'bar' });

Statt eines Namens geht auch ein eigener Pfad: Drehpunkt ist 0/0, die Spitze zeigt nach -y, die Einheiten sind die der viewBox. Länge, Überstand und Breite bekommt eine Pfadfunktion als Argumente:

clock.setHandShape({
    h: 'M -1 3 L 1 3 L 0 -22 Z',                        // fester Pfad
    m: (len, tail, w) => `M ${-w / 2} ${tail} H ${w / 2} V ${-len} Z`  // gerechnet
});

Die Trefferfläche zum Ziehen bleibt in jedem Fall eine dicke, unsichtbare Linie - sie fängt jede Form ab, egal wie schmal die Spitze ist.

Keine geschützten Designs. Die mitgelieferten Formen sind Grundformen - Rechteck, Trapez, Lanzette, Pfeil. Bekannte Zeiger- und Zifferblattdesigns (etwa das der Schweizer Bahnhofsuhr) sind marken- bzw. designrechtlich geschützt und werden hier bewusst nicht nachgebildet. Wer so etwas braucht, klärt die Lizenz und übergibt einen eigenen Pfad.

Sektoren

zones legt farbige Kreisausschnitte unter die Teilung: Nachtstunden, Öffnungszeiten, Belegung, ein Fortschrittsbogen. Sie liegen über dem Blatt und unter allem anderen, verdecken also weder Ziffern noch Zeiger.

<div class="analogclock" data-clock-dial="24"
     data-clock-zones='[{"from":"20:00","to":"07:00"}]'></div>

clock.setZones([
    { from: '09:00', to: '12:00', inner: 26, outer: 36 },
    { from: '13:00', to: '15:30', inner: 26, outer: 36, color: '#c98a2e' }
]);
FeldBedeutung
from / to"HH:MM", eine Zahl oder ein Date. Gerechnet wird im Uhrzeigersinn, to darf kleiner sein als from (20 → 7).
scalehour (Standard) folgt dem Zifferblatt - 12 oder 24 Stunden je Umlauf. minute rechnet 0-60 auf den Vollkreis.
inner / outerRadien in viewBox-Einheiten (Blatt = 100, Mitte = 50). inner: 0 ergibt ein Tortenstück, größer 0 ein Ringstück.
color / opacityüberschreiben --ac-zone und --ac-zone-opacity für diesen einen Sektor.
classNamezusätzliche Klasse am Pfad - für eigenen Add-on- oder Theme-Stil.

Gleiche Grenzen meinen den Vollkreis, nicht „nichts": { from: 0, to: 0, inner: 30 } ist ein geschlossener Ring. setZones() zeichnet auch dann neu, wenn dasselbe Array an Ort und Stelle verändert wurde - für einen mitlaufenden Bogen also einmal je Minute aufrufen, nicht je Frame.

Zielzeiger

ghost zeichnet eine blasse Sollstellung mit, die sich nicht bewegt - gedacht als Auflösung einer Lernaufgabe oder als Marke für einen Termin:

<div class="analogclock" data-clock-ghost="15:40"></div>

clock.setGhost('15:40');        // "HH:MM[:SS]", Date oder { h, m, s }
clock.setGhost(null);           // wieder ausblenden

ghostHands bestimmt, welche Zeiger der Zielzeiger zeigt - Standard "hm", weil eine Sollsekunde selten gemeint ist. Farbe und Deckkraft hängen an --ac-ghost und --ac-ghost-opacity; Form, Länge und Breite übernimmt er von den echten Zeigern.

Weiterhin nicht enthalten. Der Zielzeiger zeigt nur. Ob eine Eingabe richtig ist, entscheidet nach wie vor die Anwendung - siehe Zeiger einzeln stellen.

Zeitmodell

Angezeigt wird immer eine Wanduhrzeit. Sie entsteht auf zwei Wegen:

ZustandAnzeige
laufendjetzt + offset, danach in die Zielzone umgerechnet
eingefrorender eingefrorene Wert, direkt
clock.setTime('14:35');                    // stehenbleiben auf 14:35
clock.setTime('14:35', { freeze: false }); // laufend, zeigt jetzt 14:35
clock.shift(+1, 'h');                      // eine Stunde vor
clock.shift(-30, 's');                     // 30 Sekunden zurück
clock.freeze();                            // anhalten
clock.thaw();                              // ab dem Standwert weiterlaufen
clock.reset();                             // zurück auf die echte Zeit

setTime() versteht Date, einen ms-Zeitstempel, "HH:MM", "HH:MM:SS" und alles, was new Date(string) parst. thaw() übernimmt standardmäßig den Standwert in den Offset - die Uhr läuft also von dort weiter. thaw({ absorb: false }) springt stattdessen zur echten Zeit zurück.

setTime() und thaw() setzen eine Zeigerstellung, keinen Kalendertag: der Offset nimmt immer den kürzesten Weg dorthin, also +15 min statt -23 h 45 min. shift() normalisiert bewusst nicht - dort ist shift(3, 'd') eine Ansage.

getTime() gibt die Zeigerstellung des heutigen Tages zurück. Wer das Datum braucht - etwa ein Add-on für Mondphase oder Wochentag -, nimmt getInstant(): dort ist ein shift(3, 'd') auch als Datum sichtbar.

Zeitzonen

Ohne Angabe gilt die Gerätezone. Jede IANA-Zone ist ohne Netzwerk und ohne Berechtigung möglich, Sommerzeit inklusive - die Umrechnung macht Intl.DateTimeFormat:

<div class="analogclock" data-clock-timezone="Asia/Tokyo"></div>

clock.setTimezone('America/New_York');
AnalogClock.deviceTimezone();   // "Europe/Berlin"
Kein Ortsbezug. Standort zu Zeitzone ginge nur über eine Freigabe des Nutzers plus eine API oder einen mehrere hundert Kilobyte großen Polygondatensatz. Wer Städtenamen braucht, pflegt besser eine kleine eigene Tabelle Stadt → IANA-ID in der Anwendung.

Eine zweite Zone dazu

Der Abstand zweier Zonen ist zu jedem Zeitpunkt eine feste Minutenzahl - über das Jahr aber nicht, denn beide stellen an unterschiedlichen Tagen um. Die Uhr bildet ihn deshalb immer zu dem Zeitpunkt, den sie gerade darstellt:

clock.offsetTo('America/New_York');          // Minuten, Sommerzeit inklusive
		clock.getParts({ zone: 'Asia/Kolkata' });    // deren Zeit
		clock.getAngles({ zone: 'Asia/Kolkata' });   // deren Zeigerwinkel
		clock.getAngles({ offsetMinutes: -330 });    // roher Versatz, ohne Zone

		AnalogClock.zoneOffset(zone, base, at);      // ohne Instanz, zu einem Zeitpunkt
		AnalogClock.zoneCity('Europe/Berlin');       // 'Berlin'
		AnalogClock.formatOffset(330);               // 'UTC+5:30'

Bezug ist die angezeigte Zone der Uhr, nicht die des Geräts. Ohne Argument geben getParts() und getAngles() unverändert die eigene Zeit - die Erweiterung ist rein additiv, es gibt kein neues Attribut und kein neues Token.

Gerechnet wird in Minuten, nicht in Stunden: Indien liegt auf +5:30, Nepal auf +5:45, die Chatham-Inseln auf +12:45. Der Sekundenzeiger bleibt unberührt - alle heutigen Zonenversätze sind volle Minuten.

Während eines Ziehens und bei gelöster Kopplung hält die Uhr den Bezugszeitpunkt fest. Eine gezogene Zeigerstellung ist kein belastbarer Zeitpunkt: wer den Stundenzeiger lange genug dreht, kreuzt sonst eine Zeitumstellung, und ein Sprung um eine Stunde mitten im Ziehen sähe wie ein Fehler aus.

Eine unbekannte Zone gibt keinen Versatz und meldet sich einmal auf der Konsole - sie fällt ausdrücklich nicht still auf die Gerätezone zurück.

Anzeigen statt nur rechnen kann das Ganze das Add-on subdial: GMT-Hilfsblätter mit Ortsnamen, mehrere pro Uhr.

Zeigerbewegung

WertVerhalten
sweepalle Zeiger stufenlos (Standard)
tickSekundenzeiger springt sekündlich, Minutenzeiger minütlich
stop-to-goSekundenzeiger in 58,5 s einmal herum, dann Pause bis zum Minutenimpuls

Aktualisiert wird über requestAnimationFrame, geschrieben wird nur bei sichtbarer Winkeländerung. In einem inaktiven Tab pausiert der Loop und synchronisiert sich beim Zurückkommen selbst.

Bei prefers-reduced-motion: reduce schaltet sweep automatisch auf tick herunter - die Uhr geht weiter richtig, bewegt sich aber nicht mehr dauerhaft.

Zeiger ziehen

drag nennt die ziehbaren Zeiger als Zeichenkette - "hms", "m", "" für aus. Getroffen wird über unsichtbare, dicke Trefferlinien im SVG; die Stapelreihenfolge entscheidet bei Überlagerung (Sekunde vor Minute vor Stunde).

<div class="analogclock" data-clock-drag="hm" data-clock-snap="15"></div>

Beim Anfassen friert die Uhr automatisch ein - ohne eigenes Event, damit die Folge wie dokumentiert mit clock:start beginnt. Gerechnet wird mit der Winkeldifferenz, nicht mit dem absoluten Winkel - dadurch trägt der Minutenzeiger beim Überlauf sauber in die Stunde weiter, und der Stundenzeiger nimmt die Minuten mit.

snap rastet in Minuten (0 = stufenlos, 1, 5, 15). Für den Sekundenzeiger bedeutet ein gesetztes snap: ganze Sekunden.

Das Verzahnen lässt sich abschalten - siehe Zeiger einzeln stellen weiter unten.

Zeiger einzeln stellen

Standardmäßig sind die Zeiger verzahnt wie im Uhrwerk. Für Lernaufgaben ist das hinderlich: das Kind soll Stunden- und Minutenzeiger unabhängig voneinander setzen können. coupled: false löst die Verzahnung - der angefasste Zeiger bewegt sich, die anderen bleiben stehen, es gibt keinen Übertrag.

<div class="analogclock"
     data-clock-coupled="false"
     data-clock-drag="hm"
     data-clock-hands="hm"
     data-clock-snap="1"></div>

Wie die Stellung abgelesen wird

Ohne Verzahnung bilden die Zeiger nicht mehr zwingend eine gültige Uhrzeit ab. getParts() gibt deshalb wieder, was dasteht - und liest wie ein Mensch:

ZeigerRegelBeispiel
Stundeabgerundet - die zuletzt überschrittene ZahlZeiger kurz vor der 4 → h: 3
Minutegerundet - die nächstgelegene MarkeZeiger bei 39,6 → m: 40
SekundegerundetZeiger bei 44,7 → s: 45

Auf einem 12er-Blatt kann das Zifferblatt kein AM/PM ausdrücken. h bleibt zwar im Bereich 0-23 (der Übertrag beim Überdrehen der 12 wird mitgeführt), verlässlich vergleichbar ist aber nur h % 12.

Feiner prüfen mit getAngles()

Wer eine eigene Toleranz braucht, nimmt die rohen Winkel in Grad - 0 ist 12 Uhr, gezählt im Uhrzeigersinn:

const a = clock.getAngles();   // { h: 110.2, m: 239.6, s: 0 }
const istMinute = a.m / 6;     // 39.93

Ein typischer Vergleich gegen eine Sollzeit, wie ihn der Aufgabenblock im Demo-Tab verwendet:

function stimmt(clock, sollH, sollM, tolMin = 1.5, tolGrad = 15) {
    const a = clock.getAngles();

    // Minutenzeiger: kürzester Abstand in Minuten
    const istMin  = a.m / 6;
    const diffMin = Math.abs(((istMin - sollM + 30) % 60) - 30);

    // Stundenzeiger: Sollposition schließt den Minutenanteil ein
    const sollGrad = ((sollH % 12) + sollM / 60) * 30;
    const diffGrad = Math.abs(((a.h - sollGrad + 180) % 360) - 180);

    return diffMin <= tolMin && diffGrad <= tolGrad;
}
Bewusst nicht enthalten. Aufgaben stellen, Toleranzen festlegen, Rückmeldung geben - das gehört in die Anwendung, nicht in die Bibliothek. AnalogClock liefert nur die Zeigerstellung - und auf Wunsch einen Zielzeiger, der die Lösung anzeigt.

Zurück zur Verzahnung

Jede Zeitoperation stellt die Verzahnung wieder her: setTime(), shift(), thaw(), reset() und update({ coupled: true }). Dabei wird die abgelesene Uhrzeit übernommen und die Zeiger rasten auf die dazu passende Stellung ein - steht der Stundenzeiger frei auf 3,1 und der Minutenzeiger auf 40, ergibt das 3:40, und der Stundenzeiger rückt auf 110°. clock.isUncoupled sagt, ob die Zeiger gerade einzeln stehen.

Add-ons

Alles, was nicht jede Uhr braucht, kommt als Add-on dazu statt als weitere Option: eine Erweiterung meldet sich einmal an und zeichnet danach in eine eigene SVG-Ebene mit - mit eigenem Stil, eigenen Optionen und eigenem Lebenszyklus.

<script src="analogclock.js"></script>
<script src="analogclock-moonphase.js"></script>

<div class="analogclock" data-clock-addons="moonphase"></div>

Mitgeliefert wird die Mondphase als vollständiges Beispiel. Aufbau der Schnittstelle, Ebenen, Kontextobjekt, Download und eine Schritt-für-Schritt-Anleitung für ein eigenes Add-on stehen im eigenen Bereich:

Data-Attribute

AttributWerteStandard
data-clock-dial12 · 24 · 12+2412
data-clock-ticksboth · minute · hour · quarter · offboth
data-clock-numeralslatin · roman · off · Ziffernsystem · JSON-Arraylatin
data-clock-numeral-stepauto · 1 bis 12auto
data-clock-minute-labelsoff · 5 · quarter · alloff
data-clock-handsTeilmenge von h/m/s · true · falsehms
data-clock-hand-shapestick · bar · taper · leaf · arrow · outline · JSON-Objekt · Pfadstick
data-clock-motionsweep · tick · stop-to-gosweep
data-clock-timezoneIANA-NameGerätezone
data-clock-localeBCP-47, z. B. de-DEGerätesprache
data-clock-timeHH:MM[:SS] oder Datum-
data-clock-offsetz. B. +90s, -15m, 2h0
data-clock-dragTeilmenge von h/m/s · true · falseaus
data-clock-snapMinuten, 0 = stufenlos0
data-clock-coupledfalse löst die Verzahnung der Zeigertrue
data-clock-zonesJSON-Array von Sektoren-
data-clock-ghostHH:MM[:SS] - Sollstellung-
data-clock-ghost-handsTeilmenge von h/m/shm
data-clock-addonsName, mehrere durch Leerzeichen, oder JSON-Objekt-
data-clock-themepaper · mono · slate · neon · terminal · auto · contrast · contrast-dark · contrast-yellowDefault
data-clock-numeral-fontCSS-Fontstack-
data-clock-numeral-sizez. B. 8px-
data-clock-labelfalse = aus, sonst eine Vorlage wie {hh}:{mm} Uhran
data-clock-autostartfalsean

JavaScript-API

MethodeWirkung
start() / stop()Aktualisierung an/aus (pause() ist ein Alias)
setTime(v, o)Uhrzeit setzen, { freeze: true|false }
shift(n, unit)verschieben, unit s/m/h/d
freeze(o) / thaw(o)anhalten ({ silent: true } unterdrückt update/set) / weiterlaufen
reset()zurück auf die echte Zeit
setTimezone(tz)Zone wechseln, null = Gerätezone
setTicks(mode)Teilung wechseln
setHandShape(v)Zeigerform wechseln
setZones(list)Sektoren setzen, null entfernt sie
setGhost(v)Zielzeiger setzen, null blendet ihn aus
update(opts)beliebige Optionen zur Laufzeit ändern
getTime()angezeigte Zeit als Date (Zeigerstellung heute)
getInstant()zugrunde liegender Zeitpunkt inkl. Datum
getParts(o){ h, m, s, ms } - die abgelesene Stellung; mit { zone } oder { offsetMinutes } die einer zweiten Zone
getAngles(o){ h, m, s } in Grad, für eigene Toleranzen - dasselbe Argument
offsetTo(zone)Versatz einer zweiten Zone in Minuten, gegen die angezeigte Zone
isUncoupledstehen die Zeiger gerade einzeln?
on(name, fn)Event binden, Namensraum erlaubt: 'set.meins'
off(name)lösen - '.meins', 'change' oder alles
addon(name[, i])Kontext eines an dieser Uhr laufenden Add-ons - Gruppe, Optionen, Zustand. i wählt den Platz, wenn dasselbe Add-on mehrfach hängt.
addonNames()Namen der an dieser Uhr aktiven Add-ons (die statische Variante nennt alle angemeldeten)
destroy()Listener, Loop, Add-ons und Shadow-Inhalt abräumen
isFrozen / offsetSecondsZustand lesen
AnalogClock.init(sel)idempotent nachinitialisieren
AnalogClock.deviceTimezone()IANA-Name der Gerätezone
AnalogClock.zoneOffset(z, b, at)Versatz zweier Zonen in Minuten, zu einem Zeitpunkt
AnalogClock.zoneCity(z)Ortsteil eines Zonennamens - 'Berlin'
AnalogClock.formatOffset(min)Versatz als Text - 'UTC+5:30'
AnalogClock.addon(name, def)Add-on anmelden
AnalogClock.addonNames()Namen aller angemeldeten Add-ons
AnalogClock.RIM_OUTERäußere Kante des freien Bands (49.6) - dort beginnen Ring-Add-ons
AnalogClock.versionVersionszeichenkette

Events

Alle Events bubbeln am Host im Light DOM.

Eventwann
clock:startein Zeiger wurde angefasst
clock:slideein Zeiger bewegt sich während des Ziehens
clock:updatejede gesetzte Änderung - Ziehen wie API
clock:changelosgelassen und der Wert hat sich geändert
clock:setder Wert steht fest: Ende eines Ziehens oder ein programmatischer Setter
clock:endlosgelassen, unabhängig von einer Änderung
clock:tickdie angezeigte Sekunde hat gewechselt

Reihenfolge

Beim Ziehen: start → (slideupdate)* → changesetend. Bei einem programmatischen Setter nur updateset. Das Protokoll in der Spielwiese zeigt das live mit.

Nutzlast

{
    type:      'change',              // Eventname ohne Präfix
    instance:  AnalogClock,
    hand:      'm' | 'h' | 's' | null, // auslösender Zeiger
    source:    'drag' | 'setTime' | 'shift' | 'freeze'
             | 'thaw' | 'reset' | 'options' | 'tick' | 'bind',
    parts:     { h, m, s, ms },       // abgelesene Stellung
    angles:    { h, m, s },           // rohe Winkel in Grad
    date:      Date,
    uncoupled: false                  // stehen die Zeiger einzeln?
}

clock:end trägt zusätzlich changed - ob das Ziehen den Wert verändert hat.

Binden

Wahlweise klassisch mit addEventListener oder über on() mit Namensräumen. Der Rückruf von on() bekommt die Nutzlast direkt und läuft mit this auf der Instanz:

clock.on('change', function (d) {
    pruefe(d.angles, soll);
});

// Mit Namensraum - alles auf einen Schlag wieder lösen
clock.on('update.aufgabe', zeigeStellung);
clock.on('set.aufgabe', speichern);
clock.off('.aufgabe');

clock.off('change');   // alle change, gleich welcher Namensraum
clock.off();           // alles

Events feuern update zusätzlich sofort beim Binden (mit source: 'bind'), damit eine angebundene Anzeige nicht leer startet. Das geht nur über on() - addEventListener kennt so etwas nicht. destroy() löst alle über on() gebundenen Zuhörer automatisch.

Events aus Add-ons

Ein Add-on darf eigene Events feuern. Sie tragen seinen Namen als Präfix statt clock: und werden genauso gebunden - ein Typ mit Doppelpunkt wird wörtlich genommen, alles andere bekommt weiterhin clock: davor:

clock.on('bezel:rotate', d => anzeige.textContent = d.minutes);
clock.on('bezel:set.timer', starteTimer);
clock.off('.timer');

// nativ geht ebenso - die Events bubbeln am Host und sind composed
host.addEventListener('bezel:set', e => console.log(e.detail));

Die Nutzlast enthält immer type, addon, instance und parts; alles Weitere legt das Add-on selbst dazu. Welche Events ein Add-on kennt, steht auf seiner Seite.

Beschriftung für Screenreader

Das SVG trägt role="img" und ein aria-label, das sich mit der angezeigten Minute ändert - nicht sekündlich, sonst redet der Screenreader ununterbrochen. Ohne weitere Angabe schreibt Intl.DateTimeFormat die Uhrzeit in der Sprache des Geräts: „14:35" auf Deutsch, „2:35 PM" auf Englisch. Drei Wege, das zu ändern:

<!-- feste Sprache -->
<div class="analogclock" data-clock-locale="de-DE"></div>

<!-- eigene Vorlage: {h} {m} {s} bzw. {hh} {mm} {ss} mit führender Null -->
<div class="analogclock" data-clock-label="Es ist {hh}:{mm} Uhr"></div>

<!-- volle Kontrolle -->
new AnalogClock(el, {
    label: (t) => `${t.h} Uhr ${t.m}, Zielzeit noch nicht erreicht`
});

data-clock-label="false" bzw. label: false schaltet das Label ganz ab - sinnvoll, wenn die Uhrzeit daneben ohnehin als Text steht.

Kontrast-Themes

Drei mitgelieferte Blätter für Sehbeeinträchtigung, Beamerprojektion, Sonnenlicht oder großen Betrachtungsabstand. „Hoher Kontrast" heißt nicht für alle dasselbe, deshalb drei Varianten:

WertFarbegedacht für
contrastSchwarz auf Weiß (21:1)der Maximalwert, Standardfall
contrast-darkWeiß auf FastschwarzBlendempfindlichkeit - Katarakt, Photophobie, Migräne
contrast-yellowGelb auf SchwarzMakuladegeneration, getrübte Medien - die klassische Kombination der Sehbehindertenarbeit

Die drei ändern mehr als nur Farben: das Blatt ist größer (--ac-size bis 24rem), die Ziffern sind fetter und größer, Zeiger, Achse und Striche dicker, Zielzeiger und Sektoren deutlich kräftiger, und der Schlagschatten ist abgeschaltet - weiche Kanten kosten Trennschärfe. Alle Farbpaare liegen über 7:1 (WCAG AAA für Text), Nebenteilungen über 4,5:1.

Die Zeigerfarbe trägt bewusst keine Information: Stunde, Minute und Sekunde sind an Länge und Dicke zu unterscheiden. Der rote Sekundenzeiger des Defaults kommt auf Weiß nur auf 4,8:1 und fällt bei Rot-Grün-Schwäche optisch mit Schwarz zusammen - in contrast steht dort deshalb ein Dunkelblau.

Weil alle drei Namen mit contrast beginnen, greift der Präfix-Selektor [data-clock-theme^="contrast"]. Eine eigene Ableitung erbt die Geometrie dadurch automatisch und braucht nur noch Farben:

.analogclock[data-clock-theme="contrast-blau"] {
    --ac-face: #ffffff;
    --ac-bezel: #002d6b;
    --ac-numeral: #002d6b;
    --ac-hand-hour: #002d6b;
    --ac-hand-minute: #002d6b;
    --ac-hand-second: #000000;
    --ac-cap: #002d6b;
    --ac-cap-dot: #ffffff;
}
Systemwunsch „mehr Kontrast". Am Ende von analogclock.css steht ein auskommentierter Block für @media (prefers-contrast: more). Er legt die contrast-Tokens auf alle Uhren ohne eigenes data-clock-theme. Bewusst nicht vorbelegt - die Uhr soll ihr Aussehen nicht ungefragt wechseln.

Hell und dunkel: theme „auto"

Dasselbe Prinzip in freiwillig: data-clock-theme="auto" zeigt das helle Standardblatt und wechselt bei prefers-color-scheme: dark auf eine abgedunkelte Variante. Nur wer auto setzt, bekommt den Wechsel. Eigene Paare baut man nach demselben Muster:

.analogclock[data-clock-theme="auto-papier"] {
    --ac-face: #f5f1e6;   /* … hell … */
}

@media (prefers-color-scheme: dark) {
    .analogclock[data-clock-theme="auto-papier"] {
        --ac-face: #221f1a;   /* … dunkel … */
    }
}

Eigene Themes per CSS-Variablen

.analogclock.blaupause {
    --ac-face: #10314f;
    --ac-bezel: #4d7fa8;
    --ac-tick: #2f5f88;
    --ac-tick-hour: #9dc7e8;
    --ac-numeral: #e6f2fb;
    --ac-hand-hour: #e6f2fb;
    --ac-hand-minute: #e6f2fb;
    --ac-hand-second: #ffb03a;
    --ac-cap: #e6f2fb;
    --ac-cap-dot: #10314f;
    --ac-zone: #9dc7e8;
    --ac-ghost: #e6f2fb;
    --ac-shadow: rgba(0, 0, 0, .45);
    --ac-accent: #ffb03a;
}
GruppeTokens
Größe--ac-size
Blatt--ac-face, --ac-bezel, --ac-bezel-width
Teilung--ac-tick, --ac-tick-width, --ac-tick-hour, --ac-tick-hour-width
Ziffern--ac-numeral, --ac-numeral-font, --ac-numeral-size, --ac-numeral-weight, --ac-numeral-24, --ac-numeral-24-size
Minutenring--ac-minute, --ac-minute-font, --ac-minute-size
Zeiger--ac-hand-hour, --ac-hand-minute, --ac-hand-second, --ac-hand-hour-width, --ac-hand-minute-width, --ac-hand-second-width, --ac-hand-outline-width
Zeigerlängen--ac-hand-hour-length, --ac-hand-minute-length, --ac-hand-second-length und je -tail für den Überstand hinter der Achse
Achse--ac-cap, --ac-cap-dot, --ac-cap-r, --ac-cap-dot-r, --ac-weight-r
Sektoren--ac-zone, --ac-zone-opacity
Zielzeiger--ac-ghost, --ac-ghost-opacity
Bedienung--ac-grab-events (none nimmt den Zeigern die Trefferfläche), --ac-grab-width (Breite der unsichtbaren Trefferlinie, Standard 9)
Sonstiges--ac-shadow, --ac-accent
Zeigerlängen sind bewusst nicht vorbelegt. Ohne sie ergeben sich Länge und Überstand aus der Blattgeometrie - schaltet man etwa die Minutenbeschriftung ein, rückt alles automatisch nach innen. Wer die Tokens setzt, übernimmt diese Anpassung selbst.

Hinweise

Größe. Der Host ist inline-block mit aspect-ratio: 1. Die Breite kommt aus --ac-size; alles darin skaliert über die viewBox mit, ein Pixel-Snapping wie bei FlipArt ist deshalb nicht nötig.

Genauigkeit. Die Uhr ist so genau wie die Systemuhr des Geräts. Wer sekundengenau sein muss, setzt den Versatz einmalig aus einem Server-Zeitstempel: clock.shift(serverMs - Date.now(), 's') - oder besser über einen kleinen HEAD-Request und den Date-Header.

Bahnhofsuhren. Der stop-to-go-Modus bildet nur die Mechanik nach. Das Zifferblattdesign der Schweizer Bahnhofsuhr ist geschützt und lizenzpflichtig - ein eigenes Blatt mit dieser Zeigermechanik ist unproblematisch, eine Nachbildung des Designs nicht. Aus demselben Grund bringt handShape nur Grundformen mit.

Ein Add-on ist eine Erweiterung, die sich einmal anmeldet und danach pro Uhr zuschaltbar ist. Es bekommt eine eigene Gruppe im SVG, einen eigenen Stil im Shadow Root, eigene Optionen und wird bei jedem Neuzeichnen sowie im Sekundentakt aufgerufen. Damit bleibt die Bibliothek klein: was nicht jede Uhr braucht, wird kein weiteres Attribut, sondern eine Datei, die man dazulegt oder eben nicht.

Vorhandene Add-ons

Rezepte

Interessant wird es dort, wo mehrere Add-ons dasselbe Blatt teilen. Diese Seiten zeigen kein einzelnes Add-on, sondern ihr Zusammenspiel.

Wie das System funktioniert

Einbinden und zuschalten

Die Add-on-Datei kommt nach analogclock.js. Danach ist sie angemeldet, zeichnet aber erst dort mit, wo sie ausdrücklich zugeschaltet wird:

<script src="analogclock.js"></script>
<script src="analogclock-moonphase.js"></script>

<!-- ein Add-on -->
<div class="analogclock" data-clock-addons="moonphase"></div>

<!-- mehrere, durch Leerzeichen getrennt -->
<div class="analogclock" data-clock-addons="moonphase datum"></div>

<!-- mit Optionen: JSON im Attribut … -->
<div class="analogclock"
     data-clock-addons='{"moonphase":{"radius":9,"label":true}}'></div>
// … oder aus JavaScript
new AnalogClock(el, {
    addons: { moonphase: { radius: 9, angle: 180, label: true } }
});

// zur Laufzeit an- und abschalten
clock.update({ addons: 'moonphase' });
clock.update({ addons: null });

AnalogClock.addonNames();          // ['moonphase']
AnalogClock.hasAddon('moonphase'); // true

Ein nicht angemeldeter Name wird übersprungen und einmal auf der Konsole gemeldet - eine fehlende Add-on-Datei legt die Uhr also nicht lahm.

Ein eigenes Add-on schreiben

Ein Add-on ist ein Objekt mit bis zu drei Funktionen. Angemeldet wird es einmal global:

AnalogClock.addon('name', { /* Definition */ });

Die Definition

FeldBedeutung
layerZeichenebene: face, dial (Standard) oder over
cssStil, der im Shadow Root gilt. Wird einmal je Add-on-Name in ein geteiltes Stylesheet übernommen - beliebig viele Uhren kosten also nur einen Parse-Vorgang.
defaultsVorgabeoptionen; die Angaben der Uhr werden darübergelegt.
rimBreite, die das Add-on aus dem Außenband beansprucht - eine Zahl oder eine Funktion (options, clock). Zugeteilt wird sie als ctx.rim.
frametrue ruft paint() bei jedem Frame statt einmal je Sekunde. Nur nehmen, wenn wirklich etwas mitlaufen muss.
build(ctx)Aufbau. Läuft bei jedem Neuzeichnen des Blatts - die Gruppe ist dann leer, alle Radien stehen fest.
paint(ctx)Aktualisierung. Läuft direkt nach build() und danach je Sekunde.
destroy(ctx)Abbau. Läuft vor einem Neuaufbau und bei clock.destroy() - nötig nur für eigene Timer oder Listener außerhalb der Gruppe.

Der Stapel

1 Blatt circle.ac-face
2 Sektoren g.ac-zones
3 Ebene face Add-ons · Flächen, Bögen
4 Teilung g.ac-ticks
5 Minutenbeschriftung g.ac-minutes
6 Ziffern g.ac-numerals
7 Ebene dial Add-ons · Standardebene
8 Zielzeiger g.ac-ghosts
9 Zeiger und Achse g.ac-hands
10 Ebene over Add-ons · nie verdeckt

Oben liegt, was zuletzt gezeichnet wird

Wo ein Add-on landet

SVG kennt kein z-index - es gilt allein die Dokumentreihenfolge. Die drei Add-on-Ebenen werden beim Aufbau des Blattes an ihre Stelle gehängt und stehen damit fest, bevor ein Add-on überhaupt zeichnet. Daraus folgt alles Weitere:

<!-- zwei Add-ons in derselben Ebene: sonne liegt unter mond -->
<div class="analogclock" data-clock-addons="sonne mond"></div>
 
<!-- umgekehrt -->
<div class="analogclock" data-clock-addons="mond sonne"></div>

Die Ebenen

Ebeneliegtgut für
faceüber dem Blatt, unter der TeilungHintergründe, Muster, Bögen
dialüber den Ziffern, unter den ZeigernAnzeigen, Fenster, Logos
overüber allemGlasreflex, Marken, Overlays

Die Gruppe eines Add-ons trägt class="ac-addon" und data-addon="name" und ist standardmäßig für Zeiger durchlässig (pointer-events: none) - sie nimmt einem ziehbaren Zeiger also nichts weg. Wer Interaktion braucht, nimmt ctx.grab(); das setzt pointer-events am angefassten Element mit.

Der Kontext

FeldInhalt
ctx.clockdie Instanz - volle API, also auch getParts(), getAngles(), offsetTo(), on()
ctx.groupdie eigene <g>-Gruppe. Alles hier hinein.
ctx.geoalle Radien des aktuellen Blatts: rFace, rTickOuter, rTickHourInner, rNumeral, rZone, lenHour, lenMinute
ctx.rim{ outer, inner, width } - das zugeteilte Band im Außenring. Ohne rim in der Definition ist width: 0 und beide Radien zeigen auf die freie Kante.
ctx.optionsdefaults plus die Angaben der Uhr
ctx.partsabgelesene Stellung { h, m, s, ms }, vor jedem paint() frisch
ctx.instantzugehöriger Zeitpunkt inklusive Datum
ctx.svg / ctx.hostdas SVG bzw. das Host-Element im Light DOM
ctx.stateObjekt, das jeden Neuaufbau überlebt - je Uhr und je Add-on-Platz. Hier gehört alles hin, was sich nicht aus den Optionen herleiten lässt.
ctx.grab(el, h)macht ein eigenes Element anfassbar - siehe „Bedienbare Add-ons"
ctx.fire(name, d)feuert <addon>:<name> am Host
ctx.val(name, f)liest eine Option und ruft sie auf, falls sie eine Funktion ist - damit liest ein datengetriebenes Add-on seinen Wert selbst, ohne update()
eigene FelderDas Kontextobjekt überlebt von build() bis zum nächsten Neuaufbau - kurzlebige Merker (Elementreferenzen, letzter Wert) einfach daranhängen. Was länger halten soll, gehört in ctx.state.

Das Außenband

Zwischen dem Blattrand (Radius 47) und der Kante der viewBox (50) liegt ein freies Band. Dort sitzt alles, was die Uhr zum Objekt macht: Drehring, Gehäuse, Drücker. Ein Add-on meldet mit rim an, wie breit es das braucht, und bekommt sein Band als ctx.rim zugeteilt:

AnalogClock.addon('bezel', {
    layer: 'over',
    rim: (o) => o.width,          // Zahl oder Funktion(options, clock)
    defaults: { width: 6 },

    build(ctx) {
        const { outer, inner } = ctx.rim;   // z.B. 49.6 und 43.6
        ctx.group.appendChild(AnalogClock.svgEl('path', {
            d: AnalogClock.sectorPath(0, 360, outer, inner)
        }));
    }
});

Vergeben wird von außen nach innen, in der Reihenfolge aus addons: bei "gehaeuse bezel" liegt das Gehäuse außen und die Lünette darunter. Nur so lassen sich zwei Ringe überhaupt kombinieren - beide würden sonst an derselben Kante hängen. Die äußerste Kante ist AnalogClock.RIM_OUTER, also 49.6 statt 50: ein Ring genau auf der viewBox-Kante franst an den vier Seiten aus.

Das Zifferblatt bleibt dabei unverändert - ein breiter Ring überdeckt die Teilung, er schiebt sie nicht nach innen. Bis Breite 5 wird nichts verdeckt, ab 6 die äußerste Kante der Teilung, ab etwa 9,5 die Striche ganz; der Ziffernkranz bleibt frei. Wer mehr braucht, nimmt ein ruhigeres Blatt (ticks, numeralStep). Mit eingeschalteter Minutenbeschriftung ist bei Breite 5 Schluss - die Zahlen sitzen dort außen.

Zustand über den Neuaufbau

Jede strukturelle Änderung zeichnet das Blatt neu, und build() läuft wieder. Alles, was am Kontextobjekt hing, ist dann fort. ctx.state überlebt das - je Uhr und je Platz, also auch bei drei Hilfsblättern an derselben Uhr getrennt:

build(ctx) {
    if (ctx.state.winkel == null) ctx.state.winkel = 0;   // nur beim ersten Mal
    zeichne(ctx, ctx.state.winkel);
}

Verworfen wird der Zustand erst mit clock.destroy() - nicht beim Abschalten des Add-ons. Eine Lünette steht nach einem Themenwechsel also noch dort, wo der Nutzer sie hingedreht hat.

Bedienbare Add-ons

ctx.grab() macht ein eigenes Element anfassbar. Pointer-Capture, touch-action, Cursor und das Abräumen beim Neuaufbau übernimmt die Uhr; das Add-on bekommt nur Winkel:

ctx.grab(ring, {
    start(d) { … },
    move(d)  { ctx.state.winkel += d.delta; zeichne(ctx); },
    end(d)   { ctx.fire('set', { minuten: … }); }
});
FeldInhalt
d.angleZeigerwinkel des Pointers in Grad, 0 = 12 Uhr
d.deltakürzeste Differenz seit dem letzten Ruf - zählt über 12 Uhr hinweg richtig weiter
d.totalSumme seit dem Anfassen
d.startWinkel beim Anfassen
d.event / d.targetdas Pointer-Ereignis und das angefasste Element

Mit dem Zeigerziehen kommt sich das nicht ins Gehege: entschieden wird über das Ziel des Ereignisses, und ein Add-on-Element ist keine Zeigergruppe. Wo sich beide überlagern, gewinnt das oben liegende - ein Ring auf der Ebene over nimmt dem Sekundenzeiger dort also die Trefferlinie. Der Rückgabewert von grab() löst die Bindung vorzeitig, nötig ist das selten. Tastaturbedienung ist noch nicht dabei - ein keys-Feld wird angenommen und vorerst ignoriert.

Eigene Events

ctx.fire() meldet, was das Add-on tut - der Name des Add-ons kommt als Präfix davor:

ctx.fire('rotate', { minuten: 12 });   // feuert bezel:rotate am Host

clock.on('bezel:rotate', d => …);      // Typ mit Doppelpunkt gilt wörtlich
clock.on('bezel:set.timer', …);        // Namensräume wie gewohnt

Die Nutzlast wird um type, addon, instance und parts ergänzt. Die Events bubbeln und sind composed, kommen also auch außerhalb des Shadow Roots an.

Werkzeug

AnalogClock.svgEl('circle', { cx: 50, cy: 50, r: 10 });  // Element mit Attributen
AnalogClock.polar(90, 30);      // Punkt auf dem Blatt: 0° = 12 Uhr → { x, y }
AnalogClock.sectorPath(0, 90, 40, 25);  // Pfad eines Ring- oder Tortenstücks
AnalogClock.HAND_SHAPES.leaf(30, 4, 2); // die Zeigerformen selbst

Vollständiges Beispiel: Datumsfenster

Rund 40 Zeilen, alles dabei - Ebene, Stil, Optionen, Aufbau, Aktualisierung:

AnalogClock.addon('datum', {
    layer: 'dial',

    defaults: { angle: 90, distance: 26, breite: 11, hoehe: 7 },

    css: `
        .datum-fenster {
            fill: var(--ac-face);
            stroke: var(--ac-tick-hour);
            stroke-width: 0.4;
        }
        .datum-text {
            fill: var(--ac-numeral);
            font-family: var(--ac-numeral-font);
            font-size: 4.4px;
            text-anchor: middle;
            dominant-baseline: central;
        }`,

    build(ctx) {
        const o = ctx.options;
        const p = AnalogClock.polar(o.angle, o.distance);

        ctx.group.appendChild(AnalogClock.svgEl('rect', {
            class: 'datum-fenster',
            x: p.x - o.breite / 2, y: p.y - o.hoehe / 2,
            width: o.breite, height: o.hoehe, rx: 1
        }));

        ctx.textEl = AnalogClock.svgEl('text', {
            class: 'datum-text', x: p.x, y: p.y
        });
        ctx.group.appendChild(ctx.textEl);
    },

    paint(ctx) {
        // instant statt getTime(): hier steckt das Datum drin
        const tag = ctx.instant.getDate();
        if (ctx.letzterTag === tag) return;   // nur bei Wechsel schreiben
        ctx.letzterTag = tag;
        ctx.textEl.textContent = String(tag);
    }
});
<div class="analogclock" data-clock-addons="datum"></div>

Zweites Beispiel: ein Ring zum Drehen

Dieselben vier Bausteine an einem Stück - Außenband, Zustand, Bedienung, Event. Der Ring unten ist echt: anfassen und drehen, die Marke bleibt stehen, wo sie losgelassen wurde.

Marke auf den Minutenzeiger drehen - ab da liest man die verstrichene Zeit am Ring ab

seit der Marke: 0 min

AnalogClock.addon('ringdemo', {
    layer: 'over',
    rim: (o) => Number(o.width) || 6,
    defaults: { width: 6 },

    css: `.rd-band { fill: var(--ac-face); stroke: var(--ac-bezel); stroke-width: 0.4; }
          .rd-tick { stroke: var(--ac-tick-hour); stroke-width: 0.5; stroke-linecap: round; }
          .rd-mark { fill: var(--ac-accent); }`,

    build(ctx) {
        const r = ctx.rim;

        // Alles Drehbare in eine Gruppe - gedreht wird das Transform,
        // nicht ein Dutzend Einzelteile.
        ctx.dreh = AnalogClock.svgEl('g', { transform: 'rotate(0 50 50)' });
        ctx.group.appendChild(ctx.dreh);

        const band = AnalogClock.svgEl('path', {
            class: 'rd-band',
            d: AnalogClock.sectorPath(0, 360, r.outer, r.inner)
        });
        ctx.dreh.appendChild(band);

        for (let i = 0; i < 12; i++) {
            const p1 = AnalogClock.polar(i * 30, r.outer - 0.9);
            const p2 = AnalogClock.polar(i * 30, r.inner + 0.9);
            ctx.dreh.appendChild(AnalogClock.svgEl('line', {
                class: 'rd-tick', x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y
            }));
        }

        const b = r.width * 0.3;   // Dreiecksmarke, Spitze nach innen
        ctx.dreh.appendChild(AnalogClock.svgEl('path', {
            class: 'rd-mark',
            d: 'M ' + (50 - b) + ' ' + (50 - r.outer + 0.7) +
               ' L ' + (50 + b) + ' ' + (50 - r.outer + 0.7) +
               ' L 50 ' + (50 - r.inner - 0.7) + ' Z'
        }));

        if (ctx.state.winkel == null) ctx.state.winkel = 0;
        this.zeige(ctx);

        ctx.grab(band, {
            move(d) {
                ctx.state.winkel += d.delta;
                this.zeige(ctx);
                ctx.fire('rotate', { minuten: this.minuten(ctx) });
            },
            end() { ctx.fire('set', { minuten: this.minuten(ctx) }); }
        });
    },

    zeige(ctx) {
        ctx.dreh.setAttribute('transform',
            'rotate(' + this.gerastet(ctx) + ' 50 50)');
    },

    gerastet(ctx) { return Math.round(ctx.state.winkel / 6) * 6; },

    minuten(ctx) {
        const m = ctx.clock.getAngles().m;
        return Math.round(((((m - this.gerastet(ctx)) % 360) + 360) % 360) / 6);
    }
});
const uhr = new AnalogClock(document.getElementById('demo-ring'), {
    addons: 'ringdemo', ticks: 'hour', numeralStep: 3
});
uhr.on('ringdemo:rotate', d => ausgabe.textContent = 'seit der Marke: ' + d.minuten + ' min');

Gerastet wird auf ganze Minuten, gerechnet aber stufenlos: ctx.state.winkel sammelt die Rohdifferenzen, gezeichnet wird der gerundete Wert. Andersherum bliebe der Ring bei langsamem Ziehen stehen, weil jede einzelne Differenz wegrundet.

Worauf zu achten ist

build() läuft öfter als einmal. Jede strukturelle Änderung - anderes Zifferblatt, andere Teilung, neue Sektoren - zeichnet das Blatt neu und ruft build() erneut auf. Die Gruppe ist dann leer, gemerkte Elementreferenzen sind ungültig. Deshalb gehören sie ans Kontextobjekt und werden in build() neu gesetzt.

paint() ist ein heißer Pfad. Auch im Sekundentakt gilt: erst prüfen, ob sich sichtbar etwas geändert hat, dann ins DOM schreiben. Beide Beispiele oben machen genau das.

Fehler bleiben lokal. Wirft ein Add-on, wird das gemeldet und die Uhr läuft weiter - ein kaputtes Add-on nimmt nicht das ganze Blatt mit.

Kein Zugriff von außen. Der Stil eines Add-ons gilt nur im Shadow Root der Uhren, die es zugeschaltet haben. Umgekehrt kommt die Seite nicht an die Elemente des Add-ons heran - was für Konfiguration gedacht ist, gehört in defaults oder in eigene --ac-*-Tokens am Host.

--:--:--
läuft
Sichtbare Zeiger
Ziehbare Zeiger
Verzahnung
Add-ons
Verschieben
Lauf
Theme

				
Event-Protokoll