Programmeren voor beginners

..en voor wie een nieuwe programmeer-superkracht wil ontdekken.

Introductie

Op deze site kun je leren programmeren.
Je hebt geen voorkennis nodig. We zullen de benodigde fundamenten (basis concepten) geleidelijk en spelenderwijs uitleggen.
In de cursus leer je tekeningen en animaties maken op het scherm, en eenvoudige spelletjes die je in je internetbrowser kunt spelen! 🕹

Maar eerst moet je wat fundamenten leren kennen. Ga je de uitdaging aan??

Wat is een computerprogramma?

Een programma is niks meer dan een tekst die aan bepaalde regels voldoet. De computer verwerkt deze instructies en voert ze uit.

Iemand die dit soort teksten schrijft, heet een software developer of een programmeur. En het soort tekst dat deze persoon schrijft, die op de computer wordt ingeladen, heeft een speciale naam: programmeercode (meestal afgekort tot code of broncode, of op zijn Engels source code). In deze cursus, wordt deze soort tekst steeds code genoemd.

Het geheel van regels dat beschrijft hoe een programma moet worden geschreven, is gedefinieerd als een programmeertaal. Er zijn in de evolutie van de informatica ("informatiekunde" of "computerkunde") diverse talen ontstaan. In deze cursus gebruiken we een taal genaamd Elm.

Aan het begin van iedere les wordt een nieuw onderwerp geintroduceerd en daarna volgen er opdrachten. Het is erg belangrijk de opdrachten te maken. Ze vormen het belangrijkste (en leukste) deel van de hele les. Sommige opdrachten zijn makkelijk, andere uitdagender. Het is normaal er wat moeite mee te hebben; dat is onderdeel van het leerproces.

Exploreer, experimenteer en wees niet bang fouten te maken. Het ergste dat kan gebeuren is dat je een foutmelding krijgt. 😆

Maar, hoe schrijf je dan een programma?

Hieronder hebben we ons eerste voorbeeld, het beroemde "Hallo Wereld!", dat niks anders doet dan een tekst op het scherm tonen.

import Html exposing (text)

main =
  text "Hallo Wereld!"

Wat moeten we doen om deze code uit te voeren? In deze cursus kun je dat in je internetbrowser doen! 😄

Open een nieuw tabblad en ga naar de volgende site: https://elm-lang.org/try. Bewaar de link, want die ga je in deze cursus steeds gebruiken. Kopieer daarna de hele code hierboven en plak het in het linkerdeel van de site.

Druk op Rebuild ("Opnieuw bouwen" in het engels) midden onderin het scherm en je zult het resultaat van het uitvoeren aan de rechterkant op je scherm zien, zoals in het plaatje hieronder:

Ons eerste stuk code

Gefeliciteerd! Je hebt je eerste programma gemaakt in de programmeertaal Elm! 🎉

En nu?

In de volgende lessen leer je figuren te tekenen op het scherm en leer je een aantal fundamenten van het programmeren kennen. Je leert wat functies zijn, parameters en nog veel meer!

Ga nu door met Les 2, veel succes!

Les 2 - Tekenen op het scherm.

Wat leer je in deze les?

  1. Wat is een import?
  2. Teken een bol.
  3. Wat is main?
  4. Wat is picture?
    4.1 Maar wat is een lijst?
  5. Wat is circle?

1- Wat is een import?

De eerste regel van alle computerprogramma's die we in deze cursus zullen schrijven, zal zijn:

import Playground exposing (..)

Het woord import betekent importeren (binnenbrengen) in het Engels. Deze regel is nodig omdat het je toegang geeft tot de codes die zijn gedefinieerd in Playground. In les 5 wordt dit meer in detail uitgelegd, maar zonder dat zou het niet mogelijk zijn om op het scherm te tekenen. Daarom beginnen alle computerprogramma's in deze cursus met deze regel.

2- Teken een rondje.

Tijd om te programmeren!
Zullen we een cirkel op het scherm tekenen?
Open de website opnieuw https://elm-lang.org/try in een ander tabblad van jouw browser en plak de onderstaande code in het linkerscherm.

import Playground exposing (..)

main =
  picture
    [ circle green 100 ]

Druk vervolgens op Rebuild en bekijk het resultaat. Lijkt het op onderstaande afbeelding?

Een cirkel tekenen

Maar wat gebeurt er eigenlijk?!

3- Wat is main?

Het woord main betekent voornaamst, of hoofdzaak in het Engels. Hiermee vertel je de computer waar je programma begint.
Dus al jouw programma's moeten deze regel hebben:

main =

En al het andere staat voor wat je wilt dat de de computer voor jou doet.

🚨 Let op: er mag géén spatie staan vóór het woord main.

4- Wat is picture?

Het woord picture betekent tekening in het Engels. Daarmee geef je de computer aan dat je iets op het scherm wilt tekenen.
Een lijst (weergegeven door de symbolen [ en ]) van geometrische vormen, vormt de tekening (picture). In dit voorbeeld bestaat de tekening uit slechts één geometrische vorm: een cirkel.

4.1- Maar wat is een lijst?

Als ik naar de supermarkt ga, schrijf ik een lijst op van producten die ik wil kopen. Zoiets als:

  • 5Kg rijst
  • 1Kg bonen
  • 3 grote aardappelen
  • 2 mango's

Het begrip 'lijst' in programmeren lijkt er sterk op: het is gewoon een structuur om een reeks van gegevens te ordenen en met elkaar in verband te brengen.

Om in Elm bijvoorbeeld de verzameling van getallen tussen nul en tien weer te geven, schrijven we

[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

Een lijst kan heel lang zijn. Hij kan ook maar één element bevatten of het kan zelfs leeg zijn!

Andere voorbeelden van lijsten:

Een lege lijst:

[]

Een lijst met één element:

[ 1 ]

Een lijst met 3 elementen:

[ 8, 13, 311839 ]

Nog een lijst met 3 elementen:

["Aardappel", "Wortel", "Pompoen"... ]

Tijdens deze cursus zullen we veel lijsten maken!

5- Wat is circle?

Een lijst van geometrische vormen kan cirkels, rechthoeken, zeshoeken en vele andere vormen hebben. In het vorige voorbeeld werd een cirkel (circle in het Engels) gebruikt. Om een cirkel te tekenen moet je 2 parameters invoeren:
De eerste parameter staat voor de kleur. In het voorbeeld werd de kleur groen (green) gebruikt.
De tweede parameter geeft de diameter van de cirkel aan. In dit geval is het 100.

En nu?

Nu is het tijd om aan de slag te gaan en een beetje te oefenen!

Ga naar Les 2 opdrachten en veel succes met oefenen!

Les 2: Opdrachten

Om dat wat je leert te onthouden, moet je oefenen.

In dit gedeelte bieden wij jou enkele opdrachten aan om zelf te proberen. Het is zeer belangrijk dat je ze probeert op te lossen voordat je de antwoorden ziet!

Open eerst het volgende adres in een ander tabblad in jouw browser: https://elm-lang.org/try.

Kopieer en voer dezelfde code uit die we in les 2 zagen:

import Playground exposing (..)

main =
  picture
    [ circle green 100 ]

Probeer nu de code te veranderen om onderstaande opdrachten te kunnen maken. En, wees niet bang om een fout te maken!
Probeer het eens, testen, onderzoeken, fouten maken. Het ergste dat kan gebeuren is dat je een foutmelding krijgt

OPDRACHT 1: De grootte van een cirkel veranderen.

Verander de waarde van de cirkelgrootte in een willekeurig getal.
Het kan een kleine waarde zijn zoals 1 of 2, of een grote waarde zoals 999999999.
Klik dan op Rebuild en zie het resultaat!

OPDRACHT 2: De kleur van de cirkel veranderen.

Verander de kleurwaarde van de cirkel in de kleur van jouw voorkeur.
Vergeet niet dat je de naam van de kleuren in het Engels moet schrijven. Als je moeite hebt met Engels, kijk dan hieronder op de lijst met beschikbare kleuren.

Lijst met kleuren:

red, orange, yellow, green, blue, purple, brown, lightRed, lightOrange, lightYellow, lightGreen, lightBlue, lightPurple, lightBrown, darkRed, darkOrange, darkYellow, darkGreen, darkBlue, darkPurple, darkBrown, white, lightGrey, grey, darkGrey, lightCharcoal, charcoal, darkCharcoal, black, lightGray, gray and darkGray.

OPDRACHT 3 (uitdagend): Maak 2 cirkels, de één binnen de ander.

Onze tekening (picture) heeft momenteel slechts één cirkel. Probeer een tweede cirkel van een andere kleur en een beetje kleiner dan de eerste toe te voegen.

👩‍🏫 Tips:

  • Vergeet niet dat de symbolen [ en ] een lijst aangeven. Dus de tweede cirkel moet binnen deze symbolen vallen.
  • Zie de lijst als vergelijkbaar met een boodschappenlijstje. Maar in plaats van voedsel, bevat onze lijst geometrische vormen.
  • Gebruik een komma om aan te geven dat je een tweede cirkel maakt binnen de lijst van geometrische vormen.
  • Let op de volgorde! Als de kleinste cirkel voor de grootste in de lijst staat, komt hij achter de grootste te staan en kun je hem niet zien.

OPDRACHT 4 (uitdagend): Maak 4 cirkels, de één binnen de ander.

Vergelijkbaar met de vorige opdracht, maar deze keer moeten er 4 cirkels zijn: de ene binnen de andere, met verschillende maten en kleuren.

En nu?

Heb je alle oefeningen kunnen doen? Had je moeite met een van hen?

Ga naar antwoorden van de opdrachten om de oplossing te zien.

Les 2: Antwoorden van de opdrachten

Open het volgende adres in een ander tabblad in uw browser: htts://elm-lang.org/try.

Kopieer hieronder het antwoord op elke oefening en probeer de code te begrijpen voordat je op Rebuild drukt.

OPDRACHT 1 (eenvoudig): De grootte van een cirkel veranderen.

Verander de waarde van de cirkelgrootte in een willekeurig getal.
Het kan een kleine waarde zijn zoals 1 of 2, of een grote waarde zoals 999999999.
Klik dan op Rebuild en zie het resultaat!

Antwoord

Deze opdracht was best gemakkelijk, nietwaar?! Het enige dat je moest doen, was de waarde van de grootte van de cirkel veranderen.

import Playground exposing (..)

main =
  picture
    [ circle green 5 ]

OPDRACHT 2 (eenvoudig): De kleur van de cirkel veranderen.

Verander de kleurwaarde van de cirkel in de kleur van jouw voorkeur.
Vergeet niet dat je de naam van de kleuren in het Engels moet schrijven. Als je moeite hebt met Engels, kijk dan hieronder op de lijst met beschikbare kleuren.

Lijst met kleuren:

red, orange, yellow, green, blue, purple, brown, lightRed, lightOrange, lightYellow, lightGreen, lightBlue, lightPurple, lightBrown, darkRed, darkOrange, darkYellow, darkGreen, darkBlue, darkPurple, darkBrown, white, lightGrey, grey, darkGrey, lightCharcoal, charcoal, darkCharcoal, black, lightGray, gray and darkGray.

Antwoord

Deze was ook vrij gemakkelijk. Je hoefde alleen maar de eerste parameter van de cirkel (circle) naar een andere kleur te wijzigen.

import Playground exposing (..)

main =
  picture
    [ circle red 5 ]

OPDRACHT 3 (uitdagend): Maak 2 cirkels, de één binnen de ander.

Onze tekening (picture) heeft momenteel slechts één cirkel. Probeer een tweede cirkel van een andere kleur en een beetje kleiner dan de eerste toe te voegen.

👩‍🏫 Tips:

  • Vergeet niet dat de symbolen [ en ] een lijst aangeven. Dus de tweede cirkel moet binnen deze symbolen vallen.
  • Zie de lijst als vergelijkbaar met een boodschappenlijstje. Maar in plaats van voedsel, bevat onze lijst geometrische vormen.
  • Gebruik een komma om aan te geven dat je een tweede cirkel maakt binnen de lijst van geometrische vormen.
  • Let op de volgorde! Als de kleinste cirkel voor de grootste in de lijst staat, komt hij achter de grootste te staan en kun je hem niet zien. Als je meer dan één geometrische figuur wilt tekenen, scheidt dan elk figuur met een komma. In het voorbeeld hieronder hebben we een rode cirkel binnen een groene cirkel geplaatst.
    Vergeet niet dat de symbolen [ en ] een lijst aangeven.

Antwoord

import Playground exposing (..)

main =
  picture
    [ circle green 100
    , circule red 50
    ]

Je moet bovenstaande code kunnen lezen en als volgt interpreteren:
Ons programma (main) is gedefinieerd als een tekening (picture) met een lijst (aangegeven door de symbolen [ en ]) van twee figuren die in dit geval twee cirkels zijn.

OPDRACHT 4 (uitdagend): Maak 4 cirkels, de één binnen de ander.

Vergelijkbaar met de vorige opdracht, maar deze keer moeten er 4 cirkels zijn: de ene binnen de andere, met verschillende maten en kleuren.

Antwoord

Als je moeite had met oefeningen 3 en 4, waarom probeer je dan nu niet oefening 4 te doen voordat je het antwoord ziet, en nu je het antwoord op oefening 3 hebt gezien?
Zie het antwoord hieronder.

Voordat je de onderstaande code uitvoert, kun je je voorstellen wat het op het scherm zal tekenen?

import Playground exposing (..)

main =
  picture
    [ circle black 200
    , circle green 150
    , circle yellow 100
    , circle red 50
    ]

En nu?

Ga nu door met Les 3, veel succes!

Les 3 - Elementen positioneren.

Wat leer je in deze les?

  1. Hoe teken je andere geometrische figuren.
  2. Een geometrische vorm positioneren.
    2.1 De vorm verplaatsen.
    2.2 De vorm draaien.

1 - Hoe teken je andere geometrische figuren.

Naast cirkels kunnen we ook tekenen:

  • Driehoeken (triangle)
  • Vierkanten (square)
  • Rechthoeken (rectangle)
  • Ovalen (oval)
  • Vijfhoeken (pentagon)
  • Zeshoeken (hexagon)
  • Octagons (octagon)
  • Polygonen (polygon)

Naast het tekenen van statische afbeeldingen kunnen we ook animaties maken! Maar dit wordt in een andere les behandeld. 😉
Vandaag gaan we leren hoe we driehoeken, vierkanten en rechthoeken moeten tekenen.

Eerst gaan we een programma maken dat een vierkant in een cirkel tekent, een driehoek binnen dit vierkant en tenslotte een kleine rechthoek.

Open nogmaals een tabblad met de site https://elm-lang.org/try in jouw browser. Plak de onderstaande code en druk op Rebuild om het resultaat te zien..

import Playground exposing (..)

main =
  picture
    [ circle blue 200
    , square green 250
    , triangle red 120
    , rectangle yellow 70 30
    ]

Merk op dat in het geval van de cirkel de tweede parameter de grootte van de straal is. Voor het vierkant is de tweede parameter de lengte van de zijden (denk eraan dat de zijden van een vierkant altijd even lang zijn, dus we hoeven maar één getal door te geven).

In het geval van de driehoek, wordt een driehoek gelijkzijdig getekend (alle zijden even lang). Maar, het getal in de tweede parameter is niet de lengte van de zijden, maar de radius (straal), dat wil zeggen de afstand tussen het middelpunt van de driehoek en de drie punten die de driehoek vormen (vergelijkbaar met de straal van een cirkel).
Twijfel je? Verander de grootte van de driehoek maar eens en maak deze gelijk als die van de cirkel. Kijk eens wat er gebeurt?

Merk ook op dat de rechthoek, naast de kleur, twee numerieke parameters heeft.
Zoals je misschien al geraden hebt, staat het eerste getal voor de breedte van de rechthoek en het tweede getal voor de hoogte ervan.

2 - Een geometrische vorm positioneren.

2.1 - Het verplaatsen van de vorm.

Je hebt misschien gemerkt dat elke geometrische vorm die tot nu toe getekend is, precies in het midden van het scherm verschijnt, toch? Op deze manier is het moeilijk om nog leukere dingen te tekenen, zoals een boom of een auto. Om complexe tekeningen te maken, moeten we de elementen positioneren op het scherm. Om dat te doen, plaats je net nadat je een geometrische vorm hebt opgegeven, het |> symbool. Dit geeft aan dat we een transformatie willen toepassen (iets met de vorm willen doen). Vervolgens kun je de computer vertellen dat je de vorm wilt verplaatsen met behulp van de move functie.

Om iets te verplaatsen, moet je twee parameters opgeven: de waarde van de verplaatsing op de x-as (het eerste getal) en de waarde van de verplaatsing op de y-as (het tweede getal).
De waarde op de x-as zal de vorm rechts van het midden van het scherm plaatsen (als de waarde positief is) of links van het midden (als de waarde negatief is).
De waarde op de y-as zal de vorm omlaag plaatsen (als de waarde negatief is) of omhoog (als de waarde positief is).

🚨 Let op: het punt 0,0 ligt precies in de het midden van het scherm. Dit is precies het punt vanaf waar we de geometrische vormen verplaatsen.

In het onderstaande voorbeeld zijn twee cirkels getekend, de ene naast de andere. Kijk goed naar de code en probeer het te begrijpen. Daarna kopieer je de code naar een ander tabblad van je browser en klik op Rebuild om het resultaat te bekijken.

import Playground exposing (..)

main =
 picture
   [ circle blue 100
       |> move -100 0
   , circle red 100
       |> move 100 0
   ]

Probeer de waarden te veranderen en bekijk het resultaat.

2.2 De geometrische vorm draaien.

Naast het bewegen langs de x- en y-assen kunnen we vormen ook roteren.
We kunnen de driehoek bijvoorbeeld een beetje scheef maken. Dit biedt ons meer vrijheid bij het tekenen.
Het roteren van een vorm lijkt heel erg op wat we deden om een vorm te verplaatsen. We gebruiken het symbool |> gevolgd door het woord rotate.

De rotate heeft maar één parameter nodig. Dit betreft een getal tussen 0 en 360 dat staat voor de graad van de hoek. Deze waarde kan ook negatief zijn.
Positieve waarden draaien het figuur TEGEN de klok in. Negatieve waarden met de klok mee.
Kun jij onderstaande code lezen? Probeer je voor te stellen wat er getekend gaat worden..

Kopieer de code in het andere tabblad van je browser en klik op Rebuild om het resultaat te zien.

import Playground exposing (..)

main =
  picture
   [ triangle green 100
       |> move -100 0
       |> rotate 45
   , triangle red 100
       |> move 100 0
       |> rotate -45
   ]

Makkelijk, hè? Probeer de getallen te veranderen en kijk wat er gebeurt.

En nu?

Nu is het tijd om aan de slag te gaan en nog meer te oefenen!

Ga naar Les 3 opdrachten en veel succes met oefenen!

Les 3: Opdrachten

Om dat wat je leert te onthouden, moet je oefenen.

In dit gedeelte bieden wij jou enkele opdrachten aan om zelf te proberen. Het is zeer belangrijk dat je ze probeert op te lossen voordat je de antwoorden ziet!

Open eerst het volgende adres in een ander tabblad in jouw browser: htts://elm-lang.org/try.

OPDRACHT 1: Teken een auto.

Gebruik een rechthoek die de auto moet voorstellen en twee cirkels die zijn wielen moeten voorstellen.
Gebruik de move instructie om de wielen te plaatsen.

OPDRACHT 2: Teken een boom.

Gebruik een bruine rechthoek die de stam moet voorstellen en een groene cirkel die de bladeren moeten voorstellen.

OPDRACHT 3 (uitdagend): Teken een ster.

Gebruik driehoeken om een 6-puntige ster te tekenen.

OPDRACHT 4 (uitdagend): Teken een bus.

Probeer ramen te plaatsen, de voorkant van de bus en al het andere dat je maar wilt! Er is geen fout antwoord. Gebruik je fantasie!

En nu?

Heb je alle oefeningen kunnen doen? Had je moeite met een van hen?

Ga naar antwoorden van de opdrachten om de oplossing te zien.

Les 3: Antwoorden van de opdrachten

OPDRACHT 1 (eenvoudig): Teken een auto.

Gebruik een rechthoek die de auto moet voorstellen en twee cirkels die zijn wielen moeten voorstellen.
Gebruik de move instructie om de wielen te plaatsen.

Antwoord

import Playground exposing (..)

main =
  picture
    [ rectangle darkGreen 450 150
    , circle darkRed 60
        |> move -100 -100
    , circle darkRed 60
        |> move 100 -100
    ]

OPDRACHT 2 (eenvoudig): Teken een boom.

Gebruik een bruine rechthoek die de stam moet voorstellen en een groene cirkel die de bladeren moeten voorstellen.

Antwoord

import Playground exposing (..)

main =
  picture
    [ rectangle darkBrown 60 250
        |> move 0 -150
    , circle green 150
        |> move 0 50
    ]

OPDRACHT 3 (uitdagend): Teken een ster.

Gebruik driehoeken om een 6-puntige ster te tekenen.

Antwoord

import Playground exposing (..)

main =
   picture
     [ triangle blue 150
     , triangle blue 150
         |> rotate 180
     ]

OPDRACHT 4 (uitdagend): Teken een bus.

Probeer ramen te plaatsen, de voorkant van de bus en al het andere dat je maar wilt! Er is geen fout antwoord. Gebruik je fantasie!

Antwoord

import Playground exposing (..)

main =
   picture
     [ circle black 40
         |> move -120 -90
     , circle gray 20
         |> move -120 -90
     , circle black 40
         |> move 120 -90
     , circle gray 20
         |> move 120 -90
     , rectangle yellow 500 180
     , square white 80
         |> move -190 0
     , square white 50
         |> move -100 0
     , square white 50
         |> move 0 0
     , square white 50
         |> move 100 0
     , square white 50
         |> move 200 0
     , square white 50
         |> move 200 0
     , rectangle black 15 10
         |> move -250 -85
     , square darkYellow 10
         |> move -250 -60
     , rectangle yellow 20 3
         |> move -270 -60
     , rectangle yellow 20 3
         |> move -270 -73
         |> rotate 40
     , rectangle yellow 20 3
         |> move -270 -47
         |> rotate -40
     ]

En nu?

Ga nu door met Les 4, veel succes!

Les 4 - Functies aanmaken.

Deze les is meer theoretisch. Maar, wat je vandaag zult leren is essentieel om alles te kunnen begrijpen van de rest van de lessen. Dus, laten we beginnen met begrijpen wat een functie eigenlijk is?!

Wat leer je in deze les?

  1. Wat is een functie?
    1.1 Wiskundige functies
    1.2 Functies bij het programmeren
  2. Hoe maak je je eigen functies?
  3. Voordelen van het maken van functies

1 - Wat is een functie?

Naarmate je code groeit, wordt het steeds moeilijker en lastiger om te (over)zien wat alle code-onderdelen betekenen.
Neem als voorbeeld de code waarmee een bus getekend werd in de opdracht in les 2. Hoe meer details je in de tekening stopte, hoe groter main werd.
Maar wat is dat main eigenlijk? Het is een functie.
In de programmeertaal Elm geldt voor bijna alles wat we schrijven dat het functies zijn! Dat komt omdat het een taal is die het functionele paradigma volgt. Er zijn verschillende soorten talen (paradigma's): Object-georiënteerd, Imperatief, Logisch en ook Functioneel. Elke paradigma heeft zijn voor- en nadelen. In deze cursus leer je het functionele paradigma.

1.1 - Wiskundige functies

Je hebt waarschijnlijk al gehoord over functies in de wiskundelessen op school. Dingen zoals:

x = y + 2

Maar wat betekent x = y + 2? Eigenlijk betekent het dat overal waar je het symbool x hebt, we dit kunnen vervangen door y + 2, en vice versa. Als we bijvoorbeeld de volgende reeks vergelijkingen hebben:

x = 5
y = 10
z = x + y

Om de waarde van z te vinden, vervangen we de waarde van y en dan die van x.

De oorspronkelijke z-functie is:

z = x + y

We kunnen bijvoorbeeld eerst de waarde van y vervangen:

z = x + 10

En dan de waarde van x:

z = 5 + 10

We komen dan tot de conclusie dat 15 de enige mogelijke waarde is voor z.

1.2 Functies bij het programmeren

Bij programmeren (vooral in functionele talen, zoals Elm of Haskell), is het begrip 'functie' zeer vergelijkbaar.
In ons vorige voorbeeld hing de waarde van z af van y en x.
We kunnen iets soortgelijks doen met onze functie main, waardoor het afhankelijk wordt van andere, kleinere en eenvoudigere functies. Dit zorgt ervoor dat de code veel gemakkelijker te begrijpen en te veranderen is.

Vond je het erg verwarrend? Maak je geen zorgen, het zal gemakkelijker te begrijpen zijn met het voorbeeld hieronder.

2 - Hoe maak je je eigen functies?

Laten we beginnen met een functie die een boom tekent en splits het dan op in verschillende kleinere functies.

Originele main functie:

import Playground exposing (..)

main =
  picture
    [ rectangle darkBrown 60 250
        |> move 0 -150
    , circle green 150
        |> move 0 50
    ]

We kunnen onze boom opsplitsen door de definitie van zijn bladeren in een andere functie te plaatsen:

import Playground exposing (..)

main =
  picture
      [ rectangle darkBrown 60 250
        |> move 0 -150
    , bladeren
    ]

bladeren =
  circle green 150
    |> move 0 50

Deze nieuwe code is gelijk aan de vorige. We hebben er alleen net een deel uit gehaald voor een andere functie.

Om een nieuwe functie te definiëren, geef je deze gewoon een naam (elk woord kan, in dit geval bladeren) en dan zet je het symbool =, op dezelfde manier als bij wiskunde.
Alles na het gelijkheidsteken zal deel uitmaken van onze nieuwe functie.

Is de functie eenmaal gedefinieerd, dan kun je het op een of meer plaatsen in de code gebruiken.

Om een functie te gebruiken, schrijf je gewoon zijn naam, net als in wiskunde. In het vorige voorbeeld wordt de functie bladeren gebruikt als onderdeel van de functie main.

Het is zeer belangrijk om dit concept te begrijpen. Neem een moment om rustig de code hierboven te bekijken om er zeker van te zijn dat je het begrijpt.

🚨 Let op: in Elm is de volgorde waarin de functies worden gedefinieerd niet relevant. Je kunt de main functie eerst instellen en dan de bladeren functie of eerst de functie bladeren en dan main.

Nu kun je ook de stam van de boom scheiden in een andere functie:

import Playground exposing (..)

main =
  picture
    [ stam
    , bladeren
    ]

stam =
  rectangle darkBrown 60 250
    |> move 0 -150

bladeren =
  circle green 150
    |> move 0 50

Probeer je voor te stellen dat het woord bladeren, in de functie main, wordt vervangen door de inhoud die gedefinieerd is in de functie bladeren. En hetzelfde voor het woord stam.

En je kunt een stap verder gaan, als je wilt, en een boom functie maken:

import Playground exposing (..)

main =
  picture
    boom

boom =
  [ stam
  , bladeren
  ]

stam =
  rectangle darkBrown 60 250
    |> move 0 -150

bladeren =
  circle green 150
    |> move 0 50

In dit laatste voorbeeld geeft de functie boom een lijst weer met geometrische vormen die een stam en een blad bevatten.

3 - Voordelen van het maken van functies

Er zitten twee grote voordelen aan het splitsen van de codes in verschillende functies.
Het eerste voordeel is dat het gemakkelijker wordt om je intenties uit te drukken. Kijk en vergelijk hoe de functie main omschreven is in het eerste en laatste voorbeeld. Deze laatste manier geeft veel duidelijker aan wat je probeert te tekenen.

Een ander groot voordeel van op deze manier programmeren, is dat de stam en bladeren nu ontkoppeld zijn.
Je kunt bijvoorbeeld de functie stam opnieuw gebruiken om andere soorten bomen te tekenen. Of je kunt een nieuw stam type maken en de bladeren opnieuw gebruiken.

👩🏫 Hint: Bij programmeren is dit een ander zeer belangrijk begrip: hergebruik van code.

En nu?

Nu is het tijd om aan de slag te gaan en nog meer te oefenen!

Ga naar Les 4 opdrachten en veel succes met oefenen!

Les 4: Opdrachten

OPDRACHT 1 (eenvoudig): Maak de tekening van de auto af

Hieronder staat een onvolledig stuk code voor het tekenen van een auto.
Schrijf de drie ontbrekende functies om de auto te tekenen.

import Playground exposing (..)

main =
  picture
    auto

auto =
  [ carrosserie
  , voorwiel
  , achterwiel
  ]

OPDRACHT 2 (eenvoudig): Teken fruit aan de boom

De onderstaande code stelt een boom zonder fruit voor.

import Playground exposing (..)

main =
  picture
    boom

boom =
  [ stam
  , bladeren
  ]

stam =
  rectangle darkBrown 60 250
    |> move 0 -150

bladeren =
  circle green 150
    |> move 0 50

Voeg wat meer functies toe om 4 vruchten aan de boom weer te geven. De vruchten kunnen eenvoudige rode bollen zijn.

👩‍🏫 Tip: De gemakkelijkste manier is om 4 nieuwe functies te maken: fruit1, fruit2, fruit3 en fruit4. In de volgende les zullen we een betere manier zien om dit soort problemen op te lossen.

En nu?

Is het je gelukt om alle oefeningen te doen? Had je moeite met een van hen?

Ga naar antwoorden van de opdrachten om de oplossing te zien.

Les 4: Antwoorden van de opdrachten

OPDRACHT 1 (eenvoudig): Maak de tekening van de auto af

Schrijf de drie ontbrekende functies om de auto te tekenen.

Antwoord

import Playground exposing (..)

main =
  picture
    auto

auto =
  [ carrosserie
  , voorwiel
  , achterwiel
  ]

carrosserie =
  rectangle darkGreen 450 150

voorwiel =
  circle darkRed 60
    |> move -100 -100

achterwiel =
  circle darkRed 60
     |> move 100 -100

OPDRACHT 2 (eenvoudig): Teken fruit aan de boom

Antwoord

import Playground exposing (..)

main =
  picture
    [ stam
    , bladeren
    , fruit1
    , fruit2
    , fruit3
    , fruit4
    ]

stam =
  rectangle darkBrown 60 250
    |> move 0 -150

bladeren =
  circle green 150
    |> move 0 50

fruit1 =
  circle red 20
    |> move 50 50

fruit2 =
  circle red 20
    |> move -40 20

fruit3 =
  circle red 20
    |> move -50 100

fruit4 =
  circle red 20
    |> move 40 130

Makkelijk, niet? Maar misschien vraag je je af: Wanneer moeten we een nieuwe functie creëren? Begrijpen wanneer het een goed idee is om een functie in verschillende kleine functies te "breken", is iets wat we leren door ervaring.
Over het algemeen is het een goed idee om een ​​functie in kleinere functies te "breken" wanneer de code verwarrend wordt en het moeilijk wordt om te onderscheiden welk deel van de code wat doet.

Binnenkort zullen we leren hoe we ditzelfde soort probleem op een elegantere manier kunnen oplossen: het is beter om de code opnieuw te gebruiken.

En nu?

Ga nu door met Les 5, veel succes!

Les 5 - Parameters doorgeven.

Wat leer je in deze les??

  1. Parameters doorgeven.
  2. Software bibliotheken (libraries).
  3. Hoe maak je je eigen functies met parameters?
  4. Parameters op naam.

1 - Parameters doorgeven

Zoals je in andere lessen hebt kunnen lezen: bijna alles in Elm zijn functies. Dit omvat ook de woorden circle, triangle, square, en andere.
Als we bijvoorbeeld typen:

circle yellow 100

circle is de naam van een functie die twee parameters heeft: een kleur en een diameter. Dus, wanneer we een cirkel maken, moeten we 2 argumenten (in volgorde) doorgeven.

Deze circle-functie is gedefinieerd binnen Playground, dat een bibliotheek van Elm is.

2 - Software bibliotheken (libraries)

Bij het ontwikkelen van software kom je verschillende problemen tegen die andere mensen al eerder zijn tegengekomen en hebben opgelost. In deze gevallen kun je reeds bestaande oplossingen hergebruiken, waardoor jouw werk gemakkelijker wordt.
Bijvoorbeeld: het tekenen van een element op het scherm is een terugkerende taak en wordt gebruikt voor verschillende programma's. Daarom heeft een andere ontwikkelaar dit probleem al opgelost en kun je zijn of haar werk hergebruiken om jouw programma's te ontwikkelen.
Deze set code, geschreven door andere mensen, noemen we bibliotheken (libraries). Tot nu toe hebben we één bibliotheek gebruikt, de Playground. Deze bevat functies die we kunnen gebruiken om figuren op het scherm te tekenen en te animeren.

3 - Hoe maak je je eigen functies met parameters?

De functies die je definieert in jouw codes, kunnen ook parameters hebben.
Kijk naar het onderstaande voorbeeld en probeer te begrijpen wat er gebeurt. Geef speciaal aandacht aan de definitie van de functie fruit.

import Playground exposing (..)

main =
  picture
    [ stam
    , bladeren
    , fruit 50 50
    , fruit -40 20
    , fruit -50 100
    , fruit 40 130
    ]

stam =
  rectangle darkBrown 60 250
    |> move 0 -150

bladeren =
  circle green 150
    |> move 0 50

fruit x y =
  circle red 20
    |> move x y

Het eindresultaat is hetzelfde als van de uitdaging uit de vorige les: een boom met fruit. Maar de code is kleiner en eenvoudiger.
Bovendien is het nu gemakkelijker om nieuw fruit aan je boom te tekenen!

De functie fruit gedefinieerd in de bovenstaande code is nu afhankelijk van twee parameters: x en y. Dit betekent dat wanneer je deze functie gebruikt, je twee argumenten (waarden) moet doorgeven.

👩🏫 Tip: In sommige programmeertalen moeten we expliciet specificeren welk type elke variabele is. In Elm is dit niet nodig. De taal is slim genoeg om uit te zoeken dat x en y, in dit geval, getallen zijn.

Merk ook op dat we binnen de fruit functie de waarden van x en y doorgeven aan een andere functie op de volgende regel:

move x y

Dat wil zeggen, move is ook een functie met parameters. In feite verwachten de meeste functies in Elm ten minste 1 parameter.

4 - Parameters op naam

Hoewel de namen van de parameters in het vorige voorbeeld uit slechts één karakter (x en y) bestaan, kun je langere en meer betekenisvolle namen kiezen. Een voorbeeld hiervan zou kunnen zijn:

fruit positieX positieY =
 circle red 20
   |> move positieX positieY

Maar in dit specifieke geval waren de vorige namen (x en y) misschien al duidelijk genoeg.

🚨 Belangrijk: goede namen geven aan onze variabelen en functies is een van de moeilijkste taken in het programmeren! Dus, denk lang en goed na voordat je een naam kiest en verander hem, indien nodig, naar een meer beschrijvende naam wanneer je vindt dat de code verwarrend wordt.

En nu?

Nu is het tijd om aan de slag te gaan en nog meer te oefenen!

Ga naar Les 5 opdrachten en veel succes met oefenen!

Les 5: Opdrachten

OPDRACHT 1 (eenvoudig): Maak meer fruit

import Playground exposing (..)

main =
  picture
    [ stam
    , bladeren
    , fruit 50 50
    , fruit -40 20
    , fruit -50 100
    , fruit 40 130
    ]

stam =
  rectangle darkBrown 60 250
    |> move 0 -150

bladeren =
  circle green 150
    |> move 0 50

fruit x y =
  circle red 20
    |> move x y

Wijzig de hierboven gedefinieerde code en maak nog drie vruchten in jouw boom.

OPDRACHT 2 (gemiddeld): Het formaat van de bladeren instelbaar maken

Maak in dezelfde code als in opdracht 1 een parameter voor de bladeren functie om de grootte van de cirkel weer te geven.
Probeer de grootte van de cirkel te vergroten en te verkleinen.

👩🏫 Tip: Als je een waarde doorgeeft die te groot of te klein is, zal jouw boom er waarschijnlijk vreemd uitzien, omdat de bladeren de stam niet raken. Maak je hier voorlopig geen zorgen over.

En nu?

Is het je gelukt om alle oefeningen te doen? Had je moeite met een van hen?

Ga naar antwoorden van de opdrachten om de oplossing te zien.

Les 5: Antwoorden van de opdrachten

OPDRACHT 1 (eenvoudig): Maak meer fruit

Antwoord

Om deze opdracht op te lossen, hoefden we alleen maar de fruit functie nog een paar keer aan te roepen.

import Playground exposing (..)

main =
  picture
    [ stam
    , bladeren
    , fruit 50 50
    , fruit -40 20
    , fruit -50 100
    , fruit 40 130
    , fruit 10 10
    , fruit -10 -50
    , fruit 70 -40
    ]

stam =
  rectangle darkBrown 60 250
    |> move 0 -150

bladeren =
  circle green 150
    |> move 0 50

fruit x y =
  circle red 20
    |> move x y

Wat zou er gebeuren als we probeerden fruit buiten de boom te tekenen? Op dit moment hebben we geen manier om dat te blokkeren, maar in een echt systeem zouden we manieren moeten bedenken om dat te voorkomen.

OPDRACHT 2 (gemiddeld): Het formaat van de bladeren instelbaar maken

Maak in dezelfde code als in opdracht 1 een parameter voor de bladeren functie om de grootte van de cirkel weer te geven.
Probeer de grootte van de cirkel te vergroten en te verkleinen.

👩‍🏫 Tip: Als je een waarde doorgeeft die te groot of te klein is, zal jouw boom er waarschijnlijk vreemd uitzien, omdat de bladeren de stam niet raken. Maak je hier voorlopig geen zorgen over.

Antwoord

Om deze opdracht op te lossen, maken we een nieuwe parameter in de functie bladeren aan, genaamd diameter. Vervolgens geef je de waarde van deze variabele door bij het aanroepen van de circle functie.

import Playground exposing (..)

main =
  picture
    [ stam
    , bladeren 150
    , fruit 50 50
    , fruit -40 20
    , fruit -50 100
    , fruit 40 130
    , fruit 10 10
    , fruit -10 -50
    , fruit 70 -40
    ]

stam =
  rectangle darkBrown 60 250
    |> move 0 -150

bladeren diameter =
  circle green diameter
    |> move 0 50

fruit x y =
  circle red 20
    |> move x y

En nu?

Ga nu door met Les 6, veel succes!

Les 6 - Je eerste animatie

Deze les is meer theoretisch, omdat je de basisprincipes leert om animaties te kunnen maken.

Wat leer je in deze les?

  1. Hoe maak je een animatie.
    1.1. wat is de animation functie?
  2. Hoe draai je een figuur volgens de tijd.
    2.1 Een beter begrip krijgen van het beheersen van de tijd.

1- Hoe maak je een animatie

Aan het einde van deze les zul je in staat zijn om de volgende code te begrijpen:

import Playground exposing (..)

main =
  animation view

view time =
    [ triangle red 100
          |> rotate (spin 8 time)
    ]

Maar daarvoor moet je de animation functie kennen en ook weten hoe je de tijd moet beheersen.

1.1 - Wat is de animation functie?

animation is de naam van de functie die je moet activeren zodat de computer begrijpt dat de tekening die je wilt maken een geanimeerde afbeelding is.

De animation functie ontvangt een andere soort parameter dan wat je in de vorige lessen hebt geleerd: het verwacht dat een functie als argument wordt doorgegeven. Met andere woorden, het is een functie die een andere functie als parameter neemt.

🚨 Belangrijk: Dit roept vaak vragen op. In Elm (en ook JavaScript, Clojure en vele andere programmeertalen) is dit heel gebruikelijk: soms ontvangen functies "normale" waarden, zoals getallen of tekst, maar in andere gevallen ontvangt het een functie als argument. Dat wil zeggen, animation is een functie die een verwijzing naar een andere functie als argument neemt. 🤯

In het vorige voorbeeld krijgt de animation-functie als argument een verwijzing naar de view functie (view betekent in het Engels zicht of zien).
In Elm is dit meestal de standaardnaam voor de functie waar we definiëren wat er op het scherm komt.

Merk op dat de view functie een parameter krijgt met de naam time. (wat staat voor tijd).
Je kunt het elke andere naam geven, maar volgens afspraak is dit de naam die we gewoonlijk voor deze parameter gebruiken.

Via een animatie kun je je tekening na een tijdje veranderen. Daarom heb je deze time-parameter nodig om de huidige waarde van de tijd te kennen en dus aan te geven hoe de tekening moet worden weergegeven.

Zie het zo: stel je voor dat je een voetbal in een rechte lijn trapt. Als je weet in welke richting de bal gaat en wat de intensiteit van je trap is, hoe weet je dan de huidige positie van de bal? Het antwoord is: dat hangt ervan af! Het hangt af van de tijd. Nul seconden nadat je de bal schopt, raakt het nog steeds je voet. Een seconde later ben je een stukje verder weg. Met andere woorden, om uit te vinden waar je deze bal kan tekenen, is het nodig dat je de verstreken tijd weet vanaf het moment dat je er tegenaan trapte.

Terug naar de code. De functie view wordt meerdere keren (automatisch door de computer) uitgevoerd, en bij elke uitvoering zal de waarde van de variabele time anders zijn (met daarin de huidige tijd op het moment van die uitvoering).
Dit is wat er gebeurt als deze functie de eerste keer wordt opgeroepen: de driehoek wordt in één richting getekend en, als de waarde van de tijd (time) verandert, dan verandert ook de manier waarop onze driehoek wordt getekend.

Alle animaties in deze cursus worden gemaakt volgens deze zelfde logica: hun functies zullen de verstreken tijd nodig hebben (time) om het huidige moment van de animatie te kennen.

2. Hoe draai je een figuur volgens de tijd

Je hebt de rotate functie eerder gebruikt om een afbeelding te laten draaien. Zoals jij je misschien herinnert, krijgt de functie rotate een parameter met de hoek die je wilt draaien (een getal tussen -360 en 360). In het vorige voorbeeld is de waarde, die als parameter is doorgegeven, ddeze:

rotate (spin 8 time)

🚨 Belangrijk: De eerste opmerking die ik hier moet maken betreft het gebruik van haakjes. Als je deze haakjes verwijdert, zal je programma niet werken. Ze zijn belangrijk omdat ze de computer vertellen dat hij prioriteit moet geven aan het oplossen van alles binnen de haakjes om vervolgens het resultaat van deze bewerkingen te krijgen en de waarde van dit resultaat te gebruiken als argument voor de rotate functie.

Je kunt een verband leggen met wiskunde:

x = 2 * 1 + 1  
y = 2 * (1 + 1)  

In bovenstaand voorbeeld is x gelijk aan 3 en y gelijk aan 4.
Net als in de wiskunde zijn haakjes erg belangrijk bij het programmeren!

Terug naar het voorbeeld, eerst zal de waarde van spin 8 time worden bepaald en daarna zal die waarde gebruikt worden als een argument voor de rotate functie.

Spin betekent in het Engels draaien. Kun je je voorstellen wat het doet? Stop en denk hier even over na voordat je verder leest.

Nogmaals, zoals bijna alles in Elm, is spin een functie. En zoals je misschien hebt gemerkt, zijn er twee parameters nodig.
De eerste parameter is de periode. Deze geeft aan hoeveel seconden elke rotatie van het beeld moet nemen. Hoe lager de waarde, hoe sneller zal de rotatiesnelheid zijn. In het voorbeeld is deze waarde 8. Dit betekent dat onze driehoek 1 keer volledig ronddraait per elke 8 seconden.
De tweede parameter is de verstreken tijd. Hier zul je in het algemeen gewoon de waarde doorgeven die je al als argument doorkreeg in je functie. De computer controleert de tijd en geeft deze informatie door aan ons.

2.1 - Een beter begrip krijgen van het beheersen van de tijd

Je programma begint met de main functie. Maar wie activeert deze functie? De computer! Of meer technisch gesproken, de Elm runtime. Maar dat is slechts een detail. Wat je moet begrijpen is dat in dit voorbeeld de main functie als volgt is gedefinieerd:

main =
  animation view

Op een bepaald moment zal de animation -functie de view -functie activeren en, zoals we eerder hebben gezien, ontvangt de functie view een parameter die de tijd aangeeft (de time parameter):

view time =

Je hoeft je geen zorgen te maken over het beheersen van deze variabele, dat doet de computer voor je. Je moet alleen weten wie deze waarde zal ontvangen en wat hij vertegenwoordigt.

Als deze hele uitleg te verwarrend was, maak je dan geen zorgen! Alles wordt duidelijker als je oefent.

En nu?

Nu is het tijd om aan de slag te gaan en nog meer te oefenen!

Ga naar Les 6 opdrachten en veel succes met oefenen!

Les 6: Opdrachten

OPDRACHT 1 (eenvoudig): verander de snelheid

import Playground exposing (..)

main =
  animation view

view time =
    [ triangle red 100
          |> rotate (spin 8 time)
    ]

Verander de hierboven gedefinieerde code zodat de driehoek 2x sneller draait.
Verander het dan zodat het 2x langzamer draait.

OPDRACHT 2 (gemiddeld): meer geometrische vormen

Voeg in dezelfde code als in opdracht 1 een geel vierkant toe. Het vierkant moet 2x langzamer draaien dan de driehoek.

OPDRACHT 3 (uitdagend): met de klok meedraaien

Tot nu toe draaien onze animaties tegen de klok in (naar links). Verander de code van opdracht 2 zodat de driehoek met de klok mee draait (naar rechts) en het vierkant gewoon verder draait, tegen de klok in.

👩‍🏫 Tips:

  • Onthoud dat de functie rotate zowel positieve waarden als negatieve waarden kan ontvangen. Positieve waarden zorgen ervoor dat het figuur tegen de klok in wordt gedraaid en negatieve waarden zorgen ervoor dat het figuur met de klok mee draait.
  • Om tegen de klok in te draaien, moet het resultaat van de (spin 8 time) waarde negatief zijn.

En nu?

Is het je gelukt om alle oefeningen te doen? Had je moeite met een van hen?

Ga naar antwoorden van de opdrachten om de oplossing te zien.

Les 6: Antwoorden van de opdrachten

OPDRACHT 1 (eenvoudig): verander de snelheid

import Playground exposing (..)

main =
  animation view

view time =
    [ triangle red 100
          |> rotate (spin 8 time)
    ]

Verander de hierboven gedefinieerde code zodat de driehoek 2x sneller draait.
Verander het dan zodat het 2x langzamer draait.

Antwoord

Om de driehoek 2x sneller te laten draaien, halveer je de eerste parameter van de spin methode. Onthoud: de eerste parameter geeft aan hoeveel seconden het duurt om het figuur een volledige rotatie te laten maken. Dus, hoe korter de tijd, hoe sneller de draaisnelheid van het figuur is.

Code met 2x snellere rotatie:

import Playground exposing (..)

main =
  animation view

view time =
    [ triangle red 100
          |> rotate (spin 4 time)
    ]

Code met 2x langzamere rotatie:

import Playground exposing (..)

main =
  animation view

view time =
    [ triangle red 100
          |> rotate (spin 16 time)
    ]

OPDRACHT 2 (gemiddeld): meer geometrische vormen

Voeg in dezelfde code als in opdracht 1 een geel vierkant toe. Het vierkant moet 2x langzamer draaien dan de driehoek.

Onze view-functie definieert een lijst met geometrische vormen. Om een ​​vierkant aan de onderkant van de driehoek toe te voegen, hoef je alleen een square aan het begin van de lijst met geometrische vormen aan te brengen.
Omdat we willen dat het vierkant op halve snelheid draait, hebben we het volgende nodig: pas de transformatie via de functie rotate toe op een vergelijkbare manier als wat we met de driehoek hebben gedaan. Maar, we geven een grotere waarde door aan de spin functie, zodat het rotatie-interval groter is. Onthoud dat we in een lijst de elementen scheiden met een komma.

Antwoord

We gaan het antwoord in 2 delen splitsen. Laten we eerst het vierkant (square in het Engels) onderaan de afbeelding toevoegen, zonder animatie:

import Playground exposing (..)

main =
  animation view

view time =
    [ square yellow 300
    , triangle red 100
          |> rotate (spin 16 time)
    ]

Laten we nu de opdracht afmaken door ons vierkant te laten draaien:

import Playground exposing (..)

main =
  animation view

view time =
    [ square yellow 300
          |> rotate (spin 16 time)
    , triangle red 100
          |> rotate (spin 8 time)
    ]

OPDRACHT 3 (uitdagend): met de klok meedraaien

Tot nu toe draaien onze animaties tegen de klok in (naar links). Verander de code van opdracht 2 zodat de driehoek met de klok mee draait (naar rechts) en het vierkant verder gaat, draaiend tegen de klok in.

👩‍🏫 Tips:

  • Onthoud dat de functie rotate zowel positieve waarden als negatieve waarden kan ontvangen. Positieve waarden zorgen ervoor dat de afbeelding tegen de klok in wordt gedraaid en negatieve waarden zorgen ervoor dat de afbeelding met de klok mee draait.
  • Om tegen de klok in te draaien, moet het resultaat van de (spin 8 time) waarde negatief zijn.

Antwoord

Er zijn een paar manieren om dit probleem op te lossen. Misschien wel de eenvoudigste manier is uitgaan van de waarde 360 ​​(graden) en van deze waarde het resultaat van de bewerking (spin 8 time) aftrekken. Op deze manier zullen we, wanneer de waarde van de bewerking (spin 8 time) 0 is, dit resultaat transformeren naar 360. En als het 360 is, transformeren we het naar 0. Wanneer de waarde 180 is, maken we er -180 van, enzovoort.

Vergeet niet om nieuwe haakjes te plaatsen zodat de waarde eerst wordt berekend, anders krijg je een foutmelding.

import Playground exposing (..)

main =
  animation view

view time =
    [ square yellow 300
          |> rotate (spin 16 time)
    , triangle red 100
          |> rotate (360 - (spin 8 time))
    ]

En nu?

Ga nu door met Les 7, veel succes!

Les 7 - Golfbewegingen maken.

Wat leer je in deze les?

  1. Hoe animeer je een figuur met behulp van golfbewegingen.
    1.1 De wave functie.

1 - Hoe animeer je een figuur met behulp van golfbewegingen.

Laten we ons eerst eens herinneren hoe je een cirkel op het scherm tekent:

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue 100 ]

Stel je voor dat je een animatie wilt maken op deze figuur, waardoor hij gaat "pulseren" (vergroten en verkleinen van de diameter gedurende de tijd).

In bovenstaand voorbeeld is de diameter van de cirkel vastgesteld op 100. Om je doel ("pulseren") te bereiken, moet je ervoor zorgen dat de waarde van de diameter variabel kan zijn in plaats van vast, en gedurende de tijd kan veranderen (met behulp van die variabele time die we in les 6 zagen).

Een van de manieren om dit toe te passen (implementeren), is het gebruik van een functie genaamd wave (wat in het Engels golf betekent).

Open de https://elm-lang.org/try website opnieuw, kopieer en voer de volgende code uit.
Maak je voor nu geen zorgen over het interpreteren van alles. Bekijk gewoon het resultaat om het gedrag van deze functie beter te begrijpen.

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 50 100 7 time)
  ]

1.1 - De wave functie.

Zoals je misschien hebt gemerkt, wordt nu de functie wave aangeroepen en het resultaat van deze oproep bepaalt de grootte van de diameter van de cirkel.

De functie wave heeft 4 parameters. De eerste 2 parameters komen overeen met de minimale waarde en de maximale grootte van de diameter. De laatste twee parameters hangen samen met de snelheid waarmee deze waarde moet veranderen.
In dit voorbeeld geeft de waarde 7 aan hoeveel seconden de totale cyclus van de animatie duurt (dat is: het tijdsinterval dat moet verstrijken zodat de waarde varieert tussen het minimum en maximum en weer terug naar de minimumwaarde).
De time is gewoon de huidige tijd (die we als parameter krijgen in de view functie en die we dezelfde waarde doorgeven).

De waarde van time wordt automatisch bijgewerkt en de functie view wordt steeds opnieuw aangeroepen, elke keer met een andere waarde voor de variabele time. Maar je hoeft je daar geen zorgen over te maken: geef time gewoon door als een parameter aan de wave- functie en het zal weten wat de grootte van de cirkel op dit moment moet zijn.

Herlees nu de vorige code en probeer beter te begrijpen wat er gebeurt. Verander de waarden 50, 100 en 7 naar andere waarden en bekijk het resultaat. Maar voordat je de code uitvoert (via Rebuild), probeer je eerst voor te stellen hoe de animatie eruit zal zien.

En nu?

Nu is het tijd om aan de slag te gaan en nog meer te oefenen!

Ga naar Les 7 opdrachten en veel succes met oefenen!

Les 7: Opdrachten

OPDRACHT 1 (eenvoudig): Het veranderen van de snelheid

Lees onderstaande code en probeer je voor te stellen wat er op het scherm zal verschijnen. En, wat betekenen de waarden 10, 100 en 12?

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 10 100 12 time)
  ]

Verander nu bovenstaande code zodat de cirkel 4 keer zo snel pulseert.
Verander het dan zodat het 4 keer langzamer pulseert dan in de originele code.

OPDRACHT 2 (eenvoudig): Voeg een cirkel in een andere

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 10 100 10 time)
  ]

Neem in de bovenstaande code een nieuwe rode cirkel op die statisch/stil is. Deze nieuwe cirkel moet onder de blauwe cirkel liggen ​​en een diameter van 200 hebben.

OPDRACHT 3 (gemiddeld): Maak 2 cirkels die pulseren

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 20 80 2 time)
  ]

Wijzig de bovenstaande code om aan de volgende criteria te voldoen:

  • Verplaats de cirkel 200 eenheden naar links.
  • Maak een nieuwe blauwe cirkel met dezelfde golf (wave) als deze en plaats hem 200 eenheden naar rechts.

Het verwachte eindresultaat is: twee cirkels die op hetzelfde moment pulseren in hetzelfde ritme, de ene naast de andere. Misschien kunnen we dit voor ons zien als de animatie van twee knipperende ogen?

OPDRACHT 4 (uitdagend): Maak een driehoek tussen de cirkels

Vul de code van opdracht 2 aan en maak een statische/stilstaande gele driehoek tussen de twee cirkels.

Als we ons voorstellen dat de cirkels 2 ogen zijn, dan zou de driehoek een neus kunnen zijn! De driehoek moet aan de volgende regels voldoen:

  • Hij moet geel van kleur zijn.
  • Hij moet een diameter van 50 hebben.
  • Hij moet 100 eenheden naar beneden op de verticale as (y-as). ten opzichte van het centrum en rechts in het midden (0 eenheden) op de verticale as.
  • Hij moet zo worden gedraaid dat de bovenkant plat is en de onderkant een hoek/punt vormt.

Het verwachte eindresultaat is: twee cirkels die op hetzelfde moment pulseren in hetzelfde ritme, de ene naast de andere én een gele driehoek die een neus zou kunnen voorstellen. Of misschien de snavel van een vogel! Gebruik je fantasie :)

OPDRACHT 5 (uitdagend/vrij): Maak een tekening van een dier

Tijdens de lessen hebben we geleerd hoe we driehoeken, cirkels, rechthoeken en vierkanten kunnen tekenen. We hebben ook geleerd hoe we ze moeten draaien, positioneren (een plek geven) op het scherm en vandaag leerden we een eerste animatievorm: de golfbeweging (wave).

Gebruik alle kennis die je tot nu toe hebt opgedaan en probeer een afbeelding van een dier op het scherm te tekenen.

Als je geen inspiratie hebt, kun je proberen het volgende te tekenen: een vogel, een aap, een hond, een kat... Maak je er geen zorgen over dat je de perfecte tekening moet maken. Het belangrijkste is dat je oefent!

En nu?

Is het je gelukt om alle oefeningen te doen? Had je moeite met een van hen?

Ga naar antwoorden van de opdrachten om de oplossing te zien.

Les 7: Antwoorden van de opdrachten

OPDRACHT 1 (eenvoudig): Het veranderen van de snelheid

Lees onderstaande code en probeer je voor te stellen wat er op het scherm zal verschijnen. En, wat betekenen de waarden 10, 100 en 12?

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 10 100 12 time)
  ]

Verander nu bovenstaande code zodat de cirkel 4 keer zo snel pulseert.
Verander het dan zodat het 4 keer langzamer pulseert dan in de originele code.

Antwoord

10 stelt de minimale waarde van de cirkel voor (wanneer is hij op zijn kleinst). 100 stelt de maximale grootte van de diameter voor. Het 12 time deel van de code bepaalt de snelheid van de animatie. Onthoud hier: hoe kleiner die waarde, hoe sneller de animatie.
Dit gebeurt omdat deze waarde het aantal seconden aangeeft dat we willen dat de animatie duurt. Hoe korter de totale tijd van de animatie, hoe sneller deze wordt weergegeven.

Om de animatie 4 keer zo snel te laten verlopen, delen we gewoon de waarde 12 time door 4:

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 10 100 3 time)
  ]

Om de animatie 4 keer langzamer te maken, vermenigvuldigen we de oorspronkelijke waarde met 4:

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 10 100 48 time)
  ]

Nu zal onze animatie zeer traag zijn, en 48 seconden duren om de hele cyclus af te maken.

OPDRACHT 2 (eenvoudig): Voeg een cirkel in een andere

Neem in de bovenstaande code een nieuwe rode cirkel op die statisch/stil is. Deze nieuwe cirkel moet onder de blauwe cirkel liggen ​​en een diameter van 200 hebben.

Antwoord

Deze keer hoefden we alleen maar een rode cirkel toe te voegen aan de lijst met figuren:

import Playground exposing (..)

main =
  animation view

view time =
  [ circle red 200
    , circle blue (wave 20 100 10 time)
  ]

OPDRACHT 3 (gemiddeld): Maak 2 cirkels die pulseren

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 20 80 2 time)
  ]

Wijzig de bovenstaande code om aan de volgende criteria te voldoen:

  • Verplaats de cirkel 200 eenheden naar links.
  • Maak een nieuwe blauwe cirkel met dezelfde golf (wave) als deze en plaats hem 200 eenheden naar rechts.

Het verwachte eindresultaat is: twee cirkels die op hetzelfde moment pulseren in hetzelfde ritme, de ene naast de andere. Misschien kunnen we dit voor ons zien als de animatie van twee knipperende ogen?

Antwoord

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 20 80 2 time)
      |> move -200 0
    , circle blue (wave 20 80 2 time)
      |> move 200 0
  ]

OPDRACHT 4 (uitdagend): Maak een driehoek tussen de cirkels

Vul de code van opdracht 2 aan en maak een statische/stilstaande gele driehoek tussen de twee cirkels.

Als we ons voorstellen dat de cirkels 2 ogen zijn, dan zou de driehoek een neus kunnen zijn! De driehoek moet aan de volgende regels voldoen:

  • Hij moet geel van kleur zijn.
  • Hij moet een diameter van 50 hebben.
  • Hij moet 100 eenheden naar beneden op de verticale as (y-as). ten opzichte van het centrum en rechts in het midden (0 eenheden) op de verticale as.
  • Hij moet zo worden gedraaid dat de bovenkant plat is en de onderkant een hoek/punt vormt.

Het verwachte eindresultaat is: twee cirkels die op hetzelfde moment pulseren in hetzelfde ritme, de ene naast de andere én een gele driehoek die een neus zou kunnen voorstellen. Of misschien de snavel van een vogel! Gebruik je fantasie :)

Antwoord

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue (wave 20 80 2 time)
      |> move -200 0
    , circle blue (wave 20 80 2 time)
      |> move 200 0
    , triangle yellow 50
      |> move 0 -100
      |> rotate 180
  ]

OPDRACHT 5 (uitdagend/vrij): Maak een tekening van een dier

Tijdens de lessen hebben we geleerd hoe we driehoeken, cirkels, rechthoeken en vierkanten kunnen tekenen. We hebben ook geleerd hoe we ze moeten draaien, positioneren (een plek geven) op het scherm en vandaag leerden we een eerste animatievorm: de golfbeweging (wave).

Gebruik alle kennis die je tot nu toe hebt opgedaan en probeer een afbeelding van een dier op het scherm te tekenen.

Als je geen inspiratie hebt, kun je proberen het volgende te tekenen: een vogel, een aap, een hond, een kat... Maak je er geen zorgen over dat je de perfecte tekening moet maken. Het belangrijkste is dat je oefent!

Antwoord

Dit was een vrije opdracht en heeft daarom geen goede of foute antwoorden :)

En nu?

Ga nu door met Les 8, veel succes!

Les 8 - Zigzagbewegingen maken

Wat leer je in deze les?

  1. Hoe animeer je een figuur met een zigzagbeweging.
    1.1 De zigzag functie.

1 - Hoe animeer je een figuur met een zigzagbeweging

We beginnen met het tekenen van een cirkel op het scherm, en verplaatsen het een beetje naar links..

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue 100 
        |> move -200 0
  ]

Stel je voor dat je een animatie wilt maken op deze figuur, waardoor deze "zigzagt" (zijn richting steeds afwisselt).

In het bovenstaande voorbeeld is de positie van de cirkel vastgesteld op -200 op de horizontale as en 0 op de verticale as.
Om de "zigzag"-beweging te maken, moet deze waarde veranderen naarmate de tijd verstrijkt (die variabele time waar je over geleerd hebt in de vorige les.

Om bijvoorbeeld een animatie te maken waarbij de cirkel van links naar rechts beweegt, is het noodzakelijk om de positie ervan te wijzigen op de horizontale as. Dat wil zeggen, vervang de waarde -200 door een waarde die lineair verandert in de tijd. En de zigzag functie vervult precies deze taak.

1.1 - De zigzag functie

zigzag is een functie die vergelijkbaar is met wave. Het ontvangt ook vier parameters, waarbij de eerste twee parameters de minimale en maximum waarden aangeven. De derde parameter geeft het aantal seconden aan dat de animatie nodig heeft om te voltooien.

Nu je de zigzag functie en zijn doel kent, stel je dan het volgende voor: hoe zou de code eruit kunnen zien om een cirkel van links naar rechts te laten bewegen, tussen positie -200 en 200 met een interval van 5 seconden?

import Playground exposing (..)

main =
  animation view

view time =
  [ circle blue 100 
        |> move (zigzag -200 200 5 time) 0
  ]

Nogmaals, let op: je mag de haakjes niet vergeten, want de eerste parameter van de functie move moet het resultaat zijn van de zigzag functie. Om dit te doen, is het noodzakelijk om aan de computer door te geven (via het gebruik van haakjes) dat je eerst de waarde van de zigzag-functie wilt verwerken en berekenen om vervolgens de move-functie te verwerken. En, kun je nu al de mogelijkheden voorstellen van het maken van complexere animaties en wie weet zelfs spelletjes met deze functies? 😃

En nu?

Nu is het tijd om aan de slag te gaan en nog meer te oefenen!

Ga naar Les 8 opdrachten en veel succes met oefenen!

Les 8: Opdrachten

OPDRACHT 1 (eenvoudig): Verticaal bewegen

Verander onderstaande code zodat het vierkant verticaal beweegt (van boven naar beneden). Dit doe je door de positie van het vierkant op de y -as te variëren tussen -100 en 100.
De animatie moet 2 seconden duren om de cyclus te voltooien.

import Playground exposing (..)

main =
  animation view

view time =
  [ square darkGreen 100 
        |> move -0 0
  ]

OPDRACHT 2 (eenvoudig): Diagonaal (schuin) bewegen

Verander de code in opdracht 1 zodat het vierkant diagonaal beweegt. De beweging moet beginnen op het punt -100, -100 en eindigen op 100, 100.

OPDRACHT 3 (vrij): Mix de bewegingen

Teken met behulp van de functies zigzag en wave figuren die over het scherm bewegen.
Je kunt de wielen van een auto animeren, de ogen van een dier, of gewoon verschillende figuren tekenen die verwoed bewegen op het scherm! Gebruik je fantasie.

En nu?

Is het je gelukt om alle oefeningen te doen? Had je moeite met een van hen?

Ga naar antwoorden van de opdrachten om de oplossing te zien.

Les 8: Antwoorden van de opdrachten

OPDRACHT 1 (eenvoudig): Verticaal bewegen

Verander onderstaande code zodat het vierkant verticaal beweegt (van boven naar beneden). Dit doe je door de positie van het vierkant op de y -as te variëren tussen -100 en 100.
De animatie moet 2 seconden duren om de cyclus te voltooien.

import Playground exposing (..)

main =
  animation view

view time =
  [ square darkGreen 100 
        |> move -0 0
  ]

Antwoord

Stel gewoon de x-as altijd in op 0 en laat de y-as variëren tussen -100 en 100 met behulp van de zigzag-functie.

import Playground exposing (..)

main =
  animation view

view time =
  [ square darkGreen 100 
        |> move 0 (zigzag -100 100 2 time)
  ]

OPDRACHT 2 (eenvoudig): Diagonaal (schuin) bewegen

Verander de code in opdracht 1 zodat het vierkant diagonaal beweegt. De beweging moet beginnen op het punt -100, -100 en eindigen op 100, 100.

Antwoord

Er zijn een paar manieren om deze oefening op te lossen. De eenvoudigste manier is om dezelfde code, die we voor de y-as in de vorige opdracht gebruikten, ook te gebruiken voor de x-as. We verplaatsen dus de 2 assen altijd op dezelfde manier. Dit zorgt voor een diagonale beweging

import Playground exposing (..)

main =
  animation view

view time =
  [ square darkGreen 100 
        |> move (zigzag -100 100 2 time) (zigzag -100 100 2 time)
  ]

OPDRACHT 3 (vrij): Mix de bewegingen

Teken met behulp van de functies zigzag en wave figuren die over het scherm bewegen.
Je kunt de wielen van een auto animeren, de ogen van een dier, of gewoon verschillende figuren tekenen die verwoed bewegen op het scherm! Gebruik je fantasie.

Antwoord

Vrije oefeningen hebben geen goede of foute antwoorden! Het belangrijkste is om te oefenen en plezier te hebben.

En nu?

Ga nu door met Les 9, veel succes!

Les 9 - Begrijpen wat Records zijn

Wanneer je veel informatie in je programma's hebt, moet je beginnen na te denken over hoe je het organiseert.

Je hebt al geleerd hoe je lijsten kunt maken met de symbolen [ en ]. Dit is een manier om te organiseren. Een andere manier is door gebruik te maken van Records.

Deze les is weer vrij theoretisch, maar je moet enkele belangrijke concepten begrijpen om verder te kunnen komen met je Elm-studie.

Wat leer je in deze les?

  1. Wat zijn Records.
  2. Hoe maak je een functie die een Record als parameter gebruikt.

1 - Wat zijn Records

Records is een manier om onze gegevens in de programmeertaal Elm te organiseren.

Laten we beginnen met een code die is gemaakt zonder Records te gebruiken, om te begrijpen met wat voor soort problemen we waarschijnlijk te maken zullen krijgen.

Wat we tot nu toe over functies hebben geleerd, is dat we om een ​​functie te maken die als parameter een punt in de ruimte neemt, we twee afzonderlijke numerieke argumenten doorgeven: x en y, zoals in het onderstaande voorbeeld:

import Playground exposing (..)

main =
  picture
  [ groeneCirkel 10 20
  ]

groeneCirkel x y =
  circle green 50
    |> move x y

Begrijp je wat er in bovenstaande code gebeurt? Als je het niet meer precies weet, ga terug naar les 4 om te bekijken hoe je een functie met parameters kunt maken.

In dit voorbeeld neemt onze groeneCirkel functie twee numerieke parameters aan die we x en y noemen. In dit geval is dat voldoende en werkt het goed. Maar we kunnen het ook op een andere manier doen: we kunnen een structuur (of een Record) maken die een Punt voorstelt. Het idee is om in de code nadrukkelijker aan te geven waar deze informatie voor staat.

Dus, Record is een gegevensstructuur met labels. Hieronder volgt een voorbeeld van een Record.

{ x = 10, y = 15 }

In de taal Elm is alles tussen { en } een record.

2 - Hoe maak je een functie die een Record als parameter gebruikt

We kunnen nu de vorige code veranderen zodat onze functie groeneCirkel een Record krijgt:

import Playground exposing (..)

main =
  picture
  [ groeneCirkel {x = 10, y = 20}
  ]

groeneCirkel {x, y} =
  circle green 50
    |> move x y

Als we willen, kunnen we deze waarden ook opslaan in een variabele, zoals in onderstaand voorbeeld:

import Playground exposing (..)

positie = {x = 10, y = 20}

main =
  picture
  [ groeneCirkel positie
  ]

groeneCirkel {x, y} =
  circle green 50
    |> move x y

Nu is het iets duidelijker wat de 10 en de 20 betekenen, maar we kunnen een stapje verder gaan. Het is mogelijk om een naam (een alias) aan deze structuur te geven.

Een alias aanmaken

In de onderstaande code definiëren we wat een Point2D is. We kunnen het elke naam geven die we willen, maar het moet verplicht beginnen met een hoofdletter.

type alias Point2D =
  { x : Number 
  , y : Number
  }

In deze code leggen we de computer uit dat een Point2D een gegevensstructuur is die twee velden bevat: de x en de y. Deze keer vertellen we ook aan de computer dat deze velden gegevens moeten bevatten van het type Number (nummer in het Engels).

De programmeertaal Elm heeft een reeks gegevenstypen die we kunnen gebruiken. Enkele voorbeelden zijn: Number, Int, String, Bool, List, Float. Maar maak je geen zorgen over het onthouden daarvan! Gedurende de hele les zullen we leren wanneer we ze moeten gebruiken.

Bij het maken van een nieuwe alias Point2D bepalen we alleen wat het vertegenwoordigt, maar we maken niet effectief een punt. De taal Elm zal automatisch een nieuwe functie, genaamd Point2D, beschikbaar stellen, waarvan de parameters gedefinieerd worden in alias, die we een constructor noemen (of Type Constructor). Door deze functie te activeren, kunnen we een nieuw punt bouwen:

import Playground exposing (..)

type alias Point2D =
  { x : Number 
  , y : Number
  }

positie = Point2D 10 20

main =
  picture
  [ groeneCirkel positie
  ]

groeneCirkel {x, y} =
  circle green 50
    |> move x y

In dit voorbeeld krijgt de functie groeneCirkel steeds een Record op een eenvoudigere manier, met behulp van de toetsen. Maar we kunnen verder gaan en deze parameter een naam geven:

import Playground exposing (..)

type alias Point2D =
  { x : Number 
  , y : Number
  }

posicao = Point2D 10 20

main =
  picture
  [ groeneCirkel positie
  ]

groeneCirkel punt =
  circle green 50
    |> move punt.x punt.y

Nu is het heel duidelijk in onze functie groeneCirkel dat de verwachte parameter een punt is en dat dit punt twee velden (x en y) heeft en die beiden getallen zijn. Om toegang te krijgen tot deze velden gebruiken we het puntteken ("."). Dus, om toegang te krijgen tot het veld x, typ je gewoon point.x.

Conclusie

Je vraagt je waarschijnlijk af waarom dit allemaal zo is! Deze nieuwe versie van onze code is groter en misschien een beetje meer verwarrend. En wij zouden zeggen... je hebt gelijk! In dit voorbeeld maken we een oplossing die eenvoudig was, moeilijker en dit is geen goede zaak. Maar we moesten dit doen om de concepten aan jou uit te leggen. In andere situaties zal het gebruik van Records de code erg vereenvoudigen. Daarom is het heel belangrijk om dit concept te begrijpen.

En nu?

Deze les was behoorlijk theoretisch, dus het is nu tijd om aan de slag te gaan en nog meer te oefenen!

Ga naar Les 9 opdrachten en veel succes met oefenen!

Les 9: Opdrachten

OPDRACHT 1 (eenvoudig): het concept van de Boom abstraheren

Kijk naar onderstaande code en probeer te begrijpen wat er gebeurt.

import Playground exposing (..)

type alias Boom =
  { hoogte : Number 
  , breedte : Number
  , straalKruin: Number
  }

mijnBoom = Boom 150 40 75

main =
  picture (tekenBoom mijnBoom)

-- Deze functie is niet compleet.
tekenBoom boom =
  [ circle green boom.straalKruin
  ]

Kun je je bedenken wat er gebeurt als je deze code uitvoert?

Door een type alias te gebruiken, leggen we eerst aan de computer uit wat een boom is. In dit geval wordt een boom gevormd door 3 velden: hoogte, breedte en straalKruin. De eerste twee velden vertegenwoordigen informatie van de stam en de laatste van de kroon/bladeren.
Met deze informatie kunnen we, vanuit deze structuur, bomen tekenen die lijken op degene die we hebben gedaan in les 3 en les 4 van deze cursus.

Zoals je misschien hebt gemerkt, is de tekenBoom functie onvolledig en tekenen we alleen de kroon/bladeren van onze boom.

Voordat je verder gaat, open je het volgende adres in een in een ander tabblad van jouw browser: htts://elm-lang.org/try en voer de hierboven gedefinieerde code uit.

Verander nu de tekenBoom functie zodat die ook de stam van onze boom tekent.

OPDRACHT 2 (moeilijk): ogen tekenen

Laten we een aanpak bedenken om ogen op het scherm te tekenen met behulp van parameters.

Je moet een functie met de naam oog maken die een Record als parameter ontvangt. Maak hiervoor een type-alias aan met de naam Positie met daarin de velden x en y, beiden van het type Number.

De functie oog moet een lijst van cirkels opleveren die staan voor een oog op het scherm. Ons oog zal uit minstens twee cirkels bestaan, de ene in de andere. Gebruik je fantasie om het te tekenen!

Maak vervolgens 2 andere functies genaamd linkerOog en rechterOog. Deze functies moeten de oog-functie activeren door de positie langs te gaan. Het linkeroog wordt getekend vanuit het punt (-100, 20) en het rechteroog vanuit het punt (100, 20).

Ten slotte moet uw main-functie de functies linkerOog en rechterOog activeren om de plaatjes op het scherm te tekenen.

👩‍🏫 Hint: zowel de functies linkerOog en rechterOog geven een lijst met plaatjes. Het zal nodig zijn om deze twee lijsten samen te voegen tot één lijst voordat de functie main wordt aangeroepen (die een enkele lijst met plaatjes verwacht). Hiervoor kun je het symbool ++ gebruiken. Voorbeeld:

kleineGetallen = [1,2,3]
groteGetallen = [100,101,102]
lijstMetGetallen = kleineGetallen ++ groteGetallen

In bovenstaande code zal lijstMetGetallen de volgende lijst bevatten: [1,2,3,100,101,102].

Probeer de opdracht op te lossen. Als het je veel moeite kost, kun je de structuur volgen die we hieronder gemaakt hebben:

import Playground exposing (..)

type alias Positie =
  -- stel hier de x- en y-velden in

linkerOog = 
  -- hier moet je de oogfunctie activeren (denk aan de haakjes!)

rechterOog =
  -- hier moet je de oogfunctie opnieuw activeren

main =
  -- Geef als parameter van de functie *picture* het resultaat van de samenvoeging van de functies linkerOog + rechterOog door.

oog positie =
  [ 
-- teken hier een oog met minstens 2 cirkels.
  ]

OPDRACHT 3 (vrij): de rest van het gezicht tekenen

Creëer andere functies voor andere delen van het gezicht. Bijvoorbeeld: neus, oor, mond, wenkbrauw... gebruik je fantasie!

En nu?

Is het je gelukt om alle oefeningen te doen? Had je moeite met een van hen?

Ga naar antwoorden van de opdrachten om de oplossing te zien.

Les 9: Antwoorden van de opdrachten

OPDRACHT 1 (eenvoudig): het concept van de Boom abstraheren

Kijk naar onderstaande code en probeer te begrijpen wat er gebeurt.

import Playground exposing (..)

type alias Boom =
  { hoogte : Number 
  , breedte : Number
  , straalKruin: Number
  }

mijnBoom = Boom 150 40 75

main =
  picture (tekenBoom mijnBoom)

-- Deze functie is niet compleet.
tekenBoom boom =
  [ circle green boom.straalKruin
  ]

Kun je je bedenken wat er gebeurt als je deze code uitvoert?

oor een type alias te gebruiken, leggen we eerst aan de computer uit wat een boom is. In dit geval wordt een boom gevormd door 3 velden: hoogte, breedte en straalKruin. De eerste twee velden vertegenwoordigen informatie van de stam en de laatste van de kroon/bladeren.
Met deze informatie kunnen we, vanuit deze structuur, bomen tekenen die lijken op degene die we hebben gedaan in les 3 en les 4 van deze cursus.

Zoals je misschien hebt gemerkt, is de tekenBoom functie onvolledig en tekenen we alleen de kroon/bladeren van onze boom.

Voordat je verder gaat, open je het volgende adres in een in een ander tabblad van jouw browser: htts://elm-lang.org/try en voer de hierboven gedefinieerde code uit.

Verander nu de tekenBoom functie zodat die ook de stam van onze boom tekent.

Antwoord

Er zijn verschillende manieren om deze uitdaging op te lossen. Het moeilijkste deel heeft niets te maken met programmeren, maar met wiskunde! :)

We moeten bepalen waar we de rechthoek tekenen die de stam van onze boom voorstelt, en deze keer komt deze waarde tot stand via parameters.
Het tekenen van de rechthoek is het makkelijke deel:

import Playground exposing (..)

type alias Boom =
  { hoogte : Number 
  , breedte : Number
  , straalKruin: Number
  }

mijnBoom = Boom 150 40 75

main =
  picture (tekenBoom mijnBoom)

tekenBoom boom =
  [ circle green boom.straalKruin
  , rectangle darkBrown boom.breedte boom.hoogte
  ]

Maar, als we deze code uitvoeren, zien we dat de stam bovenop de bladeren wordt getekend. We moeten het naar beneden verplaatsen zodat het tegen de rand van de cirkel aan staat.

Hier had je verschillende strategieën kunnen toepassen. Wij kiezen ervoor om het exacte punt te berekenen waar de cirkel eindigt om de stam naar deze plek te verplaatsen. Daarvoor moeten we 2 waarden toevoegen: de straal van de cirkel plus de helft van de hoogte van de stam. Op deze manier zal de stam precies aan het einde van de bladeren staan:

import Playground exposing (..)

type alias Boom =
  { hoogte : Number 
  , breedte : Number
  , straalKruin: Number
  }

mijnBoom = Boom 150 40 75

main =
  picture (tekenBoom mijnBoom)

tekenBoom boom =
  [ circle green boom.straalKruin
  , rectangle darkBrown boom.breedte boom.hoogte
    |> move 0 -(boom.straalKruin + (boom.hoogte / 2))
  ]

OPDRACHT 2 (moeilijk): ogen tekenen

Laten we een aanpak bedenken om ogen op het scherm te tekenen met behulp van parameters.

Je moet een functie met de naam oog maken die een Record als parameter ontvangt. Maak hiervoor een type-alias aan met de naam Positie met daarin de velden x en y, beiden van het type Number.

De functie oog moet een lijst van cirkels opleveren die staan voor een oog op het scherm. Ons oog zal uit minstens twee cirkels bestaan, de ene in de andere. Gebruik je fantasie om het te tekenen!

Maak vervolgens 2 andere functies genaamd linkerOog en rechterOog. Deze functies moeten de oog-functie activeren door de positie langs te gaan. Het linkeroog wordt getekend vanuit het punt (-100, 20) en het rechteroog vanuit het punt (100, 20).

en slotte moet uw main-functie de functies linkerOog en rechterOog activeren om de plaatjes op het scherm te tekenen.

👩‍🏫 Hint: zowel de functies linkerOog en rechterOog geven een lijst met plaatjes. Het zal nodig zijn om deze twee lijsten samen te voegen tot één lijst voordat de functie main wordt aangeroepen (die een enkele lijst met plaatjes verwacht). Hiervoor kun je het symbool ++ gebruiken. Voorbeeld:

kleineGetallen = [1,2,3]
groteGetallen = [100,101,102]
lijstMetGetallen = kleineGetallen ++ groteGetallen

In bovenstaande code zal lijstMetGetallen de volgende lijst bevatten: [1,2,3,100,101,102].

Probeer de opdracht op te lossen. Als het je veel moeite kost, kun je de structuur volgen die we hieronder gemaakt hebben:

import Playground exposing (..)

type alias Positie =
  -- stel hier de x- en y-velden in

linkerOog = 
  -- hier moet je de oogfunctie activeren (denk aan de haakjes!)

rechterOog =
  -- hier moet je de oogfunctie opnieuw activeren

main =
  -- Geef als parameter van de functie *picture* het resultaat van de samenvoeging van de functies linkerOog + rechterOog door.

oog positie =
  [ 
-- teken hier een oog met minstens 2 cirkels.
  ]

Antwoord

Er zijn oneindig veel manieren om deze opdracht op te lossen! Het belangrijkste is om te blijven oefenen. Hieronder zie je als voorbeeld ons antwoord staan. Daarin worden voor elk oog 3 cirkels gebruikt om een oog te maken.

import Playground exposing (..)

type alias Positie =
  { x : Number
  , y : Number
  }

linkerOog = 
  oog (Positie -100 20)

rechterOog =
  oog (Positie 100 20)
main =
  picture (linkerOog ++ rechterOog)

oog positie =
  [ circle gray 50 
    |> move positie.x positie.y
  , circle black 20
    |> move (positie.x - 10) (positie.y - 5)
  , circle white 5
    |> move (positie.x - 15) (positie.y - 8)
  ]

OPDRACHT 3 (vrij): de rest van het gezicht tekenen

Creëer andere functies voor andere delen van het gezicht. Bijvoorbeeld: neus, oor, mond, wenkbrauw... gebruik je fantasie!

Antwoord

Vrije oefeningen hebben geen goede of foute antwoorden! Het belangrijkste is om te oefenen en plezier te hebben.

En nu?

Deze cursus is nog volop in ontwikkeling en heeft voorlopig een beperkt aantal lessen. Gedurende de hele cursus zullen nieuwe lessen worden gepubliceerd in komende weken!

Vond je het idee van dit project leuk? Wil je een suggestie sturen of een vraag stellen? Nog vragen? Neem contact op met de auteur door een e-mail te sturen naar marcio@segunda.tech.

Les 10 - True, False en condities

Wat leer je in deze les?

  1. Waar zijn condities (voorwaarden) voor?
  2. Condities gebruiken.
  3. De code leesbaarder maken.

1 - Waar zijn condities (voorwaarden) voor?

Stel je voor dat je een 2D platformspel ontwikkelt, vergelijkbaar met die in de afbeelding hieronder.

Imagem do jogo SuperTux

De spelregels leggen bepaalde voorwaarden op. Bijvoorbeeld:

Als de speler een munt aanraakt, dan:
    verdwijnt de munt en
    moet het aantal munten dat de speler heeft met 1 worden verhoogd.
Anders:
    moet de munt getoond blijven en
    moet het aantal munten hetzelfde blijven.

Wij kunnen het bovenstaande voorbeeld in 3 delen opsplitsen:

  1. De conditie (voorwaarde), die WAAR (True in het Engels) of ONWAAR (False in het Engels) kan zijn;
  2. De gevolgen als die conditie waar is en;
  3. De gevolgen als de conditie onwaar is.

Zonder voorwaarden zou het onmogelijk zijn onze regels toe te passen en de hoofdpersoon zou dan nooit winnen of verliezen. Dus, ons spel zou dan erg saai zijn!

2 - Condities gebruiken

De eenvoudigste manier om een conditie te creëren is door if te gebruiken (dit betekent als in het Engels).

Stel dat we een conditie moeten maken om te weten of ons spel moet doorgaan of niet. De regel zou dan moeten zijn: Als de speler meer had 0 levens heeft, dan moet het spel doorgaan. Anders moeten we het spel stoppen.

Om onze uitvoering te vereenvoudigen, doen we het volgende: als de conditie aangeeft dat het spel moet doorgaan, dan verschijnt er een groene cirkel op het scherm. Als het spel niet moet doorgaan, tonen we een rode cirkel.

Laten we beginnen met het tekenen van een rode cirkel in het midden van het scherm:

import Playground exposing (..)

main =
  picture [
    circle red 100
  ]

Voorlopig tekenen we altijd een rode cirkel.
Nu moeten we een conditie toevoegen. Maar, laten we eerst een variabele aanmaken waar we het aantal levens van de speler bepalen:

import Playground exposing (..)

hoeveelheidLevens = 1

main =
  picture [
    circle red 100
  ]

Klaar! Nu hebben we alles wat we nodig hebben om onze conditie te maken. Als hoeveelheidLevens een waarde groter dan 1 bevat, tekenen we een groene cirkel. Anders een rode cirkel.

import Playground exposing (..)

hoeveelheidLevens = 1

main =
  picture [ 
    if hoeveelheidLevens > 0 then
      circle green 100 
    else 
      circle red 100
  ]

Merk op dat telkens wanneer we een voorwaarde (if) definiëren, we ook het woord then (wat dan betekent in het Engels) zetten. En, tot slot definiëren we een tweede blok, met behulp van het woord else (anders in het Engels).

Het soort inhoud dat wordt gedefinieerd tussen de woorden then en else, en de inhoud die wordt gedefinieerd vanaf het woord else moet hetzelfde zijn. In dit geval zijn beide een figuur.

Probeer de waarde van hoeveelheidLevens te veranderen in 0 of -1 en voer het programma opnieuw uit.

3 - De code leesbaarder maken

In ons voorbeeld kan het voor iemand anders gemakkelijk zijn om naar de volgende regel code te kijken en direct te begrijpen wat we bedoelen:

if hoeveelheidLevens > 0 then

Maar het is meer gebruikelijk dat onze conditionele regels veel complexer zijn dan dat. En het is heel belangrijk dat andere mensen begrijpen wat de aanleiding was voor het maken van die conditie.

In deze scenario's wordt aanbevolen om dit deel van de code los te trekken en in een functie te plaatsen. Zo kunnen we duidelijker maken wat de werkelijke bedoeling van die conditie is, zoals in het onderstaande voorbeeld.

import Playground exposing (..)

hoeveelheidLevens = 1

spelerLeeftNog =
  hoeveelheidLevens > 0

main =
  picture [ 
    if spelerLeeftNog then
      circle green 100 
    else 
      circle red 100
  ]

In dit laatste voorbeeld zal de functie spelerLeeftNog het resultaat True weergeven als hoeveelheidLevens groter is dan 0 en False in elk ander geval.

Er zijn verschillende manieren om deze code te ordenen. Dit is er slechts één van.

En nu?

Deze les was behoorlijk theoretisch, dus het is nu tijd om aan de slag te gaan en nog meer te oefenen!

Ga naar Les 10 opdrachten en veel succes met oefenen!

Les 10: Opdrachten

OPDRACHT 1 (gemiddeld): Teken een ballon

De onderstaande code tekent een groene ballon op het scherm.

Voer de code uit om het resultaat te zien.

import Playground exposing (..)

lengteBallonDraad = 50

dikteBallonDraad = 3

kleurBallon = green

radiusVanMijnBallon = 60

main =
  picture
   (ballon radiusVanMijnBallon)


ballon radius =
  [ circle kleurBallon radius
  , rectangle lightRed dikteBallonDraad lengteBallonDraad
    |> move 0 (-radius - (lengteBallonDraad / 2))
  ]

Deze keer hebben we ervoor gekozen om de waarden in variabelen te definiëren, zodat we ze namen kunnen geven en zo hun betekenis explicieter kunnen maken.

Toegegeven, de ballon lijkt niet echt op een ballon, maar... probeer maar een beetje je fantasie te gebruiken! ;)

Het moeilijkste deel van deze code is waarschijnlijk de volgende regel:

|> move 0 (-radius - (lengteBallonDraad / 2))

We hebben al eerder code gezien die hier sterk op lijkt in les 9 opdrachten. Maak je geen zorgen als je het nog steeds niet goed begrijpt. Al wat je doet is gewoon het touwtje van de ballon aan de onderkant plaatsen.

Verander nu de vorige code zodat de ballon de kleur groen (green) krijgt als zijn straal (radius) kleiner is dan 50 en de kleur rood als zijn straal deze waarde overschrijdt.

OPDRACHT 2 (gemiddeld): Nog een kleur toevoegen

Wijzig je antwoord van de vorige opdracht om een derde kleur toe te voegen, volgens de volgende regels:

  • De ballon moet groen (green) gekleurd zijn als hij een straal heeft van minder dan 50;
  • De ballon moet geel (yellow) gekleurd zijn als hij een straal heeft van 50 of meer, en minder dan 65;
  • De ballon moet rood (red) gekleurd zijn als hij een straal heeft van meer dan 65.

Wijzig na het schrijven van de code de waarde van radiusVanMijnBallon zodat het eerst groen wordt, dan geel en tenslotte rood.

En nu?

Is het je gelukt om alle oefeningen te doen? Had je moeite met een van hen?

Ga naar antwoorden van de opdrachten om de oplossing te zien.

Les 10: Antwoorden van de opdrachten

OPDRACHT 1 (gemiddeld): Teken een ballon

De onderstaande code tekent een groene ballon op het scherm.

Voer de code uit om het resultaat te zien.

import Playground exposing (..)

lengteBallonDraad = 50

dikteBallonDraad = 3

kleurBallon = green

radiusVanMijnBallon = 60

main =
  picture
   (ballon radiusVanMijnBallon)


ballon radius =
  [ circle kleurBallon radius
  , rectangle lightRed dikteBallonDraad lengteBallonDraad
    |> move 0 (-radius - (lengteBallonDraad / 2))
  ]

Deze keer hebben we ervoor gekozen om de waarden in variabelen te definiëren, zodat we ze namen kunnen geven en zo hun betekenis explicieter kunnen maken.

Toegegeven, de ballon lijkt niet echt op een ballon, maar... probeer maar een beetje je fantasie te gebruiken! ;)

Het moeilijkste deel van deze code is waarschijnlijk de volgende regel:

|> move 0 (-radius - (lengteBallonDraad / 2))

We hebben al eerder code gezien die hier sterk op lijkt in les 9 opdrachten. Maak je geen zorgen als je het nog steeds niet goed begrijpt. Het enige dat je doet is gewoon het touwtje van de ballon aan de onderkant plaatsen.

Verander nu de vorige code zodat de ballon de kleur groen (green) krijgt als zijn straal (radius) kleiner is dan 50 en de kleur rood als zijn straal deze waarde overschrijdt.

Antwoord

We moeten de functie kleurBallon veranderen zodat deze de waarde van de radius als parameter krijgt. Dit maakt het gemakkelijk om deze functie te veranderen zodat deze green teruggeeft als het minder dan 50 is en rood als het gelijk is aan of groter is dan dit getal.

import Playground exposing (..)

lengteBallonDraad  = 50

dikteBallonDraad = 3

kleurBallon radius = 
  if radius < 50 then
    green
  else 
    red

radiusVanMijnBallon = 60

main =
  picture
   (ballon radiusVanMijnBallon)


ballon radius =
  [ circle (kleurBallon radius) radius
  , rectangle lightRed dikteBallonDraad lengteBallonDraad
    |> move 0 (-radius - (lengteBallonDraad / 2))
  ]

Een stukje code dat je misschien moeilijk te begrijpen vindt, is deze:

circle (kleurBallon radius) radius

Kun je zien waarom we twee verwijzingen naar radius hebben op deze regel?

In eerste instantie geven we de waarde gewoon door aan de functie kleurBallon, die deze informatie nu nodig heeft om te beslissen welke kleur onze ballon zal krijgen. De uitkomst van (kleurBallon radius) zal een kleur zijn: ofwel green of red. Deze uitkomst wordt gebruikt als het eerste argument van de functie circle. De radius die aan het einde van de regel verschijnt, is het tweede argument dat ook wordt doorgegeven aan de functie circle.

Probeer de waarde van de variabele radiusVanMijnBallon te wijzigen en kijk wat er gebeurt als deze waarde kleiner is dan 50.

OPDRACHT 2 (gemiddeld): Nog een kleur toevoegen

Wijzig je antwoord van de vorige opdracht om een derde kleur toe te voegen, volgens de volgende regels:

  1. De ballon moet groen (green) gekleurd zijn als hij een straal heeft van minder dan 50;
  2. De ballon moet geel (yellow) gekleurd zijn als hij een straal heeft van 50 of meer, en minder dan 65;
  3. De ballon moet rood (red) gekleurd zijn als hij een straal heeft van meer dan 65.

Wijzig na het schrijven van de code de waarde van radiusVanMijnBallon zodat het eerst groen wordt, dan geel en tenslotte rood.

Antwoord

Om deze opdracht op te lossen, moeten we een conditie creëren binnen de andere conditie.

  1. Eerst controleren we of de waarde van de straal minder dan 50 is. Als dat zo is, weten we al dat de kleur groen is. Zo niet, dan moeten we een ​​tweede controle doen:
  2. We moeten weten of de waarde van de straal minder is dan 65. Als op dit moment de waarde minder is dan 65, weten we dat het een getal tussen de 50 en 65 is. Dat hadden we immers al gecheckt bij de eerste controle/validatie (als het minder dan 50 is). De kleur is dan geel.
  3. En als deze tweede validatie ook onwaar is, weten we dat de straal een waarde boven de 65 heeft. In dat geval moeten we de rode kleur gebruiken.

Of, met andere woorden, we moeten een if maken binnen onze if:

import Playground exposing (..)

lengteBallonDraad = 50

dikteBallonDraad = 3

kleurBallon radius = 
  if radius < 50 then
    green
  else 
    if radius < 65 then
      yellow
    else
      red

radiusVanMijnBallon = 64

main =
  picture
   (ballon radiusVanMijnBallon)


ballon radius =
  [ circle (kleurBallon radius) radius
  , rectangle lightRed dikteBallonDraad lengteBallonDraad
    |> move 0 (-radius - (lengteBallonDraad / 2))
  ]

Let op het inspringen van de ifs. De tweede is iets verder naar rechts en indien nodig kunnen we andere geneste ifs toevoegen. Maar wees hier voorzichtig mee want dit kan ervoor zorgen dat onze code erg moeilijk te begrijpen wordt.

En nu?

Deze cursus is nog steeds in ontwikkeling en heeft voorlopig een beperkt aantal lessen. De komende weken worden er nieuwe lessen gepubliceerd!

Vond je het idee van dit project leuk? Wil je een suggestie sturen of een vraag stellen? Neem contact met de auteur op door een e-mail te sturen naar marcio@segunda.tech of zoek hem op op twitter: @marciofrayze

Words

Laat wat woorden zien!

import Playground exposing (..)

main =
  picture
    [ words black "Hello! How are you?"
    ]

Naam compleet geschreven of in losse letters zodat je die afzonderlijk kan aanpassen (color, shape, animate, ..)

scale:

Maak een vorm groter of kleiner. Dus, als je wilt dat sommige woorden groter zijn, zou je kunnen zeggen:

import Playground exposing (..)

main =
  picture
    [ words black "Hello, nice to see you!"
        |> scale 3
    ]

rotate:

Draai vormen in graden.

import Playground exposing (..)

main =
  picture
    [ words black "These words are tilted!"
        |> rotate 10
    ]

fade:

Vervaag een vorm. Hiermee maak je vormen doorzichtig of zelfs helemaal onzichtbaar. Hier is een vorm die in en uit vervaagt:

import Playground exposing (..)

main =
  animation view

view time =
  [ square orange 30
  , square blue 200
      |> fade (zigzag 0 1 3 time)
  ]

Het getal moet tussen 0 en 1 liggen, waarbij 0 volledig transparant is en 1 volledig solide is.

source: https://package.elm-lang.org/packages/evancz/elm-playground/latest/Playground#scale

Programmeren met Elm - cheat sheet

Waar kun je programmeren?

  • Op de site van Rob: https://compurob.nl/workshop
    • Daar kun je makkelijk je programma uit kopieëren om bv in Notes te plakken en te bewaren
    • Daar kun je "Show" aanzetten, zodat je een schermvullend plaatje kunt maken, of een eigen pdf-bestand ervan!
    • Heeft "undo" en "redo", en handige "|>", "[" en "]" knoppen (want dat zijn op de ipad lastige tekens)
  • Op de officiële Elm site: https://elm-lang.org/try

Waar kun je de lessen zien?

Op https://compurob.nl/workshop/boek (als je minder wilt typen: je komt er ook met doorklikken vanaf compurob.nl)

Hoe kun je zien wat je code doet?

Door in Rob's workshop op "Compileren!" (dat betekent "samenstellen" of "in elkaar zetten") of bij Try Elm op "Rebuild" (dat betekent "opnieuw bouwen") te drukken.

Waar kun je voorbeeldcode vinden?

Op https://elm-lang.org/examples en in het boek met lessen https://compurob.nl/workshop/boek.

Waar kun je je eigen cheats opschrijven?

Hier:

Concepten

  • import
  • packages / libraries / modules
  • exposing
  • main
  • functies
  • parameters
  • waarden
  • input
  • output
  • pipe |> ("buis")
  • lijsten
  • open source samenwerking

Listings

Vroeger stonden er in de computertijdschriften altijd lange lappen code die je kon overtypen op je eigen computer (internet om vanaf te copy-pasten was er nog niet).

Een "listing" heette zo'n stuk programmacode (nu praten we eerder over "source code" en "apps", maar toen had men het meer over "computerprogramma's").

In dit gedeelte vind je dus listings, maar deze kun je gemakkelijk kopiëren en plakken in een online code editor zoals

Listing: poppetje

Dit poppetje is opgebouwd uit de basisvormen rectangle en square.

Zie je dat je een opmerking (zomaar een zinnetje) kunt typen als je er twee streepjes -- voor typt?

import Playground exposing (..)

main =
  picture
    [ square brown 100
      |> move 0 110
    , rectangle lightGreen 190 140
    , rectangle brown 100 35
      |> rotate 90
      |> move 77 -20
    , rectangle brown 35 100
--    , rectangle brown 100 35
--      |> rotate 90
      |> move -77 -20
    , rectangle blue 200 115
      |> rotate 90
      |> move 0 -170
    ]

Listing: eekhoorn

Deze eekhoorn is opgebouwd uit de basisvormen circle, oval en triangle. Het beestje kijkt naar beneden - misschien kun je het in een andere richting laten kijken?

Zie je dat je een opmerking (zomaar een zinnetje) kunt typen als je er twee streepjes -- voor typt?

import Playground exposing (..)

main =
  picture
    [ rectangle gray 400 1 -- horizontale lijn op hoogte 0
    , oval darkBrown 100 150
      |> move 20 50
    , staart
      |> rotate 12
      |> move -45 100
    , poot
    , oval brown 100 40
      |> move 50 80
    , kop
    ]

staart =
  group
    [ circle brown 50
      |> move -26 60
    , oval brown 65 200
    ]

poot =
  group
    [ oval brown 80 100
    , oval brown 110 45
      |> rotate -10
      |> move 23 -35
    ]

kop =
  group
    [ circle brown 40
      |> move 20 150
    , triangle brown 20
      |> rotate 10
      |> move 5 190
    , oog (kijk NaarBeneden)
      |> move 40 170
    ]

oog transformatie =
  group
    [ circle white 10
    , circle black 5
      |> transformatie
    ]

type Richting
  = NaarBeneden
  | NaarBoven
  | NaarVoren

kijk richting =
  case richting of
    NaarBeneden -> move 5 -5
    NaarBoven -> move 5 5
    NaarVoren -> move 5 0

Listing: giraffe

Dit is een simpel spelletje gemaakt door Rob van den Bogaard. Het idee kwam van zijn toen 4-jarige zoon, die zei dat hij wel een spelletje wilde met "een giraf die balletjes eet" :D

Wat jammer is aan deze versie, is dat het niet werkt op tablet..hoe zouden we dat het beste kunnen aanpassen?

module Main exposing (main)

import Playground exposing (..)


type Health
    = NeedVitamins Int
    | Dead
    | ReadyForNextLevel


main =
    let
        memory =
            { balls =
                [ { color = green, size = 12, x = 100, y = 50 }
                , { color = blue, size = 10, x = 900, y = -50 }
                ]
            , spots =
                []
            , x = 0
            , y = 0
            , aim = 0
            , velocity = ( 0, 0 )
            , sunlight = rgb 250 245 255
            , health = NeedVitamins 0
            }
    in
    game view updateWhenHealthy memory


updateWhenHealthy computer memory =
    case memory.health of
        NeedVitamins _ ->
            update computer memory

        Dead ->
            memory

        ReadyForNextLevel ->
            update computer memory


update computer memory =
    let
        -- aim 0 means zero degrees rotation of the face; in this case the eye
        -- and mouth are aiming 45 degrees downwards relative to the horizon
        -- we would like the giraffe to aim in the direction of the mouse cursor
        -- tan aim = (c.y - y) / (c.x - x)
        -- atan (tan aim) = aim in radians minus the 45 degree offset
        -- atan2 is to help sort out the various negative value cases
        newAim =
            45
                + (180 / pi)
                * atan2 (computer.mouse.y - memory.y) (abs (computer.mouse.x - memory.x))

        pull =
            ( computer.mouse.x - memory.x - 50, computer.mouse.y - memory.y )

        ( ( newX, newY ), newVelocity ) =
            moveGiraffe ( memory.x, memory.y ) newAim memory.velocity pull

        movedBalls =
            List.map (moveBall computer.time) memory.balls

        ( ballsEaten, ballsAfterEating ) =
            List.foldl
                (maybeEatBall memory.x memory.y memory.aim)
                ( 0, [] )
                movedBalls

        spotsAfterEating =
            if ballsEaten > 0 then
                let
                    random1 =
                        cos (spin 1 computer.time)

                    random2 =
                        sin (spin 1 computer.time)

                    newSpots =
                        ( random1, random2 ) :: List.reverse memory.spots
                in
                List.reverse newSpots

            else
                memory.spots

        newHealth =
            case memory.health of
                NeedVitamins amount ->
                    if List.length spotsAfterEating > 9 then
                        ReadyForNextLevel

                    else if ballsEaten > 0 then
                        NeedVitamins 0

                    else if amount < 1000 then
                        NeedVitamins (amount + 1)

                    else
                        Dead

                Dead ->
                    Dead

                ReadyForNextLevel ->
                    ReadyForNextLevel
    in
    { memory
        | aim = newAim
        , x = newX
        , y = newY
        , velocity = newVelocity
        , balls = ballsAfterEating
        , spots = spotsAfterEating
        , health = newHealth
    }


moveGiraffe ( x, y ) aim ( vx, vy ) ( pullX, pullY ) =
    let
        drag =
            2

        vx_ =
            (vx + pullX / 100) / drag

        vy_ =
            (vy + pullY / 100) / drag

        x_ =
            clamp -1000 1000 (x + vx_)

        y_ =
            clamp -150 200 (y + vy_)
    in
    ( ( x_, y_ ), ( vx_, vy_ ) )


moveBall time b =
    let
        x_ =
            b.x - 2
    in
    { b
        | size = b.size + wave -0.1 0.1 1 time
        , x =
            if x_ > -1000 then
                x_

            else
                1000
        , y = b.y + wave -0.2 0.2 20 time
    }


maybeEatBall x y aim b ( eaten, ballsSoFar ) =
    -- if the ball is behind the giraffe it can't be eaten; in that case we just
    -- return the amount eaten so far and add the ball unchanged to the list
    -- of balls we checked so far
    -- we compare with x + 5, just to the right of the position of the giraffe
    -- because its mouth is on the right side of its head, and to avoid division
    -- by zero in further calculations
    if b.x < x + 5 then
        ( eaten, b :: ballsSoFar )

    else
        let
            distanceToBall =
                sqrt ((b.x - x) ^ 2 + (b.y - y) ^ 2)

            aimToBall =
                45 + (180 / pi) * atan2 (b.y - y) (b.x - x)
        in
        if distanceToBall > 50 || distanceToBall < 5 then
            -- this ball is too far away or to close by to get eaten; just
            -- return the number of eaten balls so far and add the ball
            -- unchanged to the list of balls already checked
            ( eaten, b :: ballsSoFar )

        else if abs (aimToBall - aim) > 20 then
            -- the ball is near but the giraffe is not aiming its mouth in its
            -- direction, so it won't get eaten
            ( eaten, b :: ballsSoFar )

        else
            -- yesss! we can eat the ball; increase the "eaten" score and move
            -- the ball out of sight to the right so it'll reappear as a new one
            ( eaten + 1, { b | x = 1000 } :: ballsSoFar )


view computer memory =
    [ background memory.sunlight
    , giraffe computer.time memory.sunlight memory.health memory.spots memory.aim
        |> move memory.x memory.y
    , balls memory.balls
    ]


background sunlight =
    group
        [ rectangle sunlight 2000 2000
        , rectangle lightBrown 2000 400
            |> moveDown 400
        ]


giraffe time sunlight health listOfSpots nod =
    let
        color =
            case health of
                NeedVitamins amount ->
                    if amount < 500 then
                        yellow

                    else
                        lightYellow

                Dead ->
                    gray

                ReadyForNextLevel ->
                    rgb (wave 0 255 3 time) (wave 0 255 5 time) (wave 0 255 1 time)
    in
    group
        [ head sunlight color nod
        , legs color 40 4
            |> move 0 (-40 * 8)
        , spots health listOfSpots
            |> move -20 -60
        ]
        |> (if health == ReadyForNextLevel then
                moveUp (wave 0 50 0.5 time)

            else
                identity
           )


head sunlight color nod =
    group
        [ headNeck color 40 8
        , face sunlight
            |> rotate nod
        ]


face sunlight =
    group
        [ circle black 15
            |> move 10 10
        , circle sunlight 10
            |> move 25 -25
        , headNeck orange 10 3
            |> moveUp 60
            |> moveLeft 10
        ]


headNeck color size neckLength =
    group
        [ circle color size
        , rectangle color size (size * neckLength)
            |> moveDown (size * (neckLength / 2))
            |> moveLeft (size / 2)
        ]


legs color size legLength =
    let
        leg =
            rectangle color (size * 2 / 3) (size * legLength)
                |> moveLeft (size / 3)
                |> moveDown (size * legLength / 2)
    in
    group
        [ moveLeft (size * 4 / 3) leg
        , leg
        , circle color size
            |> moveLeft size
            |> moveDown (size / 6)
        ]


spot health i ( x, y ) =
    let
        color =
            case health of
                NeedVitamins amount ->
                    if amount < 500 then
                        brown

                    else
                        lightBrown

                Dead ->
                    darkGray

                ReadyForNextLevel ->
                    brown
    in
    circle color 12
        |> move (x * 8) -(toFloat i * 25 + y * 5)


spots health =
    group << List.indexedMap (spot health)


balls =
    group << List.map ball


ball { color, size, x, y } =
    move x y (circle color size)

Listing: Flatris

Het bekende spel in platte vorm, gemaakt door Andrey Kuzmin. Deze listing kun je copy pasten in de Compurob Workshop.

module Main exposing (main)

import Browser
import Browser.Dom exposing (Viewport, getViewport)
import Browser.Events exposing (onAnimationFrameDelta, onKeyDown, onKeyUp, onResize)
import Html exposing (Html, div, text, button)
import Html.Attributes exposing (style)
import Html.Events exposing (keyCode, onMouseUp, onMouseDown, onClick, on)
import Json.Decode as Decode
import Json.Encode exposing (Value)
import Task
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
import Random
import Svg
import Svg.Attributes as SvgAttrs

random : Random.Seed -> ( Grid Color, Random.Seed )
random seed =
    let
        number =
            Random.int 0 (List.length tetriminos - 1)

        tetrimino n =
            Maybe.withDefault empty (List.head (List.drop n tetriminos))
    in
    Random.step (Random.map tetrimino number) seed


tetriminos : List (Grid Color)
tetriminos =
    List.map
        (\( a, b ) -> fromList a b)
        [ ( rgb 60 199 214, [ ( 0, 0 ), ( 1, 0 ), ( 2, 0 ), ( 3, 0 ) ] )
        , ( rgb 251 180 20, [ ( 0, 0 ), ( 1, 0 ), ( 0, 1 ), ( 1, 1 ) ] )
        , ( rgb 176 68 151, [ ( 1, 0 ), ( 0, 1 ), ( 1, 1 ), ( 2, 1 ) ] )
        , ( rgb 57 147 208, [ ( 0, 0 ), ( 0, 1 ), ( 1, 1 ), ( 2, 1 ) ] )
        , ( rgb 237 101 47, [ ( 2, 0 ), ( 0, 1 ), ( 1, 1 ), ( 2, 1 ) ] )
        , ( rgb 149 196 61, [ ( 1, 0 ), ( 2, 0 ), ( 0, 1 ), ( 1, 1 ) ] )
        , ( rgb 232 65 56, [ ( 0, 0 ), ( 1, 0 ), ( 1, 1 ), ( 2, 1 ) ] )
        ]


type State
    = Paused
    | Playing
    | Stopped


decodeState : String -> State
decodeState string =
    case string of
        "paused" ->
            Paused

        "playing" ->
            Playing

        _ ->
            Stopped


encodeState : State -> String
encodeState state =
    case state of
        Paused ->
            "paused"

        Playing ->
            "playing"

        Stopped ->
            "stopped"


type alias AnimationState =
    Maybe { active : Bool, elapsed : Float }


type alias Model =
    { size : ( Float, Float )
    , active : Grid Color
    , position : ( Int, Float )
    , grid : Grid Color
    , lines : Int
    , next : Grid Color
    , score : Int
    , seed : Random.Seed
    , state : State
    , acceleration : Bool
    , moveLeft : Bool
    , moveRight : Bool
    , direction : AnimationState
    , rotation : AnimationState
    , width : Int
    , height : Int
    }


initial : Model
initial =
    let
        ( next, seed ) =
            random (Random.initialSeed 0)
    in
    spawnTetrimino
        { size = ( 0, 0 )
        , active = empty
        , position = ( 0, 0 )
        , grid = empty
        , lines = 0
        , next = next
        , score = 0
        , seed = Random.initialSeed 0
        , state = Stopped
        , acceleration = False
        , moveLeft = False
        , moveRight = False
        , rotation = Nothing
        , direction = Nothing
        , width = 10
        , height = 20
        }


spawnTetrimino : Model -> Model
spawnTetrimino model =
    let
        ( next, seed ) =
            random model.seed

        ( x, y ) =
            initPosition model.width model.next
    in
    { model
        | next = next
        , seed = seed
        , active = model.next
        , position = ( x, toFloat y )
    }


decode : Decode.Decoder Model
decode =
    Decode.map8
        (\active positionX positionY grid lines next score state ->
            { initial
                | active = active
                , position = ( positionX, positionY )
                , grid = grid
                , lines = lines
                , next = next
                , score = score
                , state = state
            }
        )
        (Decode.field "active" (gridDecode colorDecode))
        (Decode.field "positionX" Decode.int)
        (Decode.field "positionY" Decode.float)
        (Decode.field "grid" (gridDecode colorDecode))
        (Decode.field "lines" Decode.int)
        (Decode.field "next" (gridDecode colorDecode))
        (Decode.field "score" Decode.int)
        (Decode.field "state" (Decode.map decodeState Decode.string))


encode : Int -> Model -> String
encode indent model =
    Encode.encode
        indent
        (Encode.object
            [ ( "active", gridEncode colorEncode model.active )
            , ( "positionX", Encode.int (Tuple.first model.position) )
            , ( "positionY", Encode.float (Tuple.second model.position) )
            , ( "grid", gridEncode colorEncode model.grid )
            , ( "lines", Encode.int model.lines )
            , ( "next", gridEncode colorEncode model.next )
            , ( "score", Encode.int model.score )
            , ( "state", Encode.string (encodeState model.state) )
            ]
        )

type Msg
    = Start
    | Pause
    | Resume
    | Tick Float
    | UnlockButtons
    | MoveLeft Bool
    | MoveRight Bool
    | Rotate Bool
    | Accelerate Bool
    | Resize Int Int
    | GetViewport Viewport
    | Noop

main : Program Value Model Msg
main =
    Browser.element
        { init =
            \value ->
                ( value
                    |> Decode.decodeValue decode
                    |> Result.withDefault initial
                , Task.perform GetViewport getViewport
                )
        , update = update
        , view = view
        , subscriptions = subscriptions
        }


subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ if model.state == Playing then
            onAnimationFrameDelta Tick

          else
            Sub.none
        , onKeyUp (Decode.map (key False) keyCode)
        , onKeyDown (Decode.map (key True) keyCode)
        , onResize Resize
        ]


key : Bool -> Int -> Msg
key on keycode =
    case keycode of
        37 ->
            MoveLeft on

        39 ->
            MoveRight on

        40 ->
            Accelerate on

        38 ->
            Rotate on

        _ ->
            Noop

------------
-- Update --
------------

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Resize width height ->
            ( { model | size = ( toFloat width, toFloat height ) }
            , Cmd.none
            )

        GetViewport { viewport } ->
            ( { model
                | size =
                    ( viewport.width
                    , viewport.height
                    )
              }
            , Cmd.none
            )

        Start ->
            ( { model
                | state = Playing
                , lines = 0
                , score = 0
                , grid = empty
              }
            , Cmd.none
            )

        Pause ->
            ( { model | state = Paused }
            , Cmd.none
            )
        Resume ->
            ( { model | state = Playing }
            , Cmd.none
            )

        MoveLeft on ->
            ( startMove { model | moveLeft = on }
            , Cmd.none
            )

        MoveRight on ->
            ( startMove { model | moveRight = on }
            , Cmd.none
            )

        Rotate False ->
            ( { model | rotation = Nothing }
            , Cmd.none
            )

        Rotate True ->
            ( { model | rotation = Just { active = True, elapsed = 0 } }
            , Cmd.none
            )

        Accelerate on ->
            ( { model | acceleration = on }
            , Cmd.none
            )

        UnlockButtons ->
            ( { model | rotation = Nothing, direction = Nothing, acceleration = False }
            , Cmd.none
            )

        Tick time ->
            (model
                |> animate (min time 25)
            , Cmd.none
            )
        Noop ->
            ( model, Cmd.none )


animate : Float -> Model -> Model
animate elapsed model =
    model
        |> moveTetrimino elapsed
        |> rotateTetrimino elapsed
        |> dropTetrimino elapsed
        |> checkEndGame


direction : Model -> Int
direction { moveLeft, moveRight } =
    case ( moveLeft, moveRight ) of
        ( True, False ) ->
            -1

        ( False, True ) ->
            1

        _ ->
            0


startMove : Model -> Model
startMove model =
    if direction model /= 0 then
        { model | direction = Just { active = True, elapsed = 0 } }

    else
        { model | direction = Nothing }


moveTetrimino : Float -> Model -> Model
moveTetrimino elapsed model =
    case model.direction of
        Just state ->
            { model | direction = Just (activateButton 150 elapsed state) }
                |> (if state.active then
                        moveTetrimino_ (direction model)

                    else
                        identity
                   )

        Nothing ->
            model


moveTetrimino_ : Int -> Model -> Model
moveTetrimino_ dx model =
    let
        ( x, y ) =
            model.position

        x_ =
            x + dx
    in
    if collide model.width model.height x_ (floor y) model.active model.grid then
        model

    else
        { model | position = ( x_, y ) }


activateButton : Float -> Float -> { a | active : Bool, elapsed : Float } -> { a | active : Bool, elapsed : Float }
activateButton interval elapsed state =
    let
        elapsed_ =
            state.elapsed + elapsed
    in
    if elapsed_ > interval then
        { state | active = True, elapsed = elapsed_ - interval }

    else
        { state | active = False, elapsed = elapsed_ }


rotateTetrimino : Float -> Model -> Model
rotateTetrimino elapsed model =
    case model.rotation of
        Just rotation ->
            { model | rotation = Just (activateButton 300 elapsed rotation) }
                |> (if rotation.active then
                        rotateTetrimino_

                    else
                        identity
                   )

        Nothing ->
            model


rotateTetrimino_ : Model -> Model
rotateTetrimino_ model =
    let
        ( x, y ) =
            model.position

        rotated =
            rotate True model.active

        shiftPosition deltas =
            case deltas of
                dx :: remainingDeltas ->
                    if collide model.width model.height (x + dx) (floor y) rotated model.grid then
                        shiftPosition remainingDeltas

                    else
                        { model
                            | active = rotated
                            , position = ( x + dx, y )
                        }

                [] ->
                    model
    in
    shiftPosition [ 0, 1, -1, 2, -2 ]


checkEndGame : Model -> Model
checkEndGame model =
    if List.any identity (mapToList (\_ ( _, y ) -> y < 0) model.grid) then
        { model | state = Stopped }

    else
        model


dropTetrimino : Float -> Model -> Model
dropTetrimino elapsed model =
    let
        ( x, y ) =
            model.position

        speed =
            if model.acceleration then
                25

            else
                max 25 (800 - 25 * toFloat (level model - 1))

        y_ =
            y + elapsed / speed
    in
    if collide model.width model.height x (floor y_) model.active model.grid then
        let
            score =
                List.length (mapToList (\_ _ _ -> True) model.active)
        in
        { model
            | grid = stamp x (floor y) model.active model.grid
            , score =
                model.score
                    + score
                    * (if model.acceleration then
                        2

                       else
                        1
                      )
        }
            |> spawnTetrimino
            |> clearLines

    else
        { model | position = ( x, y_ ) }


clearLines : Model -> Model
clearLines model =
    let
        ( grid, lines ) =
            gridClearLines model.width model.grid

        bonus =
            case lines of
                0 ->
                    0

                1 ->
                    100

                2 ->
                    300

                3 ->
                    500

                _ ->
                    800
    in
    { model
        | grid = grid
        , score = model.score + bonus * level model
        , lines = model.lines + lines
    }


level : Model -> Int
level model =
    model.lines // 10 + 1


-----------
-- Color --
-----------

type Color
    = Color { red : Int, green : Int, blue : Int }


rgb : Int -> Int -> Int -> Color
rgb red green blue =
    Color { red = red, green = green, blue = blue }


toRgb : Color -> { red : Int, green : Int, blue : Int }
toRgb (Color rawRgb) =
    rawRgb


toString : Color -> String
toString (Color { red, green, blue }) =
    "rgb("
        ++ String.fromInt red
        ++ ","
        ++ String.fromInt green
        ++ ","
        ++ String.fromInt blue
        ++ ")"


colorDecode : Decode.Decoder Color
colorDecode =
    Decode.map3 rgb
        (Decode.index 0 Decode.int)
        (Decode.index 1 Decode.int)
        (Decode.index 2 Decode.int)


colorEncode : Color -> Encode.Value
colorEncode (Color { red, green, blue }) =
    Encode.list Encode.int [ red, green, blue ]


----------
-- Grid --
----------

type alias Cell a =
    { val : a
    , pos : ( Int, Int )
    }


type alias Grid a =
    List (Cell a)


fromList : a -> List ( Int, Int ) -> Grid a
fromList value =
    List.map (Cell value)


mapToList : (a -> ( Int, Int ) -> b) -> Grid a -> List b
mapToList fun =
    List.map (\{ val, pos } -> fun val pos)


empty : Grid a
empty =
    []



-- rotates grid around center of mass


rotate : Bool -> Grid a -> Grid a
rotate clockwise grid =
    let
        ( x, y ) =
            centerOfMass grid

        fn cell =
            if clockwise then
                { cell | pos = ( 1 + y - Tuple.second cell.pos, -x + y + Tuple.first cell.pos ) }

            else
                { cell | pos = ( -y + x + Tuple.second cell.pos, 1 + x - Tuple.first cell.pos ) }
    in
    List.map fn grid



-- stamps a grid into another grid with predefined offset


stamp : Int -> Int -> Grid a -> Grid a -> Grid a
stamp x y sample grid =
    case sample of
        [] ->
            grid

        cell :: rest ->
            let
                newPos =
                    ( Tuple.first cell.pos + x, Tuple.second cell.pos + y )

                newCell =
                    { cell | pos = newPos }
            in
            stamp x y rest ({ cell | pos = newPos } :: List.filter (\{ pos } -> pos /= newPos) grid)



-- collides a positioned sample with bounds and a grid


collide : Int -> Int -> Int -> Int -> Grid a -> Grid a -> Bool
collide wid hei x y sample grid =
    case sample of
        [] ->
            False

        cell :: rest ->
            let
                ( x_, y_ ) =
                    ( Tuple.first cell.pos + x, Tuple.second cell.pos + y )
            in
            if (x_ >= wid) || (x_ < 0) || (y_ >= hei) || List.member ( x_, y_ ) (List.map .pos grid) then
                True

            else
                collide wid hei x y rest grid



-- finds the first full line to be cleared


fullLine : Int -> Grid a -> Maybe Int
fullLine wid grid =
    case grid of
        [] ->
            Nothing

        cell :: _ ->
            let
                lineY =
                    Tuple.second cell.pos

                ( inline, remaining ) =
                    List.partition (\{ pos } -> Tuple.second pos == lineY) grid
            in
            if List.length inline == wid then
                Just lineY

            else
                fullLine wid remaining



-- returns updated grid and number of cleared lines


gridClearLines : Int -> Grid a -> ( Grid a, Int )
gridClearLines wid grid =
    case fullLine wid grid of
        Nothing ->
            ( grid, 0 )

        Just lineY ->
            let
                clearedGrid =
                    List.filter (\{ pos } -> Tuple.second pos /= lineY) grid

                ( above, below ) =
                    List.partition (\{ pos } -> Tuple.second pos < lineY) clearedGrid

                droppedAbove =
                    List.map (\c -> { c | pos = ( Tuple.first c.pos, Tuple.second c.pos + 1 ) }) above

                ( newGrid, lines ) =
                    gridClearLines wid (droppedAbove ++ below)
            in
            ( newGrid, lines + 1 )


size : Grid a -> ( Int, Int )
size grid =
    let
        ( x, y ) =
            List.unzip (List.map .pos grid)

        dimension d =
            Maybe.withDefault 0 (List.maximum (List.map (\a -> a + 1) d))
    in
    ( dimension x, dimension y )


centerOfMass : Grid a -> ( Int, Int )
centerOfMass grid =
    let
        len =
            toFloat (List.length grid)

        ( x, y ) =
            List.unzip (List.map .pos grid)
    in
    ( round (toFloat (List.sum x) / len)
    , round (toFloat (List.sum y) / len)
    )


gridDecode : Decoder a -> Decoder (Grid a)
gridDecode cell =
    Decode.list
        (Decode.map2
            Cell
            (Decode.field "val" cell)
            (Decode.field "pos" (Decode.map2 (\a b -> ( a, b )) (Decode.index 0 Decode.int) (Decode.index 1 Decode.int)))
        )


gridEncode : (a -> Value) -> Grid a -> Value
gridEncode cell grid =
    let
        encodeCell { val, pos } =
            Encode.object
                [ ( "pos"
                  , Encode.list Encode.int
                        [ Tuple.first pos
                        , Tuple.second pos
                        ]
                  )
                , ( "val", cell val )
                ]
    in
    Encode.list encodeCell grid


initPosition : Int -> Grid a -> ( Int, Int )
initPosition wid grid =
    let
        ( x, _ ) =
            centerOfMass grid

        y =
            Maybe.withDefault 0 (List.maximum (List.map (Tuple.second << .pos) grid))
    in
    ( wid // 2 - x, -y - 1 )


----------
-- View --
----------

onTouchStart : Msg -> Html.Attribute Msg
onTouchStart msg =
    on "touchstart" (Decode.succeed msg)


onTouchEnd : Msg -> Html.Attribute Msg
onTouchEnd msg =
    on "touchend" (Decode.succeed msg)


renderBox : (Color -> Color) -> Color -> ( Int, Int ) -> Html Msg
renderBox fun c ( x, y ) =
    Svg.rect
        [ SvgAttrs.width (String.fromInt 30)
        , SvgAttrs.height (String.fromInt 30)
        , SvgAttrs.fill (toString (fun c))
        , SvgAttrs.stroke (toString (fun c))
        , SvgAttrs.strokeWidth "0.5"
        , SvgAttrs.x (String.fromInt (x * 30))
        , SvgAttrs.y (String.fromInt (y * 30))
        ]
        []


renderNext : Grid Color -> Html Msg
renderNext grid =
    let
        ( width, height ) =
            size grid
    in
    grid
        |> mapToList
            (renderBox (always (rgb 236 240 241)))
        |> Svg.svg
            [ SvgAttrs.width (String.fromInt (width * 30))
            , SvgAttrs.height (String.fromInt (height * 30))
            ]


renderWell : Model -> Html Msg
renderWell { width, height, active, grid, position } =
    grid
        |> stamp (Tuple.first position) (floor (Tuple.second position)) active
        |> mapToList (renderBox identity)
        |> (::)
            (Svg.rect
                [ SvgAttrs.width (String.fromInt (width * 30))
                , SvgAttrs.height (String.fromInt (height * 30))
                , SvgAttrs.fill "rgb(236, 240, 241)"
                ]
                []
            )
        |> Svg.svg
            [ SvgAttrs.width (String.fromInt (width * 30))
            , SvgAttrs.height (String.fromInt (height * 30))
            ]


renderTitle : String -> Html Msg
renderTitle txt =
    div
        [ style "color" "#34495f"
        , style "font-size" "40px"
        , style "line-height" "60px"
        , style "margin" "30px 0 0"
        ]
        [ text txt ]


renderLabel : String -> Html Msg
renderLabel txt =
    div
        [ style "color" "#bdc3c7"
        , style "font-weight" "300"
        , style "line-height" "1"
        , style "margin" "30px 0 0"
        ]
        [ text txt ]


renderCount : Int -> Html Msg
renderCount n =
    div
        [ style "color" "#3993d0"
        , style "font-size" "30px"
        , style "line-height" "1"
        , style "margin" "5px 0 0"
        ]
        [ text (String.fromInt n) ]


renderGameButton : State -> Html Msg
renderGameButton state =
    let
        ( txt, msg ) =
            case state of
                Stopped ->
                    ( "New game", Start )

                Playing ->
                    ( "Pause", Pause )

                Paused ->
                    ( "Resume", Resume )
    in
    button
        [ style "background" "#34495f"
        , style "border" "0"
        , style "bottom" "30px"
        , style "color" "#fff"
        , style "cursor" "pointer"
        , style "display" "block"
        , style "font-family" "Helvetica, Arial, sans-serif"
        , style "font-size" "18px"
        , style "font-weight" "300"
        , style "height" "60px"
        , style "left" "30px"
        , style "line-height" "60px"
        , style "outline" "none"
        , style "padding" "0"
        , style "position" "absolute"
        , style "width" "120px"
        , onClick msg
        ]
        [ text txt ]


renderPanel : Model -> Html Msg
renderPanel { score, lines, next, state } =
    div
        [ style "bottom" "80px"
        , style "color" "#34495f"
        , style "font-family" "Helvetica, Arial, sans-serif"
        , style "font-size" "14px"
        , style "left" "300px"
        , style "padding" "0 30px"
        , style "position" "absolute"
        , style "right" "0"
        , style "top" "0"
        ]
        [ renderTitle "Flatris"
        , renderLabel "Score"
        , renderCount score
        , renderLabel "Lines Cleared"
        , renderCount lines
        , renderLabel "Next Shape"
        , div
            [ style "margin-top" "10px"
            , style "position" "relative"
            ]
            [ renderNext next ]
        , renderGameButton state
        ]


renderControlButton : String -> List (Html.Attribute Msg) -> Html Msg
renderControlButton txt attrs =
    div
        ([ style "background" "#ecf0f1"
         , style "border" "0"
         , style "color" "#34495f"
         , style "cursor" "pointer"
         , style "text-align" "center"
         , style "-webkit-user-select" "none"
         , style "display" "block"
         , style "float" "left"
         , style "font-family" "Helvetica, Arial, sans-serif"
         , style "font-size" "24px"
         , style "font-weight" "300"
         , style "height" "60px"
         , style "line-height" "60px"
         , style "margin" "20px 20px 0 0"
         , style "outline" "none"
         , style "padding" "0"
         , style "width" "60px"
         ]
            ++ attrs
        )
        [ text txt ]


renderControls : Html Msg
renderControls =
    div
        [ style "height" "80px"
        , style "left" "0"
        , style "position" "absolute"
        , style "top" "600px"
        ]
        [ renderControlButton "↻"
            [ onMouseDown (Rotate True)
            , onMouseUp (Rotate False)
            , onTouchStart (Rotate True)
            , onTouchEnd (Rotate False)
            ]
        , renderControlButton "←"
            [ onMouseDown (MoveLeft True)
            , onMouseUp (MoveLeft False)
            , onTouchStart (MoveLeft True)
            , onTouchEnd (MoveLeft False)
            ]
        , renderControlButton "→"
            [ onMouseDown (MoveRight True)
            , onMouseUp (MoveRight False)
            , onTouchStart (MoveRight True)
            , onTouchEnd (MoveRight False)
            ]
        , renderControlButton "↓"
            [ onMouseDown (Accelerate True)
            , onMouseUp (Accelerate False)
            , onTouchStart (Accelerate True)
            , onTouchEnd (Accelerate False)
            ]
        ]


renderInfo : State -> Html Msg
renderInfo state =
    div
        [ style "background" "rgba(236, 240, 241, 0.85)"
        , style "color" "#34495f"
        , style "font-family" "Helvetica, Arial, sans-serif"
        , style "font-size" "18px"
        , style "height" "600px"
        , style "left" "0"
        , style "line-height" "1.5"
        , style "padding" "0 15px"
        , style "position" "absolute"
        , style "top" "0"
        , style "width" "270px"
        , style "display"
            (if state == Playing then
                "none"

             else
                "block"
            )
        ]
        []


pixelWidth : Float
pixelWidth =
    480


pixelHeight : Float
pixelHeight =
    680


view : Model -> Html Msg
view model =
    let
        ( w, h ) =
            model.size

        r =
            if w / h > pixelWidth / pixelHeight then
                min 1 (h / pixelHeight)

            else
                min 1 (w / pixelWidth)
    in
    div
        [ onTouchEnd UnlockButtons
        , onMouseUp UnlockButtons
        , style "width" "100%"
        , style "height" "100%"
        , style "position" "absolute"
        , style "left" "0"
        , style "top" "0"
        ]
        [ div
            [ style "width" (String.fromFloat pixelWidth ++ "px")
            , style "height" (String.fromFloat pixelHeight ++ "px")
            , style "position" "absolute"
            , style "left" (String.fromFloat ((w - pixelWidth * r) / 2) ++ "px")
            , style "top" (String.fromFloat ((h - pixelHeight * r) / 2) ++ "px")
            , style "transform-origin" "0 0"
            , style "transform" ("scale(" ++ String.fromFloat r ++ ")")
            ]
            [ renderWell model
            , renderControls
            , renderPanel model
            , renderInfo model.state
            ]
        ]

Listing: one-two-one

Het verradelijk verslavende blok-kantelspel, naar voorbeeld van het oudere Flash-spel Bloxorz (vroeger waren veel online spellen gemaakt in Flash), gemaakt door Wiktor Toporek. De uitgeklede versie hieronder is aangepast zodat het in één geheel in Try Elm kan worden gekopieerd en worden gespeeld.

Packages

Pas op: Omdat dit spel gebruikt maakt van 3D, moet je een aantal "packages" installeren. Dat installeren kan niet in de CompuRob workshop, maar wel bij Try Elm.

Je kunt packages installeren door op "packages" te klikken en in het zoekveld steeds de naam van een package te typen, en dan op het plusje naast de juiste te klikken. De benodigde packages zijn:

  • elm/json
  • avh4/elm-color
  • ianmackenzie/elm-units (je kunt elm-units typen, maar pas op dat je de juiste hebt, met ianmackenzie)
  • ianmackenzie/elm-geometry (je kunt elm-geometry typen)
  • ianmackenzie/elm-3d-scene
  • ianmackenzie/elm-3d-camera

Als je het blok wilt bewegen met de pijltjestoetsen op een toetsenbord, dan moet je eerst even in het spel klikken; daarna luistert het spel naar de toetsen.

Challenges (vragen en uitdagingen)

  • Kun je de kleur van het blok aanpassen?
  • Maak eens wat extra gaten in de vloer van bijvoorbeeld level 1..
  • Hoe zou je het blok op een andere tegel kunnen laten vallen in het begin van het level?
  • Snap je hoe je een brug kan laten open- of dichtklappen?
  • Hoe zou je een nieuwe soort tegel aan de vloeren kunnen toevoegen? In welke gedeeltes van de code moet dan wat veranderd worden?

De code

module Main exposing (main)

import Angle
import Array exposing (Array)
import Axis3d
import Block3d exposing (Block3d)
import Browser
import Browser.Dom
import Browser.Events
import Camera3d
import Color exposing (Color)
import Dict exposing (Dict)
import Direction3d
import Html exposing (Html)
import Html.Attributes as Html
import Html.Events as Event
import Json.Decode as Decode
import Length exposing (Length)
import Pixels
import Point3d
import Scene3d
import Scene3d.Material as Material
import Task
import Vector3d
import Viewpoint3d



-- SETTINGS --


tileSize =
    Length.centimeters 1


tileSizeCm =
    Length.inCentimeters tileSize


playerWidthCm =
    1 * tileSizeCm


playerHeightCm =
    2 * tileSizeCm


animationSpeed =
    1 / 250



-- THE GAME --


type alias Model =
    { screen : { width : Int, height : Int }
    , player : Player
    , level : Level
    , nextLevels : List Level
    , control : Maybe Direction
    }


type Direction
    = Up
    | Right
    | Left
    | Down


type Msg
    = ViewportSize ( Int, Int )
    | AnimationTick Float
    | KeyDown String
    | KeyUp String


init : () -> ( Model, Cmd Msg )
init () =
    ( { screen = { width = 0, height = 0 }
      , player = playerInit (getStartingPosition level1)
      , level = level1
      , nextLevels = List.drop 1 levels
      , control = Nothing
      }
    , Browser.Dom.getViewport
        |> Task.perform (\{ viewport } -> ViewportSize ( floor viewport.width, floor viewport.height ))
    )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ViewportSize ( width, height ) ->
            ( { model | screen = { width = width, height = height } }, Cmd.none )

        AnimationTick delta ->
            let
                controlledPlayer =
                    controlPlayer model.control model.player

                ( animatedPlayer, playerCmd ) =
                    controlledPlayer
                        |> playerUpdate delta model.level

                ( updatedPlayer, interactionMsg ) =
                    animatedPlayer
                        |> interact model.level

                updatedLevel =
                    model.level
                        |> levelUpdate delta
            in
            case interactionMsg of
                InternalUpdate ->
                    ( { model | player = updatedPlayer, level = updatedLevel }, playerCmd )

                FinishedLevel ->
                    case model.nextLevels of
                        [] ->
                            ( { model
                                | player = playerInit (getStartingPosition level1)
                                , level = level1
                                , nextLevels = List.drop 1 levels
                              }
                            , playerCmd
                            )

                        nextLevel :: otherLevels ->
                            ( { model
                                | player = playerInit (getStartingPosition nextLevel)
                                , level = nextLevel
                                , nextLevels = otherLevels
                              }
                            , playerCmd
                            )

                PushDownTile zOffset ->
                    ( { model
                        | player = updatedPlayer
                        , level = shiftTile (getPosition updatedPlayer) zOffset model.level
                      }
                    , playerCmd
                    )

                TriggerActions actions ->
                    let
                        previousInteractionMsg =
                            model.player
                                |> interact model.level
                                |> Tuple.second

                        ( actionUpdatedLevel, actionCommand ) =
                            case previousInteractionMsg of
                                TriggerActions _ ->
                                    ( updatedLevel, playerCmd )

                                _ ->
                                    -- Trigger only if not trigged in last update already
                                    triggerActions actions updatedLevel
                    in
                    ( { model
                        | player = updatedPlayer
                        , level = actionUpdatedLevel
                      }
                    , actionCommand
                    )

                RestartedLevel ->
                    ( { model
                        | player = updatedPlayer
                        , level = restart model.level
                      }
                    , playerCmd
                    )

                _ ->
                    ( { model
                        | player = updatedPlayer
                        , level = updatedLevel
                      }
                    , playerCmd
                    )

        KeyDown key ->
            ( key
                |> keyToDirection
                |> Maybe.map (\direction -> { model | control = Just direction })
                |> Maybe.withDefault model
            , Cmd.none
            )

        KeyUp key ->
            ( if keyToDirection key == model.control then
                { model | control = Nothing }

              else
                model
            , Cmd.none
            )


subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ case ( model.player, model.control, levelIsBusy model.level ) of
            ( Cuboid Standing _, Nothing, False ) ->
                Sub.none

            ( Cuboid (Lying _) _, Nothing, False ) ->
                Sub.none

            _ ->
                Browser.Events.onAnimationFrameDelta AnimationTick
        , Browser.Events.onKeyDown (Decode.map KeyDown keyDecoder)
        , Browser.Events.onKeyUp (Decode.map KeyUp keyDecoder)
        ]


keyDecoder : Decode.Decoder String
keyDecoder =
    Decode.field "key" Decode.string


keyToDirection : String -> Maybe Direction
keyToDirection string =
    case string of
        "ArrowLeft" ->
            Just Left

        "ArrowRight" ->
            Just Right

        "ArrowUp" ->
            Just Up

        "ArrowDown" ->
            Just Down

        _ ->
            Nothing


controlPlayer : Maybe Direction -> Player -> Player
controlPlayer control player =
    case control of
        Just direction ->
            move direction player

        Nothing ->
            player


main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }


view : Model -> Html Msg
view { screen, player, level, control } =
    let
        zoomOut =
            max (800 / toFloat screen.width) 1

        camera =
            Camera3d.perspective
                { viewpoint =
                    Viewpoint3d.orbitZ
                        { focalPoint = Point3d.centimeters 5 5 0
                        , azimuth = Angle.degrees 35
                        , elevation = Angle.degrees 25
                        , distance = Length.centimeters (15 * zoomOut)
                        }
                , verticalFieldOfView = Angle.degrees 30
                }

        info =
            String.join "; "
                [ playerInfo, controlInfo, levelInfo ]

        playerInfo =
            "player = "
                ++ (case player of
                        Cuboid Standing _ ->
                            "standing"

                        Cuboid (Lying _) _ ->
                            "lying"

                        Cuboid (KnockingOver _ _) _ ->
                            "being knocked over"

                        Cuboid (GettingUp _ _) _ ->
                            "getting up"

                        Cuboid (Rolling _ _) _ ->
                            "rolling"

                        Cuboid (SlideIn _) _ ->
                            "sliding in"

                        Cuboid (FallingUnbalanced _ _) _ ->
                            "falling unbalanced"

                        Cuboid (FallingInHorizontalOrientation _ _) _ ->
                            "falling horizontally"

                        Cuboid (FallingInVerticalOrientation _) _ ->
                            "falling vertically"

                        Cuboid (FallingFromTheSky _) _ ->
                            "falling from the sky"

                        Cuboid (FallingWithTheFloor _) _ ->
                            "falling with the floor"
                   )

        controlInfo =
            "control = "
                ++ (case control of
                        Nothing ->
                            "stay"

                        Just Left ->
                            "left"

                        Just Right ->
                            "right"

                        Just Up ->
                            "up"

                        Just Down ->
                            "down"
                   )

        levelInfo =
            "level = "
                ++ (if levelIsBusy level then
                        "in action"

                    else
                        "idle"
                   )
    in
    Html.div []
        [ Html.div
            [ Html.style "position" "absolute"
            , Html.style "top" "0"
            , Html.style "left" "0"
            , Html.style "right" "0"
            , Html.style "font-size" "25px"
            , Html.style "text-align" "center"
            , Html.style "white-space" "pre"
            ]
            [ Html.text info ]
        , Scene3d.sunny
            { entities = [ playerView player, levelView level ]
            , camera = camera
            , upDirection = Direction3d.z
            , sunlightDirection = Direction3d.xz (Angle.degrees -120)
            , background = Scene3d.transparentBackground
            , clipDepth = Length.centimeters 1
            , shadows = True
            , dimensions = ( Pixels.int screen.width, Pixels.int screen.height )
            }
        , mobileControls player
        ]


onTouchStart msg =
    Event.on "touchstart" (Decode.succeed msg)


onTouchEnd msg =
    Event.on "touchend" (Decode.succeed msg)


mobileControls : Player -> Html Msg
mobileControls player =
    Html.div
        [ Html.style "position" "absolute"
        , Html.style "right" "0"
        , Html.style "bottom" "0"
        , Html.style "width" "30vw"
        , Html.style "height" "30vw"
        , Html.style "font-size" "10vw"
        ]
        [ Html.div
            [ onTouchStart (KeyDown "ArrowUp")
            , onTouchEnd (KeyUp "ArrowUp")
            , Html.style "position" "absolute"
            , Html.style "top" "0"
            , Html.style "left" "10vw"
            ]
            [ Html.text "⬆️️" ]
        , Html.div
            [ onTouchStart (KeyDown "ArrowLeft")
            , onTouchEnd (KeyUp "ArrowLeft")
            , Html.style "position" "absolute"
            , Html.style "top" "9vw"
            , Html.style "left" "0"
            ]
            [ Html.text "⬅️" ]
        , Html.div
            [ onTouchStart (KeyDown "ArrowRight")
            , onTouchEnd (KeyUp "ArrowRight")
            , Html.style "position" "absolute"
            , Html.style "top" "9vw"
            , Html.style "right" "0"
            ]
            [ Html.text "➡️️" ]
        , Html.div
            [ onTouchStart (KeyDown "ArrowDown")
            , onTouchEnd (KeyUp "ArrowDown")
            , Html.style "position" "absolute"
            , Html.style "bottom" "0"
            , Html.style "left" "10vw"
            ]
            [ Html.text "⬇️️️" ]
        ]



-- PLAYER --


type Player
    = Cuboid BlockAnimationState ( Int, Int )


type BlockAnimationState
    = Standing
    | Lying Direction
    | KnockingOver Direction Float
    | GettingUp Direction Float
    | Rolling Direction Float
    | SlideIn Float
    | FallingUnbalanced Direction Float
    | FallingInHorizontalOrientation Direction Float
    | FallingInVerticalOrientation { zOffset : Length, progress : Float }
    | FallingFromTheSky Float
    | FallingWithTheFloor Float


type Cube
    = Cube CubeAnimationState ( Int, Int )


type CubeAnimationState
    = Stable
    | Rotating Direction Float
    | Falling Float


type InteractionMsg
    = InternalUpdate
    | FinishedLevel
    | PushDownTile Length
    | RestartedLevel
    | TriggerActions (List TriggerAction)
    | EmitSound String


playerInit : ( Int, Int ) -> Player
playerInit ( x, y ) =
    Cuboid (FallingFromTheSky 0) ( x, y )


move : Direction -> Player -> Player
move direction player =
    case player of
        Cuboid orientation ( x, y ) ->
            case ( orientation, direction ) of
                ( Standing, fallDirection ) ->
                    Cuboid (KnockingOver fallDirection 0) ( x, y )

                -- Lying Up
                ( Lying Up, Left ) ->
                    Cuboid (Rolling Left 0) ( x, y )

                ( Lying Up, Right ) ->
                    Cuboid (Rolling Right 0) ( x, y )

                ( Lying Up, Up ) ->
                    Cuboid (GettingUp Up 0) ( x - 2, y )

                ( Lying Up, Down ) ->
                    Cuboid (GettingUp Down 0) ( x + 1, y )

                -- Lying Right
                ( Lying Right, Up ) ->
                    Cuboid (Rolling Up 0) ( x, y )

                ( Lying Right, Down ) ->
                    Cuboid (Rolling Down 0) ( x, y )

                ( Lying Right, Left ) ->
                    Cuboid (GettingUp Left 0) ( x, y - 1 )

                ( Lying Right, Right ) ->
                    Cuboid (GettingUp Right 0) ( x, y + 2 )

                -- Lying Down
                ( Lying Down, Up ) ->
                    Cuboid (GettingUp Up 0) ( x - 1, y )

                ( Lying Down, Down ) ->
                    Cuboid (GettingUp Down 0) ( x + 2, y )

                ( Lying Down, Left ) ->
                    Cuboid (Rolling Left 0) ( x + 1, y )

                ( Lying Down, Right ) ->
                    Cuboid (Rolling Right 0) ( x + 1, y )

                -- Lying Left
                ( Lying Left, Up ) ->
                    Cuboid (Rolling Up 0) ( x, y - 1 )

                ( Lying Left, Down ) ->
                    Cuboid (Rolling Down 0) ( x, y - 1 )

                ( Lying Left, Left ) ->
                    Cuboid (GettingUp Left 0) ( x, y - 2 )

                ( Lying Left, Right ) ->
                    Cuboid (GettingUp Right 0) ( x, y + 1 )

                -- Player already in motion (ignore)
                _ ->
                    Cuboid orientation ( x, y )


playerView : Player -> Scene3d.Entity coordinates
playerView player =
    player
        |> playerBlock
        |> Scene3d.blockWithShadow (Material.metal { baseColor = Color.orange, roughness = 2.5 })


playerBlock : Player -> Block3d Length.Meters coordinates
playerBlock player =
    case player of
        Cuboid orientation ( x, y ) ->
            let
                positionX =
                    toFloat x * tileSizeCm

                positionY =
                    toFloat y * tileSizeCm

                block =
                    Block3d.with
                        { x1 = Length.centimeters 0
                        , x2 = Length.centimeters playerWidthCm
                        , y1 = Length.centimeters 0
                        , y2 = Length.centimeters playerWidthCm
                        , z1 = Length.centimeters 0
                        , z2 = Length.centimeters playerHeightCm
                        }
            in
            (case orientation of
                -- +
                Standing ->
                    block

                -- |
                -- 0
                Lying Up ->
                    block
                        |> Block3d.rotateAround topAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.x tileSize

                -- 0-
                Lying Right ->
                    block
                        |> Block3d.rotateAround rightAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.negativeY tileSize

                -- 0
                -- |
                Lying Down ->
                    block
                        |> Block3d.rotateAround bottomAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.negativeX tileSize

                -- -0
                Lying Left ->
                    block
                        |> Block3d.rotateAround leftAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.y tileSize

                KnockingOver Up progress ->
                    block
                        |> Block3d.rotateAround topAxis (Angle.degrees (progress * 90))

                KnockingOver Right progress ->
                    block
                        |> Block3d.rotateAround rightAxis (Angle.degrees (progress * 90))

                KnockingOver Down progress ->
                    block
                        |> Block3d.rotateAround bottomAxis (Angle.degrees (progress * 90))

                KnockingOver Left progress ->
                    block
                        |> Block3d.rotateAround leftAxis (Angle.degrees (progress * 90))

                GettingUp Up progress ->
                    block
                        |> Block3d.rotateAround bottomAxis (Angle.degrees ((1 - progress) * 90))

                GettingUp Right progress ->
                    block
                        |> Block3d.rotateAround leftAxis (Angle.degrees ((1 - progress) * 90))

                GettingUp Down progress ->
                    block
                        |> Block3d.rotateAround topAxis (Angle.degrees ((1 - progress) * 90))

                GettingUp Left progress ->
                    block
                        |> Block3d.rotateAround rightAxis (Angle.degrees ((1 - progress) * 90))

                Rolling Left progress ->
                    block
                        |> Block3d.rotateAround topAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.x tileSize
                        |> Block3d.rotateAround leftAxis (Angle.degrees (progress * 90))

                Rolling Right progress ->
                    block
                        |> Block3d.rotateAround topAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.x tileSize
                        |> Block3d.rotateAround rightAxis (Angle.degrees (progress * 90))

                Rolling Up progress ->
                    block
                        |> Block3d.rotateAround rightAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.negativeY tileSize
                        |> Block3d.rotateAround topAxis (Angle.degrees (progress * 90))

                Rolling Down progress ->
                    block
                        |> Block3d.rotateAround rightAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.negativeY tileSize
                        |> Block3d.rotateAround bottomAxis (Angle.degrees (progress * 90))

                SlideIn progress ->
                    Block3d.with
                        { x1 = Length.centimeters 0
                        , x2 = Length.centimeters playerWidthCm
                        , y1 = Length.centimeters 0
                        , y2 = Length.centimeters playerWidthCm
                        , z1 = Length.centimeters 0
                        , z2 = Length.centimeters (playerHeightCm - (progress * progress * 2))
                        }

                FallingUnbalanced Left progress ->
                    block
                        |> Block3d.rotateAround leftAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.y tileSize
                        |> Block3d.rotateAround leftAxis (Angle.degrees (90 * progress))

                FallingUnbalanced Right progress ->
                    block
                        |> Block3d.rotateAround rightAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.negativeY tileSize
                        |> Block3d.rotateAround rightAxis (Angle.degrees (90 * progress))

                FallingUnbalanced Up progress ->
                    block
                        |> Block3d.rotateAround topAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.x tileSize
                        |> Block3d.rotateAround topAxis (Angle.degrees (90 * progress))

                FallingUnbalanced Down progress ->
                    block
                        |> Block3d.rotateAround bottomAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.negativeX tileSize
                        |> Block3d.rotateAround bottomAxis (Angle.degrees (90 * progress))

                FallingInVerticalOrientation { zOffset, progress } ->
                    block
                        |> Block3d.translateIn Direction3d.negativeZ zOffset
                        |> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)

                FallingInHorizontalOrientation Left progress ->
                    block
                        |> Block3d.rotateAround leftAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.y tileSize
                        |> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)

                FallingInHorizontalOrientation Up progress ->
                    block
                        |> Block3d.rotateAround topAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.x tileSize
                        |> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)

                FallingInHorizontalOrientation Right progress ->
                    block
                        |> Block3d.rotateAround rightAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.negativeY tileSize
                        |> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)

                FallingInHorizontalOrientation Down progress ->
                    block
                        |> Block3d.rotateAround bottomAxis (Angle.degrees 90)
                        |> Block3d.translateIn Direction3d.negativeX tileSize
                        |> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)

                FallingFromTheSky progress ->
                    block
                        |> Block3d.translateIn Direction3d.z (Length.centimeters ((1 - progress) * 10))

                FallingWithTheFloor progress ->
                    block
                        |> Block3d.translateIn Direction3d.negativeZ (Length.centimeters progress)
            )
                |> Block3d.translateBy
                    (Vector3d.centimeters positionX positionY 0)


topAxis =
    Axis3d.through (Point3d.centimeters 0 0 0) Direction3d.negativeY


rightAxis =
    Axis3d.through (Point3d.centimeters 0 playerWidthCm 0) Direction3d.negativeX


bottomAxis =
    Axis3d.through (Point3d.centimeters playerWidthCm 0 0) Direction3d.y


leftAxis =
    Axis3d.through (Point3d.centimeters 0 0 0) Direction3d.x


playerUpdate : Float -> Level -> Player -> ( Player, Cmd msg )
playerUpdate delta level player =
    case player of
        Cuboid (KnockingOver direction progress) ( x, y ) ->
            let
                newProgress =
                    progress + delta * animationSpeed

                newPlayer =
                    if newProgress >= 1 then
                        Cuboid
                            (Lying direction)
                            (case direction of
                                Up ->
                                    ( x - 1, y )

                                Right ->
                                    ( x, y + 1 )

                                Left ->
                                    ( x, y - 1 )

                                Down ->
                                    ( x + 1, y )
                            )

                    else
                        Cuboid (KnockingOver direction newProgress) ( x, y )
            in
            ( newPlayer, Cmd.none )

        Cuboid (GettingUp direction progress) ( x, y ) ->
            let
                newProgress =
                    progress + delta * animationSpeed

                newPlayer =
                    if newProgress >= 1 then
                        Cuboid Standing ( x, y )

                    else
                        Cuboid (GettingUp direction newProgress) ( x, y )
            in
            ( newPlayer, Cmd.none )

        Cuboid (Rolling direction progress) ( x, y ) ->
            let
                newProgress =
                    progress + delta * animationSpeed

                newPlayer =
                    if newProgress >= 1 then
                        case direction of
                            Left ->
                                Cuboid (Lying Up) ( x, y - 1 )

                            Right ->
                                Cuboid (Lying Up) ( x, y + 1 )

                            Up ->
                                Cuboid (Lying Right) ( x - 1, y )

                            Down ->
                                Cuboid (Lying Right) ( x + 1, y )

                    else
                        Cuboid (Rolling direction newProgress) ( x, y )
            in
            ( newPlayer, Cmd.none )

        Cuboid (SlideIn progress) ( x, y ) ->
            let
                newProgress =
                    min (progress + delta * animationSpeed * 0.5) 1
            in
            ( Cuboid (SlideIn newProgress) ( x, y ), Cmd.none )

        Cuboid (FallingUnbalanced direction progress) ( x, y ) ->
            let
                newProgress =
                    min (progress + delta * animationSpeed) 1
            in
            if newProgress == 1 then
                ( Cuboid (FallingInVerticalOrientation { zOffset = Length.centimeters (0.5 * playerHeightCm), progress = 0 })
                    (case direction of
                        Left ->
                            ( x, y - 1 )

                        Right ->
                            ( x, y + 1 )

                        Down ->
                            ( x + 1, y )

                        Up ->
                            ( x - 1, y )
                    )
                , Cmd.none
                )

            else
                ( Cuboid (FallingUnbalanced direction newProgress) ( x, y ), Cmd.none )

        Cuboid (FallingInVerticalOrientation { zOffset, progress }) ( x, y ) ->
            let
                newProgress =
                    progress + delta * animationSpeed * min (progress + 1) 5
            in
            ( Cuboid (FallingInVerticalOrientation { zOffset = zOffset, progress = newProgress }) ( x, y ), Cmd.none )

        Cuboid (FallingInHorizontalOrientation direction progress) ( x, y ) ->
            let
                newProgress =
                    progress + delta * animationSpeed * min (progress + 1) 5
            in
            ( Cuboid (FallingInHorizontalOrientation direction newProgress) ( x, y ), Cmd.none )

        Cuboid (FallingFromTheSky progress) ( x, y ) ->
            let
                newProgress =
                    min (progress + delta * animationSpeed * 0.3) 1
            in
            if newProgress == 1 then
                ( Cuboid Standing ( x, y ), Cmd.none )

            else
                ( Cuboid (FallingFromTheSky newProgress) ( x, y ), Cmd.none )

        Cuboid (FallingWithTheFloor progress) ( x, y ) ->
            let
                newProgress =
                    progress + delta * animationSpeed * min (progress + 1) 5
            in
            ( Cuboid (FallingWithTheFloor newProgress) ( x, y ), Cmd.none )

        plr ->
            ( plr, Cmd.none )


getPosition : Player -> ( Int, Int )
getPosition (Cuboid _ position) =
    position


fall : Maybe Direction -> Player -> Player
fall unbalancedDirection player =
    case player of
        Cuboid state ( x, y ) ->
            case ( unbalancedDirection, state ) of
                ( _, Standing ) ->
                    Cuboid (FallingInVerticalOrientation { zOffset = Length.centimeters 0, progress = 0 }) ( x, y )

                ( Just Left, Lying Left ) ->
                    Cuboid (FallingUnbalanced Left 0) ( x, y )

                ( Just Left, Lying Right ) ->
                    Cuboid (FallingUnbalanced Left 0) ( x, y + 1 )

                ( Just Right, Lying Left ) ->
                    Cuboid (FallingUnbalanced Right 0) ( x, y + 1 )

                ( Just Right, Lying Right ) ->
                    Cuboid (FallingUnbalanced Right 0) ( x, y )

                ( Just Up, Lying Up ) ->
                    Cuboid (FallingUnbalanced Up 0) ( x, y )

                ( Just Up, Lying Down ) ->
                    Cuboid (FallingUnbalanced Up 0) ( x + 1, y )

                ( Just Down, Lying Up ) ->
                    Cuboid (FallingUnbalanced Down 0) ( x - 1, y )

                ( Just Down, Lying Down ) ->
                    Cuboid (FallingUnbalanced Down 0) ( x, y )

                ( Nothing, Lying direction ) ->
                    Cuboid (FallingInHorizontalOrientation direction 0) ( x, y )

                _ ->
                    Cuboid state ( x, y )


occupiedPositions : Player -> List ( Int, Int )
occupiedPositions player =
    case player of
        Cuboid Standing ( x, y ) ->
            [ ( x, y ) ]

        Cuboid (Lying Left) ( x, y ) ->
            [ ( x, y - 1 ), ( x, y ) ]

        Cuboid (Lying Up) ( x, y ) ->
            [ ( x - 1, y ), ( x, y ) ]

        Cuboid (Lying Right) ( x, y ) ->
            [ ( x, y ), ( x, y + 1 ) ]

        Cuboid (Lying Down) ( x, y ) ->
            [ ( x, y ), ( x + 1, y ) ]

        _ ->
            []


occupiedTiles : Player -> Level -> List LevelTile
occupiedTiles player level =
    player
        |> occupiedPositions
        |> List.map (getTileAt level)


unstablePosition : Player -> Level -> Bool
unstablePosition player level =
    occupiedTiles player level
        |> List.any ((==) Empty)


interact : Level -> Player -> ( Player, InteractionMsg )
interact level player =
    let
        playerOccupiedTiles =
            occupiedTiles player level
    in
    case player of
        Cuboid state ( x, y ) ->
            case ( playerOccupiedTiles, state ) of
                -- Falling off the stage
                ( [ Empty ], _ ) ->
                    ( fall Nothing player, EmitSound "fall" )

                ( [ Empty, Empty ], _ ) ->
                    ( fall Nothing player, EmitSound "fall" )

                ( [ Empty, _ ], Lying Left ) ->
                    ( fall (Just Left) player, EmitSound "fall" )

                ( [ Empty, _ ], Lying Right ) ->
                    ( fall (Just Left) player, EmitSound "fall" )

                ( [ Empty, _ ], Lying Up ) ->
                    ( fall (Just Up) player, EmitSound "fall" )

                ( [ Empty, _ ], Lying Down ) ->
                    ( fall (Just Up) player, EmitSound "fall" )

                ( [ _, Empty ], Lying Left ) ->
                    ( fall (Just Right) player, EmitSound "fall" )

                ( [ _, Empty ], Lying Right ) ->
                    ( fall (Just Right) player, EmitSound "fall" )

                ( [ _, Empty ], Lying Up ) ->
                    ( fall (Just Down) player, EmitSound "fall" )

                ( [ _, Empty ], Lying Down ) ->
                    ( fall (Just Down) player, EmitSound "fall" )

                -- Stomp on rusty tile
                ( [ RustyFloor ], Standing ) ->
                    ( Cuboid (FallingWithTheFloor 0) ( x, y ), EmitSound "break-tile" )

                ( [], FallingWithTheFloor progress ) ->
                    if progress >= 30 then
                        ( playerInit (getStartingPosition level), RestartedLevel )

                    else
                        ( player, PushDownTile (Length.centimeters progress) )

                -- Success
                ( [ Finish ], _ ) ->
                    ( Cuboid (SlideIn 0) ( x, y ), EmitSound "slide-in" )

                ( [], SlideIn progress ) ->
                    ( player
                    , if progress >= 1 then
                        FinishedLevel

                      else
                        InternalUpdate
                    )

                -- Restart
                ( [], FallingInHorizontalOrientation _ progress ) ->
                    if progress >= 30 then
                        ( playerInit (getStartingPosition level), RestartedLevel )

                    else
                        ( player, InternalUpdate )

                ( [], FallingInVerticalOrientation { progress } ) ->
                    if progress >= 30 then
                        ( playerInit (getStartingPosition level), RestartedLevel )

                    else
                        ( player, InternalUpdate )

                -- Trigger activation
                ( [ Trigger _ actions, _ ], Lying _ ) ->
                    ( player, TriggerActions actions )

                ( [ _, Trigger _ actions ], Lying _ ) ->
                    ( player, TriggerActions actions )

                ( [ Trigger _ actions ], Standing ) ->
                    ( player, TriggerActions actions )

                -- Nothing to be done
                _ ->
                    ( player, InternalUpdate )



-- LEVELS --


levels =
    [ level1, level2, level3 ]


level1 =
    levelFromData
        [ [ Floor, Floor, Floor, Empty, Empty, Empty, Floor, Floor, Floor ]
        , [ Floor, Floor, Floor, Floor, Floor, Floor, Floor, Finish, Floor ]
        , [ Empty, Empty, Empty, Floor, Floor, Floor, Floor, Floor, Floor ]
        , []
        ]
        ( 1, 1 )


level2 =
    levelFromData
        [ [ Floor, Floor, Floor ]
        , [ Floor, Floor, Floor, Floor, Floor, Floor ]
        , [ Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor ]
        , [ Empty, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor ]
        , [ Empty, Empty, Empty, Empty, Empty, Floor, Floor, Finish, Floor, Floor ]
        , [ Empty, Empty, Empty, Empty, Empty, Empty, Floor, Floor, Floor ]
        , []
        ]
        ( 1, 1 )


level3 =
    levelFromData
        [ [ Floor, Floor, Floor, Floor, Empty, Empty, Floor, Floor, Floor ]
        , [ Floor
          , Floor
          , Trigger Color.red
                [ ToggleTriggerColor ( 1, 2 ) Color.green
                , ToggleBridge ( 3, 4 )
                , ToggleBridge ( 3, 5 )
                ]
          , Floor
          , Empty
          , Empty
          , Floor
          , Finish
          , Floor
          ]
        , [ Floor, Floor, Floor, Floor, Empty, Empty, Floor, Floor, Floor ]
        , [ Floor, Floor, Floor, Floor, Bridge Left False, Bridge Right False, Floor, Floor, Floor ]
        , []
        ]
        ( 3, 1 )


type LevelTile
    = Empty
    | Floor
    | RustyFloor
    | Finish
    | Bridge Direction Bool
    | Trigger Color (List TriggerAction)


type TriggerAction
    = ToggleBridge ( Int, Int )
    | CloseBridge ( Int, Int )
    | OpenBridge ( Int, Int )
    | SetTriggerColor ( Int, Int ) Color
    | ToggleTriggerColor ( Int, Int ) Color


type TileState
    = PushDown Length
    | BridgeState Bool Float
    | TriggerState Color


type Level
    = Level
        { tiles : Array (Array LevelTile)
        , tileStates : Dict ( Int, Int ) TileState
        , startingPosition : ( Int, Int )
        , big : Bool
        }


levelFromData : List (List LevelTile) -> ( Int, Int ) -> Level
levelFromData tiles start =
    let
        width =
            List.map List.length tiles |> List.foldl max 0

        height =
            List.length tiles
    in
    Level
        { tiles =
            tiles
                |> List.map Array.fromList
                |> Array.fromList
        , tileStates = Dict.empty
        , startingPosition = start
        , big = width >= 15 || height >= 15
        }


getStartingPosition : Level -> ( Int, Int )
getStartingPosition (Level { startingPosition }) =
    startingPosition


getTileAt : Level -> ( Int, Int ) -> LevelTile
getTileAt ((Level { tileStates }) as level) location =
    let
        originalTile =
            getTileAtInternal level location
    in
    case originalTile of
        Bridge _ initiallyClosed ->
            case Dict.get location tileStates of
                Just (BridgeState closed _) ->
                    if closed then
                        originalTile

                    else
                        Empty

                _ ->
                    if initiallyClosed then
                        originalTile

                    else
                        Empty

        a ->
            a


getTileAtInternal : Level -> ( Int, Int ) -> LevelTile
getTileAtInternal (Level { tiles }) ( x, y ) =
    Array.get x tiles
        |> Maybe.map (\row -> Array.get y row |> Maybe.withDefault Empty)
        |> Maybe.withDefault Empty


shiftTile : ( Int, Int ) -> Length -> Level -> Level
shiftTile location zOffset (Level level) =
    Level { level | tileStates = Dict.insert location (PushDown zOffset) level.tileStates }


triggerActions : List TriggerAction -> Level -> ( Level, Cmd a )
triggerActions actions ((Level levelData) as level) =
    List.foldl
        (\action ( levelAcc, cmdAcc ) ->
            (case action of
                ToggleBridge ( x, y ) ->
                    toggleBridge not ( x, y ) levelAcc

                CloseBridge ( x, y ) ->
                    toggleBridge (always True) ( x, y ) levelAcc

                OpenBridge ( x, y ) ->
                    toggleBridge (always False) ( x, y ) levelAcc

                SetTriggerColor ( x, y ) newColor ->
                    ( Level { levelData | tileStates = Dict.insert ( x, y ) (TriggerState newColor) levelData.tileStates }
                    , Cmd.none
                    )

                ToggleTriggerColor ( x, y ) secondColor ->
                    ( Level
                        { levelData
                            | tileStates =
                                case Dict.get ( x, y ) levelData.tileStates of
                                    Just _ ->
                                        Dict.remove ( x, y ) levelData.tileStates

                                    Nothing ->
                                        Dict.insert ( x, y ) (TriggerState secondColor) levelData.tileStates
                        }
                    , Cmd.none
                    )
            )
                |> Tuple.mapSecond (\cmd -> Cmd.batch [ cmdAcc, cmd ])
        )
        ( level, Cmd.none )
        actions


toggleBridge : (Bool -> Bool) -> ( Int, Int ) -> Level -> ( Level, Cmd a )
toggleBridge mapPreviousState ( x, y ) (Level level) =
    case Dict.get ( x, y ) level.tileStates of
        Just (BridgeState closed progress) ->
            ( Level { level | tileStates = Dict.insert ( x, y ) (BridgeState (mapPreviousState closed) progress) level.tileStates }
            , Cmd.none
            )

        _ ->
            let
                initiallyClosed =
                    case getTileAtInternal (Level level) ( x, y ) of
                        Bridge _ initValue ->
                            initValue

                        _ ->
                            False

                newClosed =
                    mapPreviousState initiallyClosed

                progress =
                    if newClosed then
                        0

                    else
                        1
            in
            ( Level { level | tileStates = Dict.insert ( x, y ) (BridgeState newClosed progress) level.tileStates }
            , Cmd.none
            )


restart : Level -> Level
restart (Level level) =
    Level { level | tileStates = Dict.empty }


levelUpdate : Float -> Level -> Level
levelUpdate delta (Level level) =
    Level { level | tileStates = Dict.map (always (updateTileState delta)) level.tileStates }


updateTileState : Float -> TileState -> TileState
updateTileState delta tile =
    case tile of
        BridgeState True progress ->
            BridgeState True (min (progress + animationSpeed * delta) 1)

        BridgeState False progress ->
            BridgeState False (max (progress - animationSpeed * delta) 0)

        a ->
            a


levelView : Level -> Scene3d.Entity coordinates
levelView (Level { tiles, tileStates }) =
    tiles
        |> Array.indexedMap
            (\x row ->
                Array.indexedMap
                    (\y tile ->
                        case tile of
                            Floor ->
                                floorEntity Color.gray ( x, y )

                            Empty ->
                                Scene3d.nothing

                            Finish ->
                                floorEntity Color.black ( x, y )

                            RustyFloor ->
                                floorEntity Color.lightOrange ( x, y )
                                    |> (case Dict.get ( x, y ) tileStates of
                                            Just (PushDown zOffset) ->
                                                Scene3d.translateIn Direction3d.negativeZ zOffset

                                            _ ->
                                                identity
                                       )

                            Trigger initialColor _ ->
                                triggerEntity
                                    (case Dict.get ( x, y ) tileStates of
                                        Just (TriggerState color) ->
                                            color

                                        _ ->
                                            initialColor
                                    )
                                    ( x, y )

                            Bridge openingDirection initiallyClosed ->
                                floorEntity Color.darkYellow ( x, y )
                                    |> (case
                                            Maybe.withDefault
                                                (BridgeState initiallyClosed
                                                    (if initiallyClosed then
                                                        1

                                                     else
                                                        0
                                                    )
                                                )
                                                (Dict.get ( x, y ) tileStates)
                                        of
                                            BridgeState _ progress ->
                                                let
                                                    axis =
                                                        case openingDirection of
                                                            Up ->
                                                                Axis3d.through (Point3d.centimeters (toFloat x * tileSizeCm) (toFloat y * tileSizeCm) -0.1) Direction3d.negativeY

                                                            Right ->
                                                                Axis3d.through (Point3d.centimeters (toFloat x * tileSizeCm) (toFloat (y + 1) * tileSizeCm) -0.1) Direction3d.negativeX

                                                            Down ->
                                                                Axis3d.through (Point3d.centimeters (toFloat (x + 1) * tileSizeCm) (toFloat y * tileSizeCm) -0.1) Direction3d.y

                                                            Left ->
                                                                Axis3d.through (Point3d.centimeters (toFloat x * tileSizeCm) (toFloat y * tileSizeCm) -0.1) Direction3d.x
                                                in
                                                Scene3d.rotateAround axis (Angle.degrees (-180 * (1 - progress)))

                                            _ ->
                                                identity
                                       )
                    )
                    row
                    |> Array.toList
            )
        |> Array.toList
        |> List.concatMap identity
        |> Scene3d.group


floorEntity color ( x, y ) =
    let
        tileBorderSizeCm =
            tileSizeCm * 0.02
    in
    Scene3d.block
        (Material.matte color)
        (Block3d.with
            { x1 = Length.centimeters (tileSizeCm * toFloat x + tileBorderSizeCm)
            , x2 = Length.centimeters (tileSizeCm * toFloat (x + 1) - tileBorderSizeCm)
            , y1 = Length.centimeters (tileSizeCm * toFloat y + tileBorderSizeCm)
            , y2 = Length.centimeters (tileSizeCm * toFloat (y + 1) - tileBorderSizeCm)
            , z1 = Length.centimeters (tileSizeCm * -0.1)
            , z2 = Length.centimeters 0
            }
        )


triggerEntity color ( x, y ) =
    let
        buttonPaddingCm =
            tileSizeCm * 0.2
    in
    Scene3d.group
        [ floorEntity Color.gray ( x, y )
        , Scene3d.block
            (Material.matte color)
            (Block3d.with
                { x1 = Length.centimeters (tileSizeCm * toFloat x + buttonPaddingCm)
                , x2 = Length.centimeters (tileSizeCm * toFloat (x + 1) - buttonPaddingCm)
                , y1 = Length.centimeters (tileSizeCm * toFloat y + buttonPaddingCm)
                , y2 = Length.centimeters (tileSizeCm * toFloat (y + 1) - buttonPaddingCm)
                , z1 = Length.centimeters 0
                , z2 = Length.centimeters (tileSizeCm * 0.1)
                }
            )
        ]


isBigLevel : Level -> Bool
isBigLevel (Level { big }) =
    big


levelIsBusy : Level -> Bool
levelIsBusy (Level { tileStates }) =
    tileStates
        |> Dict.values
        |> List.any
            (\state ->
                case state of
                    BridgeState True x ->
                        x < 1

                    BridgeState False x ->
                        x > 0

                    _ ->
                        False
            )