Ich liebe es, CSS Dinge tun zu lassen, die es nicht tun sollte. Es ist die Art von Problemlösung und Gehirntraining, die man beim Bauen eines Rechners in Minecraft bekommt, nur dass man wahrscheinlich keinen Job im Bereich Minecraft Redstone bekommt, egal wie gut man darin wird, während CSS-Kenntnisse bares Geld wert sind und viele Generalisten-Programmierer Angst vor CSS haben, so dass das Studium desselben eine Möglichkeit sein kann, sich vom Rudel abzuheben. Außerdem, wenn man das Unmögliche mit CSS geschafft hat, scheinen alle normalen CSS-Aufgaben einfach zu sein.
Ich habe interessante Diskussionen im Web darüber gelesen, ob CSS eine Turing-vollständige Sprache ist und ob CSS und HTML als Programmiersprachen gelten. Ich habe mich noch nicht entschieden, aber ich kann sagen, dass im Streben nach der Unterstützung gängiger UI-Muster auf standardisierte Weise einige der neueren CSS-Features die Grenze zwischen Styling und Funktionalität verwischen.
Uns selbst herauszufordern, logische Probleme nur mit CSS und HTML zu lösen, kann uns zwingen, wertvolle Zeit mit einigen der neueren, programmierähnlichen Funktionen von CSS zu verbringen, wie z. B. benutzerdefinierten Eigenschaften und logischen Funktionen. Es war immer noch nicht klar, wie diese verwendet werden könnten, um einen Sudoku-Löser nur mit CSS zu erstellen, aber so verrückt die Idee auch klang, die constraintsbasierte Logik von Sudoku schien mit der deklarativen Natur von CSS kompatibel zu sein, so dass ich nicht schockiert war, als jemand anderes behauptete, eine „CSS3 Sudoku Lösungslöser“ gebaut zu haben. Wie sich herausstellte, war dies eher ein Sudoku-Validator in CSS als ein Löser. Er verwendete auch ein winziges bisschen JavaScript, um mit Textfeldern zu arbeiten.
Nach Tagen des tapferen Versuchs, eine vollständige Sudoku-Löser- und Generator-App in reinem CSS zu bauen, habe ich drei Dinge gelernt.
- Man kann Sass-Funktionen und Mixins unit-testen, was fantastisch ist. Wenn Sie diese Sass-Features stark nutzen und wiederverwenden, wofür sie gedacht sind, werden sie so missionskritisch und beängstigend zu ändern wie jeder andere Teil Ihrer Codebasis. Sie verdienen Tests rundherum.
- Chrome DevTools zeigt einen unendlichen Spinner des Todes an, wenn man 50 MB Sass-generiertes CSS darauf wirft.
- Vielleicht ist es unmöglich, etwas wie dieses Python-Skript in reines CSS zu übersetzen. *Vielleicht.*
Wir können jedoch eine Sudoku-Löser- und Generator-App für 16-Felder-Sudoku erreichen, mit der Sie unten spielen können, und dann werden wir aufschlüsseln, wie ihre Funktionen funktionieren. Wo ist dein Gott jetzt, einfaches Rätsel für kleine Kinder?
Der Wertauswähler
Da wir mit CSS experimentieren, sind wir vertraglich verpflichtet, etwas visuell Interessantes einzubeziehen, obwohl nichts zu übertriebenes, da Sudoku-Spieler eine Benutzeroberfläche zu schätzen scheinen, die nicht stört. Meiner Meinung nach könnte die Art und Weise, wie man Zahlen in einigen Sudoku-Apps auswählt, intuitiver sein, also habe ich beschlossen, das Radialmenü-UI-Muster anzuwenden, das bis in die Zeit von Schwarz-Weiß-Macintosh zurückreicht und in modernen Videospielen immer noch beliebt ist. Jemand hat tatsächlich eine schöne reine CSS-Bibliothek für Radialmenüs erstellt, aber ich habe mich in React Planet verliebt, weil ich liebe, wie es sowohl die Auswahl eines Elements mit dem Kreis darum als auch die attraktive Anzeige der verfügbaren Aktionen erfasst. Ich wollte sehen, ob ich einen ähnlichen Effekt nur mit CSS erzielen kann.

Ich habe einige des gestrichelten Kreises-Codes aus diesem Pen übernommen und dann die Zahlen aus Labels mit dem alten Trick border-radius: 50% gemacht, dann habe ich absolute Positionierung verwendet, um die Zahlen am richtigen Punkt auf dem gestrichelten Kreis "haften" zu lassen, selbst wenn die Animation ihre Größe ändert.
.context .number.top {
background: green;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: -12.5px;
}
.context .number.left {
background: orange;
margin-top: auto;
margin-bottom: auto;
top: 0;
bottom: 0;
left: -12.5px;
}
Die Animation blendet den Zahlenwähler ein, während ihr z-index höher wird, damit sie klickbar wird. Wir animieren auch den oberen und linken Rand von 50 % auf null, damit der Kreis sich vom Zentrum aus ausdehnt, um den verfügbaren Platz auszufüllen.
@keyframes bounce-out {
0% {
z-index: -1;
width: 35%;
height: 35%;
margin-left: 50%;
margin-top: 50%;
opacity: 0;
}
100% {
z-index: 2;
opacity: 1;
width: var(--circle-radius);
height: var(--circle-radius);
}
}
Um dann eine anmutende Physik zu simulieren, ähnlich wie bei React Planet, verwende ich eine cubic-bezier() Funktion für die Animation. Die Website easings.net war eine große Hilfe, um Timing-Funktionen einfach zu machen.
.context {
animation: bounce-out cubic-bezier(.68,-0.6,.32, 2.5) .5s forwards;
}
Sowohl die Auswahl von Werten als auch das Verhalten des Öffnens des Wertauswählers für das ausgewählte Feld funktionieren über Radio-Button-Hacks, um sich zu merken, welche Werte ausgewählt wurden und um gegenseitige Ausschließlichkeit zu erreichen. CSS-Tricks hat einen ausgezeichneten Artikel über Checkbox- und Radio-Button-Hacks, daher werde ich diese Informationen hier nicht wiederholen, aber ich werde zeigen, wie wir CSS-Variablen auf der Ebene des Sudoku-CSS-Gitters basierend auf Checkboxen setzen, da dies zentral für die Funktionsweise dieses Experiments ist.
Da wir Variablen verwenden, können wir das gleiche Verhalten erzielen, wenn ein Wert gesetzt ist, unabhängig davon, ob der Benutzer eine Box anklickt, um einen Wert anzugeben, oder ob der Generator das gleiche Wert für das Feld setzt. Es gibt viele Kombinationen von Feldern und Werten, daher verwenden wir Sass, anstatt alle Kombinationen von Hand zu schreiben. Wir erstellen auch separate Bitwerte für jede Wert-Feld-Kombination und eine weitere benutzerdefinierte Eigenschaft, die uns sagt, ob das Feld ungelöst ist. Das liegt daran, dass CSS uns nur begrenzte Möglichkeiten bietet, einen Wert mit einem anderen zu vergleichen (es ist möglich, aber kann knifflig sein). Wir definieren diese Werte auf eine Weise, die auf den ersten Blick etwas seltsam aussehen mag, aber uns das Leben erleichtern wird, wenn es darum geht, zu validieren, ob ein Satz von Sudoku-Feldwerten lösbar ist oder nicht.
@for $i from 1 through 16 {
@for $j from 1 through 4 {
#select-#{$j}-value-square-#{$i}:checked ~ .sudoku {
--square-#{$i}-unsolved: 0;
--square-#{$i}-equals-#{$j}: 1;
}
}
}
Validierung des Gitters

Doctor Google sagt uns, dass es selbst bei nur 16 Feldern viermilliarden mögliche Kombinationen von vier Zahlen gibt. Aber ein Programm, das all diese Kombinationen per Brute-Force durchgeht und die ausgibt, die gemäß den Sudoku-Regeln gültig sind, zeigt, dass es nur 288 gültige Lösungen in 4×4 Sudoku gibt, was ein großer Unterschied zu der Anzahl möglicher gültiger Lösungen in einem 9×9 Gitter ist. Mit nur 288 möglichen Lösungen kann Sass hier wirklich zur Geltung kommen. Ich bin mir immer noch nicht sicher, ob CSS eine Turing-vollständige Sprache ist, aber Sass ist es, und es gibt uns einige richtige Datenstrukturen, wie Listen. Mit ein bisschen Regex-Magie können wir die Liste der obigen gültigen 4×4-Puzzles in eine Sass-gestützte zweidimensionale Liste verwandeln!
$solutions: ((1,2,3,4,3,4,1,2,2,1,4,3,4,3,2,1),(3,1,2,4,2,4,1,3,1,3,4,2,4,2,3,1),(1,2,3,4,3,4,1,2,2,3,4,1,4,1,2,3),/*...many lines later...*/(2,4,3,1,3,1,4,2,4,2,1,3,1,3,2,4),(4,3,2,1,2,1,4,3,3,4,1,2,1,2,3,4));
Super! Wenn unser CSS-Hack eine mehrstufige Anwendung wäre, wäre dies unsere Datenbank. Die Validierung hätte den gleichen Ansatz verwenden können, die Zeilen- und Spaltenwerte zu überprüfen, wie der 9×9-Validator, den wir in der Einleitung gesehen haben, aber da wir die Antwort kennen, scheint es, dass wir uns nicht um die Überprüfung von Blöcken, Spalten und Zeilen kümmern müssen. Stattdessen können wir prüfen, ob die eingegebenen Zahlen noch ein gültiges Rätsel sein könnten oder nicht. In Pseudocode könnte das ungefähr so aussehen:
foreach (s in squares)
{
if (solutionsContains(s.value, s.index) or s.isUnsolved())
{
showValidationError();
}
}
Erinnern Sie sich, dass wir diese seltsamen Variablen erstellt haben, wenn ein Feldwert ausgewählt wurde?
--square-#{$i}-unsolved: 0;
--square-#{$i}-equals-#{$j}: 1;
Nun, wir haben jetzt Antworten auf beide Fragen in der Bedingung in Zeile 3 des obigen Pseudocodes, aber wie können wir einen logischen ODER-Operator in CSS verwenden? Es gibt einen großartigen Artikel auf CSS-Tricks über die Verwendung von calc() zur Simulation von Logikoperatoren in CSS, und ich bin mir nicht sicher, ob ich überhaupt auf einige des Codes in meinem Sudoku-Löser ohne ihn gekommen wäre, aber einige der in diesem Artikel erklärten Formeln werden etwas unhandlich, besonders wenn man verschachtelte UNDs und ODERS mit mehr als zwei Operanden durchführen möchte. Zum Beispiel benötigen wir das CSS-Äquivalent dieses Pseudocodes
if ((squareOneEqualsOne and squareTwoEqualsTwo /*...*/ and squareSixteenEqualsOne) or (squareOneEqualsOne and squareTwoEqualsThree /*...*/ and squareSixteenEqualsOne))
{
sudokuIsValid();
}
}
Nun, dieser Artikel, der zeigt, wie man Logik mit calc() macht, wurde 2019 geschrieben. Heutzutage haben wir neben calc() die gut unterstützten min() und max() mathematischen Funktionen, die unseren Anforderungen noch besser gerecht werden. Wenn Sie "CSS min, max und clamp" googeln (letzteres ist nur eine bequeme Zucker für eine Kombination aus min() und max()), finden Sie viele Beispiele, die zeigen, wie sie zur Vereinfachung von fließfähiger Typografie verwendet werden können. Das ist ein überzeugender Anwendungsfall, aber man kann diese mathematischen Funktionen überall dort verwenden, wo man eine Zahl verwenden würde, was CSS viel Power verleiht. Wenn Sie beispielsweise Bitflag-Variablen an CSS min() übergeben, ist das gleichbedeutend mit AND. Wenn Sie die gleichen Flags an CSS max() übergeben, ist das gleichbedeutend mit OR. Wir können dies anhand der folgenden Wahrheitstabellen beweisen.
| A | B | A UND B | min(A, B) |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 |
| 0 | 1 | 0 | 0 |
| 1 | 1 | 1 | 1 |
| A | B | A ODER B | max(A, B) |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 0 | 1 | 1 |
| 0 | 1 | 1 | 1 |
| 1 | 1 | 1 | 1 |
Wir können damit ziemlich ausgefeilt werden, besonders wenn man die hilfreiche Tatsache hinzufügt, dass wir alles tun dürfen, was calc() innerhalb von min() und max() kann. CSS hat einen Schritt in Richtung einer eigenen seltsamen Skriptsprache gemacht. Jetzt können wir die Bedingung in unserem Validierungs-Pseudocode oben in CSS implementieren. (In der Praxis generieren wir dies aus Sass, da es sehr repetitiv ist.)
.sudoku {
--square-1-matches-puzzle-1: max(var(--square-1-unsolved), var(--square-1-equals-1, 0));
--square-2-matches-puzzle-1: max(var(--square-2-unsolved), var(--square-2-equals-2, 0));
/*...*/
--square-16-matches-puzzle-1: max(var(--square-16-unsolved), var(--square-16-equals-1, 0));
--puzzle-1-found: min(var(--square-1-matches-puzzle-1),
/*...*/
var(--square-16-matches-puzzle-1));
--solution-found: max(var(--puzzle-1-found), /*...*/ var(--puzzle-288-found));
}
Indem wir prüfen, ob jedes Feld entweder ungelöst ist oder einen Wert hat, der an der gleichen Position in einer unserer vorausberechneten Lösungen aus der Sass-2D-Liste existiert, können wir eine Variable produzieren, die uns sagt, ob die derzeit definierten Felder in einem gültigen 4×4 Sudoku-Rätsel existieren. Solange wir etwas Numerisches finden können, das ein Verhalten in CSS antreibt, können wir dieses CSS-Verhalten auf --solution-found basieren. Um beispielsweise unser Gitter rot werden zu lassen, wenn es ungültig ist, können wir dies in jedes Feld einfügen
.square {
color: rgb(calc(255 * (1 - var(--solution-found))), 0, 0);
}
Nicht jede CSS-Eigenschaft kann durch eine Zahl gesteuert werden, aber viele können es, und sowohl z-index als auch opacity sind besonders vielseitige CSS-Eigenschaften für diese Verwendung. Andere Verhaltensweisen können kniffliger sein, sind aber oft erreichbar. Zum Beispiel war ich etwas ratlos, wie man die Shake-Animation für ein ungültiges Gitter mit nur einer numerischen Bitflag-Eigenschaft auslösen könnte, damit das Gitter jedes Mal schüttelt, wenn es ungültig wird, aber dies ist ein großartiges Beispiel dafür, wie das Hacken von CSS Sie zwingt, die Spezifikationen zu lesen und die Randfälle für jede Eigenschaft zu verstehen. Ich fand meine Lösung auf dieser Seite über animation-duration.
Ein Wert von
0s, was der Standardwert ist, bedeutet, dass keine Animation stattfinden soll.
So können wir die Animationsdauer der Shake-Animation auf --solution-found basieren und die Animation jedes Mal entfernen, wenn eine Zahl geklickt wird, indem wir die Pseudo-Klasse :active verwenden, damit sich die Animation jedes Mal wiederholt, wenn die Lösung ungültig wird, und sonst nichts tut.
#select-#{$j}-value-square-#{$i}:active {
animation: none;
}
#select-#{$j}-value-square-#{$i}:checked ~ .sudoku {
animation: shake cubic-bezier(.36,.07,.19,.97) calc((clamp(0, 1 - var(--solution-found), 1)) * 1s) forwards;
}
Eine reine CSS-Sudoku-App wäre wahrscheinlich unmöglich, wenn wir keine CSS-benutzerdefinierten Eigenschaften hätten, und sie sind leistungsfähiger, als sie auf den ersten Blick erscheinen mögen. Die Art und Weise, wie sie neu bewertet werden und die Benutzeroberfläche aktualisieren, wenn sich eine Eigenschaft ändert, auf die sie angewiesen sind, ähnelt einer einfacheren Version der Reaktivität, die man von einem schicken JavaScript-Framework wie Vue erhält. Es ist fair zu sagen, dass Reaktivität in Form von CSS-Variablen direkt im Browser eingebaut ist!
Nun, da wir diesen Ansatz zur Validierung haben und unsere Stylesheet die Lösung in seinem Unterbewusstsein kennt, sobald wir gültige Werte in unserem Sudoku gesetzt haben, sind wir kurz davor, den Löser zu implementieren!
Jedes 4×4 Sudoku lösen

Erinnern Sie sich, als wir diese Zwischenvariablen eingeführt haben?
.sudoku {
--puzzle-1-found: min(var(--square-1-matches-puzzle-1),
/*...*/
var(--square-16-matches-puzzle-1));
}
Das diente nicht nur dazu, den Validierungscode einfacher zu schreiben und zu verstehen. Zu wissen, welche der 288 möglichen Rätsel übereinstimmen, ermöglicht uns, den Löser zu schreiben!
#no-solution {
z-index: 1;
color: red;
}
@for $solution-index from 1 through 288 {
label[for=solution-#{$solution-index}] {
cursor: pointer;
z-index: calc(var(--puzzle-#{$solution-index}-found) * #{$solution-index});
}
#solution-#{$solution-index}:checked ~ .sudoku {
@for $square from 1 through 16 {
--square-#{$square}-solution:"#{nth(nth($solutions, $solution-index), $square)}";
--square-#{$square}-color: grey;
--auto-#{$square}: 1;
}
Ich habe die optionale Pluralform im Wort „Rätsel(n)“ oben hinzugefügt, denn wenn der Benutzer nicht viele Felder ausgefüllt hat, ist es möglich, dass es mehrere gültige Lösungen gibt. Ich mag Löser wie diesen JavaScript-Löser, der schnell eine Lösung liefern kann, auch wenn Sie nicht genügend Werte angegeben haben, damit ein Mensch ihn ohne Raten lösen kann.
Der Trick in meinem CSS-Löser ist, dass der „Lösen“-Button, obwohl er wie ein einzelner Button aussieht, tatsächlich aus 288 übereinander gestapelten Radio-Button-Labels besteht – aber sie sehen alle gleich aus. Stellen Sie sich einen Stapel Karten vor: Sie haben alle das gleiche Design auf der Rückseite, aber unterschiedliche Werte auf der Vorderseite. Die Löserlogik legt die Karte mit der Lösung ganz oben auf den Stapel mit z-index, sodass, wenn Sie sie hochnehmen und die andere Seite lesen, Sie immer die richtige Lösung haben. Es funktioniert auch dann, wenn es mehrere richtige Lösungen gibt, denn die Lösung, die später in unserer Liste der gültigen Antworten steht, wird oben platziert, da wir den z-index berechnen, indem wir das Flag mit $solution-index multiplizieren. Wenn keine Lösungen übereinstimmen, ist der z-index aller Lösungs-Buttons null, und da die deaktivierte Version des Buttons mit der Nachricht „Ungültiges Rätsel“ einen z-index von eins hat, wird er darüber angezeigt. Wenn Rätsel Nummer eins die Lösung ist, werden wir trotzdem den Rätsel-eins-Button sehen, da der ungültige Button früher im HTML kommt.
Stacking Context kann unerwartet funktionieren, wenn man sich nicht damit beschäftigt hat, daher ist dies eine schöne Veranschaulichung eines der nicht offensichtlichen Stack-Verhalten.
Rätsel generieren

Wir können das Generieren von Rätseln als eine weitere Version des Lösers mit zusätzlichen Anforderungen betrachten.
- Einige zufällige Felder müssen ungelöst bleiben, wenn der Rätselgenerator-Button gedrückt wird.
- Die Kombination aus zufällig ungelösten Feldern und einer korrekten Lösung sollte sich jedes Mal ändern, wenn der Generator-Button gedrückt wird.
- Das Drücken des Lösungsbuttons sollte die vollständige Lösung enthüllen.
- Wenn der Benutzer das generierte Rätsel manuell löst, möchten wir ihn mit einem Sieg-Bildschirm belohnen, der ihm Feedback darüber gibt, wie schnell er es gelöst hat.
CSS hat keine random() Funktion (obwohl Sass sie hat), daher ist es vielleicht nicht offensichtlich, wie wir bei jedem Drücken desselben Buttons ein anderes Verhalten erzielen können. Aber die Erklärung des Lösers oben war ein kleiner Spoiler, da sie bereits etwas Ähnliches mit einem Button macht, der wie ein einzelnes Element aussieht, aber tatsächlich unterschiedlich ist, abhängig von der aktuellen gültigen Lösung.
Die Frage bei dem „Generieren“-Button ist, wie wir jedes Mal ein unvorhersehbares Ergebnis erzielen können, wenn wir klicken. Volle Anerkennung gebührt Alvaro Montoro für seinen Artikel auf CSS-Tricks darüber, wie man scheinbar zufällige Werte nur mit CSS generiert. Die Kombination aus Radio-Button-Hacks und der Animation der Stapelreihenfolge scheint gut zu funktionieren. Ich habe hart versucht, ob ich es ohne zusätzliche Markups schaffen könnte, aber ich kam zu dem Schluss, dass dieser Ansatz der beste und einfachste ist. Um die Kartenstapel-Analogie aus der Erklärung des Lösers wiederzuverwenden, ist es so, als würde sich das Deck der Rätselkarten ständig unsichtbar mischen, sodass, wann immer man eine Karte zieht, man entdeckt, dass sie eine andere Vorderseite hat.
Wir können diese Pseudo-Zufälligkeit mit der tatsächlichen Zufälligkeit der Sass random() Funktion kombinieren, um unserem Sudoku-Spiel Wiederspielwert zu verleihen.
@for $j from 0 through 287 {
label[for=generate#{$j}] {
animation-delay: #{$j * .35s};
}
label[for=generate#{$j}]:active:after {
z-index: 300;
width: 100%;
}
#generate#{$j}:checked ~ .sudoku {
$blockCounts: (1: 2, 2: 2, 3: 3, 4: 2);
$shuffleSquares: (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
@for $square from 1 through 16 {
$index1: random(16);
$index2: random(16);
$temp: nth($shuffleSquares, $index1);
$shuffleSquares: set-nth($shuffleSquares, $index1, nth($shuffleSquares, $index2));
$shuffleSquares: set-nth($shuffleSquares, $index2, $temp);
}
@each $square in $shuffleSquares {
$row: ceil($square/4);
$column: 1 + ($square - 1) % 4;
$block: if($row < 3, 1, 3) + if($column < 3, 0, 1);
$count: map-get($blockCounts, $block);
$val: nth(nth($solutions, $j + 1), $square);
--square-#{$square}-solution-#{$val}: 1;
@if ($count > 0) {
$blockCounts: map-merge($blockCounts, ($block: $count - 1));
--square-#{$square}-unsolved: 0;
--square-#{$square}-equals-#{$val}: 1;
@for $other-value from 1 through 4 {
@if ($other-value != $val) {
--square-#{$square}-equals-#{$other-value}: 0;
}
}
--square-#{$square}-color: grey;
--auto-#{$square}: 1;
}
}
}
}
Für jeden „Block“ (das ist Sudoku-Sprache für die 4×4-Abschnitte des Sudoku-Gitters mit dem dicken Rahmen darum) wählen wir mit Sass zufällig zwei von vier Feldern zum Lösen aus, außer bei einem „Gimme“-Feld, das nur ein ungelöstes Feld hat. Da die Validierungs- und Löserlogik auf den Variablen basiert und nicht direkt darauf, welche Werte über den Wertauswähler ausgewählt wurden, verhalten sich die Validierungs- und Löserlogik auf die gleiche Weise. Das bedeutet, dass generierte Werte genauso behandelt werden, als hätte der Benutzer jeden Wert einzeln ausgewählt.
Der Lösungs-Timer
Hier läuft der Timer in den ersten elf Sekunden.

Wir werden uns gleich mit dem CSS für den Lösungs-Timer beschäftigen, aber zeigen wir zuerst, wie eine der Ziffern aussieht, wenn CSS overflow nicht auf hidden gesetzt ist, und mit einem grünen Rahmen um das Element, um den Teil zu zeigen, der dem Benutzer bei jedem Schritt der Animation sichtbar wäre.

Wir verwenden eine unendlich wiederholende Keyframes-Animation, um die Liste der möglichen Ziffern in einem gewünschten Intervall nach links zu verschieben (wir verwenden eine nichtproportionale Schriftart, damit wir sicher sein können, dass jedes Zeichen die gleiche exakte Breite einnimmt). Die Sekundenziffer geht von null bis neun, und die nächste Ziffer sollte nur bis fünf gehen und sich einmal pro zehn Sekunden erhöhen, bevor beide Ziffern der Sekunden auf null zurückgesetzt werden müssen.
Jede Ziffer wird mit der gleichen Technik animiert, die Sie verwenden können, um ein Spritesheet in CSS zu animieren, außer dass wir anstatt eines animierten Verschiebens eines Hintergrundbildes, um einen Animationseffekt zu erzielen, ein Pseudo-Element verschieben, das die möglichen Ziffern enthält.
Wie bei vielen Aufgaben in CSS gibt es mehr als eine Möglichkeit, einen animierten Zähler in CSS zu erstellen. Aber einige funktionieren nicht browserübergreifend und erfordern wirklich einen Präprozessor, um den Code prägnant zu halten. Ich mag meinen Ansatz, weil er ziemlich kurz ist. CSS erledigt die Hauptarbeit, um herauszufinden, wann und wie zur nächsten Ziffer gewechselt werden soll. Die gesamte Markierung muss nur einen Platzhalter erstellen, an dem jede Ziffer steht, was uns einige Freiheiten bei der Präsentation unseres Timers gibt.
Hier ist die Markierung
<div class="stopwatch">
<div class="symbol"></div>
<div class="symbol">:</div>
<div class="symbol"></div>
<div class="symbol"></div>
</div>
…und das CSS
.stopwatch {
text-align: center;
font-family: monospace;
margin-bottom: 10px;
}
.symbol {
width: 1ch;
overflow: hidden;
display: inline-flex;
font-size: 5ch;
}
.symbol:nth-child(1)::after {
animation: tens 3600s steps(6, end) infinite;
content: '012345';
}
.symbol:nth-child(2)::after {
animation: units 600s steps(10, end) infinite;
content: '0123456789';
}
.symbol:nth-child(4)::after {
animation: tens 60s steps(6, end) infinite;
content: '012345';
}
.symbol:nth-child(5)::after {
animation: units 10s steps(10, end) infinite;
content: '0123456789';
}
@keyframes units {
to {
transform: translateX(-10ch);
}
}
@keyframes tens {
to {
transform: translateX(-6ch);
}
}
Sie werden feststellen, dass der Zähler nach einer Stunde wieder von vorne beginnt. Das liegt daran, dass alle Iterationsanzahlen auf infinite eingestellt sind. Wir könnten das beheben, aber ich schätze, wenn jemand eine Stunde braucht, um eines davon zu lösen, hat er größere Probleme als ein Kindersudoku-Rätsel. 😛
Was jedoch unfair wäre, ist, wenn wir erlauben würden, dass derselbe Timer einfach weiterläuft, auch wenn der Benutzer ein neues Rätsel generiert. Können wir es zurücksetzen? Es stellt sich heraus, dass wir dieses Problem bereits im ersten Schritt dieses Artikels gelöst haben, wo wir die Animation für unseren Zahlenwähler mit der Pseudo-Klasse :active entfernt und bedingt wieder hinzugefügt haben. Diesmal ist es tatsächlich einfacher, denn jedes Mal, wenn wir den „Generieren“-Button drücken, wollen wir die Animation auf allen Ziffern entfernen, um sie auf null zurückzusetzen. Dann beginnt die Animation wieder, wenn der Radio-Button nicht mehr aktiv ist. Es ist also nur eine Zeile CSS, die wir brauchen, um den Timer jedes Mal zurückzusetzen, wenn wir generieren!
input[name=generate]:active ~ .stopwatch .symbol::after {
animation: none;
}
Sudokumeter™️

Auch wenn das Rätsel gelöst ist, möchte ich Wiederspielwert bieten, indem ich dem Spieler visuelles Feedback zu seiner Zeitleistung gebe und ihn herausfordere, Rätsel schneller zu lösen. Ich möchte auch *Sie* dafür belohnen, dass Sie es bis hierher im Artikel geschafft haben, indem ich Ihnen eine minimalistische Kreis-Gauge gebe, die Sie in Ihren eigenen Projekten wiederverwenden können. Hier ist ein eigenständiger Pen mit der Kreis-Gauge zum Experimentieren
Wir wenden die gleichen Prinzipien an, die im Siegesbildschirm des Spiels verwendet werden, außer dass in diesem Pen die angezeigte Bewertung über Radio-Button-Hacks gesteuert wird, während sie im Spiel über eine Animation gesteuert wird, die langsam zu einer niedrigeren Bewertung übergeht, während die Zeit vergeht. Die Gauge im Spiel wird mit Null-Deckkraft versteckt und nur angezeigt (und pausiert), wenn wir erkennen, dass das Rätsel manuell gelöst wurde.
Erklären wir, wie wir die Illusion eines Halbkreises erzeugen, der durch Farbe in zwei Seiten geteilt ist. Es ist tatsächlich ein voller CSS-Kreis, dessen untere Hälfte mit overflow: hidden versteckt ist.

Wir tragen die beiden Farben mit einem Pseudo-Element auf, das die Hälfte des <div> ausfüllt.

Dann schneiden wir ein Loch in die Mitte, um einen Donut zu machen, indem wir einen weiteren Kreis mit der Hintergrundfarbe des Spiels füllen und diesen mit Flexbox in der Mitte des größeren Kreises zentrieren.

Verstecken Sie dann die Hälfte davon, indem Sie die Größe des Containers halb so hoch wie der volle Kreis machen und wieder overflow: hidden verwenden.
Wenn wir nun unseren Donut drehen, sieht es so aus, als würde sich die Gauge mit Grün füllen oder Grün verlieren, je nachdem, ob wir unseren Donut um negative oder positive Grad drehen!
Wir möchten sowohl am Anfang als auch am Ende der Gauge Beschriftungen anbringen und dazwischen eine Beschreibung. Und es stellt sich heraus, dass Flexbox eine elegante Lösung ist
#rating {
font-size: 30px;
display: flex;
width: 300px;
justify-content: space-between;
}
Hier ist die Markierung
<div id="rating">
<div id="turtle">🐢</div>
<div id="feedback"></div>
<div id="rabbit">🐇</div>
</div>
Das ist alles, was wir brauchen, um unsere Beschriftungen zu positionieren. Wenn das Bewertungs-<div> die Breite des Durchmessers unseres Kreises hat, positioniert Flexbox die Emoji-Beschriftungen an den Enden und die Beschreibung in der Mitte des Kreises!
Was die Steuerung des Inhalts der Beschreibung betrifft, so ist es ähnlich wie der Trick, den wir für unseren Timer verwendet haben, nur dass wir diesmal vertikal statt horizontal tun, da die Feedback-Beschreibungen variable Längen haben. Aber sie haben immer die gleiche Höhe.

Fazit
Ich habe diesen Artikel mit Fragen eröffnet, ob CSS eine Programmiersprache ist. Es ist schwer zu argumentieren, dass die Logik, die wir nur mit CSS implementieren konnten, nicht Programmierung war, aber einige davon sind gelinde gesagt ungewöhnliche Verwendungen von CSS. Wie bei vielen Dingen in der Technologiewelt scheint die Antwort „kommt darauf an“ zu sein, und so viel wir durch dieses Experiment über CSS gelernt haben, haben wir auch veranschaulicht, dass Programmieren genauso viel mit der Denkweise wie mit der Technik zu tun hat.
Kein CSS-Hacking-Artikel ist vollständig ohne den Haftungsausschluss, dass wir zwar gezeigt haben, dass wir komplexe Logik in CSS implementieren und dabei viel lernen können, wir dies aber meistens wahrscheinlich nicht in der Produktion tun sollten, wegen Wartbarkeit, Zugänglichkeit und einigen anderen Wörtern, die auf „-keit“ enden.
Aber wir haben auch gesehen, dass einige Dinge – wie das, was ich die integrierte Reaktivität von CSS-Variablen nenne – in CSS recht praktisch sind, aber wir uns vielleicht durch Hürden in JavaScript kämpfen müssten und wahrscheinlich ein Framework verwenden müssten. Indem wir die Grenzen von CSS ausreizen, haben wir eine Kreis-Gauge erstellt, die meiner Meinung nach durchaus in einer Produktionsanwendung verwendet werden könnte und vielleicht sogar das Richtige ist, im Vergleich zur Verwendung eines JavaScript-Widgets, das schwer sein und mehr tun könnte, als wir wirklich brauchen.
Auf meiner Wunschliste für die CSS-Sudoku-App steht ein Reset-Button. Derzeit müssen Sie die Seite neu laden, wenn Sie ein neues Sudoku-Spiel starten möchten. Das ist eine inhärente Einschränkung von Radio-Button-Hacks, die CSS-Hacking von konventioneller Programmierung unterscheidet. Zu einem Zeitpunkt glaubte ich, eine Lösung gefunden zu haben, als ich dachte, Animationen könnten verwendet werden, um CSS-Variablen zu setzen – aber es stellt sich heraus, dass dies Teil von CSS Houdini ist und nur in Chromium-basierten Browsern unterstützt wird. Wenn und wenn das überall unterstützt wird, wird es eine Büchse der Pandora der Hacks öffnen und viel Spaß machen. In einem zukünftigen Artikel werde ich vielleicht sogar untersuchen, warum dieses harmlose Feature, das wir in Chrome haben, ein Gamechanger für CSS-Hacking ist.
Die Jury ist sich noch nicht einig, ob ein vollständiger 81-Felder-Sudoku-Löser in CSS möglich ist, aber wenn Sie neugierig sind, finden Sie es heraus, hinterlassen Sie Ihr Feedback in den Kommentaren. Wenn genug Leute es wollen, könnten wir uns gemeinsam auf diese Kaninchenjagd begeben und sehen, welche dunklen Ecken von CSS wir dabei beleuchten können.
Ich habe nur 2 Fragen zu diesem Artikel. Erstens, ich weiß, dass Sie gerne CSS für Dinge programmieren möchten, die es nicht tun sollte, aber warum haben Sie JavaScript komplett vermieden, anstatt lange, unnötige CSS-Hacks zu schreiben? Zweitens, wie kann ein Screenreader-Benutzer mit Ihrem Rätsel interagieren?
Die Frage nach dem Screenreader ist berechtigt. Wenn jemand das untersuchen und Ergebnisse veröffentlichen möchte, wäre das großartig. Ich gebe zu, es war keine Priorität, als ich diesen Beitrag zur Veröffentlichung auswählte und bearbeitete. Hier passiert viel, und ich finde es interessant, es aus der Perspektive des jeweiligen Problems zu betrachten.
Die Frage „Warum würde man das jemals in dieser Sprache tun?“ ermüdet mich. Das Ausloten von Möglichkeiten in Sprachen ist eine interessante Perspektive. Grenzen verschieben. Spaß haben. Das bedeutet nicht, dass derjenige, der es tut, unwissend ist. Ich vermute, dass die Art von Leuten, die so etwas tun, interessantere, neugierigere, intelligentere Menschen sind als die Leute, die es unaufhörlich hinterfragen.
Das ist sowohl fantastisch als auch beängstigend zugleich. Ich liebe die Art und Weise, wie Sie ein riesiges Array verwenden, um das Sudoku schnell zu „berechnen“, und ich bin erstaunt, wie das alles ohne Skripting möglich ist.
Was nützliche Dinge angeht, die ich in Zukunft produktiv einsetzen werde, der Trick, den Sie zum Ändern von Text verwendet haben, gefällt mir sehr. Aus Neugier, ist das nicht auch mit der
content-Eigenschaft auf einer Pseudo-Klasse machbar, wenn Sie keine gleitende Animation benötigen?Vielen Dank, dass Sie uns diesen erstaunlichen Hack gezeigt haben, und ich hoffe, in Zukunft mehr von Ihren Artikeln zu sehen!
Danke fürs Lesen und das Lob M! Das motiviert mich wirklich. Es ist ein bisschen beängstigend, was aus CSS geworden ist, aber zumindest werden Sie sich nächstes Mal, wenn Sie eine Aufgabe in CSS haben, nicht fragen: „Kann ich das in CSS machen?“, sondern „Sollte ich?“ ;)
Das Animieren der Content-Eigenschaft hat einige Einschränkungen bei der Browserunterstützung, die hier erwähnt werden https://css-tricks.de/animating-the-content-property/ und ich denke auch, dass der Timer umständlicher gewesen wäre, wenn ich diesen Weg gegangen wäre, aber anscheinend funktioniert das Animieren der Content-Eigenschaft heutzutage, außer auf iOS. Bedenken Sie, dass die Content-Eigenschaft wahrscheinlich die Barrierefreiheit beeinträchtigt https://www.levelaccess.com/csscontentproperty/, daher möchten Sie sich nicht darauf verlassen, um wichtige Informationen zu übermitteln, wenn Sie Benutzer mit Screenreadern haben.
Angesichts der Tatsache, dass es in einer 9×9 Sudoku-Matrix etwa 10^21 Lösungen gibt, wäre die Speicherung in einer Liste nicht ideal. Sie bräuchten einen cleveren Generator/Checker, um die Aufgabe zu erledigen.
Das gesagt, habe ich die Verwendung von min() und max() als Äquivalente für AND oder OR nicht in Betracht gezogen, bis ich diesen Artikel gelesen habe. Tolle Idee, Lee. Ich spiele seit einiger Zeit mit CSS-Binärbits herum und habe mir ein paar Projekte angesehen, die hier relevant sein könnten… Halten Sie in den nächsten Wochen die Augen offen!
Danke für den Kommentar, Eliseo! Ja, es beruht absolut auf einer kleinen Anzahl möglicher Lösungen, und die Logik ist einfach im Vergleich zu dem, was erforderlich wäre, wenn es zu viele Lösungen zum Vorausberechnen gäbe. Ich konnte meine Ideen für einen 9×9-Löser nicht umsetzen. Irgendwie lief jeder CSS-Hack, den ich je gemacht habe, darauf hinaus, alle/viele der Möglichkeiten im Voraus aufzuschreiben, entweder von Hand oder mit einem Präprozessor, daher wäre es erstaunlich zu sehen, wie jemand einen 9×9 CSS-Sudoku-Löser baut!
Vielleicht könnten wir irgendwie den rekursiven DOM ausnutzen, um die rekursive Logik zu simulieren, die für Sudoku-Löser in anderen Sprachen verwendet wird, ohne eine unerschwinglich große CSS-Datei zu generieren, aber für den Moment hat mich die 9×9-Version verblüfft.