8 points par GN⁺ 2023-08-10 | 1 commentaires | Partager sur WhatsApp
  • g9.js est une bibliothèque de graphismes interactifs qui relie des données numériques sous forme de paires clé-valeur à une fonction de rendu ; lorsqu’un utilisateur fait glisser un élément graphique, elle trouve de nouvelles valeurs de données et redessine toute la scène
  • Son API principale se résume à g9(initialData, render[, onChange]), qui transmet initialData et le contexte de dessin à render(data, ctx) pour dessiner des points, lignes, cercles, images, etc.
  • Lorsqu’une manipulation comme un glisser-déposer se produit, g9 optimise l’espace des valeurs de données possibles afin de trouver celles qui se rapprochent le plus du changement souhaité, puis transmet si besoin les nouvelles données via onChange
  • La galerie présente progressivement des exemples allant du simple miroir de points aux courbes de Bézier, au cercle défini par trois points, au cube, à la courbe du dragon, aux images, à l’horloge, à LOGO Turtle, à Grid Warp et jusqu’à Crank and Piston
  • Des motifs comme affects, ctx.pure, getData, setData, isManipulating et Math.max/Math.min permettent d’organiser la plage des variables manipulables, le rerendu et le traitement des entrées utilisateur pendant les animations

Fonctionnement de g9.js

  • g9.js expose une seule fonction g9(initialData, render[, onChange])
  • On commence par créer des données clé-valeur contenant des valeurs numériques
    • L’exemple utilise un objet initialData comme foo: 10
    • La documentation correspondante se trouve dans initialData in the docs
  • Les données et le contexte de dessin sont transmis à render(data, ctx), et la fonction de rendu décide quoi dessiner à partir des valeurs des données
    • L’exemple dessine un point avec ctx.point(data.foo, 17)
    • La documentation correspondante se trouve dans render in the docs
  • Quand l’utilisateur fait glisser un élément vers une nouvelle position, g9 cherche dans l’espace des valeurs de données possibles celles qui produisent la variation la plus proche
    • Si l’on fait glisser le cercle vers la gauche, l’exemple peut produire de nouvelles données comme {foo: 8}
  • Une fois les nouvelles données déterminées, toute la scène est rerendue pour conserver la cohérence
    • Si un callback onChange est fourni, g9 transmet les nouvelles données afin de permettre la mise à jour d’autres parties de la page
  • L’API complète est disponible dans les Docs

Formes de base et contraintes sur les données

  • L’exemple Basic est un exemple minimal qui n’utilise que deux points
    • La fonction de rendu dessine un point en (x, y) et l’autre en (y, x)
    • Quand on fait glisser un point, l’autre se déplace comme s’ils étaient le reflet l’un de l’autre
  • Rings place des points sur deux cercles à l’aide de données radius et angle injectées dans des fonctions trigonométriques
    • Le point intérieur est dessiné en rouge
    • En décommentant ctx.line(...), on peut ajouter des lignes entre les points des deux cercles
  • The affects option montre l’option affects acceptée par les formes de base de g9
    • affects indique quelles parties des données une forme peut modifier
    • Les trois lignes utilisent des valeurs affects différentes
    • L’une ne modifie que l2_start_x et l2_start_y, tandis qu’une autre modifie l3_start_x et l3_end_y
  • Using Max and Min to limit a point's range of motion limite la plage de déplacement d’un point avec Math.max(Math.min(...))
    • La position du point central est limitée entre x1 + 20 et x3 - 20
    • Un point séparé affiche directement la valeur data.x2

Exemples de courbes, géométrie et 3D

  • Bézier Curve est une version interactive de l’animation de courbe de Bézier quadratique de Wikipedia
    • Les données utilisent les coordonnées start, middle, end ainsi qu’une valeur t
    • L’étiquette tween= est affichée et plusieurs segments de ligne sont utilisés pour construire une courbe fluide
    • Les lignes auxiliaires intermédiaires et les segments de la courbe sont définis avec affects: ['t']
  • Three points define a circle définit un cercle à partir de trois points
    • Le code calcule les segments entre les trois points, leurs milieux et des droites perpendiculaires
    • Il place un point au centre calculé, puis dessine un cercle vert dont le rayon est la distance jusqu’au premier point
  • cube crée les sommets et les arêtes d’un cube, puis les dessine en lignes 2D après rotation, translation et projection 3D
    • Les fonctions rx, ry, rz calculent les rotations autour des axes
    • La fonction project projette avec 300 * p[0] / p[2] et 300 * p[1] / p[2]
    • Les arêtes utilisent affects: ['ax', 'ay'] afin de modifier les valeurs de rotation par glisser-déposer

Fractales, dessin procédural et images

  • dragon dessine une courbe du dragon avec une fonction récursive dragon(...)
    • Les données incluent squareness, fromX, fromY, toX, toY
    • Les options de ligne utilisent affects: ['squareness'] pour manipuler la forme de la courbe
    • Le point de départ et le point d’arrivée sont marqués par des points de rayon 5
  • tree dessine un arbre de manière récursive
    • Les données sont deltaAngle, attenuation, startLength
    • Les extrémités des branches et les lignes utilisent affects: ['deltaAngle', 'attenuation']
    • Si n > 3, la ligne est dessinée en brun ; sinon, en vert
    • Le callback onChange affiche les données modifiées dans la console
  • LOGO Turtle simulator crée une interface Turtle simple avec make_turtle(ctx) puis trace des lignes de façon répétée
    • Les données sont r et theta
    • forward avance dans la direction courante en dessinant une ligne, et dturn convertit une rotation en degrés en radians
  • Grid Warp applique une transformation qui déforme les points d’une grille
    • Les données sont spin et scale
    • Après calcul de la distance r et de l’angle theta de chaque point, la rotation est modifiée avec theta + data.spin * r
    • Les points sont dessinés pour les combinaisons de i et j allant de -10 à 10
  • image dessine une image avec ctx.image
    • L’URL de l’image, x, y, la largeur 200 et la hauteur 200 sont spécifiées

Mécanismes et interactions basées sur le temps

  • Kevin Clock dessine une horloge à partir des données time
    • La grande aiguille utilise data.time / 60 et la trotteuse data.time pour calculer leurs extrémités avec Math.cos et Math.sin
    • setInterval augmente time de 0.01 toutes les 50 ms
    • Le temps n’est mis à jour via getData() et setData(data) que lorsque graphics.isManipulating n’est pas vrai
  • Crank and Piston dessine un mécanisme de manivelle et de piston
    • Les données se limitent à time
    • La couleur du carter varie selon une valeur HSL construite à partir de Math.cos(data.time)
    • La manivelle, l’arbre et le piston sont dessinés avec des lignes, rectangles et cercles
    • Le bloc d’animation est commenté ; s’il est activé, time augmente de 0.05 toutes les 10 ms
  • tongs construit une forme de pince à l’aide de rotations et de segments répétés
    • Les données sont a et b
    • rotate(x, y, a) fait tourner les coordonnées, puis deux lignes de segments sont tracées sur quatre itérations
    • Le callback onChange affiche les données modifiées dans la console

1 commentaires

 
GN⁺ 2023-08-10
Avis sur Hacker News
  • Quand on saisit et déplace un nœud rendu à 5 ou 6 niveaux de profondeur dans un arbre récursif, voir le reste de l’arbre réagir paraît magique.
    Cela fonctionne même sans fonction qui déduise explicitement les paramètres initiaux de l’arbre à partir de la position arbitraire d’un nœud feuille.
    Je me demande comment c’est implémenté, et dans quelle mesure cela fonctionne quand les positions des points ne sont pas reliées par une fonction suffisamment lisse.

    • À vue de nez, pour l’objet déplacé, l’effet de chaque paramètre est calculé par dérivation partielle avec deux échantillonnages de la fonction, puis on effectue une descente de gradient classique et on répète jusqu’à convergence.
      Ou bien on pourrait simplement y brancher un optimiseur prêt à l’emploi.
    • Cela ressemble probablement à de la cinématique inverse.
      Il existe beaucoup de façons de modéliser et de résoudre les problèmes de cinématique inverse.
      Les méthodes les plus flexibles cherchent généralement une solution approchée par optimisation itérative, parce que les équations de cinématique directe sont difficiles à inverser et que l’espace des solutions peut être vide.
      Le principe de plusieurs méthodes consiste à modéliser les équations de cinématique directe par un développement en série de Taylor, ce qui peut être plus facile à inverser que le système d’origine.
  • Au début, j’ai trouvé que c’était une API simple, sympa et élégante, et je la préférerais à d3 pour faire la même chose.
    Mais après avoir lu « quand quelqu’un interagit avec un graphique, par exemple en essayant de faire glisser un élément vers une nouvelle position, g9 optimise dans l’espace des valeurs de données possibles afin de trouver celles qui se rapprochent le plus de ce changement », ça m’a semblé être une manipulation du DOM assez magique.
    Les applications où il faut modifier les données en manipulant un graphique semblent pour la plupart assez spécifiques, mais l’usage ludique, lui, est universel.
    Ce serait très agréable de tomber sur ce genre de cas d’usage.

    • Ce n’est pas fait pour les graphiques, mais pour les démos interactives.
      Je me demande si tu as aussi manipulé les autres exemples de la page.
    • Côté applications, il y a beaucoup de concepts STEM qui peuvent être transmis efficacement si l’on autorise l’interaction.
    • Je n’appellerais pas ça de la science avancée.
      Si, sur les premières frames d’un événement de drag, on moyenne la vitesse et le vecteur, puis qu’on continue à mettre à jour cette moyenne avec davantage de frames, avant de commencer à déplacer le tout vers le point d’arrivée prédit avec un ease-in-out fluide, on se rapproche pas mal d’une « optimisation dans l’espace des valeurs possibles ».
  • Quand cela avait été discuté sur HN il y a quelque temps, il y avait des commentaires intéressants : https://news.ycombinator.com/item?id=12572412

  • J’aimerais voir une explication de son fonctionnement. Pour moi, cela ressemble à de la magie noire.
    Par exemple, s’il y a deux points, l’un en (0, 10) et l’autre en (10, 0), et que l’on fait glisser le second vers (20, 0), comment le système sait-il que le premier point doit aussi être réfléchi de la même distance ?
    Encore une fois, c’est magique, et la personne qui l’a créé a vraiment bien travaillé.

    • On définit une fonction qui mesure l’erreur entre la position de l’élément dessiné et la nouvelle position demandée par l’action de glisser-déposer.
      On calcule les dérivées partielles de cette fonction par rapport aux variables de données, on minimise la fonction pour trouver les nouvelles valeurs de données, puis on rerend tout avec ces nouvelles valeurs optimales.
      Ce qui prête un peu à confusion, c’est que ce qu’on appelle normalement ici les « données » serait plutôt appelé paramètres en optimisation mathématique, et poids en statistiques ou en machine learning.
      En machine learning, ce qui est fixe serait appelé « données » ; ici, ce sont donc les positions des éléments déplacés qui correspondent aux données.
    • Toute mathématique suffisamment avancée est indiscernable de la magie, et ici le seuil de « suffisamment avancée » est plus bas qu’on ne l’imagine.
  • Ça a l’air assez cool.
    Un petit regret : il n’y a pas de marqueurs ni d’indications de sélection, donc si l’on ne lit pas d’abord l’explication, il n’est pas très évident qu’on puisse faire glisser les objets.
    La façon dont la position des objets est réajustée est aussi un peu déroutante.
    Normalement, on définirait un gestionnaire de pointeur et on appliquerait une transformation en fonction de l’entrée ; si j’ai bien compris, le moteur essaie d’automatiser cela avec sa meilleure estimation.

  • Il y a quelques années, j’ai vu ça et, enthousiaste, je l’ai porté vers Pyodide pour le faire tourner dans le navigateur avec une bibliothèque façon PyTorch : https://srush.github.io/g9py/
    C’est un peu moins fluide qu’en JavaScript, mais le simple fait que ça fonctionne est déjà étonnant.

  • On dirait de la sorcellerie. C’est extrêmement impressionnant.

  • On dirait qu’il y a des points communs avec les outils d’esquisse à contraintes de CAO 2D.

  • Je ne sais pas trop à quoi l’utiliser, mais c’est vraiment ingénieux.

    • Potentiellement, cela pourrait servir dans n’importe quelle interface de glisser-déposer.
      La façon d’aborder le problème est tellement différente qu’il faudrait que je l’essaie moi-même sur quelque chose pour bien la saisir.
  • Cela pourrait être très utile en contexte éducatif pour transmettre la trigonométrie ou d’autres concepts mathématiques.
    Beaucoup d’apprenants visuels ont du mal à faire correspondre les formules à un espace visuel, et ajouter un peu d’interaction aide beaucoup à consolider la compréhension.