Establezcamos un objetivo puramente práctico: implementar un lienzo sin fin con la capacidad de moverlo y escalarlo con el mouse. Tal lienzo, por ejemplo, puede servir como un sistema de coordenadas en movimiento en un editor gráfico. La implementación de nuestra idea no es tan complicada, pero el proceso de comprensión está asociado con objetos matemáticos y físicos fundamentales, que consideraremos a medida que nos desarrollemos.
ResultadoDeclaración del problema.
Desde nuestro lienzo, queremos realizar solo dos funciones: mover el mouse y el movimiento del mouse, así como hacer zoom al desplazarse. La arena de nuestras transformaciones elegirá un navegador. Las armas, en este caso, no tienen que elegir.
Máquina de estado
El comportamiento de tales sistemas se describe convenientemente mediante transiciones entre sus estados, es decir máquina de estado donde - la función de transición entre estados, que muestra muchos estados en sí misma.
En nuestro caso, el diagrama de estado se ve así:

Al implementar la función de transición, es conveniente hacer que el evento sea dependiente. Esto se hará evidente en el futuro. Del mismo modo, es conveniente poder suscribirse a un cambio en el estado de la máquina.
type State = string | number; type Transition<States = State> = { to: States, where: (event: Event) => Array<boolean>, }; type Scheme<States = State> = { [key: States]: Array<Transition<States>> }; interface FSM<States = State> { constructor(state: States, scheme: Scheme<States>): void; // Returns true if the scheme had a transition, false otherwise get isActive(): boolean; // Dispatch event and try to do transition dispatch(event: Event): void; // subscribe on state change on(state: States, cb: (event: Event) => any): FSM<States>; // remove subscriber removeListener(state: States, cb: (event: Event) => any): void; };
Pospondremos la implementación por un tiempo y retomaremos las transformaciones geométricas que subyacen en nuestra tarea.
Geometrías
Si el problema con mover el lienzo es tan obvio que no nos detendremos en él, entonces el estiramiento debe considerarse con más detalle. En primer lugar, requerimos que el estiramiento deje un solo punto estacionario: el cursor del mouse. La condición de reversibilidad también debe cumplirse, es decir La secuencia inversa de las acciones del usuario debe llevar el lienzo a su posición original. ¿Qué geometría es adecuada para esto? Consideraremos un grupo de transformaciones puntuales del plano en sí mismo , que generalmente se expresa mediante la introducción de nuevas variables definido como funciones de antaño:
De acuerdo con el principio de dualidad en las matemáticas, tales transformaciones pueden interpretarse como un cambio en el sistema de coordenadas, así como una transformación del espacio en sí mismo con este último fijo. La segunda interpretación es conveniente para nuestros propósitos.
Una comprensión moderna de la geometría es diferente de una comprensión de los antiguos. Según F. Klein , - la geometría estudia invariantes con respecto a ciertos grupos de transformación. Entonces, en un grupo de movimientos la invariante es la distancia entre dos puntos . Incluye separación silábica paralela en vector rotación relativo al origen en un ángulo y reflexiones relativo a alguna línea . Tales movimientos se llaman elementales. La composición de los dos movimientos pertenece a nuestro grupo y a veces se reduce a primaria. Entonces, por ejemplo, dos reflexiones especulares consecutivas en relación con las líneas rectas y dé una rotación alrededor de cierto centro en cierto ángulo (verifíquelo usted mismo):
Seguramente ya habrás adivinado que tal grupo de movimientos forma geometría euclidiana. Sin embargo, el estiramiento no preserva la distancia entre dos puntos, sino su relación. Por lo tanto, un grupo de movimientos, aunque debería incluirse en nuestro esquema, pero solo como un subgrupo.
La geometría que nos conviene se basa en un grupo de estiramiento. a lo cual, además de los movimientos anteriores, se agrega homotetería por coeficiente .
Bueno, el ultimo. El elemento inverso debe estar presente en el grupo. Y por lo tanto hay un neutral (o solo) que no cambia nada. Por ejemplo
significa primero estirar en
veces y luego en
.
Ahora podemos describir el estiramiento, dejando el punto del cursor del mouse fijo, en lenguaje grupal teórico:
Nota 1En el caso general, la reorganización de las acciones no es conmutativa (primero puede quitarse el abrigo y luego la camisa, pero no al revés).
Expresa el movimiento y y su composición como funciones de un vector en código
type Scalar = number; type Vec = [number, number]; type Action<A = Vec | Scalar> = (z: Vec) => (v: A) => Vec;
Dolor 1Es muy extraño que en JavaScript no haya sobrecarga del operador. Parece que con un uso tan extendido de gráficos vectoriales y de trama, es mucho más conveniente trabajar con vectores o números complejos en una forma "clásica". Los conceptos de acción reemplazarían las operaciones aritméticas. Entonces, por ejemplo, rotación alrededor de algún vector en la esquina se expresaría de manera trivial:
Desafortunadamente, JS no sigue el desarrollo de la maravillosa idea de Pitágoras "El mundo es un número", a diferencia, al menos, de Python.
Tenga en cuenta que hasta ahora hemos estado trabajando con un grupo de transformaciones continuas. Sin embargo, la computadora no funciona con cantidades continuas, por lo tanto, siguiendo a Poincare, entenderemos un grupo continuo como un grupo infinito de operaciones discretas. Ahora que hemos descubierto la geometría, deberíamos recurrir a la relatividad del movimiento.
Cosmología del lienzo. Rejilla modular
Durante un siglo, como humanidad, la expansión del universo ha sido conocida. Observando objetos distantes, galaxias y cuásares, registramos el desplazamiento del espectro electromagnético hacia ondas más largas, el denominado desplazamiento al rojo cosmológico. Cualquier medición conecta al observador, lo observado y los medios de medición en relación con los cuales hacemos nuestras mediciones. Sin instrumentos de medición, es imposible establecer relaciones invariables en la naturaleza, es decir, determinar la geometría del Universo. Sin embargo, la geometría pierde su significado sin observabilidad. Entonces, en nuestra tarea, es bueno tener puntos de referencia, como galaxias, en relación con la luz de los cuales podemos determinar la relatividad del movimiento de nuestro lienzo. Dicha estructura puede ser una red periódica, que se bifurca cada vez que el espacio se expande dos veces.
Como la red es periódica, es conveniente adoptar un álgebra modular. Por lo tanto, actuaremos como un grupo también en el toro . Dado que la pantalla del monitor no es un plano continuo, sino una red entera (descuidamos ahora que es finito), entonces la acción del grupo debe considerarse en un toro entero donde - el tamaño del borde del cuadrado :

Por lo tanto, de una vez por todas arreglando nuestro toro cerca del origen, realizaremos todos los cálculos adicionales sobre él. Luego propagarlo utilizando métodos de biblioteca de lienzo estándar. Así es como se ve un movimiento de un píxel:

Obviamente, la operación estándar de tomar el módulo x% p no es adecuada para nosotros, ya que traduce los valores negativos del argumento en negativos, pero no hay ninguno en el toro de enteros. Escribe tu función :
const mod = (x, p) => x >= 0 ? Math.round(x) % p : p + Math.round(x) % p;
Ahora volvamos a la máquina de estado final y
definirloLa clase FSM se hereda convenientemente de EventEmitter, que nos proporcionará la capacidad de suscribirnos.
class FSM<States> extends EventEmitter { static get TRANSITION() { return '__transition__'; } state: States; scheme: Scheme<States>; constructor(state: States, scheme: Scheme<States>) { super(); this.state = state; this.scheme = scheme; this.on(FSM.TRANSITION, event => this.emit(this.state, event)); } get isActive(): boolean { return typeof(this.scheme[this.state]) === 'object'; } dispatch(event: Event) { if (this.isActive) { const transition = this.scheme[this.state].find(({ where }) => where(event).every(domen => domen) ); if (transition) { this.state = transition.to; this.emit(FSM.TRANSITION, event); } } } }
A continuación, defina un esquema de transición, cree un lienzo y
Inicializar todo. canvas = document.getElementById('canvas'); ctx = canvas.getContext('2d');
Luego debe determinar la función de representación, establecer los valores iniciales necesarios y suscribirse al cambio de estado. Considere la parte más interesante del código:
fsm.on('zooming', (event: WheelEvent) => {
Primero, no nos expandimos por el coeficiente k, sino por alguna relación nk / k. Esto se debe al hecho de que el paso m de nuestro mapeo en un punto fijo expresado como
o, relativo a los valores iniciales
Obviamente el producto Hay una función no lineal del paso de iteración y converge muy rápidamente a cero o se escapa al infinito con pequeñas desviaciones iniciales.
Introducimos la variable g, que es una medida de duplicar nuestro lienzo. Obviamente, adquiere un valor constante en un cierto intervalo. Para lograr linealidad usamos sustitución homogénea
Entonces todos los miembros en el trabajo, excepto el primero y el último, se reducirán:
Además, el salto de fase g reduce la velocidad de expansión de tal manera que la estructura fractal que se desarrolla ante nosotros siempre se mueve linealmente. Por lo tanto, obtenemos una variación aproximada de la ley de poder de Hubble de la expansión del Universo.
Queda por comprender los límites de precisión de nuestro modelo.
Fluctuaciones cuánticas. Campo de número 2-adic
La comprensión del proceso de medición condujo al concepto de un número real. El principio de incertidumbre de Heisenberg apunta a sus límites. Una computadora moderna no funciona con números reales, sino con palabras de máquina, cuya longitud está determinada por la capacidad del procesador. Las palabras de máquina forman un campo de números 2-adic y se denotan como donde - longitud de palabra. El proceso de medición en este caso es reemplazado por el proceso de cálculo y está asociado con una métrica no Archimedean:
Por lo tanto, nuestro modelo tiene un límite de cálculo. Las limitaciones se describen en el estándar IEEE_754 . Comenzando en algún momento, nuestro tamaño base irá más allá del límite de precisión y la operación de tomar el módulo comenzará a generar errores que se asemejan a secuencias pseudoaleatorias. Esto se verifica simplemente eliminando la línea
if (g < min || g > max) return;
El límite final en nuestro caso se calcula mediante el método semiempírico, ya que trabajamos con varios parámetros.
Conclusión
Por lo tanto, aparentemente distantes a primera vista, las teorías están conectadas en el lienzo en el navegador. Los conceptos de acción, medición y cálculo están estrechamente relacionados entre sí. El problema de combinarlos aún no está resuelto.
ResultadoPSEstaba claro que el código fuente sería pequeño, por lo que lideré el desarrollo en el index.html ordinario. Mientras escribía este artículo, agregué la tipificación que probé en el patio de juegos TypeScript.