Erstellung einer Uhr mit den neuen CSS Sinus()- und Cosinus()-Trigonometriefunktionen

Avatar of Mads Stoumann
Mads Stoumann am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

CSS-Trigonometriefunktionen sind da! Nun ja, sie sind da, wenn Sie die neuesten Versionen von Firefox und Safari verwenden. Diese Art von mathematischer Leistung in CSS eröffnet eine ganze Reihe von Möglichkeiten. In diesem Tutorial dachte ich, wir tauchen mal kurz ins Wasser, um ein Gefühl für ein paar der neueren Funktionen zu bekommen: sin() und cos().

Es gibt weitere Trigonometriefunktionen in der Pipeline – einschließlich tan() – warum also nur auf sin() und cos() konzentrieren? Sie eignen sich perfekt für die Idee, die ich im Sinn habe, nämlich Text am Rand eines Kreises zu platzieren. Das wurde hier auf CSS-Tricks behandelt, als Chris einen Ansatz mit einem Sass-Mixin teilte. Das ist sechs Jahre her, also geben wir dem Ganzen den neuesten Schliff.

Hier ist, was ich im Sinn habe. Auch dies wird derzeit nur in Firefox und Safari unterstützt

Es ist also nicht so, dass sich Wörter zu einer Kreisform formen, aber wir platzieren Textzeichen entlang des Kreises, um ein Zifferblatt zu bilden. Hier ist ein Markup, das wir verwenden können, um loszulegen

<div class="clock">
  <div class="clock-face">
    <time datetime="12:00">12</time>
    <time datetime="1:00">1</time>
    <time datetime="2:00">2</time>
    <time datetime="3:00">3</time>
    <time datetime="4:00">4</time>
    <time datetime="5:00">5</time>
    <time datetime="6:00">6</time>
    <time datetime="7:00">7</time>
    <time datetime="8:00">8</time>
    <time datetime="9:00">9</time>
    <time datetime="10:00">10</time>
    <time datetime="11:00">11</time>
  </div>
</div>

Als Nächstes kommen hier einige super-einfache Stile für den Container .clock-face. Ich habe mich entschieden, das Tag <time> mit einem Attribut datetime zu verwenden.

.clock {
  --_ow: clamp(5rem, 60vw, 40rem);
  --_w: 88cqi;
  aspect-ratio: 1;
  background-color: tomato;
  border-radius: 50%;
  container-type: inline;
  display: grid;
  height: var(--_ow);
  place-content: center;
  position: relative;
  width: var(--_ow);
}

Ich habe die Dinge dort ein wenig dekoriert, aber nur, um die Grundform und die Hintergrundfarbe zu erhalten, damit wir sehen können, was wir tun. Beachten Sie, wie wir den Wert width in einer CSS-Variable speichern. Das werden wir später verwenden. Bisher gibt es nicht viel zu sehen

Large tomato colored circle with a vertical list of numbers 1-12 on the left.

Es sieht aus wie ein modernes Kunstexperiment, richtig? Führen wir eine neue Variable ein, --_r, um den **Radius** des Kreises zu speichern, der der Hälfte der Kreisbreite entspricht. Auf diese Weise wird, wenn sich die Breite (--_w) ändert, auch der Radiuswert (--_r) aktualisiert – dank einer weiteren CSS-Mathematikfunktion, calc()

.clock {
  --_w: 300px;
  --_r: calc(var(--_w) / 2);
  /* rest of styles */
}

Nun ein bisschen Mathematik. Ein Kreis hat 360 Grad. Wir haben 12 Beschriftungen auf unserer Uhr, also wollen wir die Zahlen alle 30 Grad (360 / 12) platzieren. In der Welt der Mathematik beginnt ein Kreis bei 3 Uhr, also ist Mittag tatsächlich **minus 90 Grad** davon entfernt, was 270 Grad entspricht (360 - 90).

Fügen wir eine weitere Variable hinzu, --_d, die wir verwenden können, um einen **Gradwert** für jede Zahl auf dem Zifferblatt festzulegen. Wir werden die Werte um 30 Grad inkrementieren, um unseren Kreis zu vervollständigen

.clock time:nth-child(1) { --_d: 270deg; }
.clock time:nth-child(2) { --_d: 300deg; }
.clock time:nth-child(3) { --_d: 330deg; }
.clock time:nth-child(4) { --_d: 0deg; }
.clock time:nth-child(5) { --_d: 30deg; }
.clock time:nth-child(6) { --_d: 60deg; }
.clock time:nth-child(7) { --_d: 90deg; }
.clock time:nth-child(8) { --_d: 120deg; }
.clock time:nth-child(9) { --_d: 150deg; }
.clock time:nth-child(10) { --_d: 180deg; }
.clock time:nth-child(11) { --_d: 210deg; }
.clock time:nth-child(12) { --_d: 240deg; }

OK, jetzt ist es an der Zeit, mit den Funktionen sin() und cos() praktisch zu werden! Was wir tun wollen, ist, sie zu verwenden, um die X- und Y-Koordinaten für jede Zahl zu erhalten, damit wir sie richtig um das Zifferblatt platzieren können.

Die Formel für die X-Koordinate lautet radius + (radius * cos(degree)). Lassen Sie uns das in unsere neue Variable --_x einfügen

--_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));

Die Formel für die Y-Koordinate lautet radius + (radius * sin(degree)). Wir haben alles, was wir brauchen, um das zu berechnen

--_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));

Es gibt ein paar organisatorische Dinge, die wir tun müssen, um die Zahlen einzurichten, also geben wir ihnen einige grundlegende Stile, um sicherzustellen, dass sie absolut positioniert sind und mit unseren Koordinaten platziert werden

.clock-face time {
  --_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));
  --_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));
  --_sz: 12cqi;
  display: grid;
  height: var(--_sz);
  left: var(--_x);
  place-content: center;
  position: absolute;
  top: var(--_y);
  width: var(--_sz);
}

Beachten Sie --_sz, das wir in Kürze für die width und height der Zahlen verwenden werden. Sehen wir uns an, was wir bisher haben.

Large tomato colored circle with off-centered hour number labels along its edge.

Das sieht definitiv mehr nach einer Uhr aus! Sehen Sie, wie die obere linke Ecke jeder Zahl an der richtigen Stelle um den Kreis herum positioniert ist? Wir müssen den Radius "schrumpfen", wenn wir die Positionen für jede Zahl berechnen. Wir können die Größe einer Zahl (--_sz) von der Größe des Kreises (--_w) **abziehen**, bevor wir den Radius berechnen

--_r: calc((var(--_w) - var(--_sz)) / 2);
Large tomato colored circle with hour number labels along its rounded edge.

Viel besser! Ändern wir die Farben, damit es eleganter aussieht

A white clock face with numbers against a dark gray background. The clock has no arms.

Wir könnten hier aufhören! Wir haben das Ziel erreicht, Text um einen Kreis zu platzieren, richtig? Aber was ist eine Uhr ohne Zeiger, die Stunden, Minuten und Sekunden anzeigen?

Dafür verwenden wir eine einzige CSS-Animation. Zuerst fügen wir drei weitere Elemente zu unserem Markup hinzu,

<div class="clock">
  <!-- after <time>-tags -->
  <span class="arm seconds"></span>
  <span class="arm minutes"></span>
  <span class="arm hours"></span>
  <span class="arm center"></span>
</div>

Dann einige gemeinsame Markups für alle drei Zeiger. Auch hier geht es hauptsächlich darum, sicherzustellen, dass die Zeiger absolut positioniert und entsprechend platziert sind

.arm {
  background-color: var(--_abg);
  border-radius: calc(var(--_aw) * 2);
  display: block;
  height: var(--_ah);
  left: calc((var(--_w) - var(--_aw)) / 2);
  position: absolute;
  top: calc((var(--_w) / 2) - var(--_ah));
  transform: rotate(0deg);
  transform-origin: bottom;
  width: var(--_aw);
}

Wir werden die **gleiche Animation** für alle drei Zeiger verwenden

@keyframes turn {
  to {
    transform: rotate(1turn);
  }
}

Der einzige Unterschied ist die Zeit, die die einzelnen Zeiger für eine volle Umdrehung benötigen. Für den **Stundenzeiger** benötigt er **12 Stunden** für eine volle Umdrehung. Die Eigenschaft animation-duration akzeptiert nur Werte in Millisekunden und Sekunden. Bleiben wir bei Sekunden, das sind 43.200 Sekunden (60 Sekunden * 60 Minuten * 12 Stunden).

animation: turn 43200s infinite;

Der **Minutenzeiger** benötigt **1 Stunde** für eine volle Umdrehung. Aber wir wollen, dass dies eine mehrstufige Animation ist, damit die Bewegung zwischen den Zeigern gestaffelt und nicht linear ist. Wir brauchen 60 Schritte, einen für jede Minute

animation: turn 3600s steps(60, end) infinite;

Der **Sekundenzeiger** ist **fast derselbe** wie der Minutenzeiger, aber die Dauer beträgt 60 Sekunden statt 60 Minuten

animation: turn 60s steps(60, end) infinite;

Aktualisieren wir die Eigenschaften, die wir in den gemeinsamen Stilen erstellt haben

.seconds {
  --_abg: hsl(0, 5%, 40%);
  --_ah: 145px;
  --_aw: 2px;
  animation: turn 60s steps(60, end) infinite;
}
.minutes {
  --_abg: #333;
  --_ah: 145px;
  --_aw: 6px;
  animation: turn 3600s steps(60, end) infinite;
}
.hours {
  --_abg: #333;
  --_ah: 110px;
  --_aw: 6px;
  animation: turn 43200s linear infinite;
}

Was ist, wenn wir zur aktuellen Zeit beginnen wollen? Dafür brauchen wir ein wenig JavaScript

const time = new Date();
const hour = -3600 * (time.getHours() % 12);
const mins = -60 * time.getMinutes();
app.style.setProperty('--_dm', `${mins}s`);
app.style.setProperty('--_dh', `${(hour+mins)}s`);

Ich habe id="app" zum Zifferblatt hinzugefügt und zwei neue benutzerdefinierte Eigenschaften darauf gesetzt, die eine negative animation-delay festlegen, wie Mate Marschalko es tat machte, als er eine reine CSS-Uhr teilte. Die Methode getHours() des JavaScript-Objekts Date verwendet das 24-Stunden-Format, daher verwenden wir den **Restoperator**, um es in das 12-Stunden-Format umzuwandeln.

Im CSS müssen wir auch die animation-delay hinzufügen

.minutes {
  animation-delay: var(--_dm, 0s);
  /* other styles */
}

.hours {
  animation-delay: var(--_dh, 0s);
  /* other styles */
}

Nur noch eine Sache. Mit CSS @supports und den bereits erstellten Eigenschaften können wir einen Fallback für Browser bereitstellen, die sin() und cos() nicht unterstützen. (Danke, Temani Afif!)

@supports not (left: calc(1px * cos(45deg))) {
  time {
    left: 50% !important;
    top: 50% !important;
    transform: translate(-50%,-50%) rotate(var(--_d)) translate(var(--_r)) rotate(calc(-1*var(--_d)))
  }
}

Und voilà! Unsere Uhr ist fertig! Hier ist noch einmal die finale Demo. Auch dies wird derzeit nur in Firefox und Safari unterstützt.

Was können wir sonst noch tun?

Ich spiele hier nur herum, aber wir können unsere Uhr schnell in eine kreisförmige Bildergalerie verwandeln, indem wir die Tags <time> durch <img> ersetzen und dann die Werte für Breite (--_w) und Radius (--_r) aktualisieren

Versuchen wir noch eins. Ich erwähnte bereits, wie die Uhr irgendwie wie ein modernes Kunstexperiment aussah. Wir können das aufgreifen und ein Muster nachbilden, das ich kürzlich auf einem Plakat (das ich leider nicht gekauft habe) in einer Kunstgalerie gesehen habe. Soweit ich mich erinnere, hieß es "Mond" und bestand aus einer Reihe von Punkten, die einen Kreis bildeten.

A large circle formed out of a bunch of smaller filled circles of various earthtone colors.

Diesmal verwenden wir eine ungeordnete Liste, da die Kreise keiner bestimmten Reihenfolge folgen. Wir werden nicht einmal alle Listenelemente in das Markup einfügen. Stattdessen injizieren wir sie mit JavaScript und fügen einige Steuerelemente hinzu, mit denen wir das Endergebnis manipulieren können.

Die Steuerelemente sind Bereichseingaben (<input type="range">), die wir in ein <form> einbetten und auf das Ereignis input hören.

<form id="controls">
  <fieldset>
    <label>Number of rings
      <input type="range" min="2" max="12" value="10" id="rings" />
    </label>
    <label>Dots per ring
      <input type="range" min="5" max="12" value="7" id="dots" />
    </label>
    <label>Spread
      <input type="range" min="10" max="40" value="40" id="spread" />
    </label>
  </fieldset>
</form>

Wir werden diese Methode bei "input" ausführen, die eine Reihe von <li>-Elementen erstellt, bei denen die Gradvariable (--_d), die wir zuvor verwendet haben, auf jedes angewendet wird. Wir können auch unsere Radiusvariable (--_r) wiederverwenden.

Ich möchte auch, dass die Punkte unterschiedliche Farben haben. Also, lassen wir die HSL-Farbe für jedes Listenelement randomisieren (nun ja, nicht *komplett* randomisieren) und speichern sie als neue CSS-Variable, --_bgc

const update = () => {
  let s = "";
  for (let i = 1; i <= rings.valueAsNumber; i++) {
    const r = spread.valueAsNumber * i;
    const theta = coords(dots.valueAsNumber * i);
    for (let j = 0; j < theta.length; j++) {
      s += `<li style="--_d:${theta[j]};--_r:${r}px;--_bgc:hsl(${random(
        50,
        25
      )},${random(90, 50)}%,${random(90, 60)}%)"></li>`;
    }
  }
  app.innerHTML = s;
}

Die Methode random() wählt einen Wert innerhalb eines definierten Zahlenbereichs aus

const random = (max, min = 0, f = true) => f ? Math.floor(Math.random() * (max - min) + min) : Math.random() * max;

Und das ist alles. Wir verwenden JavaScript, um das Markup zu rendern, aber sobald es gerendert ist, brauchen wir es eigentlich nicht mehr. Die Funktionen sin() und cos() helfen uns, alle Punkte an den richtigen Stellen zu positionieren.

Abschließende Gedanken

Das Platzieren von Dingen in einem Kreis ist ein ziemlich einfaches Beispiel, um die Kräfte von Trigonometriefunktionen wie sin() und cos() zu demonstrieren. Aber es ist *wirklich* cool, dass wir moderne CSS-Funktionen erhalten, die neue Lösungen für alte Workarounds bieten. Ich bin sicher, wir werden noch viel interessantere, komplexere und kreativere Anwendungsfälle sehen, insbesondere wenn die Browserunterstützung für Chrome und Edge kommt.