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:
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?
- Wat is een import?
- Teken een bol.
- Wat is main?
- Wat is picture?
4.1 Maar wat is een lijst? - 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?
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?
- Hoe teken je andere geometrische figuren.
- 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?
- Wat is een functie?
1.1 Wiskundige functies
1.2 Functies bij het programmeren - Hoe maak je je eigen functies?
- 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??
- Parameters doorgeven.
- Software bibliotheken (libraries).
- Hoe maak je je eigen functies met parameters?
- 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?
- Hoe maak je een animatie.
1.1. wat is de animation functie? - 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?
- 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?
- 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?
- Wat zijn Records.
- 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?
- Waar zijn condities (voorwaarden) voor?
- Condities gebruiken.
- 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.
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:
- De conditie (voorwaarde), die WAAR (True in het Engels) of ONWAAR (False in het Engels) kan zijn;
- De gevolgen als die conditie waar is en;
- 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:
- 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.
Antwoord
Om deze opdracht op te lossen, moeten we een conditie creëren binnen de andere conditie.
- 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:
- 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.
- 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
- de CompuRob Workshop https://www.compurob.nl/workshop
- de officiële plek om Elm code te proberen; "try Elm" https://elm-lang.org/try
- Ellie, waar je je code ook kunt opslaan https://ellie-app.com
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
)