
This is part 3 of a special X-mas Ionic 4 tutorial. Please read part 1 and part 2 before continuing here…
Part 3: Make it move nicely
Animate the decoration
The easiest way to bring some animation into our app is to use Animate.css.
In the header section of our index.html we add:
1 2 3 | <!-- Animate --> < link rel = "stylesheet" type = "text/css" |
Now we switch to decoration.component.html and look for a few stars there. Search for the elements with the id “star1” and add a class attribute with the values “animated flash slower infinite delay-1s”. That means: animate this star with the flash effect at a slower speed and let this animation last indefinitely. Start the whole thing with a 1s delay.
1 2 3 4 5 6 7 8 | < g id = "star1" class = "animated flash slower infinite delay-1s" > < path class = "cls-0" d = "M256.4,175.4q0,36-.1,0c-.1,19.6-.3,26.6-.9,30.1-2.4-.4-4.9-2.8-9.2-7.1,4.2,4.1,6.5,6.7,7.1,8.9-3.8.7-11.2.8-33.1,1q39.3,0,0,.1c21.9.1,29.4.3,33.1.9-.5,2.3-2.9,4.9-7.1,9.1,4.3-4.3,6.8-6.7,9.2-7.2.6,3.5.8,10.3.9,30.4q.1-36,.1,0c.2-20,.3-26.8.9-30.3,2.3.6,4.8,3,8.9,7.1-4.2-4.2-6.6-6.7-7.1-9,3.7-.7,11-.9,33.2-1-26.2-.1-26.2-.2,0-.1-22.2-.2-29.5-.4-33.2-1,.6-2.3,2.9-4.8,7.1-8.9-4.1,4.1-6.6,6.4-8.9,7C256.7,201.9,256.6,194.9,256.4,175.4Z" transform = "translate(0 -1)" /> < path class = "cls-0" d = "M244.3,220.2l1.9-1.8Z" transform = "translate(0 -1)" /> < path class = "cls-0" d = "M244.3,196.5l1.9,1.9Z" transform = "translate(0 -1)" /> < path class = "cls-0" d = "M268,220.2l-1.8-1.8Z" transform = "translate(0 -1)" /> < path class = "cls-0" d = "M268,196.5l-1.8,1.9Z" transform = "translate(0 -1)" /> </ g > |
Now add the same animation to “star2”, “star3”, “star4” and “star5”; change the delay to delay-2s, delay-3s, delay-4s and delay-5s.
Your Christmas decoration stars are now flashing great, aren’t they?
Let it snow
But what would Christmas be without snow? So let it snow!
In the assets folder create a file called snow.js with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | // Amount of Snowflakes var snowMax = 36 ; // Snowflake Colours var snowColor = [ "#CCC" , "#DDD" ] ; // Snow Entity var snowEntity = "<img draggable=" false " role=" img " class=" emoji " alt="
" src=" https : //s.w.org/images/core/emoji/15.0.3/svg/2744.svg">"; // Falling Velocity var snowSpeed = 0 . 5 ; // Minimum Flake Size var snowMinSize = 12 ; // Maximum Flake Size var snowMaxSize = 24 ; // Refresh Rate (in milliseconds) var snowRefresh = 20 ; // Additional Styles var snowStyles = "cursor: default; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; -o-user-select: none; user-select: none;" ; /* // End of Configuration // ---------------------------------------- // Do not modify the code below this line */ var snow = [ ] , pos = [ ] , coords = [ ] , lefr = [ ] , marginBottom , marginRight ; function randomise ( range ) { rand = Math . floor ( range * Math . random ( ) ) ; return rand ; } function initSnow ( ) { var snowSize = snowMaxSize - snowMinSize ; marginBottom = document . body . scrollHeight - 5 ; marginRight = document . body . clientWidth - 15 ; for ( i = 0 ; i < = snowMax ; i + + ) { coords [ i ] = 0 ; lefr [ i ] = Math . random ( ) * 15 ; pos [ i ] = 0 . 03 + Math . random ( ) / 10 ; snow [ i ] = document . getElementById ( "flake" + i ) ; snow [ i ] . style . fontFamily = "inherit" ; snow [ i ] . size = randomise ( snowSize ) + snowMinSize ; snow [ i ] . style . fontSize = snow [ i ] . size + "px" ; snow [ i ] . style . color = snowColor [ randomise ( snowColor . length ) ] ; snow [ i ] . style . zIndex = 2000 + i ; snow [ i ] . sink = snowSpeed * snow [ i ] . size / 5 ; snow [ i ] . posX = randomise ( marginRight - snow [ i ] . size ) ; snow [ i ] . posY = randomise ( 2 * marginBottom - marginBottom - 2 * snow [ i ] . size ) ; snow [ i ] . style . left = snow [ i ] . posX + "px" ; snow [ i ] . style . top = snow [ i ] . posY + "px" ; } moveSnow ( ) ; } function resize ( ) { marginBottom = document . body . scrollHeight - 5 ; marginRight = document . body . clientWidth - 15 ; } function moveSnow ( ) { for ( i = 0 ; i < = snowMax ; i + + ) { coords [ i ] + = pos [ i ] ; snow [ i ] . posY + = snow [ i ] . sink ; snow [ i ] . style . left = snow [ i ] . posX + lefr [ i ] * Math . sin ( coords [ i ] * . 3 ) + "px" ; snow [ i ] . style . top = snow [ i ] . posY + "px" ; if ( snow [ i ] . posY > = marginBottom - 2 * snow [ i ] . size | | parseInt ( snow [ i ] . style . left ) > ( marginRight - 3 * lefr [ i ] ) ) { snow [ i ] . posX = randomise ( marginRight - snow [ i ] . size ) ; snow [ i ] . posY = 0 ; } } setTimeout ( "moveSnow()" , snowRefresh ) ; } for ( i = 0 ; i < = snowMax ; i + + ) { document . write ( "<span id='flake" + i + "' style='" + snowStyles + "position:absolute;top:-" + snowMaxSize + "'>" + snowEntity + "</span>" ) ; } window . addEventListener ( 'resize' , resize ) ; window . addEventListener ( 'load' , initSnow ) ; |
The snow.js code comes from the Australian programmer and designer Kurisu Brooks and can be found under the MIT license at https://www.cssscript.com/minimalist-falling-snow-effect-with-pure-javascript-snow-js/. I modified it a little for our app.
We embed the script – as before the reference to Animate.css – in the header area of our index.html:
1 2 3 | <!-- Snow --> < script src = "assets/snow.js" ></ script > |
That’s it! Now it snows.
Moving Santa
Now to Santa! We want to use the arrow keys and the letters W, A, S and D to move him through the maze.
Think of the maze as a grid with 16×16 cells. When navigating, each cell must be checked to determine whether it has a wall on the left, top, right or bottom. Only when there’s no wall Santa can turn left, go upstairs, right or downstairs.
To implement this logic, we create a maze service.
In the terminal we enter the following:
$ ionic g service services/maze
The Ionic CLI then generates a services folder and a file called maze.service.ts in it, which we edit as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | import { Injectable } from '@angular/core' ; import { MazeCell } from 'src/app/models/maze-cell' ; import { MazePosition } from 'src/app/models/maze-position' ; @Injectable ( { providedIn : 'root' } ) export class MazeService { public Rows : number ; public Cols : number ; public CellPixels : number ; private cellMatrix = [ [ ] ] ; constructor ( ) { } public Init ( rows : number , cols : number ) { this . cellMatrix = this . getCellMatrix ( rows , cols ) ; } private getCellMatrix ( rows : number , cols : number ) { const svg = document . getElementById ( 'maze-svg' ) ; const viewBox = svg . getAttribute ( 'viewBox' ) ; const viewBoxPixels : number = parseFloat ( viewBox . split ( ' ' ) [ 2 ] ) - 1 ; const maze = document . getElementById ( 'maze' ) ; this . Rows = rows ; this . Cols = cols ; this . CellPixels = viewBoxPixels / this . Rows ; let cellMatrix = this . getEmptyCellMatrix ( this . Rows , this . Cols ) ; // Loop over all line elements (children). for ( let i = 0 ; i < maze . children . length ; i + + ) { // Grab line elements let x1 = parseFloat ( maze . children [ i ] . getAttribute ( 'x1' ) ) ; let x2 = parseFloat ( maze . children [ i ] . getAttribute ( 'x2' ) ) ; let y1 = parseFloat ( maze . children [ i ] . getAttribute ( 'y1' ) ) ; let y2 = parseFloat ( maze . children [ i ] . getAttribute ( 'y2' ) ) ; // Starting position let row : number = Math . round ( y1 / this . CellPixels ) ; let col : number = Math . round ( x1 / this . CellPixels ) ; let isVerticalLine : boolean = ( x1 = = x2 ) ; let isHorizontalLine : boolean = ( y1 = = y2 ) ; // Vertical line if ( isVerticalLine ) { let rows : number = Math . round ( ( y2 - y1 ) / this . CellPixels ) ; let isFirstCol : boolean = ( col = = 0 ) ; let isLastCol : boolean = ( col = = this . Cols ) ; // Set 'left' in current col. if ( ! isLastCol ) { for ( let r = row ; r < row + rows ; r + + ) { let cell : MazeCell = cellMatrix [ r ] [ col ] ; cell . BorderLeft = 1 ; } } // Set 'right' in previous col. if ( ! isFirstCol ) { for ( let r = row ; r < row + rows ; r + + ) { let cell : MazeCell = cellMatrix [ r ] [ col - 1 ] ; cell . BorderRight = 1 ; } } } // Horizontal line if ( isHorizontalLine ) { let cols : number = Math . round ( ( x2 - x1 ) / this . CellPixels ) ; let isFirstRow : boolean = ( row = = 0 ) ; let isLastRow : boolean = ( row = = this . Cols ) ; // Set 'top' in current row. if ( ! isLastRow ) { for ( let c = col ; c < col + cols ; c + + ) { let cell : MazeCell = cellMatrix [ row ] [ c ] ; cell . BorderTop = 1 ; } } // Set 'bottom' in previous row. if ( ! isFirstRow ) { for ( let c = col ; c < col + cols ; c + + ) { let cell : MazeCell = cellMatrix [ row - 1 ] [ c ] ; cell . BorderBottom = 1 ; } } } } // End loop over all line elements. return cellMatrix ; } private getEmptyCellMatrix ( rows : number , cols : number ) : any { let cellMatrix = [ [ ] ] ; for ( let r = 0 ; r < rows ; r + + ) { cellMatrix [ r ] = [ ] ; for ( let c = 0 ; c < cols ; c + + ) { cellMatrix [ r ] [ c ] = new MazeCell ( ) ; } } return cellMatrix ; } public CanMoveLeft ( pos : MazePosition ) { var cell : MazeCell = this . cellMatrix [ pos . Row ] [ pos . Col ] ; return cell . BorderLeft = = 0 ; } public CanMoveUp ( pos : MazePosition ) { var cell : MazeCell = this . cellMatrix [ pos . Row ] [ pos . Col ] ; return cell . BorderTop = = 0 ; } public CanMoveRight ( pos : MazePosition ) { var cell : MazeCell = this . cellMatrix [ pos . Row ] [ pos . Col ] ; return cell . BorderRight = = 0 ; } public CanMoveDown ( pos : MazePosition ) { var cell : MazeCell = this . cellMatrix [ pos . Row ] [ pos . Col ] ; return cell . BorderBottom = = 0 ; } } |
Regarding the code: We take advantage of the fact that the SVG graphic encodes the “cell walls” as line elements (see our maze.component.html). We can identify these as vertical or horizontal “walls” reading the attributes x1, x2, y1, y2. In this way, we summarize the maze in a structured manner as CellMatrix. On this basis, our service can now provide the public methods CanMoveLeft, CanMoveUp, CanMoveRight and CanMoveDown.
What is still missing are the model classes MazePosition and MazeCell imported in lines 2 and 3.
Here the code of app/models/maze-cell.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | export class MazeCell { public BorderLeft : number = 0 ; public BorderTop : number = 0 ; public BorderRight : number = 0 ; public BorderBottom : number = 0 ; constructor ( bLeft? : number , bTop? : number , bRight? : number , bBottom? : number ) { if ( bLeft ! = null ) { this . BorderLeft = bLeft ; } if ( bTop ! = null ) { this . BorderTop = bTop ; } if ( bRight ! = null ) { this . BorderRight = bRight ; } if ( bBottom ! = null ) { this . BorderBottom = bBottom ; } } } |
And here the code of app/models/maze-position.ts:
1 2 3 4 5 6 7 8 9 10 11 12 | export class MazePosition { public Row : number ; public Col : number ; constructor ( row : number , col : number ) { this . Row = row ; this . Col = col ; } } |
Last but not least, we now have to teach Santa to walk. For this we create the GameService.
In the terminal we enter the following:
$ ionic g service services/game
The Ionic CLI then generates a file called game.service.ts in the services folder and , which we edit as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | import { Injectable } from '@angular/core' ; import { MazeService } from '../services/maze.service' ; import { MazePosition } from 'src/app/models/maze-position' ; import { ToastController } from '@ionic/angular' ; @Injectable ( { providedIn : 'root' } ) export class GameService { public status : string ; /** player: position */ private playerPosition : MazePosition = new MazePosition ( 0 , 7 ) ; /** items: positions */ private itemPresent1 = new MazePosition ( 7 , 1 ) ; private itemPresent2 = new MazePosition ( 8 , 15 ) ; private itemPresent3 = new MazePosition ( 12 , 8 ) ; /** items: collected */ private hasPresent1 : boolean = false ; private hasPresent2 : boolean = false ; private hasPresent3 : boolean = false ; constructor ( private maze : MazeService , private toastController : ToastController , ) { } /** * Starts the game. */ public async Start ( ) { this . maze . Init ( 16 , 16 ) ; this . initPlayer ( ) ; } /** * Navigates with the arrow keys or WASD. * * @param keydownEvent.code event code coming from keydown */ public Navigate ( keypressEvent ) { switch ( keypressEvent ) { case 'ArrowUp' : case 'KeyW' : if ( this . maze . CanMoveUp ( this . playerPosition ) ) { this . movePlayer ( this . playerPosition . Col , this . playerPosition . Row - 1 ) ; } break ; case 'ArrowLeft' : case 'KeyA' : if ( this . maze . CanMoveLeft ( this . playerPosition ) ) { this . movePlayer ( this . playerPosition . Col - 1 , this . playerPosition . Row ) ; } else { } break ; case 'ArrowDown' : case 'KeyS' : if ( this . maze . CanMoveDown ( this . playerPosition ) ) { this . movePlayer ( this . playerPosition . Col , this . playerPosition . Row + 1 ) ; } break ; case 'ArrowRight' : case 'KeyD' : if ( this . maze . CanMoveRight ( this . playerPosition ) ) { this . movePlayer ( this . playerPosition . Col + 1 , this . playerPosition . Row ) ; } break ; } } private initPlayer ( ) { this . movePlayer ( this . playerPosition . Col , this . playerPosition . Row ) ; } private movePlayer ( col : number , row : number ) { if ( row < 0 ) { this . presentToast ( 'Hey!' , 'What kind of unreliable Santa are you?' ) ; } else if ( row < this . maze . Rows ) { this . moveSection ( 'santa' , col * ( this . maze . CellPixels ) , row * ( this . maze . CellPixels ) ) ; this . playerPosition = new MazePosition ( row , col ) ; this . checkForItemHits ( ) ; } else { if ( ! this . hasPresent1 | | ! this . hasPresent2 | | ! this . hasPresent3 ) { this . presentToast ( 'Hey!' , 'You haven\'t found all the presents yet, Santa!' ) ; } else { this . presentToast ( 'Ho ho ho!' , 'You found all the presents, Santa!\nChristmas can come now ;-)' ) ; } } } private moveSection ( elementName : string , xOffset , yOffset ) { var element = document . getElementById ( elementName ) ; if ( element ) { var transformAttr = ' translate(' + xOffset + ',' + yOffset + ')' ; element . setAttribute ( 'transform' , transformAttr ) ; } } private checkForItemHits ( ) { console . log ( this . playerPosition . Col , this . playerPosition . Row , '-' , this . itemPresent1 . Col , this . itemPresent1 . Row ) ; if ( this . playerPosition . Col = = this . itemPresent1 . Col & & this . playerPosition . Row = = this . itemPresent1 . Row ) { this . hasPresent1 = true ; this . presentToast ( 'Hey!' , 'You\'ve found present 1, Santa!' ) ; this . hide ( 'present1' ) ; } if ( this . playerPosition . Col = = this . itemPresent2 . Col & & this . playerPosition . Row = = this . itemPresent2 . Row ) { this . hasPresent2 = true ; this . presentToast ( 'Hey!' , 'You\'ve found present 2, Santa!' ) ; this . hide ( 'present2' ) ; } if ( this . playerPosition . Col = = this . itemPresent3 . Col & & this . playerPosition . Row = = this . itemPresent3 . Row ) { this . hasPresent3 = true ; this . presentToast ( 'Hey!' , 'You\'ve found present 3, Santa!' ) ; this . hide ( 'present3' ) ; } } private hide ( elementName : string ) { var element = document . getElementById ( elementName ) ; element . style . display = "none" ; } private async presentToast ( header : string , message : string ) { const toast = await this . toastController . create ( { header : header , message : message , duration : 5000 , position : 'top' } ) ; await toast . present ( ) ; } } |
In GameService we set the position of Santa and the three presents. In the Start() method we initialize the maze and the player (Santa). The Navigate() method ensures the correct movements of Santa. When Santa finds a gift, the checkForHits() method detects it and issues a toast. A found gift is hidden using the hide() method.
We have to bind the buttons to the GameService so that the navigation works. To do this, we first open arrow-buttons.component.ts and import and inject the service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import { Component , OnInit } from '@angular/core' ; import { GameService } from 'src/app/services/game.service' ; @Component ( { selector : 'app-arrow-buttons' , templateUrl : './arrow-buttons.component.html' , styleUrls : [ './arrow-buttons.component.scss' ] , } ) export class ArrowButtonsComponent implements OnInit { public isPortrait : boolean ; constructor ( public game : GameService ) { } ngOnInit ( ) { this . checkOrientation ( ) ; } private checkOrientation ( ) { var mq = window . matchMedia ( "(orientation: portrait)" ) ; this . isPortrait = mq . matches ; mq . addListener ( m = > { this . isPortrait = m . matches ; } ) ; } } |
At the end we have to equip all buttons in arrow-buttons.component.html with (click) events:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | <!-- Button group for portrait mode --> < ion-grid id = "buttons-portrait" * ngIf = "isPortrait" > < ion-row > < ion-col size = "12" > < ion-button expand = "block" (click)="game.Navigate('KeyW')"> < ion-icon name = "md-arrow-round-up" ></ ion-icon > </ ion-button > </ ion-col > </ ion-row > < ion-row > < ion-col size = "6" > < ion-button expand = "block" (click)="game.Navigate('KeyA')"> < ion-icon name = "md-arrow-round-back" ></ ion-icon > </ ion-button > </ ion-col > < ion-col size = "6" > < ion-button expand = "block" (click)="game.Navigate('KeyD')"> < ion-icon name = "md-arrow-round-forward" ></ ion-icon > </ ion-button > </ ion-col > </ ion-row > < ion-row > < ion-col size = "12" > < ion-button expand = "block" (click)="game.Navigate('KeyS')"> < ion-icon name = "md-arrow-round-down" ></ ion-icon > </ ion-button > </ ion-col > </ ion-row > </ ion-grid > <!-- Button group for landscape mode --> < ion-grid id = "buttons-landscape" * ngIf = "!isPortrait" > < ion-row > < ion-col size = "2" > < ion-button expand = "block" (click)="game.Navigate('KeyW')"> < ion-icon name = "md-arrow-round-up" ></ ion-icon > </ ion-button > </ ion-col > < ion-col size = "8" ></ ion-col > < ion-col size = "2" > < ion-button expand = "block" (click)="game.Navigate('KeyS')"> < ion-icon name = "md-arrow-round-down" ></ ion-icon > </ ion-button > </ ion-col > </ ion-row > < ion-row > < ion-col size = "2" > < ion-button expand = "block" (click)="game.Navigate('KeyA')"> < ion-icon name = "md-arrow-round-back" ></ ion-icon > </ ion-button > </ ion-col > < ion-col size = "8" ></ ion-col > < ion-col size = "2" > < ion-button expand = "block" (click)="game.Navigate('KeyD')"> < ion-icon name = "md-arrow-round-forward" ></ ion-icon > </ ion-button > </ ion-col > </ ion-row > </ ion-grid > |
Next time we’ll look at how we can get the letter keys W, A, S, and D to make Santa’s legs… Oh, and Christmas music comes along…
I hope you enjoyed this tutorial so far and you will be back on the 4th and last part next Sunday.
Happy reading and coding!
joonkuk