28. Mai 2026
Eine Web-App, die sich für einen Desktop hält
Was sich in einer React-App ändert, wenn man sie nicht mehr wie eine Webseite, sondern wie eine Desktop-Umgebung behandelt: Fenster, ein Dock und ein geteiltes 3D-Canvas.
Sascha Becker
Author18 Min. Lesezeit

Eine Web-App, die sich für einen Desktop hält
Die meisten Web-Apps im Jahr 2026 haben dieselbe Form. Ein klebriger Header oben mit Marke und Navigation. Eine permanente Sidebar links. Ein scrollender Inhaltsbereich in der Mitte. Ein Floating Action Button irgendwo unten rechts. Sie unterscheiden sich in Palette und Produkt. In der Geometrie unterscheiden sie sich nicht wirklich.
Mintables, der kleine parametrische 3D-Teile-Generator, an dem ich baue, hatte diese Form auch. Ein Monorepo browserbasierter Generatoren (Tubes, Adapters, Dividers, Leg Caps): du misst ein Teil aus, gibst die Zahlen ein, beobachtest die Live-Vorschau und lädst eine STL-Datei herunter. Die erste Version war eine ordentliche Material-App. Sidebar mit Controls, große Vorschau, Breadcrumb-Header, eine Startseite mit Hero-Text und Feature-Kacheln. Alles funktionierte. Nichts fühlte sich nach irgendetwas Bestimmtem an.
Dann habe ich einen anderen Instinkt ausprobiert: die App wie einen Desktop behandeln, nicht wie eine Webseite.

Die Startseite ist verschwunden. Der Marketing-Text ist verschwunden. Der Hero ist verschwunden. Um einen Generator zu benutzen, öffnest du ihn wie eine App: er erscheint als verschiebbares, größenveränderbares Fenster über dem Wallpaper. Um einen früheren Export wiederzufinden, öffnest du den Downloads-Ordner. Um zwischen Apps zu wechseln, drückst du Cmd-K und tippst den Namen. Die Metapher verschiebt sich von "eine Website durchblättern" zu "einen Desktop benutzen".
Dieser Post ist die Verkabelung, die diese Umstellung brauchte. Manche der Lektionen sind ästhetisch. Die meisten sind technisch. Es gibt in jedem UI-Projekt einen Moment, in dem du gewohnte Web-Pattern gegen etwas eintauschst, das für das Framework weniger ergonomisch ist, und du musst herausfinden, wie es darunter zusammengehalten wird. Diese Verkabelung möchte ich zeigen.
Fenster sind erstklassig. URLs kommen hinten an.
Die größte mentale Verschiebung war die Entscheidung, wo die Identität eines Fensters lebt. Im Web ist die URL maßgeblich. /generators/tubes IST die Tubes-Ansicht. Routing-Systeme behandeln das so. URL öffnen, Ansicht rendern. Ansicht schließen, URL stehen lassen.
Ein Desktop funktioniert so nicht. Fenster haben einen eigenen Lebenszyklus. Du kannst drei gleichzeitig offen haben, auf drei verschiedenen Routen. Du kannst eines minimieren, und die URL folgt nicht. Du kannst ein Hintergrundfenster fokussieren, indem du es anklickst, und die URL sollte sich an dem orientieren, was obenauf liegt. Routen sind Beobachter, keine Eigentümer.
In Mintables lebt der Window-Manager deshalb im React-State, nicht in der URL.
tsexport type WindowPayload =| { kind: "generator"; generatorId: string }| { kind: "folder"; folderId: FolderId };export interface OpenWindow {/** Stable id derived from payload. */id: string;payload: WindowPayload;state: "normal" | "minimized" | "maximized";x: number;y: number;w: number;h: number;z: number;}export interface WindowManagerState {windows: OpenWindow[];focusedId: string | null;nextZ: number;}
Ein Reducer übernimmt OPEN, CLOSE, FOCUS, MINIMIZE, RESTORE, MAXIMIZE_TOGGLE, MOVE, RESIZE und SET_BOUNDS. Routen werden zu dünnen Shims. Die Route /generators/tubes rendert null und dispatcht beim Mounten openWindow({ kind: "generator", generatorId: "tubes" }). Die Route besitzt den Inhalt des Fensters nicht. Der Window-Manager tut das.
Die clevere Stelle ist die umgekehrte Richtung: der Pfad des fokussierten Fensters wird zurück in die URL gespiegelt, damit Deep-Linking und Zurück-Button weiterhin funktionieren.
tsfunction useFocusUrlSync(focusedWindow: OpenWindow | null) {const router = useRouter();const pathname = usePathname();const pathnameRef = useRef(pathname);pathnameRef.current = pathname;const target = focusedWindow? pathForPayload(focusedWindow.payload): "/";useEffect(() => {if (target !== pathnameRef.current) router.replace(target);}, [target, router]);}
Dieser pathnameRef-Trick ist eines dieser Stolpersteine, die man nur findet, wenn man hineinläuft. Wenn der Nutzer von /generators/tubes zu /folders/downloads navigiert, aktualisiert sich der Pathname sofort. Der Shim der neuen Route hat seinen openWindow-Effekt aber noch nicht dispatcht. Würde useFocusUrlSync auf den Pathname reagieren, sähe es den neuen Pathname zusammen mit dem immer noch alten fokussierten Fenster und würde sofort zurück auf die alte URL replacen. Das stiehlt dem gerade geöffneten Fenster den Fokus. Den Pathname über ein Ref zu lesen, lässt uns "haben wir den schon abgeglichen?" prüfen, ohne von ihm abzuhängen. Ich habe einen Nachmittag gebraucht, um herauszufinden, warum die Navigation zu Downloads immer wieder zurück zu Tubes sprang.
Eine zweite Entscheidung lohnt sich zu erwähnen: das Verhalten einer App-Instanz. Die Fenster-ID wird aus dem Payload abgeleitet, nicht pro Klick erzeugt:
tsexport function windowIdOf(payload: WindowPayload): string {if (payload.kind === "generator") return `generator:${payload.generatorId}`;return "folder";}
Tubes zweimal zu öffnen erzeugt keine zwei Tubes-Fenster. Es fokussiert das eine, das bereits existiert. So funktionieren macOS-Apps. Beide Ordnerarten teilen sich absichtlich eine einzige ID: Downloads zu öffnen, während Presets auf dem Bildschirm liegt, lässt dasselbe Ordnerfenster zum neuen Ordner navigieren. Dieselbe Metapher wie Finder.
Drag ist eine Layout-Eigenschaft, kein State-Update
Jedes Generator-Fenster hat seine eigene Akzentfarbe, seinen eigenen Dock-Kachel-Verlauf, seine eigene SVG-Illustration. Die Oberkante des Fensters hat eine Akzent-Highlight-Linie, die beim Fokussieren aufleuchtet. Reine Verzierung, die visuelle Tonart, die signalisiert: "das hier wird wie ein echtes Ding behandelt". Aber die Verzierung ist nicht der schwere Teil. Der schwere Teil ist, den Drag eines Fensters gut anfühlen zu lassen.
Im Web ist der Instinkt, Bounds in den React-State zu legen und bei jedem Pointer-Move neu zu rendern. Das funktioniert für ein einzelnes Fenster ohne teure Kinder. Bei vier offenen Fenstern, jedes mit einer Live-3D-Vorschau, wird daraus eine Diashow. Reconciliation-Kosten holen einen schnell ein.
Die Lösung: React während des Drags nichts sagen. Den transform direkt ins DOM schreiben. React erfährt das Ergebnis erst auf pointerup.
tsconst handleTitleBarPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {const drag = dragRef.current;if (drag?.pointerId !== e.pointerId) return;const targetX = drag.startX + (e.clientX - drag.startClientX);const targetY = drag.startY + (e.clientY - drag.startClientY);const clamped = clamp(targetX, targetY, bounds.w, bounds.h);drag.lastX = clamped.x;drag.lastY = clamped.y;const el = winRef.current;if (el) {el.style.transform =`translate3d(${clamped.x}px, ${clamped.y}px, 0) ${phaseTransform[phase]}`;}invalidatePreview();};const endDrag = (e: ReactPointerEvent<HTMLDivElement>) => {const drag = dragRef.current;if (drag?.pointerId !== e.pointerId) return;onBoundsCommit(drag.lastX, drag.lastY, bounds.w, bounds.h);dragRef.current = null;};
Der Reducer-Dispatch feuert genau einmal, am Ende der Geste. Die anderen achtzig Frames sind reiner GPU-Transform. Das Aufräumen passiert in einem useLayoutEffect, der den Inline-transform strippt, sobald die neuen Bounds in den State committet sind. So übernimmt der nächste Render ohne Flackern.
Der invalidatePreview()-Aufruf ist die Steuer dafür, dass wir ein einziges 3D-Canvas über alle Fenster teilen, was der nächste Abschnitt ist. Das Canvas läuft im Demand-Modus. Wenn der Drag React umgeht, weiß das Canvas nichts vom Neuzeichnen. Die Funktion dispatcht ein Custom Event, eine Bridge-Komponente im Canvas hört darauf und stupst R3Fs invalidate() an.
Für die Resize-Handles an jeder Kante und jeder Ecke gibt es ein ähnliches Muster. Acht Handles insgesamt, alle mit demselben Ansatz "während der Geste Transform schreiben, beim Loslassen committen". Der Cursor fühlt sich nie hinterher an.
Ein WebGL-Kontext für die ganze App
Hier ist ein Problem, das ich nicht erwartet hatte: Browser begrenzen die Anzahl gleichzeitiger WebGL-Kontexte.
Die genaue Zahl variiert, aber Chromium zieht die Linie oft bei acht oder sechzehn. Wird sie überschritten, lässt der Browser den ältesten Kontext stillschweigend fallen. Jeder verlorene Kontext schwärzt sein <canvas> und friert dessen Inhalt ein. Keine Fehlermeldung. Die GPU hört einfach auf, mit dir zu reden.
Mintables ist multi-window. Jedes Fenster hat eine 3D-Vorschau. Der naive Ansatz ist ein <Canvas> pro Fenster. Der naive Ansatz scheitert: öffne und schließe Generatoren ein Dutzend Mal, und die Vorschau-Canvases beginnen einer nach dem anderen schwarz zu werden, weil alte Kontexte verdrängt werden. Die Lösung ist strukturell. Ein einziges Canvas für die ganze App, per Scissor in das reservierte div jedes Fensters gemalt.
R3F plus drei machen das mit der <View>-API praktikabel. Das PreviewPanel jedes Fensters rendert ein getracktes div mit einer <View> und seiner Szene. Ein einziges globales Canvas in der Page-Root mountet <View.Port />, das jede getrackte View zusammensetzt.
tsxexport function PreviewStage({ containerRef }: PreviewStageProps) {return (<CanvaseventSource={containerRef as EventSourceRef}eventPrefix="client"dpr={[1, 2]}frameloop="demand"gl={{ antialias: true }}style={{position: "fixed",inset: 0,pointerEvents: "none",zIndex: 1150,}}><InvalidateBridge /><View.Port /></Canvas>);}
Das Canvas liegt über den Fenstern, aber unter dem Dock. Es ist pointer-events: none, schluckt also keine Klicks. dreis <View> leitet Pointer-Events vom jeweiligen getrackten div in die richtige Szene. Die feste Positionierung bedeutet, das Canvas deckt den ganzen Viewport ab. Aber jedes <View> aktualisiert sein Scissor-Rechteck pro Frame über das getBoundingClientRect() seines getrackten divs, sodass die Szenen nur innerhalb ihrer Fenster malen.
frameloop="demand" macht das bezahlbar. Das Canvas ist im Ruhezustand, wenn sich nichts ändert. Sobald es ruht, kostet ein Fenster-Drag keine CPU-Arbeit. Die Ausnahme ist, wenn ein Fenster seine Form ändert (Drag, Resize, Minimize, Restore) und das Scissor folgen muss. Diese Momente rufen invalidatePreview() auf, damit das Canvas einen weiteren Frame malt und die View ihr Rechteck neu vermisst.

Derselbe Trick skaliert auf so viele Fenster, wie der Nutzer zu öffnen bereit ist. Hinter den Kulissen gibt es weiterhin genau einen WebGL-Kontext.
Der Desktop verdient sich sein Mobiliar
Echte Desktops zeigen kein Mobiliar, das keinen Zweck hat. Ein leerer Dokumente-Ordner bleibt unsichtbar, bis du die erste Datei speicherst. Mintables folgt dieser Regel: Die Downloads- und Presets-Ordner existieren auf dem Desktop erst, nachdem du etwas produziert hast, das hineingehört.
Die Implementierung ist klein. Die Storage-Module dispatchen bei jedem Write ein Custom Event.
tsconst CHANGE_EVENT = "mintables:downloads-changed";function emitChange(): void {if (typeof window === "undefined") return;window.dispatchEvent(new CustomEvent(CHANGE_EVENT));}export function recordDownload(/* ... */): DownloadEntry {// write to localStorage ...writeDownloads(next);emitChange();return entry;}export const DOWNLOADS_CHANGED_EVENT = CHANGE_EVENT;
Ein winziger Hook lauscht darauf, plus das native storage-Event für Updates zwischen Tabs.
tsfunction useStorageFlag(read: () => boolean,changeEvent: string,): boolean {const [flag, setFlag] = useState(false);useEffect(() => {const sync = () => {setFlag(read());};sync();window.addEventListener(changeEvent, sync);window.addEventListener("storage", sync);return () => {window.removeEventListener(changeEvent, sync);window.removeEventListener("storage", sync);};}, [read, changeEvent]);return flag;}
Der Desktop-Hub nutzt das, um seine bedingten Icons zu steuern. Kein Mutex mit dem Route-System, kein abgeleiteter State in einem Context, kein zustand-Store. Zwei Browser-Events und ein Hook. Das Icon erscheint in dem Moment, in dem du deinen ersten Download abschließt, ohne Reload.

Die Ordner-Fenster selbst benutzen eine geteilte FileExplorer-Komponente. Sie ist item-agnostisch: sie nimmt ein items-Array und ein actions-Array entgegen, kümmert sich um Selection (Einzelklick, Shift-Klick für Range, Cmd-Klick zum Togglen), Kontextmenüs, Tastatur-Shortcuts (Enter zum Öffnen, F2 zum Umbenennen, Entf zum Löschen, Cmd-A für Alles), Suche, Sortierung und Ansichtsumschaltung. Jedes Verhalten, nach dem ich in Finder greifen würde, ist da. Einen dritten Ordnertyp hinzuzufügen wäre ein neuer items-Provider.

Ein nicht offensichtlicher Bonus: state-earned UI reduziert den Design-Aufwand für Empty States. Empty States sind in den meisten Apps eine ganze Fläche. Der Mintables-Desktop hat keine, weil die Fläche, deren Empty State man designen müsste, noch gar nicht existiert.
Spotlight ist die Tastatur-Eingangstür
Ein Dock funktioniert für die Maus. Für die Tastatur willst du etwas anderes. macOS löst das mit Spotlight: Cmd+Leertaste öffnet ein globales Suchfeld, du tippst ein paar Buchstaben, drückst Enter, das Ergebnis passiert. Zwei Sekunden, keine Mausbewegung. Mintables übernimmt die Idee. Cmd-K (anderswo Ctrl-K) öffnet eine frosted-glass-Palette über allem, die unscharf über Generatoren, Presets und Downloads in einer gerankten Liste sucht.

Zwei Stellen der Implementierung sind sehenswert. Die erste ist der globale Keyboard-Handler. Er lebt in der Spotlight-Komponente selbst statt in einer separaten Shortcut-Registry, weil Spotlight das einzige Feature ist, das ihn braucht, und Lokalität schlägt frühe Zentralisierung.
tsuseEffect(() => {const onKey = (e: KeyboardEvent) => {const isCmdK =(e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k";if (!isCmdK) return;if (!open) {const t = e.target as HTMLElement | null;const inField =t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA");if (inField) return;e.preventDefault();handleOpen();return;}e.preventDefault();handleClose();};window.addEventListener("keydown", onKey);return () => {window.removeEventListener("keydown", onKey);};}, [open, handleOpen, handleClose]);
Der "in field"-Ausstieg ist derselbe Trick, den der größere Window-Shortcut-Listener verwendet: ist das Event-Ziel ein <input> oder <textarea>, lass es in Ruhe, damit der Nutzer ein literales K tippen kann. Die Ausnahme ist, dass Cmd-K mit Fokus innerhalb von Spotlights eigenem Input die Palette trotzdem schließt. Ausschalten ist nützlicher, als ein K im Suchfeld zu landen.
Die zweite Stelle ist das Aktivierungsmuster. Jede Ergebnisart benutzt eine einzige Primitive.
tsconst activate = useCallback((result: Result) => {if (result.kind === "generator") {openWindow({kind: "generator",generatorId: result.generator.id,});handleClose();return;}if (result.kind === "preset") {const gen = result.generator;if (!gen) { handleClose(); return; }const target = new URL(buildShareUrl(gen.id, result.preset.config));target.searchParams.set("preset", result.preset.id);router.push(target.pathname + target.search);handleClose();return;}// Download: Generator mit der gespeicherten Config wiedereröffnen.const gen = result.generator;if (!gen) { handleClose(); return; }const target = new URL(buildShareUrl(gen.id, result.download.config));router.push(target.pathname + target.search);handleClose();},[openWindow, router, handleClose],);
Generatoren aktivieren mit openWindow(...), derselben Primitive, die ein Klick im Dock benutzt. Presets und Downloads navigieren zu einer config-codierten URL; der Route-Shim unter /generators/<id> greift den ?config=…-Query auf und dispatcht seinen eigenen openWindow beim Mounten. Der kürzeste Weg von "ich habe drei Buchstaben getippt" zu "das gewünschte Fenster liegt obenauf" läuft durch dieselbe Verkabelung wie jeder andere Eingang.
Der Zustand mit leerer Suche ist auch eine kleine Lektion. Ohne Query zeigt Spotlight alle Generatoren plus die fünf neuesten Presets und die fünf neuesten Downloads. Es funktioniert also gleichzeitig als "was habe ich kürzlich angefasst"-Oberfläche und ersetzt den Downloads-Ordner und das Presets-Menü, wenn du nur zu etwas zurück willst.
Ein kleiner Bonus: ein Custom Event namens SPOTLIGHT_OPEN_EVENT lässt jede andere Komponente (das Such-Icon in der Menüleiste, ein "Finden"-Button irgendwo auf einer Seite) die Palette öffnen, ohne dass man einen Callback weiterreichen muss. Dasselbe Muster wie die Storage-Change-Events aus dem vorherigen Abschnitt. Browser-Events sind hervorragender Komponenten-Klebstoff, wenn die Alternative ist, einen Callback durch drei Context-Schichten zu fädeln.
Animationen als Beleg
Echte Betriebssysteme verbringen den größten Teil ihres Motion-Budgets mit drei Dingen: Öffnen, Schließen und Minimieren. Jede Animation ist ein kleiner Beleg dafür, dass das gerade Geschehene eine echte Aktion mit einem echten Ziel war.
Die Minimieren-Animation ist die, auf die ich am stolzesten bin. macOS hat den "Genie"-Effekt: das Fenster verkleinert sich und gleitet in seine Dock-Kachel. Bewegung plus Ziel zusammen vermitteln "dieses Fenster ist nicht weg, es ist dort drüben geparkt". Ohne das Ziel sieht Minimieren wie ein Absturz aus.
Bevor in Mintables die Animation läuft, vermessen wir das DOM-Rechteck der Dock-Kachel und setzen das Delta als CSS-Custom-Properties auf das Fenster.
tsconst handleMinimize = () => {const win = winRef.current;if (win) {const dockTile = document.querySelector(`nav[aria-label="App dock"] [aria-label="${title}"]`,);if (dockTile) {const dockRect = dockTile.getBoundingClientRect();const winRect = win.getBoundingClientRect();const dx =dockRect.left + dockRect.width / 2 -(winRect.left + winRect.width / 2);const dy =dockRect.top + dockRect.height / 2 -(winRect.top + winRect.height / 2);win.style.setProperty("--min-dx", `${dx.toFixed(0)}px`);win.style.setProperty("--min-dy", `${dy.toFixed(0)}px`);}}setPhase("minimizing");window.setTimeout(() => {onMinimize();setPhase("open");}, 380);};
Die CSS-Transition greift var(--min-dx) und var(--min-dy) aus der minimizing-Phase auf.
tsconst phaseTransform: Record<LifecyclePhase, string> = {opening: "translateY(8px) scale(0.985)",open: "scale(1)",closing: "translateY(-4px) scale(0.97)",minimizing:"translate3d(var(--min-dx, 0), var(--min-dy, 24vh), 0) scale(0.06)",};
Das Fenster bleibt während des Minimierens gemountet, nur mit scale(0.06) und Opacity 0 gerendert, Pointer-Events aus. So bleibt der gesamte interne Shell-State (Vorschau-Kameraposition, Undo-Stack, bearbeitete Config) erhalten, und das Wiederherstellen ist sofort da. Das Wiederherstellen läuft dieselbe Animation rückwärts.
Jede Animation muss unterbrechbar sein
Klickt der Nutzer mitten im Öffnen auf Schließen, muss das Schließen von dort
übernehmen, wo das Öffnen gerade angekommen ist. Die Phase-Enum
(opening | open | closing | minimizing) plus CSS-Transitions auf transform
und opacity schenken einem das kostenlos: der Transform-Zielwert der
nächsten Phase ersetzt den aktuellen mitten im Flug, CSS interpoliert von
jetzt nach neu. Du schreibst nie Logik für "was, wenn die Animation gerade
läuft". Phasenwechsel sind atomar.
Das Wallpaper hat Cursor-Parallax (ein paar Pixel Verschiebung beim Mousemove) plus einen langsamen Fade-in beim ersten Paint. Dock-Kacheln heben sich beim Hover. Die kleinen Dinge. Nichts davon ist neu. Zusammen ergibt sich eine UI, die sich beschwert anfühlt, als würde Klicken etwas Echtes berühren.

Was du eintauschst, was du gewinnst
Eine Web-App als Desktop umzudenken ist nicht umsonst.
Du gibst die Hauptaffordances der Seiten-Metapher auf. Tiefes Scrollen ist weg (der Arbeitsbereich ist ein fester Viewport). Hero-Seiten und Marketing-Text haben kein Zuhause. Externes SEO interner Ansichten schwächt sich ab, weil jede "Seite" ein Fenster über einer einzigen Shell ist. Der erste Eindruck für einen Erstbesucher ist "was schaue ich hier an", nicht "ah, eine Website".
Du schreibst auch mehr Code. Den Window-Manager, die Arbeitsbereichs-Arithmetik, die URL-Synchronisation, die geteilte Canvas-Verkabelung, das Fokus-Modell, die Tastatur-Shortcuts, den Dock-Indicator-State, die Genie-Animation, den File-Explorer mit Selection und Rename. Nichts davon kommt aus einem Router oder einer UI-Library. Hinter jedem dieser Features steckt eine selbst geschriebene Implementierung.
Was du zurückbekommst, ist die Umkehrung dessen, was die meisten Web-UIs standardmäßig aufgeben. Echtes Multitasking: ein Tubes-Fenster und ein Downloads-Fenster gleichzeitig offen, das Modell vor dir mit einem früheren Export vergleichend. Echter Fokus: ein einzelnes Fenster besitzt den Input, bis du woanders klickst. Echte Tastatur-Ergonomie: Cmd+1..9 springt zu Apps, Cmd+W schließt das vordere Fenster, Cmd+M minimiert, Cmd+K öffnet Spotlight. Alles über einen einzigen globalen Keydown-Listener verkabelt, der bei Inputs und Contenteditables aussteigt. Ein echter Ortssinn: kommt der Nutzer morgen zurück, ist der Desktop dort, wo er ihn verlassen hat, und die Kappe, die er gestern angepasst hat, ist offen und wartet auf weitere Änderungen.
Die größte qualitative Verschiebung ist, dass der Nutzer aufhört, die App zu navigieren, und anfängt, sie zu bewohnen. Er lernt einmal, wo die Dinge sind, und handelt dann, statt sich durch einen Pfad zu klicken. Modale Unterbrechungen hören auf zu existieren. Seitenleisten hören auf zu existieren. Ein großer Teil der Fläche, auf die du Material-Politur verteilt hättest, hört auf zu existieren.
Das Schwerste ist nicht der Window-Manager oder das geteilte Canvas. Das Schwerste ist, der Sidebar-und-Content-Form zu widerstehen, nach der jede Web-App standardmäßig greift.
- Mintables (Quellcode)
Die parametrische 3D-Generator-App, die dieser Artikel beschreibt. Der Window-Manager, das geteilte R3F-Canvas und der FileExplorer leben in packages/shared.
- drei View
Die React-Three-Fiber-Portal-Primitive, die ein Canvas für viele getrackte Views praktikabel macht.
- WebGL-Kontext-Limits
Die WebGL-Spec gibt Implementierungen Spielraum bei Kontext-Zahlen. Chromiums Verdrängungsverhalten ist die praktische Einschränkung.
