Zifferblätter
Teilung
Beschriftung
Ziffernsysteme
Zeigerformen
Sektoren
Zeiger ziehen
Lernaufgabe
Stelle die Uhr auf
--:--
Das Prüfen steckt im Seitenskript, nicht in der Bibliothek · „Lösung zeigen" ist ein Zielzeiger
Zeigerbewegung
Zeitzonen
Add-on: Mondphase
Themes
Ziffernkranz ausdünnen
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.
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.
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.
<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>
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.
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).
| Wert | Aufbau | Stundenzeiger |
|---|---|---|
12 | 1-12 außen | eine Umdrehung je 12 h |
24 | 1-24 auf dem Vollkreis, 24 oben | eine Umdrehung je 24 h |
12+24 | 1-12 außen, 13-24 auf einem inneren Ring | eine 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.
ticks bestimmt, welche Striche das Blatt trägt:
| Wert | zeigt |
|---|---|
both | 60 Minutenstriche und die Stundenmarken (Standard) |
minute | nur den Minutenkranz |
hour | nur die Stundenmarken |
quarter | nur vier Marken bei 12, 3, 6 und 9 |
off | keine 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');
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.
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".
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>
minuteLabels legt einen zweiten Ring außerhalb der Stundenziffern an:
| Wert | zeigt |
|---|---|
off | nichts (Standard) |
5 | 05, 10, 15 … 60 |
quarter | 15, 30, 45, 60 |
all | 1 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.
handShape ändert den Umriss der Zeiger. Alle Formen sind rein geometrisch beschrieben:
| Wert | Form |
|---|---|
stick | Strich mit runden Enden (Standard, als <line> gezeichnet) |
bar | Rechteck mit flachen Enden |
taper | Trapez, zur Spitze zulaufend |
leaf | Lanzette |
arrow | Schaft mit Pfeilspitze |
outline | Lanzette 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.
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' }
]);
| Feld | Bedeutung |
|---|---|
from / to | "HH:MM", eine Zahl oder ein Date. Gerechnet wird im Uhrzeigersinn, to darf kleiner sein als from (20 → 7). |
scale | hour (Standard) folgt dem Zifferblatt - 12 oder 24 Stunden je Umlauf. minute rechnet 0-60 auf den Vollkreis. |
inner / outer | Radien 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. |
className | zusä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.
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.
Angezeigt wird immer eine Wanduhrzeit. Sie entsteht auf zwei Wegen:
| Zustand | Anzeige |
|---|---|
| laufend | jetzt + offset, danach in die Zielzone umgerechnet |
| eingefroren | der 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.
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"
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.
| Wert | Verhalten |
|---|---|
sweep | alle Zeiger stufenlos (Standard) |
tick | Sekundenzeiger springt sekündlich, Minutenzeiger minütlich |
stop-to-go | Sekundenzeiger 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.
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.
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>
Ohne Verzahnung bilden die Zeiger nicht mehr zwingend eine gültige Uhrzeit ab.
getParts() gibt deshalb wieder, was dasteht - und liest wie ein Mensch:
| Zeiger | Regel | Beispiel |
|---|---|---|
| Stunde | abgerundet - die zuletzt überschrittene Zahl | Zeiger kurz vor der 4 → h: 3 |
| Minute | gerundet - die nächstgelegene Marke | Zeiger bei 39,6 → m: 40 |
| Sekunde | gerundet | Zeiger 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.
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;
}
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.
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:
| Attribut | Werte | Standard |
|---|---|---|
data-clock-dial | 12 · 24 · 12+24 | 12 |
data-clock-ticks | both · minute · hour · quarter · off | both |
data-clock-numerals | latin · roman · off · Ziffernsystem · JSON-Array | latin |
data-clock-numeral-step | auto · 1 bis 12 | auto |
data-clock-minute-labels | off · 5 · quarter · all | off |
data-clock-hands | Teilmenge von h/m/s · true · false | hms |
data-clock-hand-shape | stick · bar · taper · leaf · arrow · outline · JSON-Objekt · Pfad | stick |
data-clock-motion | sweep · tick · stop-to-go | sweep |
data-clock-timezone | IANA-Name | Gerätezone |
data-clock-locale | BCP-47, z. B. de-DE | Gerätesprache |
data-clock-time | HH:MM[:SS] oder Datum | - |
data-clock-offset | z. B. +90s, -15m, 2h | 0 |
data-clock-drag | Teilmenge von h/m/s · true · false | aus |
data-clock-snap | Minuten, 0 = stufenlos | 0 |
data-clock-coupled | false löst die Verzahnung der Zeiger | true |
data-clock-zones | JSON-Array von Sektoren | - |
data-clock-ghost | HH:MM[:SS] - Sollstellung | - |
data-clock-ghost-hands | Teilmenge von h/m/s | hm |
data-clock-addons | Name, mehrere durch Leerzeichen, oder JSON-Objekt | - |
data-clock-theme | paper · mono · slate · neon · terminal · auto · contrast · contrast-dark · contrast-yellow | Default |
data-clock-numeral-font | CSS-Fontstack | - |
data-clock-numeral-size | z. B. 8px | - |
data-clock-label | false = aus, sonst eine Vorlage wie {hh}:{mm} Uhr | an |
data-clock-autostart | false | an |
| Methode | Wirkung |
|---|---|
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 |
isUncoupled | stehen 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 / offsetSeconds | Zustand 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.version | Versionszeichenkette |
Alle Events bubbeln am Host im Light DOM.
| Event | wann |
|---|---|
clock:start | ein Zeiger wurde angefasst |
clock:slide | ein Zeiger bewegt sich während des Ziehens |
clock:update | jede gesetzte Änderung - Ziehen wie API |
clock:change | losgelassen und der Wert hat sich geändert |
clock:set | der Wert steht fest: Ende eines Ziehens oder ein programmatischer Setter |
clock:end | losgelassen, unabhängig von einer Änderung |
clock:tick | die angezeigte Sekunde hat gewechselt |
Beim Ziehen: start → (slide → update)* → change →
set → end. Bei einem programmatischen Setter nur update →
set. Das Protokoll in der Spielwiese zeigt das live mit.
{
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.
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.
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.
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.
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:
| Wert | Farbe | gedacht für |
|---|---|---|
contrast | Schwarz auf Weiß (21:1) | der Maximalwert, Standardfall |
contrast-dark | Weiß auf Fastschwarz | Blendempfindlichkeit - Katarakt, Photophobie, Migräne |
contrast-yellow | Gelb auf Schwarz | Makuladegeneration, 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;
}
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.
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 … */
}
}
.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;
}
| Gruppe | Tokens |
|---|---|
| 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 |
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.
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
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 Add-on ist ein Objekt mit bis zu drei Funktionen. Angemeldet wird es einmal global:
AnalogClock.addon('name', { /* Definition */ });
| Feld | Bedeutung |
|---|---|
layer | Zeichenebene: face, dial (Standard) oder over |
css | Stil, 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. |
defaults | Vorgabeoptionen; die Angaben der Uhr werden darübergelegt. |
rim | Breite, die das Add-on aus dem Außenband beansprucht - eine Zahl oder eine Funktion (options, clock). Zugeteilt wird sie als ctx.rim. |
frame | true 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
circle.ac-face
g.ac-zones
face
Add-ons · Flächen, Bögen
g.ac-ticks
g.ac-minutes
g.ac-numerals
dial
Add-ons · Standardebene
g.ac-ghosts
g.ac-hands
over
Add-ons · nie verdeckt
Oben liegt, was zuletzt gezeichnet wird
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:
face-Add-on liegt
immer unter einem dial-Add-on, gleich in welcher Reihenfolge sie zugeschaltet
sind.addons später steht, liegt weiter oben - bei einer Zeichenkette die
Wortfolge, bei einem Array die Array-Reihenfolge, bei einem Objekt die Reihenfolge der
Schlüssel.AnalogClock.addon()
füllt nur das Register; für den Stapel zählt allein, was an der Uhr steht.<!-- 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>
| Ebene | liegt | gut für |
|---|---|---|
face | über dem Blatt, unter der Teilung | Hintergründe, Muster, Bögen |
dial | über den Ziffern, unter den Zeigern | Anzeigen, Fenster, Logos |
over | über allem | Glasreflex, 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.
| Feld | Inhalt |
|---|---|
ctx.clock | die Instanz - volle API, also auch getParts(), getAngles(), offsetTo(), on() |
ctx.group | die eigene <g>-Gruppe. Alles hier hinein. |
ctx.geo | alle 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.options | defaults plus die Angaben der Uhr |
ctx.parts | abgelesene Stellung { h, m, s, ms }, vor jedem paint() frisch |
ctx.instant | zugehöriger Zeitpunkt inklusive Datum |
ctx.svg / ctx.host | das SVG bzw. das Host-Element im Light DOM |
ctx.state | Objekt, 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 Felder | Das 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. |
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.
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.
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: … }); }
});
| Feld | Inhalt |
|---|---|
d.angle | Zeigerwinkel des Pointers in Grad, 0 = 12 Uhr |
d.delta | kürzeste Differenz seit dem letzten Ruf - zählt über 12 Uhr hinweg richtig weiter |
d.total | Summe seit dem Anfassen |
d.start | Winkel beim Anfassen |
d.event / d.target | das 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.
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.
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
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>
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.
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.
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.