< Volver 

Blocks Simulator: Color

2022-05-07

La herramienta de color tiene una rueda, campos RGB, HSV y hexadecimal, cinco colores guardados y un cuentagotas, y pinta lo que le hagas clic. Este artículo cubre la matemática polar de la rueda, por qué teñir la imagen de la rueda es una forma exacta de mostrar el valor, cómo se mantienen sincronizados los campos y algunos bugs, dos de los cuales se cancelan entre sí.

LuauRobloxUIMath
Ir a vista rápida

Una rueda de color hecha con un ángulo y una distancia.

El Color Manager: rueda de color, campos RGB y HSV, código hexadecimal y cinco colores guardados

Resumen. La herramienta de color (F) abre el Color Manager, una ventana con una rueda de color, dos sliders, campos para rojo, verde, azul, tono, saturación y valor, un campo hexadecimal, cinco colores guardados y un cuentagotas. Hacerle clic a un bloque lo pinta, y el contorno del bloque que está bajo el mouse toma el color elegido como vista previa. La ventana son unas 480 líneas en un solo script, ColorInteract, montado sobre el modelo HSV: el tono es un ángulo en la rueda, la saturación es la distancia al centro y el valor se muestra oscureciendo la rueda entera. Este artículo recorre la conversión del mouse a un color y de vuelta, por qué multiplicar la imagen de la rueda por un gris es una forma exacta de mostrar el valor, cómo se mantienen coherentes siete campos, el cuentagotas, lo que hace el servidor y algunos bugs que encontré al releerlo.

1. El problema

Un selector de color tiene que funcionar en las dos direcciones. Del mouse a un color, cuando haces clic en la rueda o en un slider, y de un color a los controles, cuando escribes un número, eliges un color guardado o copias el de un bloque, para que el marcador, los sliders y todos los campos muestren lo mismo. Con una rueda, el modelo natural es HSV, porque el tono y la saturación corresponden a coordenadas polares: el ángulo alrededor del centro y la distancia a él.

2. Del mouse a un color

Primero se normaliza la posición del mouse a la caja de la rueda, de modo que (0,0) es su esquina superior izquierda, (1,1) la inferior derecha y c=(0.5,0.5) su centro. El desplazamiento desde el centro es 𝐮=𝐯−c. Si el mouse queda fuera del círculo, que pasa cuando sigues arrastrando más allá del borde, se proyecta sobre el borde:

‖𝐮‖>12⇒𝐮←𝐮2‖𝐮‖

El tono es el ángulo entre 𝐮 y el eje horizontal eˆx. El código lo calcula en 3D, con la fórmula general del ángulo entre dos vectores:

α=atan2⁡(‖eˆx×𝐮‖, eˆx⋅𝐮)

La magnitud del producto cruz es ‖𝐮‖sin⁡α y el producto punto es ‖𝐮‖cos⁡α, así que su atan2 es α, sin los problemas de precisión que tiene acos cerca de 0° y de 180°. Pero ese ángulo no tiene signo. Una magnitud nunca es negativa, así que α solo va de 0° a 180°, y el código recupera la otra mitad a mano: si uy<0, el tono es 360°−α. Todo eso equivale a atan2⁡(uy,ux) tomado módulo 360°, que es una sola llamada.

La saturación es la distancia al centro relativa al radio, s=‖𝐮‖/0.5. Los dos van a los campos H y S, H en grados de 0 a 360 y S de 0 a 255, y desde ahí la misma función que procesa un HSV escrito a mano arma el color con Color3.fromHSV. Las coordenadas de pantalla crecen hacia abajo, así que un tono creciente avanza en el sentido de las manecillas del reloj.

3. Del color al marcador

Para ubicar el marcador de un color dado, DisplayColor hace la inversa:

P=c+s2(cos⁡2πh, sin⁡2πh)

con h y s entre 0 y 1, sacados de Color:ToHSV(). Es el mismo mapa polar con la misma Y hacia abajo, así que después de hacer clic en la rueda el marcador cae donde estaba el mouse, salvo por el redondeo de los campos.

4. El valor es un tinte

La imagen de la rueda muestra todos los tonos y saturaciones con valor completo, V=1. Para mostrar los colores con otro valor, el código no redibuja nada. Le pone a ImageColor3 de la imagen un gris de ese valor:

wheelmgr["Wheel.dll.mui"].ImageColor3 = Color3.new(val,val,val);

ImageColor3 multiplica cada píxel de la imagen por ese color, canal por canal, y para HSV eso es exacto por cómo está definido el modelo. Con C=V⋅S y m=V−C, cada canal RGB es m más una fracción de C que solo depende del tono, así que todos los canales son V por una función de H y S:

rgb⁡(H,S,V)=V⋅rgb⁡(H,S,1)

Multiplicar la rueda de valor completo por V da exactamente la rueda con valor V. Los sliders usan la misma idea: el fondo de la barra de saturación es Color3.fromHSV(h, 1, 1), el de la barra de valor es Color3.fromHSV(h, s, 1), y sus perillas quedan en 1−s y 1−v, así que el máximo está a la izquierda.

SolarFramework también trae un módulo de rueda de color prestado, acreditado en su encabezado a morashsPeasant, que trabaja en HSL. Nada lo usa.

5. Siete campos sincronizados

Los campos los actualizan tres funciones, una por fuente. UpdateFromRgb corre cuando cambian H, S o V y recalcula R, G y B; UpdateFromHsv hace lo contrario; y UpdateFromHex corre cuando cambia el código hexadecimal. Cada una lee sus campos de origen, arma el color, escribe los demás campos y llama a DisplayColor. Los nombres se refieren a los campos que cada función escribe, no a los que lee.

H se guarda como un número entero de grados, y S, V, R, G y B como enteros de 0 a 255. Cada actualización va en un solo sentido, del campo que editaste a los demás, así que los valores no se van corriendo por conversiones repetidas. La rueda sí pasa por los campos, así que solo puede producir tonos de grados enteros, 360 en total.

El campo hexadecimal acepta #RGB, que expande a #RRGGBB, y rellena los códigos más cortos con ceros a la derecha, así que #12 queda en #120000. Para volver a hexadecimal, cada canal pasa por ToPreciseHex, una conversión a base 16 que escribí a mano: toma el residuo de dividir por 16 como dígito, divide, repite, invierte la cadena y le agrega un cero adelante a los valores menores que 16. Unas líneas más arriba está Int.ToHex, que hace string.format("%X", num) y nunca se llama. "%02X" habría hecho todo ToPreciseHex en una sola llamada.

Hay dos bugs en esta parte que se cancelan o no importan. El primero: las variables se declaran con s = window:WaitForChild("Value") y v = window:WaitForChild("Saturation"), cruzadas. En la ventana, la etiqueta al lado de la caja llamada Saturation dice V, y la que está al lado de Value dice S. Los dos cruces se cancelan, y la caja que el jugador lee como S sí muestra la saturación.

El segundo está en cómo el código evita los ciclos de realimentación. Antes de escribir en los campos, cada actualización desconecta todos los handlers de FocusLost y los vuelve a conectar al final. Las conexiones se guardan como llaves de una tabla, y la reconexión solo ocurre si #connections <= 0. En Luau, # cuenta la parte de arreglo de una tabla, y una tabla cuyas llaves son conexiones no tiene, así que #connections siempre es 0, la condición siempre se cumple y la tabla guarda todas las conexiones muertas y suma catorce nuevas por actualización. Nada se dispara dos veces, porque el ciclo de desconexión recorre todas las llaves, muertas incluidas. Y toda esa rutina sobraba: FocusLost se dispara cuando el jugador sale de una caja, no cuando el código asigna Text, así que no había ningún ciclo que evitar.

6. Colores guardados y cuentagotas

Los cinco colores guardados son botones cuyo BackgroundColor3 es el color. Hacerle clic a uno escribe su R, G y B en los campos y todo se actualiza desde ahí. El botón del cuentagotas cambia su ícono por una X y espera el siguiente clic en el mundo, y el Color de la parte que está bajo el mouse pasa a los campos R, G y B. La espera es un repeat wait() until ValidTargetFound, que revisa en cada frame hasta que un clic cae en algo. Mientras está activo, una bandera CopyingColor evita que la herramienta pinte el bloque del que estás copiando.

7. Pintar

El Main.lua de Color.exe, que su ED.lua arranca y detiene, hace dos cosas. En cada frame le pone un SelectionBox a la parte que está bajo el mouse y le da el color elegido, así que el contorno muestra cómo va a quedar. Y al hacer clic, dispara ColorRep con la parte y el color. A diferencia de las herramientas de transformación, no cambia el bloque en local primero, así que el color aparece cuando se replica el cambio del servidor. El servidor hace las revisiones que describe el resumen general y asigna Color. La baseplate está Locked, así que pintarla devuelve 0xFB.

8. La primera versión

La herramienta de color que escribí como juank era un Tool con un balde de pintura en la mano, y la pintura tomaba el color elegido. Cada vez que el mouse se movía sobre una parte, arrancaba un ciclo cuya condición de salida era herramienta.Unequipped:Wait(). Wait no devuelve verdadero o falso; pausa el hilo hasta que el evento se dispare. Así que cada ciclo actualizaba el contorno una vez y se quedaba dormido hasta que se desequipaba la herramienta, y como Unequipped no pasa ningún valor, la condición daba nil, el ciclo daba otra vuelta y se quedaba esperando el siguiente desequipe. Cada movimiento del mouse dejaba un hilo más de esos esperando.

Los colores salían de una paleta más vieja que todavía está en el place: un cuadrado para tono y saturación y una barra para el valor. Convertía a RGB con una función propia que toma los seis sectores del tono, mezcla hacia el blanco según la saturación y después hacia el negro según el valor. Mezclar desde el negro según V es multiplicar por V, lo mismo que hace el tinte de la sección 4.

9. Limitaciones

  • El cuentagotas espera revisando en cada frame.
  • Pintar no tiene vista previa en el bloque mismo, solo en el contorno, así que con latencia el color llega tarde.
  • Como en todas las herramientas, el servidor no revisa de quién es el bloque.
  • Unas 480 líneas en un solo script que maneja la entrada, la conversión y la visualización.

10. Lo que me dejó

La mejor idea de esta herramienta es el tinte, y es exacto por una razón precisa: en HSV, el valor multiplica el color entero. La mayoría de los bugs que encontré son de Luau y no de color, como usar # en una tabla sin parte de arreglo, o suponer que un evento se disparaba cuando el código cambiaba el texto de una caja.

Este texto de Mastedore. Todos los derechos reservados.