8 Punkte von GN⁺ 2023-08-10 | 1 Kommentare | Auf WhatsApp teilen
  • g9.js ist eine interaktive Grafikbibliothek, die numerische Schlüssel-Wert-Daten mit einer Render-Funktion verknüpft, sodass beim Ziehen grafischer Elemente neue Datenwerte gefunden und die gesamte Szene neu gezeichnet werden
  • Die zentrale API besteht nur aus g9(initialData, render[, onChange]); dabei werden initialData und der Zeichenkontext an render(data, ctx) übergeben, um Punkte, Linien, Kreise, Bilder usw. zu zeichnen
  • Wenn eine Interaktion wie Dragging erfolgt, optimiert g9 den Raum möglicher Datenwerte, um den Wert zu finden, der der beabsichtigten Änderung am nächsten kommt, und gibt bei Bedarf neue Daten über onChange weiter
  • Die Galerie zeigt schrittweise Beispiele von einfacher Punktspiegelung über Bézier-Kurven, einen durch drei Punkte definierten Kreis, einen Würfel, die Drachenkurve, Bilder, eine Uhr, LOGO Turtle, Grid Warp bis hin zu Crank and Piston
  • Mit Mustern wie affects, ctx.pure, getData, setData, isManipulating und Math.max/Math.min lassen sich veränderbare Variablenbereiche, Re-Rendering und die Verarbeitung von Benutzereingaben während Animationen gestalten

So funktioniert g9.js

  • g9.js stellt eine einzelne Funktion g9(initialData, render[, onChange]) bereit
  • Zuerst werden Schlüssel-Wert-Daten mit numerischen Werten erstellt
  • Die Daten und der Zeichenkontext werden an render(data, ctx) übergeben, und die Render-Funktion entscheidet anhand der Datenwerte, was gezeichnet wird
    • Im Beispiel zeichnet ctx.point(data.foo, 17) einen Punkt
    • Zugehörige Dokumentation: render in the docs
  • Wenn ein Benutzer ein Element an eine neue Position zieht, sucht g9 im Raum möglicher Datenwerte nach dem Wert, der die nächstliegende Änderung erzeugt
    • Zieht man im Beispiel einen Kreis nach links, kann daraus ein neuer Datensatz wie {foo: 8} entstehen
  • Sobald die neuen Daten feststehen, wird die gesamte Szene neu gerendert, um Konsistenz zu wahren
    • Wenn ein onChange-Callback bereitgestellt wurde, übergibt g9 die neuen Daten, damit andere Teile der Seite aktualisiert werden können
  • Die komplette API ist in den Docs zu finden

Grundformen und Datenbeschränkungen

  • Das Beispiel Basic ist ein minimales Beispiel mit nur zwei Punkten
    • Die Render-Funktion zeichnet einen Punkt bei (x, y) und den anderen bei (y, x)
    • Wenn ein Punkt gezogen wird, bewegt sich der andere spiegelbildlich dazu
  • Rings speist die Daten radius und angle in trigonometrische Funktionen ein, um Punkte in Form von zwei Kreisen anzuordnen
    • Die inneren Punkte werden rot gezeichnet
    • Wenn man das auskommentierte ctx.line(...) aktiviert, lassen sich Linien zwischen den Punkten der beiden Kreise hinzufügen
  • The affects option zeigt die affects-Option, die die Grundformen von g9 akzeptieren
    • affects gibt an, welche Teile der Daten eine Form verändern darf
    • Die drei Linien verwenden unterschiedliche affects-Werte
    • Eine Linie ändert nur l2_start_x, l2_start_y, eine andere verändert l3_start_x, l3_end_y
  • Using Max and Min to limit a point's range of motion begrenzt den Bewegungsbereich eines Punkts mit Math.max(Math.min(...))
    • Die Position des mittleren Punkts ist auf den Bereich zwischen x1 + 20 und x3 - 20 begrenzt
    • Ein separater Punkt zeigt den Wert data.x2 unverändert an

Kurven, Geometrie und 3D-Beispiele

  • Bézier Curve ist eine interaktive Version der quadratischen Bézier-Kurven-Animation aus der Wikipedia
    • Als Daten werden die Koordinaten start, middle, end sowie der Wert t verwendet
    • Das Label tween= wird angezeigt, und aus mehreren Stufen von Liniensegmenten entsteht eine glatte Kurve
    • Für die mittleren Hilfslinien und die Kurvensegmente ist affects: ['t'] gesetzt
  • Three points define a circle definiert einen Kreis durch drei Punkte
    • Es werden die Liniensegmente zwischen den drei Punkten, Mittelpunkte und senkrechte Richtungen berechnet
    • Am berechneten Mittelpunkt wird ein Punkt gesetzt und ein grüner Kreis mit dem Abstand bis zum ersten Punkt als Radius gezeichnet
  • cube erzeugt die Eckpunkte und Kanten eines Würfels und zeichnet ihn nach 3D-Rotation, Verschiebung und Projektion als 2D-Linien
    • Die Funktionen rx, ry, rz berechnen die Rotation um die Achsen
    • Die Funktion project projiziert nach dem Schema 300 * p[0] / p[2], 300 * p[1] / p[2]
    • Die Kanten verwenden affects: ['ax', 'ay'], sodass sich die Rotationswerte per Dragging ändern lassen

Fraktale, prozedurales Zeichnen und Bilder

  • dragon zeichnet mit der rekursiven Funktion dragon(...) eine Drachenkurve
    • Die Daten umfassen squareness, fromX, fromY, toX, toY
    • Die Linienoptionen verwenden affects: ['squareness'], um die Kurvenform zu verändern
    • Start- und Endpunkt werden als Punkte mit Radius 5 dargestellt
  • tree zeichnet rekursiv einen Baum
    • Die Daten sind deltaAngle, attenuation, startLength
    • Die Astendpunkte und Linien verwenden affects: ['deltaAngle', 'attenuation']
    • Bei n > 3 werden die Linien braun gezeichnet, sonst grün
    • Der onChange-Callback gibt die geänderten Daten in der Konsole aus
  • LOGO Turtle simulator erstellt mit make_turtle(ctx) eine einfache Turtle-Oberfläche und zeichnet wiederholt Linien
    • Die Daten sind r und theta
    • forward bewegt sich in die aktuelle Richtung und zeichnet eine Linie, dturn wandelt eine Drehung in Grad in Bogenmaß um
  • Grid Warp wendet eine Transformation an, die Gitterpunkte verbiegt
    • Die Daten sind spin und scale
    • Nach der Berechnung von Abstand r und Winkel theta jedes Punkts wird die Rotation über theta + data.spin * r verändert
    • Es werden Punkte für die Kombinationen von i und j von -10 bis 10 gezeichnet
  • image zeichnet ein Bild mit ctx.image
    • Angegeben werden Bild-URL, x, y, Breite 200 und Höhe 200

Mechanische Vorrichtungen und zeitbasierte Interaktion

  • Kevin Clock zeichnet mithilfe der Daten time eine Uhr
    • Der Minutenzeiger verwendet data.time / 60, der Sekundenzeiger data.time, um die Endpunkte mit Math.cos und Math.sin zu berechnen
    • setInterval erhöht time alle 50 ms um 0.01
    • Die Zeit wird nur dann mit getData() und setData(data) aktualisiert, wenn graphics.isManipulating nicht wahr ist
  • Crank and Piston zeichnet einen Kurbel- und Kolbenmechanismus
    • Die Daten bestehen nur aus time
    • Die Gehäusefarbe ändert sich entsprechend einem aus Math.cos(data.time) erzeugten HSL-Wert
    • Kurbel, Welle und Kolben werden mit Linien, Rechtecken und Kreisen gezeichnet
    • Der Animationsblock ist auskommentiert; nach dem Aktivieren erhöht er time alle 10 ms um 0.05
  • tongs baut mit Rotation und wiederholten Liniensegmenten eine Zangenform auf
    • Die Daten sind a, b
    • rotate(x, y, a) rotiert Koordinaten, und in vier Wiederholungen werden zwei Reihen von Liniensegmenten gezeichnet
    • Der onChange-Callback gibt die geänderten Daten in der Konsole aus

1 Kommentare

 
GN⁺ 2023-08-10
Meinungen auf Hacker News
  • Wenn man einen gerenderten Knoten in 5–6 Ebenen Tiefe eines rekursiven Baums greift und bewegt, wirkt es wie Magie, wie der restliche Baum reagiert.
    Es funktioniert sogar ohne eine Funktion, die aus der Position eines beliebigen Blattknotens explizit die ursprünglichen Parameter des Baums ableitet.
    Ich frage mich, wie das implementiert ist und wie gut es funktioniert, wenn die Positionen der Punkte nicht durch eine einigermaßen glatte Funktion verknüpft sind.

    • Meine Vermutung: Für das bewegte Objekt wird der Einfluss jedes Parameters durch zweimaliges Sampling der Funktion partiell abgeleitet, dann wird ganz normales Gradient Descent durchgeführt und das Ganze wiederholt, bis es konvergiert.
      Oder man könnte wohl einfach einen fertigen Optimizer darauf loslassen.
    • Es dürfte ähnlich wie inverse Kinematik funktionieren.
      Es gibt viele Möglichkeiten, inverse-Kinematik-Probleme zu modellieren und zu lösen.
      Die flexibelsten davon suchen meist per iterativer Optimierung nach einer Näherungslösung, weil die Vorwärtskinematik-Gleichungen schwer umzukehren sind und der Lösungsraum auch leer sein kann.
      Der Kern mehrerer Verfahren besteht darin, die Vorwärtskinematik-Gleichungen über eine Taylorreihen-Entwicklung zu modellieren, was sich möglicherweise leichter umkehren und lösen lässt als das ursprüngliche System.
  • Zuerst dachte ich, das sei eine schicke und sauber einfache API, und für denselben Zweck würde sie mir besser gefallen als d3.
    Aber nachdem ich gelesen hatte: „Wenn jemand mit der Grafik interagiert, etwa indem er versucht, ein Element an eine neue Position zu ziehen, optimiert g9 im Raum der möglichen Datenwerte und findet den Wert, der dieser Änderung am nächsten kommt“, fühlte sich das nach ziemlich magischer DOM-Manipulation an.
    Die meisten Anwendungen, bei denen man Daten durch Manipulation eines Diagramms ändern muss, wirken ziemlich speziell, aber als Spaßanwendung ist das universell.
    Wenn so ein Use Case auftaucht, dürfte das sehr viel Freude machen.

    • Es ist nicht für Diagramme gedacht, sondern für interaktive Demos.
      Ich frage mich, ob du auch die anderen Beispiele auf der Seite selbst ausprobiert hast.
    • Als Anwendung betrachtet gibt es viele STEM-Konzepte, die sich effektiver vermitteln lassen, wenn man Interaktion zulässt.
    • Ich würde das nicht als höhere Wissenschaft bezeichnen.
      Wenn man in den ersten paar Frames eines Drag-Events Geschwindigkeit und Vektor mittelt, den Mittelwert über weitere Frames fortlaufend aktualisiert und dann alles per sanftem Ease-in-out zum vorhergesagten Endpunkt zu bewegen beginnt, kommt das „Optimieren im Raum der möglichen Werte“ ziemlich nahe.
  • Als das früher auf HN diskutiert wurde, gab es interessante Kommentare dazu: https://news.ycombinator.com/item?id=12572412

  • Ich würde gern eine Erklärung sehen, wie das funktioniert. Für mich fühlt es sich wie schwarze Magie an.
    Wenn es zum Beispiel zwei Punkte gibt, einer bei (0, 10) und der andere bei (10, 0), und man den zweiten Punkt nach (20, 0) zieht: Woher weiß das System, dass der erste Punkt um dieselbe Distanz gespiegelt werden sollte?
    Nochmals: Das ist Magie, und die Person, die es gebaut hat, hat wirklich gute Arbeit geleistet.

    • Es wird eine Funktion definiert, die den Fehler zwischen der Position des gezeichneten Elements und der durch die Drag-Aktion geforderten neuen Position misst.
      Diese Funktion wird nach den Datenvariablen partiell abgeleitet, dann wird die Funktion minimiert, um neue Datenwerte zu finden, und anschließend wird alles mit den neuen optimalen Datenwerten neu gerendert.
      Etwas verwirrend ist, dass das, was hier üblicherweise „Daten“ genannt wird, in der mathematischen Optimierung eher Parameter und in Statistik/Machine Learning eher Gewichte heißen würde.
      Im Machine Learning würde man das Feste als „Daten“ bezeichnen; hier entspricht also die Position des gezogenen Elements den Daten.
    • Hinreichend fortgeschrittene Mathematik ist von Magie nicht zu unterscheiden, wobei die Messlatte für „hinreichend fortgeschritten“ hier niedriger ist, als man vielleicht denkt.
  • Sieht ziemlich cool aus.
    Ein kleiner Wermutstropfen ist, dass es keine Marker oder Auswahlindikatoren gibt, sodass ohne vorherige Lektüre der Erklärung nicht besonders klar ist, dass man Objekte ziehen kann.
    Auch die Art, wie Objektpositionen neu angepasst werden, ist etwas verwirrend.
    Normalerweise würde man Pointer-Handler definieren und je nach Eingabe transformieren; wenn ich es richtig verstanden habe, versucht die Engine hier, das mit einer bestmöglichen Schätzung zu automatisieren.

  • Ich war vor ein paar Jahren begeistert, als ich das gesehen habe, und habe es mit Pyodide portiert, damit es im Browser zusammen mit einer PyTorch-ähnlichen Library läuft: https://srush.github.io/g9py/
    Es ruckelt etwas mehr als JavaScript, aber schon dass es überhaupt funktioniert, ist erstaunlich.

  • Das fühlt sich wie Hexerei an. Extrem beeindruckend.

  • Es scheint Gemeinsamkeiten mit Constraint-basierten Skizzierwerkzeugen in 2D-CAD zu haben.

  • Ich bin mir nicht ganz sicher, wofür man es verwenden soll, aber es ist wirklich clever gebaut.

    • Potenziell ließe es sich wohl für jede Drag-and-drop-Oberfläche einsetzen.
      Die Art, das Problem zu betrachten, ist so anders, dass ich es selbst irgendwo einsetzen müsste, um ein Gefühl dafür zu bekommen.
  • Im Bildungsbereich könnte das sehr nützlich sein, um Trigonometrie oder andere mathematische Konzepte zu vermitteln.
    Es gibt viele visuelle Lerntypen, die Schwierigkeiten damit haben, Formeln einem visuellen Raum zuzuordnen; ein wenig Interaktion kann enorm helfen, das Verständnis zu festigen.