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ächterWorü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:

RegelWas 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.

Was der Wächter nicht sieht

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.

RegelWarum
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.
Die Rolle, die es zweimal gibt

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 istyaw, 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 sagtWo es lebt
DNAwer der Kopf istcore/dna.ts
Regungwas er gerade tutapp/blick.ts
Stilwie beides aussiehtstyles/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.