8 puntos por GN⁺ 2023-08-10 | 1 comentarios | Compartir por WhatsApp
  • g9.js es una biblioteca de gráficos interactivos que conecta datos numéricos de clave-valor con una función de renderizado; cuando el usuario arrastra elementos gráficos, encuentra nuevos valores de datos y vuelve a dibujar toda la escena
  • La API principal es una sola: g9(initialData, render[, onChange]); pasa initialData y el contexto de dibujo a render(data, ctx) para dibujar puntos, líneas, círculos, imágenes y más
  • Cuando ocurre una manipulación como arrastrar, g9 optimiza el espacio de valores de datos posibles para encontrar el valor más cercano al cambio pretendido y, si hace falta, entrega los nuevos datos mediante onChange
  • La galería muestra ejemplos paso a paso, desde el espejado básico de puntos hasta curvas Bézier, un círculo definido por tres puntos, un cubo, la curva del dragón, imágenes, un reloj, LOGO Turtle, Grid Warp y Crank and Piston
  • Con patrones como affects, ctx.pure, getData, setData, isManipulating y Math.max/Math.min, se pueden configurar rangos de variables manipulables, rerenderizado y manejo de entradas del usuario durante animaciones

Flujo de funcionamiento de g9.js

  • g9.js expone una única función: g9(initialData, render[, onChange])
  • Primero se crean datos clave-valor con valores numéricos
  • Los datos y el contexto de dibujo se pasan a render(data, ctx), y la función de renderizado decide qué dibujar con base en los valores de los datos
    • El ejemplo dibuja un punto con ctx.point(data.foo, 17)
    • La documentación relacionada está en render in the docs
  • Cuando el usuario arrastra un elemento a una nueva posición, g9 busca en el espacio de valores de datos posibles aquellos que produzcan el cambio más cercano
    • Si se arrastra un círculo hacia la izquierda, en el ejemplo podrían salir nuevos datos como {foo: 8}
  • Una vez determinados los nuevos datos, vuelve a renderizar toda la escena para mantener la coherencia
    • Si se proporciona el callback onChange, g9 le pasa los nuevos datos para que pueda actualizar otras partes de la página
  • La API completa puede consultarse en Docs

Figuras básicas y restricciones de datos

  • Basic es un ejemplo mínimo que usa solo dos puntos
    • La función de renderizado dibuja un punto en (x, y) y el otro en (y, x)
    • Al arrastrar un punto, el otro se mueve como si estuvieran espejados entre sí
  • Rings coloca puntos en forma de dos círculos usando los datos radius y angle en funciones trigonométricas
    • Los puntos internos se dibujan en rojo
    • Si se descomenta ctx.line(...), se pueden agregar líneas entre los puntos de los dos círculos
  • The affects option muestra la opción affects que reciben las figuras básicas de g9
    • affects especifica qué parte de los datos puede modificar una figura
    • Las tres líneas usan distintos valores de affects
    • Una línea solo cambia l2_start_x y l2_start_y, mientras que otra cambia l3_start_x y l3_end_y
  • Using Max and Min to limit a point's range of motion limita el rango de movimiento de un punto con Math.max(Math.min(...))
    • La posición del punto central se limita entre x1 + 20 y x3 - 20
    • Un punto separado muestra el valor de data.x2 tal cual

Curvas, geometría y ejemplos 3D

  • Bézier Curve es un ejemplo que convierte la animación de la curva Bézier cuadrática de Wikipedia en una versión interactiva
    • Usa como datos las coordenadas start, middle, end y el valor t
    • Muestra la etiqueta tween= y construye una curva suave con segmentos de línea en varias etapas
    • Las líneas auxiliares intermedias y los segmentos de la curva tienen especificado affects: ['t']
  • Three points define a circle define un círculo con tres puntos
    • Calcula los segmentos entre los tres puntos, puntos medios y líneas perpendiculares
    • Marca un punto en el centro calculado y dibuja un círculo verde cuyo radio es la distancia hasta el primer punto
  • cube crea los vértices y aristas de un cubo, y los dibuja como líneas 2D tras aplicar rotación 3D, traslación y proyección
    • Calcula las rotaciones por eje con las funciones rx, ry y rz
    • La función project proyecta usando la forma 300 * p[0] / p[2], 300 * p[1] / p[2]
    • Las aristas usan affects: ['ax', 'ay'], por lo que al arrastrarlas se pueden cambiar los valores de rotación

Fractales, dibujo procedural e imágenes

  • dragon dibuja la curva del dragón con la función recursiva dragon(...)
    • Los datos incluyen squareness, fromX, fromY, toX y toY
    • Las opciones de línea usan affects: ['squareness'] para manipular la forma de la curva
    • Los puntos inicial y final se muestran como puntos de radio 5
  • tree dibuja un árbol de forma recursiva
    • Los datos son deltaAngle, attenuation y startLength
    • Los extremos de las ramas y las líneas usan affects: ['deltaAngle', 'attenuation']
    • Si n > 3, dibuja la línea en color café; en caso contrario, en verde
    • El callback onChange imprime los datos modificados en la consola
  • LOGO Turtle simulator crea una interfaz Turtle simple con make_turtle(ctx) y dibuja líneas de forma repetida
    • Los datos son r y theta
    • forward avanza en la dirección actual mientras dibuja una línea, y dturn convierte una rotación en grados a radianes
  • Grid Warp aplica una transformación que deforma puntos de una cuadrícula
    • Los datos son spin y scale
    • Después de calcular la distancia r y el ángulo theta de cada punto, cambia la cantidad de rotación con theta + data.spin * r
    • Dibuja puntos con las combinaciones de i y j de -10 a 10
  • image dibuja una imagen con ctx.image
    • Especifica la URL de la imagen, x, y, ancho 200 y alto 200

Mecanismos e interacción basada en tiempo

  • Kevin Clock dibuja una forma de reloj usando el dato time
    • La manecilla de los minutos usa data.time / 60, y la de los segundos usa data.time para calcular los extremos con Math.cos y Math.sin
    • setInterval incrementa time en 0.01 cada 50 ms
    • Solo actualiza el tiempo con getData() y setData(data) cuando graphics.isManipulating no es verdadero
  • Crank and Piston dibuja una estructura de cigüeñal y pistón
    • Usa un único dato: time
    • El color de la carcasa cambia según un valor HSL creado con Math.cos(data.time)
    • Dibuja el cigüeñal, el eje y el pistón con líneas, rectángulos y círculos
    • El bloque de animación está comentado; si se descomenta, incrementa time en 0.05 cada 10 ms
  • tongs construye una forma de pinzas con rotación y segmentos de línea repetidos
    • Los datos son a y b
    • Rota las coordenadas con rotate(x, y, a) y dibuja dos filas de segmentos mediante cuatro repeticiones
    • El callback onChange imprime los datos modificados en la consola

1 comentarios

 
GN⁺ 2023-08-10
Opiniones en Hacker News
  • Cuando tomás un nodo renderizado a 5 o 6 niveles de profundidad en un árbol recursivo y lo movés, que el resto del árbol reaccione se ve como magia.
    Funciona sin una función que infiera explícitamente los parámetros iniciales del árbol a partir de la posición de un nodo hoja arbitrario.
    Me pregunto cómo está implementado y qué tan bien funciona cuando las posiciones de los puntos no están conectadas por una función razonablemente suave.

    • Si tuviera que adivinar, diría que calcula las derivadas parciales del efecto que tiene cada parámetro sobre el objeto movido con dos muestreos de la función, hace un descenso de gradiente normal y luego repite hasta converger.
      O quizá alcanza con pasarle un optimizador ya hecho.
    • Parece algo similar a la cinemática inversa.
      Hay muchas formas de modelar y resolver problemas de cinemática inversa.
      Los métodos más flexibles suelen buscar soluciones aproximadas mediante optimización iterativa, porque es difícil resolver las ecuaciones de cinemática directa en sentido inverso y el espacio de soluciones incluso podría estar vacío.
      La idea central de varios métodos es modelar las ecuaciones de cinemática directa mediante una expansión en serie de Taylor, que puede ser más fácil de invertir que el sistema original.
  • Al principio pensé que era una API simple, genial y limpia, y me gusta más que d3 para hacer lo mismo.
    Pero después de leer “cuando alguien interactúa con el gráfico, por ejemplo al intentar arrastrar un elemento a una nueva posición, g9 optimiza en el espacio de valores de datos posibles para encontrar el valor más cercano a ese cambio”, esto se siente como una manipulación del DOM bastante mágica.
    La mayoría de las aplicaciones en las que tendrías que modificar datos manipulando un gráfico parecen bastante especiales, pero como uso recreativo es algo universal.
    Sería muy divertido cuando aparezca un caso de uso así.

    • No es para gráficos, sino para demos interactivas.
      Me pregunto si también probaste directamente los otros ejemplos de la página.
    • Como aplicación, hay muchos conceptos STEM que pueden comunicarse eficazmente si se permite la interacción.
    • Yo no lo llamaría ciencia avanzada.
      Si en los primeros frames de un evento de arrastre promediás la velocidad y el vector, seguís actualizando el promedio con más frames y luego empezás a mover todo con un ease-in-out suave hacia el punto final predicho, quedás bastante cerca de “optimizar en el espacio de valores posibles”.
  • Cuando esto se discutió antes en HN, hubo comentarios interesantes: https://news.ycombinator.com/item?id=12572412

  • Me gustaría ver una explicación de cómo funciona. Para mí se siente como magia negra.
    Por ejemplo, si hay dos puntos, uno en (0, 10) y otro en (10, 0), y arrastro el segundo punto a (20, 0), ¿cómo sabe el sistema que el primer punto también debería reflejarse la misma distancia?
    Insisto: esto es magia, y quien lo hizo lo hizo realmente bien.

    • Definís una función que mide el error entre la posición del elemento dibujado y la nueva posición exigida por la acción de arrastre.
      Calculás las derivadas parciales de esa función con respecto a las variables de datos, minimizás la función para encontrar nuevos valores de datos y luego volvés a renderizar todo con los nuevos datos óptimos.
      Lo que lo vuelve un poco confuso es que lo que acá normalmente se llama “datos” en optimización matemática se llamaría parámetros, y en estadística/machine learning, pesos.
      En machine learning, lo fijo se llamaría “datos”, así que acá la posición del elemento arrastrado corresponde a los datos.
    • La matemática suficientemente avanzada es indistinguible de la magia, pero acá el umbral de “suficientemente avanzada” es más bajo de lo que uno pensaría.
  • Se ve bastante genial.
    Una cosa que se echa de menos es que no hay marcadores ni indicadores de selección, así que no queda muy claro que se puedan arrastrar objetos si no leés primero la explicación.
    La forma en que reajusta las posiciones de los objetos también es un poco confusa.
    Normalmente definirías handlers de puntero y transformarías según la entrada, pero si entendí bien, parece que el motor intenta automatizar eso haciendo su mejor estimación.

  • Hace unos años vi esto y me entusiasmé tanto que lo porté a Pyodide para que corriera en el navegador junto con una biblioteca parecida a PyTorch: https://srush.github.io/g9py/
    Es un poco más entrecortado que JavaScript, pero ya es sorprendente que funcione.

  • Esto se siente como brujería. Impresionante.

  • Parece tener puntos en común con las herramientas de bocetado basadas en restricciones de CAD 2D.

  • No estoy muy seguro de para qué habría que usarlo, pero está hecho con mucha inteligencia.

    • Potencialmente podría usarse en cualquier interfaz de drag and drop.
      Es una forma tan distinta de ver el problema que tendría que probarlo en algo para agarrarle la mano.
  • Podría ser muy útil en educación para transmitir trigonometría u otros conceptos matemáticos.
    Hay muchos aprendices visuales a quienes les cuesta mapear fórmulas al espacio visual, y agregar un poco de interacción ayuda muchísimo a consolidar la comprensión.