Drei Schichten
Ein Kopf entsteht in drei Schritten, und jeder gehört einer anderen Schicht. Die Regel dazwischen ist ein Satz lang – und deshalb steht hinter ihr ein Programm, das sie prüft.
core/ ist stil-agnostisch: Zufall, Vokabular, Katalog,
DNA, die 3D-Geometrie des Schädels, Farbe als OKLCH. Nichts davon
weiß, dass es einen Strich gibt. styles/ hält den Vertrag
(types.ts) und darunter je einen Ordner pro Stil –
klar/ ist der Stil dieser Ausgabe. app/
verdrahtet: Raster, Blickrechnung, Zeiger, Tastatur.
Aufrufe gehen nur nach unten. app darf alles benutzen,
styles nur core, und core
kennt niemanden über sich. Wer den Kern liest, muss nie fragen,
welcher Stil gerade gemeint ist.
Wer darf wen kennen
Eine Regel ohne Wächter ist eine Bitte
Drei Skripte laufen in npm run check mit, jedes für eine
Regel, die sonst nur ein Kommentar wäre. Sie sind alle nach demselben
Muster gebaut: sie lesen Text, sie kennen ihre eigenen Lücken, und
jede Ausnahme steht als Liste darin – wer sie erweitert, tut es
sehenden Auges.
| Wächter | Worüber er wacht |
|---|---|
check-boundaries.mjs |
Die Schichten: wer wen importieren darf (die fünf Regeln unten). |
check-rollen.mjs |
Die Farben: dass niemand einen Farbwert nennt statt einer Rolle – und die zweite Grenze zwischen Seite und Zeichnung. |
check-vokabular.mjs |
Die Sprache: Bezeichner sind deutsch, außer wo das englische Wort der Fachbegriff ist. Und umgekehrt: dass niemand ein CSS- oder SVG-Wort übersetzt. |
Der erste liest jede Zeile from '…' unter
src/ und hält fünf Regeln fest:
| Regel | Was sie verhindert |
|---|---|
core importiert nie aus styles/ oder app/ |
Ein Kern, der einen Stil kennt, ist der Stil. |
styles importiert nie aus app/ |
Ein Stil, der die Blickrechnung holt, hängt am Raster. Der nächste Stil müsste dieselbe App mitbringen. |
| Kein Stil importiert aus einem anderen Stil-Ordner | Zwei Stile, die sich Formen leihen, sind ein Stil mit einem Schalter. |
core nur über @core, Stile nur über @styles |
Ein Deep-Import friert ein Internum ein: die Datei kann sich nicht mehr bewegen, ohne dass etwas bricht. |
| Die Doku darf überallhin – aber niemand in die Doku | Diese Seiten greifen absichtlich in Interna, das ist ihre Aufgabe. Umgekehrt hinge das Blatt an seiner eigenen Beschreibung. |
Die fünfte Regel ist mit dieser Doku dazugekommen. Sie ist die
einzige, die eine Richtung ganz verbietet: src/docs/ ist
eine Sackgasse. Was hier steht, darf falsch werden, ohne das Blatt
mitzureißen.
Symptom: Der Wächter meldet „ok", und trotzdem
zieht ein Stil an @core vorbei.
Ursache: Er liest Textmuster, keinen Abhängigkeits-
graphen. Ein dynamischer Import mit berechnetem Pfad
(await import(basis + name)) steht in keiner
from-Zeile, und alles außerhalb von src/
sieht er gar nicht.
Lösung: Beides kommt hier nicht vor. Kommt es
einmal vor, muss der Wächter mitwachsen – ein Wächter, dessen
Lücken man kennt, ist brauchbar; einer, den man für vollständig
hält, ist gefährlich.
Die zweite Grenze
Die Seite arbeitet mit CSS-Rollen: --flaeche,
--vordergrund, --linie,
--marke. Die Zeichnung arbeitet auch mit Rollen –
tinte, haut, akzent,
stoff –, aber sie stehen woanders, nämlich in
styles/klar/palette.ts. Dieselbe Idee, zwei Welten: die
eine löst der Browser auf, die andere eine Funktion, weil Canvas keine
Variablen kennt.
Dieses Kapitel hieß eine Zeit lang „die zweite Grenze, die kein
Wächter sieht", und der Titel hat sich als Vorhersage erwiesen. Es
gibt jetzt einen: scripts/check-rollen.mjs. Er hält vier
Regeln fest.
| Regel | Warum |
|---|---|
Farbwerte stehen nur in --ton-* |
Ein Hexwert mitten in einer Regel ist eine Farbe, die keiner Rolle gehört – und beim nächsten Umbau bleibt sie stehen. |
--ton-* liest nur die Rollen-Ebene |
Wer das Primitiv direkt nimmt, umgeht die Bedeutung. Genau das stand an drei Stellen in dieser Doku, bis der Wächter kam. |
| Kein Zeichenschritt nennt einen Farbwert | Dieselbe Regel für die andere Welt: außerhalb von
palette.ts gibt es in styles/ keine
Farben, nur Rollen. Schwarz mit Deckung ist ausgenommen – das
ist Schatten, keine Farbe. |
| Die Brücke ist eine Liste | Was die App aus dem Stilblatt holt, um es aufs Canvas zu malen, steht namentlich im Wächter. Eine dritte kommt nur dazu, wenn jemand sie dort einträgt. |
Symptom: Eine Farbe wird umbenannt, die Seite sieht
richtig aus, das Canvas zeichnet weiter im alten Ton – oder gar
nicht mehr.
Ursache: Im Canvas löst kein Browser eine
CSS-Variable auf. ctx.fillStyle = 'var(--marke)' ist
kein Fehler, sondern still wirkungslos, und
getPropertyValue auf einen Namen, den es nicht gibt,
gibt einen leeren String zurück statt zu meckern.
Lösung: Die beiden Sätze sind getrennt und bleiben
es. Über die Brücke gehen zwei Rollen:
--marke-canvas für die Namen unter den Köpfen,
--flaeche für den Schleier über dem Raster. Beide
werden ausgelesen, beide sind im Wächter eingetragen, und beide
müssen im Stilblatt deklariert sein.
Hier stand bis zuletzt „die einzige Brücke ist
--marke-canvas" – im Stilblatt, in diesem Kapitel und im
Kommentar darüber. Die App las längst zwei. So altert eine
Verabredung, die nur aufgeschrieben ist: nicht mit einem Knall,
sondern lautlos.
Die dritte Grenze: die Sprache
Dieses Projekt spricht Deutsch – in den Kommentaren, in dieser Doku, in
den Commit-Nachrichten. Eine Zeit lang sprachen die Bezeichner zwei
Sprachen: der Kern englisch (makeDna,
headPoint, FaceDna), Stil und App deutsch
(zeichneMund, Kopfdaten, Regung),
dazwischen Mischformen wie makeFrame neben
macheSchale. Das ist keine Geschmacksfrage, sondern
Reibung: wer eine Funktion sucht, rät zuerst, in welcher Sprache sie
benannt wurde, und wer eine schreibt, entscheidet es jedes Mal neu.
Die Regel lautet: Bezeichner sind deutsch. Ausgenommen
ist, wo das englische Wort der Fachbegriff ist –
yaw, pitch und roll, der Seed,
dpr, oklch, mulberry32. Und
ausgenommen ist, was uns gar nicht gehört:
canvas.width heißt width, weil es so heißt.
Diese Grenze hat zwei Richtungen, und die zweite ist die teurere. Ein
englischer Bezeichner ist unschön; ein übersetztes CSS- oder
SVG-Wort ist kaputt und sagt nichts. Beide Richtungen prüft
check-vokabular.mjs. Englisch erkennen kann er nicht –
er kennt eine Liste von Stämmen und eine Liste von Ausnahmen, und
seine Lücken stehen in seinem Kopf. Siehe
die Fallen, die kein Bild zeigt.
Identität, Regung, Stil
Drei Dinge, die man leicht in einen Topf wirft, und die hier bewusst getrennt sind:
| Was es sagt | Wo es lebt | |
|---|---|---|
| DNA | wer der Kopf ist | core/dna.ts |
| Regung | was er gerade tut | app/blick.ts |
| Stil | wie beides aussieht | styles/klar/ |
Die Regung kommt fertig in den Stil hinein – Drehung, Blickrichtung, Lidschlag, Aufmerksamkeit, Mundöffnung, Uhrzeit. Der Stil rechnet sie nicht aus. Täte er es, müsste jeder weitere Stil dieselben Federn noch einmal erfinden, und zwei Stile würden bei gleichem Zeiger unterschiedlich schauen.
Die einzige Zeit, die der Stil selbst braucht, ist
zeit – nicht für die Bewegung, sondern für den Takt
seiner Linie. Eine gezeichnete Linie steht nicht still, auch wenn der
Kopf still steht.
Die Verträge, einzeln
Stilbeschreibung
Fünf Felder, und jedes beantwortet eine Frage, die die App sonst raten
müsste: Wie heißt der Stil (id, label), wie
viel Drehung erträgt er (pose), wie viel Platz braucht
ein bestimmter Kopf in ihm (raum), und wie fängt man an
(macheBlatt). raum ist optional, weil die
meisten Stile RAUM_NORMAL nehmen können;
macheBlatt ist es nicht, denn ohne sie gibt es keinen
Stil.
Blattseite
Ein Sheet ist ein ganzes Blatt, kein einzelner Kopf: der Stil legt
beim Anlegen seinen Untergrund (bei klar das Papier samt
Korn und Randabdunklung) und komponiert am Ende in
fertig(). Der klare Stil tut dort nichts – bei ihm liegt
alles direkt auf dem Blatt.
fertig() bleibt trotzdem Pflicht. Ein Stil mit Ebenen
braucht den Aufruf, und der Aufrufer soll nicht wissen müssen,
welcher Stil welcher ist. Eine Methode, die manchmal nötig ist,
steht im Vertrag – sonst steht sie irgendwann in einem
if.
Kopfplatz
Drei Zahlen, mehr bekommt der Stil über den Ort nicht. Kein Rechteck, keine Zelle, keine Reihe und Spalte: Ein Stil, der weiß, dass er in einer Kachel sitzt, fängt an, sich danach zu richten.
Raumbedarf und RAUM_NORMAL
Der Wert ist nicht gemessen, sondern gewählt: 1.3 ist
genau so groß, dass er in jeder Zelle des Rasters aufgeht. Ihn
vorsorglich zu erhöhen sieht nach nichts aus und schrumpft in Wahrheit
jeden Kopf – die Zelle misst gegen diesen Wert, nicht gegen den
gezeichneten Kopf. Mehr dazu im Kapitel
Das Blatt.
Die Kette im Ganzen
Neuer Stil = ein Ordner und eine Zeile
Die Registry ist die einzige Stelle, die alle Stile kennt. Sie steht über den Ordnern und nicht in ihnen; ein Stil, der sich selbst registriert, zwingt jeden, der die Liste sucht, alle Ordner zu öffnen.
Das Barrel
@core ist eine Datei, die nichts tut außer weiterreichen.
Der Gewinn steht in ihrer ersten Zeile: Interna dahinter dürfen
sich frei ändern. core/geometry/wolke.ts kann
umgebaut, geteilt oder umbenannt werden, solange
umriss und wolke weiterhin aus dem Barrel
fallen. Zwanzig Deep-Imports in den Stilen wären zwanzig Gründe, es
zu lassen.
Der Preis ist eine Datei, die man beim Hinzufügen nicht vergessen darf. Der Wächter fängt das nicht – er sieht nur den Verstoß, nicht die Auslassung. Was fehlt, meldet stattdessen der Typecheck, und zwar sofort.