Gráficos con Canvas
Objetivos del capítulo
Este capítulo será un ejemplo extendido enfocado en el paquete purescript-canvas, que proporciona una forma de generar gráficos 2D desde Purescript usando la API Canvas de HTML5.
Preparación del proyecto
El módulo de este proyecto introduce las siguientes dependencias de Bower nuevas:
-
purescript-canvas, que da tipos a los métodos de la API Canvas de HTML5 -
purescript-refs, que proporciona un efecto secundario para usar referencias globales mutables
El código fuente para el capítulo está dividido en un conjunto de módulos, cada uno de los cuales define un método main. Las distintas secciones de este capítulo están implementadas en ficheros diferentes, y el módulo Main se puede cambiar modificando el comando de construcción de Pulp para ejecutar el método main del fichero adecuado en cada momento.
El fichero HTML html/index.html contiene un único elemento canvas que se usará en cada ejemplo, y un elemento script para cargar el código PureScript compilado. Para probar el código de cada sección, abre el fichero HTML en tu navegador.
Formas simples
El fichero Example/Rectangle.purs contiene un ejemplo introductorio simple que dibuja un único rectángulo azul en el centro del lienzo. El módulo importa Control.Monad.Eff, y también el módulo Graphics.Canvas que contiene acciones en la mónada Eff para trabajar con la API Canvas.
La acción main comienza, como los otros módulos, usando la acción getCanvasElementById para obtener una referencia al objeto lienzo, y la acción getContext2D para acceder al contexto de representación 2D del canvas:
1 main = void $ unsafePartial do
2 Just canvas <- getCanvasElementById "canvas"
3 ctx <- getContext2D canvas
Nota: la llamada a unsafePartial es necesaria ya que el ajuste de patrón sobre el resultado de getCanvasElementById es parcial, coincidiendo sólo con el constructor Just. Para nuestros propósitos es suficiente, pero en código de producción querremos probablemente ajustarnos también al constructor Nothing y proporcionar un mensaje de error adecuado.
Los tipos de estas acciones se pueden averiguar usando PSCi o mirando la documentación:
1 getCanvasElementById :: forall eff. String -> Eff (canvas :: CANVAS | eff) (Mayb\
2 e CanvasElement)
3
4 getContext2D :: forall eff. CanvasElement -> Eff (canvas :: CANVAS | eff) Contex\
5 t2D
CanvasElement y Context2D son tipos definidos en el módulo Graphics.Canvas. El mismo módulo define también el efecto CANVAS usado por todas las acciones del módulo.
El contexto gráfico ctx gestiona el estado del canvas y proporciona métodos para dibujar formas primitivas, fijar estilos y colores, y aplicar transformaciones.
Continuamos fijando el estilo de relleno para que sea azul mediante la acción setFillStyle:
1 setFillStyle "#0000FF" ctx
Fíjate en que la acción setFillStyle toma el contexto gráfico como argumento. Este es un patrón común en el módulo Graphics.Canvas.
Finalmente, usamos la acción fillPath para rellenar el rectángulo. fillPath tiene el siguiente tipo:
1 fillPath :: forall eff a. Context2D ->
2 Eff (canvas :: CANVAS | eff) a ->
3 Eff (canvas :: CANVAS | eff) a
fillPath toma un contexto gráfico y otra acción que construye la trayectoria a dibujar. Para construir una trayectoria podemos usar la acción rect. rect toma un contexto gráfico y un registro que proporciona la posición y tamaño del rectángulo.
1 fillPath ctx $ rect ctx
2 { x: 250.0
3 , y: 250.0
4 , w: 100.0
5 , h: 100.0
6 }
Construye el ejemplo del rectángulo proporcionando Example.Rectangle como nombre del módulo principal:
1 $ mkdir dist/
2 $ pulp build -O --main Example.Rectangle --to dist/Main.js
Ahora abre el fichero html/index.html y verifica que este código dibuja un rectángulo azul en el centro del lienzo.
Haciendo uso del polimorfismo de fila
Hay otras formas de representar trayectorias. La función arc dibuja un segmento de arco, y las funciones moveTo, lineTo y closePath se pueden usar para dibujar trayectorias lineales por tramos.
El fichero Shapes.purs dibuja tres formas: un rectángulo, un segmento de arco y un triángulo.
Hemos visto que la función rect toma un registro como argumento. De hecho, las propiedades del rectángulo se definen en un sinónimo de tipo:
1 type Rectangle =
2 { x :: Number
3 , y :: Number
4 , w :: Number
5 , h :: Number
6 }
Las propiedades x e y representan la ubicación de la esquina superior izquierda, mientras que las propiedades w y h representan el ancho y alto respectivamente.
Para dibujar un segmento de arco podemos usar la función arc pasando un registro con el siguiente tipo:
1 type Arc =
2 { x :: Number
3 , y :: Number
4 , r :: Number
5 , start :: Number
6 , end :: Number
7 }
Aquí, las propiedades x e y representan el punto central, r es el radio, y start y end representan los extremos del arco en radianes.
Por ejemplo, este código rellena un segmento de arco centrado en (300, 300) con radio 50:
1 fillPath ctx $ arc ctx
2 { x : 300.0
3 , y : 300.0
4 , r : 50.0
5 , start : Math.pi * 5.0 / 8.0
6 , end : Math.pi * 2.0
7 }
Date cuenta de que tanto Rectangle como Arc contienen propiedades x e y de tipo Number. En ambos casos, este par representa un punto. Significa que podemos escribir funciones polimórficas por fila que actúan en cualquier tipo de registro.
Por ejemplo, el módulo Shapes define una función translate que traslada una forma modificando sus propiedades x e y:
1 translate
2 :: forall r
3 . Number
4 -> Number
5 -> { x :: Number, y :: Number | r }
6 -> { x :: Number, y :: Number | r }
7 translate dx dy shape = shape
8 { x = shape.x + dx
9 , y = shape.y + dy
10 }
Fíjate en el tipo polimórfico por fila. Dice que translate acepta cualquier registro con propiedades x e y y otras propiedades cualesquiera, y devuelve el mismo tipo de registro. Los campos x e y se actualizan, pero el resto de campos permanecen intactos.
Esto es un ejemplo de sintaxis de actualización de registro (record update syntax). La expresión shape { ... } crea un nuevo registro basado en el registro shape, actualizando los campos entre llaves a los valores especificados. Date cuenta de que las expresiones entre llaves se separan de sus etiquetas por símbolos igual, no con dos puntos como en los registros literales.
La función translate se puede usar tanto con Rectangle como con Arc, como veremos en el ejemplo Shapes.
El tercer tipo de trayectoria dibujada en el ejemplo Shapes es una trayectoria lineal por tramos. Aquí está el código correspondiente:
1 setFillStyle "#FF0000" ctx
2
3 fillPath ctx $ do
4 moveTo ctx 300.0 260.0
5 lineTo ctx 260.0 340.0
6 lineTo ctx 340.0 340.0
7 closePath ctx
Usamos tres funciones aquí:
-
moveTomueve la posición actual de la trayectoria a las coordenadas especificadas -
lineTodibuja un segmento de línea entre la posición actual y las coordenadas especificadas, y actualiza la posición actual -
closePathcompleta la trayectoria dibujando un segmento de línea uniendo la posición actual y la posición inicial
El resultado de este fragmento de código es rellenar un triángulo isosceles.
Construye el ejemplo especificando Example.Shapes como módulo principal:
1 $ pulp build -O --main Example.Shapes --to dist/Main.js
y abre html/index.html de nuevo para ver el resultado. Debes ver tres tipos de formas dibujadas en el lienzo.
Dibujando círculos aleatorios
El fichero Example/Random.purs contiene un ejemplo que usa la mónada Eff para intercalar dos tipos diferentes de efectos secundarios: generación de números aleatorios y manipulación del lienzo. El ejemplo dibuja cien círculos generados aleatoriamente en el lienzo.
La acción main obtiene una referencia al contexto gráfico como antes, y fija los estilos de trazo y relleno:
1 setFillStyle "#FF0000" ctx
2 setStrokeStyle "#000000" ctx
A continuación, el código usa la función for_ para iterar por los enteros entre 0 y 100:
1 for_ (1 .. 100) \_ -> do
En cada iteración, el bloque de notación do comienza generando tres números aleatorios distribuidos entre 0 y 1. Estos números representan las coordenadas x e y y el radio de un círculo.
1 x <- random
2 y <- random
3 r <- random
A continuación, para cada círculo, el código crea un Arc basándose en estos parámetros y finalmente rellena y perfila el arco con los estilos actuales:
1 let path = arc ctx
2 { x : x * 600.0
3 , y : y * 600.0
4 , r : r * 50.0
5 , start : 0.0
6 , end : Math.pi * 2.0
7 }
8 fillPath ctx path
9 strokePath ctx path
Construye este ejemplo especificando el módulo Example.Random como módulo principal:
1 $ pulp build -O --main Example.Random --to dist/Main.js
y mira el resultado abriendo html/index.html.
Transformaciones
Podemos hacer más cosas en el lienzo que dibujar formas simples. Todos los lienzos mantienen una transformación que se usa para transformar las formas antes de dibujarse. Las formas pueden ser trasladadas, rotadas, escaladas y distorsionadas.
La biblioteca purescript-canvas soporta estas transformaciones usando las siguientes funciones:
1 translate :: forall eff
2 . TranslateTransform
3 -> Context2D
4 -> Eff (canvas :: Canvas | eff) Context2D
5
6 rotate :: forall eff
7 . Number
8 -> Context2D
9 -> Eff (canvas :: Canvas | eff) Context2D
10
11 scale :: forall eff
12 . ScaleTransform
13 -> Context2D
14 -> Eff (canvas :: CANVAS | eff) Context2D
15
16 transform :: forall eff
17 . Transform
18 -> Context2D
19 -> Eff (canvas :: CANVAS | eff) Context2D
La acción translate realiza una traslación cuyas componentes se especifican en las propiedades del registro TranslateTransform.
La acción rotate realiza una rotación respecto al origen de un número de radianes especificado como primer argumento.
La acción scale realiza un escalado con origen en el centro. El registro ScaleTransform especifica los factores de escala junto a los ejes x e y.
Finalmente, transform es la acción más general de las cuatro. Realiza una transformación afín especificada por una matriz.
Cualquier forma dibujada tras invocar a estas acciones recibirá automáticamente la transformación apropiada.
De hecho, el efecto de cada una de estas funciones es postmultiplicar la transformación por la transformación actual del contexto. El resultado es que si se aplican múltiples transformaciones una tras otra, sus efectos se aplican en orden inverso:
1 transformations ctx = do
2 translate { translateX: 10.0, translateY: 10.0 } ctx
3 scale { scaleX: 2.0, scaleY: 2.0 } ctx
4 rotate (Math.pi / 2.0) ctx
5
6 renderScene
El efecto de esta secuencia de acciones es que la escena se rota, se escala, y finalmente se traslada.
Preservando el contexto
Un caso de uso común es representar un subconjunto de la escena usando una transformación y restablecer la transformación a continuación.
La API de Canvas proporciona los métodos save y restore que manipulan una pila de estados asociados con el lienzo. purescript-canvas envuelve esta funcionalidad en las siguientes funciones:
1 save
2 :: forall eff
3 . Context2D
4 -> Eff (canvas :: CANVAS | eff) Context2D
5
6 restore
7 :: forall eff
8 . Context2D
9 -> Eff (canvas :: CANVAS | eff) Context2D
La acción save apila el estado actual del contexto (incluyendo la transformación actual y cualquier estilo) en la pila, y la acción restore desapila el estado superior de la pila y lo restaura.
Esto permite salvar el estado actual, aplicar algunos estilos y transformaciones, dibujar primitivas, y finalmente restaurar la transformación y estado originales. Por ejemplo, la siguiente función dibuja algo en el canvas, pero aplica una rotación antes de hacerlo y restaura la transformación a continuación:
1 rotated ctx render = do
2 save ctx
3 rotate Math.pi ctx
4 render
5 restore ctx
Para abstraer casos de uso comunes usando funciones de orden mayor, la biblioteca purescript-canvas proporciona la función withContext, que realiza una acción sobre el lienzo al tiempo que preserva el estado original del contexto:
1 withContext
2 :: forall eff a
3 . Context2D
4 -> Eff (canvas :: CANVAS | eff) a
5 -> Eff (canvas :: CANVAS | eff) a
Podríamos reescribir la función rotated anterior usando withContext como sigue:
1 rotated ctx render =
2 withContext ctx do
3 rotate Math.pi ctx
4 render
Estado mutable global
En esta sección usaremos el paquete purescript-refs para demostrar otro efecto en la mónada Eff.
El módulo Control.Monad.Eff.Ref proporciona un constructor de tipo para referencias a estado global mutable y un efecto asociado:
1 > import Control.Monad.Eff.Ref
2
3 > :kind Ref
4 Type -> Type
5
6 > :kind REF
7 Control.Monad.Eff.Effect
Un valor de tipo Ref a es una referencia a una celda mutable que contiene un valor de tipo a, bastante parecido a STRef h a que vimos en el capítulo anterior. La diferencia es que, mientras que el efecto ST se puede eliminar usando runST, el efecto Ref no proporciona un gestor. Mientras que ST se usa para seguir la pista a la mutación local segura, Ref se usa para mutaciones globales. Por lo tanto se debe usar escasamente.
El fichero Example/Refs.purs contiene un ejemplo que usa el efecto REF para detectar pulsaciones de ratón en el elemento canvas.
El código comienza creando una nueva referencia conteniendo el valor 0 mediante la acción newRef:
1 clickCount <- newRef 0
Dentro del gestor de pulsación de ratón, la acción modifyRef se usa para actualizar la cuenta de pulsaciones:
1 modifyRef clickCount (\count -> count + 1)
La acción readRef se usa para leer la nueva cuenta de pulsaciones:
1 count <- readRef clickCount
En la función render, usamos el contador de pulsaciones para determinar qué transformación aplicar a un rectángulo:
1 withContext ctx do
2 let scaleX = Math.sin (toNumber count * Math.pi / 4.0) + 1.5
3 let scaleY = Math.sin (toNumber count * Math.pi / 6.0) + 1.5
4
5 translate { translateX: 300.0, translateY: 300.0 } ctx
6 rotate (toNumber count * Math.pi / 18.0) ctx
7 scale { scaleX: scaleX, scaleY: scaleY } ctx
8 translate { translateX: -100.0, translateY: -100.0 } ctx
9
10 fillPath ctx $ rect ctx
11 { x: 0.0
12 , y: 0.0
13 , w: 200.0
14 , h: 200.0
15 }
Esta acción usa withContext para preservar la transformación original, y aplica la siguiente secuencia de transformaciones (recuerda que las transformaciones se aplican de abajo hacia arriba):
- El rectángulo se traslada a
(-100, -100)de manera que su centro descanse en el origen - Escalamos el rectángulo con respecto al origen
- Rotamos el rectángulo un múltiplo de
10grados respecto al origen - Trasladamos el rectángulo a
(300, 300)de manera que su centro quede en el centro del lienzo
Construye el ejemplo:
1 $ pulp build -O --main Example.Refs --to dist/Main.js
y abre el fichero html/index.html. Si pulsas sobre el lienzo repetidas veces verás un rectángulo verde rotando sobre el centro del lienzo.
Sistemas-L
En este último ejemplo, usaremos el paquete purescript-canvas para escribir una función que represente sistemas-L (o sistemas de Lindenmayer).
Un sistema-L se define mediante un alfabeto, una secuencia inicial de letras del alfabeto y un conjunto de reglas de producción. Cada regla de producción toma una letra del alfabeto y devuelve una secuencia de letras de reemplazo. Este proceso se itera un cierto número de veces comenzando con la secuencia inicial de letras.
Si cada letra del alfabeto se asocia con alguna instrucción a realizar en el lienzo, el sistema-L se puede dibujar siguiendo las instrucciones por orden.
Por ejemplo, supongamos que el alfabeto consta de las letras L (gira a la izquierda), R (gira a la derecha) y F (avanza). Podemos definir la siguiente regla de producción:
1 L -> L
2 R -> R
3 F -> FLFRRFLF
Si comenzamos con la secuencia inicial “FRRFRRFRR” e iteramos, obtenemos la siguiente secuencia:
1 FRRFRRFRR
2 FLFRRFLFRRFLFRRFLFRRFLFRRFLFRR
3 FLFRRFLFLFLFRRFLFRRFLFRRFLFLFLFRRFLFRRFLFRRFLF...
y así sucesivamente. Dibujar una trayectoria lineal por tramos correspondiente a este conjunto de instrucciones aproxima una curva llamada la curva de Koch. Incrementar el número de iteraciones incrementa la resolución de la curva.
Traduzcamos esto al lenguaje de los tipos y funciones.
Podemos representar nuestro alfabeto mediante un tipo algebraico. Para nuestro ejemplo podemos usar el siguiente tipo:
1 data Alphabet = L | R | F
Este tipo de datos define un constructor de datos para cada letra de nuestro alfabeto.
¿Cómo podemos representar la secuencia inicial de letras? Es simplemente un array de letras de nuestro alfabeto que llamaremos frase (Sentence):
1 type Sentence = Array Alphabet
2
3 initial :: Sentence
4 initial = [F, R, R, F, R, R, F, R, R]
Nuestras reglas de producción se pueden expresar como una función de Alphabet a Sentence como sigue:
1 productions :: Alphabet -> Sentence
2 productions L = [L]
3 productions R = [R]
4 productions F = [F, L, F, R, R, F, L, F]
Esto es una copia directa de la especificación de arriba.
Ahora podemos implementar una función lsystem que toma una especificación de esta forma y la dibuja en el lienzo. ¿Qué tipo debe tener lsystem? Bien, necesita tomar valores como initial y productions como argumentos, así como una función para dibujar una letra del alfabeto en el canvas.
Aquí tenemos una primera aproximación al tipo de lsystem:
1 forall eff. Sentence
2 -> (Alphabet -> Sentence)
3 -> (Alphabet -> Eff (canvas :: Canvas | eff) Unit)
4 -> Int
5 -> Eff (canvas :: CANVAS | eff) Unit
Los dos primeros argumentos corresponden a los valores initial y productions.
El tercer argumento representa una función que toma una letra del alfabeto y la interpreta realizando algunas acciones sobre el lienzo. En nuestro ejemplo, esto significaría girar a la izquierda en el caso de la letra L, girar a la derecha en el caso de la letra R, y avanzar en el caso de la letra F.
El argumento final es un número que representa el número de iteraciones de las reglas de producción que queremos realizar.
La primera observación es que la función lsystem no debe funcionar sólo para un único tipo Alphabet, sino para cualquier tipo, de manera que debemos generalizar nuestro tipo. Cambiemos Alphabet y Sentence por a y Array a para alguna variable de tipo cuantificada a:
1 forall a eff. Array a
2 -> (a -> Array a)
3 -> (a -> Eff (canvas :: CANVAS | eff) Unit)
4 -> Int
5 -> Eff (canvas :: CANVAS | eff) Unit
La segunda observación es que para implementar instrucciones como “gira a la izquierda” y “gira a la derecha” necesitamos mantener algún estado, esto es, la dirección en que la trayectoria se está moviendo en todo momento. Necesitamos modificar nuestra función para pasar el estado durante el cálculo. De nuevo, la función lsystem debe funcionar para cualquier tipo de estado, así que lo representaremos usando la variable de tipo s.
Necesitamos añadir el tipo s en tres sitios:
1 forall a s eff. Array a
2 -> (a -> Array a)
3 -> (s -> a -> Eff (canvas :: CANVAS | eff) s)
4 -> Int
5 -> s
6 -> Eff (canvas :: CANVAS | eff) s
En primer lugar, el tipo s ha sido añadido como el tipo de un argumento adicional a lsystem. Este argumento representará el estado inicial del sistema-L.
El tipo s también aparece como argumento y tipo de retorno de la función de interpretación (el tercer argumento a lsystem). La función de interpretación recibirá ahora el estado actual del sistema-L como argumento y devolverá un nuevo estado actualizado.
En el caso de nuestro ejemplo, podemos usar el siguiente tipo para representar el estado:
1 type State =
2 { x :: Number
3 , y :: Number
4 , theta :: Number
5 }
Las propiedades x e y representan la posición actual de la trayectoria, y la propiedad theta representa la dirección actual de la trayectoria especificada como el ángulo entre la dirección de la trayectoria y el eje horizontal en radianes.
El estado inicial del sistema se puede especificar como sigue:
1 initialState :: State
2 initialState = { x: 120.0, y: 200.0, theta: 0.0 }
Ahora intentemos implementar la función lsystem. Veremos que su definición es notablemente simple.
Parece razonable que lsystem recurra sobre su cuarto argumento (de tipo Int). En cada paso de la recursividad, la frase actual cambiará siendo actualizada mediante las reglas de producción. Teniendo en cuesta esto, asignemos nombres a los argumentos de la función y deleguemos en una función auxiliar:
1 lsystem :: forall a s eff
2 . Array a
3 -> (a -> Array a)
4 -> (s -> a -> Eff (canvas :: CANVAS | eff) s)
5 -> Int
6 -> s
7 -> Eff (canvas :: CANVAS | eff) s
8 lsystem init prod interpret n state = go init n
9 where
La función go trabaja recursivamente sobre su segundo argumento. Hay dos casos: cuando n es cero y cuando no lo es.
En el primer caso, la recursividad finaliza y simplemente necesitamos interpretar la frase actual de acuerdo a la función de interpretación. Tenemos una frase de tipo Array a, un estado de tipo s y una función de tipo s -> a -> Eff (canvas :: CANVAS | eff) s. Parece un trabajo para la función foldM que definimos antes y que está disponible en el paquete purescript-control:
1 go s 0 = foldM interpret state s
¿Que pasa en el caso en que no es cero? En ese caso, podemos simplemente aplicar las reglas de producción a cada letra de la frase actual, concatenando los resultados, y repetir llamando a go recursivamente:
1 go s n = go (concatMap prod s) (n - 1)
¡Eso es todo! Fíjate en cómo el uso de funciones de orden mayor como foldM y concatMap nos ha permitido comunicar nuestras ideas de manera concisa.
Sin embargo aún no hemos terminado. El tipo que hemos dado todavía es demasiado específico. Fíjate en que no usamos ninguna operación de canvas en ningún sitio de nuestra implementación. Tampoco usamos la estructura de la mónada Eff para nada. De hecho, ¡nuestra función es válida para cualquier mónada m!
Aquí tenemos el tipo más general de lsystem de la manera en que se especifica en el código fuente que acompaña este capítulo:
1 lsystem :: forall a m s
2 . Monad m
3 => Array a
4 -> (a -> Array a)
5 -> (s -> a -> m s)
6 -> Int
7 -> s
8 -> m s
Podemos entender este tipo como si dijese que nuestra función de interpretación es libre de tener efectos secundarios, capturados por la mónada m. Puede dibujar en el lienzo, o imprimir información a la consola, o soportar fallos o múltiples valores de retorno. Animamos al lector a que intente escribir sistemas-L que usen estos tipos de efectos secundarios.
Esta función es un buen ejemplo de la potencia de separar los datos de la implementación. La ventaja de este enfoque es que ganamos la libertad de interpretar nuestros datos de varias maneras distintas. Podemos incluso factorizar lsystem en dos funciones más pequeñas: la primera construiría una frase usando aplicación repetida de concatMap, y la segunda interpretaría la frase usando foldM. Dejamos esto también como un ejercicio para el lector.
Completemos nuestro ejemplo implementando la función de interpretación. El tipo de lsystem nos dice que su firma debe ser s -> a -> m s para algunos tipos a y s y un constructor de tipo m. Sabemos que queremos que a sea Alphabet y s sea State, y para la mónada m podemos elegir Eff (canvas :: CANVAS). Esto nos da el siguiente tipo:
1 interpret :: State -> Alphabet -> Eff (canvas :: CANVAS) State
Para implementar esta función necesitamos gestionar los tres constructores del tipo Alphabet. Para interpretar las letras L (girar a la izquierda), y R (girar a la derecha), simplemente tenemos que actualizar el estado para cambiar el ángulo theta de manera apropiada:
1 interpret state L = pure $ state { theta = state.theta - Math.pi / 3 }
2 interpret state R = pure $ state { theta = state.theta + Math.pi / 3 }
Para interpretar la letra F (avanzar), podemos calcular la nueva posición de la trayectoria, dibujar un segmento y actualizar el estado como sigue:
1 interpret state F = do
2 let x = state.x + Math.cos state.theta * 1.5
3 y = state.y + Math.sin state.theta * 1.5
4 moveTo ctx state.x state.y
5 lineTo ctx x y
6 pure { x, y, theta: state.theta }
Date cuenta de que en el código fuente de este capítulo, la función interpret se define usando una ligadura let dentro de la función main, de manera que el nombre ctx esté en ámbito. Sería posible también mover el contexto al tipo State, pero esto no sería apropiado porque no es una parte cambiante del estado del sistema.
Para representar este sistema-L podemos simplemente usar la acción strokePath:
1 strokePath ctx $ lsystem initial productions interpret 5 initialState
Compila el ejemplo del sistema-L usando
1 $ pulp build -O --main Example.LSystem --to dist/Main.js
y abre html/index.html. Debes ver la curva de Koch dibujada en el lienzo.
Conclusión
En este capítulo hemos aprendido cómo usar la API Canvas de HTML5 desde PureScript usando la biblioteca purescript-canvas. Vimos también una demostración práctica de muchas de las técnicas que ya habíamos aprendido: asociaciones y pliegues, registros y polimorfismo de fila, y la mónada Eff para gestionar efectos secundarios.
Los ejemplos demuestran también la potencia de las funciones de orden mayor y la separación de datos de la implementación. Sería posible extender estas ideas para separar por completo la representación de una escena de su función de dibujado usando un tipo de datos algebraico, por ejemplo:
1 data Scene
2 = Rect Rectangle
3 | Arc Arc
4 | PiecewiseLinear (Array Point)
5 | Transformed Transform Scene
6 | Clipped Rectangle Scene
7 | ...
Este es el enfoque tomado por el paquete purescript-drawing y aporta la flexibilidad de ser capaz de manipular la escena como datos de varias maneras antes de dibujar.
En el siguiente capítulo veremos cómo implementar bibliotecas como purescript-canvas que envuelven funcionalidad JavaScript existente, usando la interfaz para funciones externas de PureScript.