You wanna get new articles as RSS feed? Click this icon: Get articles as RSS feed!
Articles
March 21, 2020AnnouncementIonic 5 / Create aweseome apps for iOS, Android, Desktop and Web. This book is aimed at software developers who previously had little or nothing to do with programming apps or who had worked with other tools and frameworks and would like to build cool apps in an easy way. The book spans from the idea of the popular app framework and its installation to the realization of a complete app including its publication on Apple App Store, Google Play, for Desktop by using Electron or as Progressive Web App (PWA). Each of the twelve chapters is dedicated to its own aspect of Ionic. In the process, new functionalities are gradually being added to an initially simply designed tourism app called „BoB Tours“. At the end of this book, the reader not only knows the key features of Ionic, but has also understood how the whole works in context. He/She finally has the necessary knowledge to be able to develop his/her own awesome apps with Ionic.   At this website you’ll find all source code we write in this book – to every chapter the respective development progress – always up to date! You can download it from the download area here and compare it to your own code or use it as a base to start from whatever chapter you want. Ionic 5 / Create aweseome apps for iOS, Android, Desktop and Web is a comprehensive introduction to the Ionic framework on more than 600 pages. Level: Beginner ++ | Intermediate +++ | Advanced +     Table of Contents: 1. Introduction 2. Angular Essentials3. The first app4. Navigation5. Services and Storages6. UI Components7. Form validation8. Theming, styling, customizing9. Ionic Native10. Communication and Messaging11. Debugging and Testing12. Build, Deploy and Publish Bonus chapters: B1. Ionic without any frameworkB2. Ionic and CapacitorB3. Ionic and ReactB4. Ionic and Vue Get it now on Amazon! [...]
December 22, 2019TutorialThis is part 4 of an Ionic 4 tutorial. Please read part 1, part 2 and part 3 before continuing here… Part 4: Make it sound x-massy To let our app sound x-massy, I’ve prepared some audio files that you can download here:SantaClausIsComing.mp3Hohoho.mp3 After downloading the files copy them into the assets folder.   Embed audio There are various ways of integrating audio in an app. We make it easy with the howler.js library. Install howler.js in the terminal with $ npm install howler Then we create a service with $ ionic g service services/audio In the new audio.service.ts we code the following lines: import { Injectable } from '@angular/core'; import { Howl } from 'howler'; @Injectable({ providedIn: 'root' }) export class AudioService { private hohoho: Howl; constructor() { } Init() { new Howl({ src: ['../assets/SantaClausIsComing.mp3'], preload: true, autoplay: true, loop: true }); this.hohoho = new Howl({ src: ['../assets/Hohoho.mp3'], preload: true, autoplay: false, loop: false }); } SayHohoho() { this.hohoho.play(); } } Now we can import and initialize this service in home.page.ts: import { Component } from '@angular/core'; import { AudioService } from '../services/audio.service'; import { GameService } from '../services/game.service'; @Component({ selector: 'app-home', templateUrl: 'home.page.html', styleUrls: , }) export class HomePage { constructor( public audio: AudioService, public game: GameService ) { } ngOnInit() { this.audio.Init(); this.game.Start(); } } Since our SantaClausIsComing song was configured with the parameter autoplay: true, the song starts already, as soon as the app is restarted.   Trigger audio in a specific situation With howler.js it’s easy to trigger a sound to a certain event. In our app it makes sense that Santa calls “hohoho!” when he picks up one of the presents. So we go to our game.service.ts file and add the highlighted lines: import { Injectable } from '@angular/core'; import { MazeService } from '../services/maze.service'; import { MazePosition } from 'src/app/models/maze-position'; import { ToastController } from '@ionic/angular'; import { AudioService } from '../services/audio.service'; import { Router } from '@angular/router'; @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 audio: AudioService, 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.audio.SayHohoho(); this.presentToast('Ho ho ho!', 'You\'ve 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() { if (this.playerPosition.Col == this.itemPresent1.Col && this.playerPosition.Row == this.itemPresent1.Row) { this.hasPresent1 = true; this.audio.SayHohoho(); 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.audio.SayHohoho(); 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.audio.SayHohoho(); 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(); } } So, give it a try and be happy with Santa when he finds the presents in the maze and finally the exit 😉   Improvements There is of course a lot of room for improvement in this gaming app: A nice sound when Santa takes a step and/or another sound when he hits a wall. And other characters (bots) that make life difficult for Santa because they also chase after the gifts and snap them away from him. And so on… Feel free to implement further features and share them with us!   Credits Without the great graphics, tools and audio files, which are available free of charge on the web, I would have had to spend a lot more time on this app. Therefore I would like to finally thank: mazegenerator.netWith this amazing tool you can generate all kinds of labyrinths … and export them as SVG!freepik.comThis is where the pretty decoration, our Santa and the presents come from. Thanks to kipargeter for this!soundcloud.comThe song “Santa Claus is coming” is by Andi del Mar and is freely available on SoundCloud. I hope you enjoyed this tutorial and you’ve found some inspiration for your own awesome apps. You want more of this cool stuff? Then look at my Ionic 4 book. Happy reading and coding! [...]
December 15, 2019TutorialThis 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: <!-- Animate --> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.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. <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: // Amount of Snowflakes var snowMax = 36; // Snowflake Colours var snowColor = ; // Snow Entity var snowEntity = "❄"; // 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 = 0; lefr = Math.random() * 15; pos = 0.03 + Math.random() / 10; snow = document.getElementById("flake" + i); snow.style.fontFamily = "inherit"; snow.size = randomise(snowSize) + snowMinSize; snow.style.fontSize = snow.size + "px"; snow.style.color = snowColor; snow.style.zIndex = 2000 + i; snow.sink = snowSpeed * snow.size / 5; snow.posX = randomise(marginRight - snow.size); snow.posY = randomise(2 * marginBottom - marginBottom - 2 * snow.size); snow.style.left = snow.posX + "px"; snow.style.top = snow.posY + "px"; } moveSnow(); } function resize() { marginBottom = document.body.scrollHeight - 5; marginRight = document.body.clientWidth - 15; } function moveSnow() { for (i = 0; i <= snowMax; i++) { coords += pos; snow.posY += snow.sink; snow.style.left = snow.posX + lefr * Math.sin(coords * .3) + "px"; snow.style.top = snow.posY + "px"; if (snow.posY >= marginBottom - 2 * snow.size || parseInt(snow.style.left) > (marginRight - 3 * lefr)) { snow.posX = randomise(marginRight - snow.size); snow.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: <!-- 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: 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(' ')) - 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.getAttribute('x1')); let x2 = parseFloat(maze.children.getAttribute('x2')); let y1 = parseFloat(maze.children.getAttribute('y1')); let y2 = parseFloat(maze.children.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; cell.BorderLeft = 1; } } // Set 'right' in previous col. if (!isFirstCol) { for (let r = row; r < row + rows; r++) { let cell: MazeCell = cellMatrix; 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; cell.BorderTop = 1; } } // Set 'bottom' in previous row. if (!isFirstRow) { for (let c = col; c < col + cols; c++) { let cell: MazeCell = cellMatrix; 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 = []; for (let c = 0; c < cols; c++) { cellMatrix = new MazeCell(); } } return cellMatrix; } public CanMoveLeft(pos: MazePosition) { var cell: MazeCell = this.cellMatrix; return cell.BorderLeft == 0; } public CanMoveUp(pos: MazePosition) { var cell: MazeCell = this.cellMatrix; return cell.BorderTop == 0; } public CanMoveRight(pos: MazePosition) { var cell: MazeCell = this.cellMatrix; return cell.BorderRight == 0; } public CanMoveDown(pos: MazePosition) { var cell: MazeCell = this.cellMatrix; 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: 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: 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: 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: 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: <!-- 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! [...]
December 8, 2019TutorialIn the first part of our X-Maze tutorial we created an Ionic project with three components called “Arrow-Buttons”, “Decoration” and “Maze”. Today we we start to fill these components with content and make everything look pretty. Part 2: Make it look pretty Arrange the ArrowButtonsComponent Let’s fill the ArrowButtonsComponent with a few buttons. For this we remove the automatically generated placeholder entry in arrow-buttons.component.html. <p> arrow-buttons works! </p> and replace it with this: <!-- Button group for portrait mode --> <ion-grid id="buttons-portrait" *ngIf="isPortrait"> <ion-row> <ion-col size="12"> <ion-button expand="block"> <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"> <ion-icon name="md-arrow-round-back"></ion-icon> </ion-button> </ion-col> <ion-col size="6"> <ion-button expand="block"> <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"> <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"> <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"> <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"> <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"> <ion-icon name="md-arrow-round-forward"></ion-icon> </ion-button> </ion-col> </ion-row> </ion-grid> We have created two different button groups based on ion-grid. Depending on the orientation of the screen, one or the other group should be displayed. This is ensured by the *ngIf instructions. For these to work, we need to program the appropriate logic. In arrow-buttons.component.ts we add this: import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-arrow-buttons', templateUrl: './arrow-buttons.component.html', styleUrls: ['./arrow-buttons.component.scss'],}) export class ArrowButtonsComponent implements OnInit { public isPortrait: boolean; constructor() { } ngOnInit() { this.checkOrientation(); } private checkOrientation() { var mq = window.matchMedia("(orientation: portrait)"); this.isPortrait = mq.matches; mq.addListener(m => { this.isPortrait = m.matches; }); } }  With the help of some CSS, we position our button groups in arrow-buttons.component.scss as overlays: #buttons-portrait { position: absolute; bottom: 10px; width: 100%; z-index: 1001; } #buttons-landscape { position: absolute; top: 50%; transform: translateY(-50%); width: 100%; z-index: 1001; } If the screen is in portrait orientation, the buttons are placed in a group at the bottom of the screen (see following figure). If the screen is in landscape mode, the buttons are placed in the middle of the left and right edges of the screen. There they are good for gamer thumbs to reach 😉 (see following figure). Assets To make our app pretty, I’ve prepared some graphics that you can download here:background.svgdecoration.svgmaze.svgtitle.svg Copy background.svg and title.svg into the assets folder of our app project (you can delete the file shapes.svg at the moment). Did I already say that I love Scalable Vector Graphics (SVG)? Why? You can read the answer in my Ionic 4 book in chapter 8.8. A few cool things that you can do with SVGs I’ll show you in this tutorial, too. Background First of all we want to use the background.svg file as a background in our app. To do this, we enter the following in home.page.scss: ion-content { --background: url(../../assets/background.svg) no-repeat 50% center/cover; } Before I explain for long what this CSS instruction does, look for yourself: Title We want to place the title now. You remember: In home.page.html we have a <div id=title></div> (see the first part of our X-Maze tutorial). That’s the placeholder for our title. For this we write the following CSS in home.page.scss: @media screen and (min-height: 500px) { #title { content: url(../../assets/title.svg); position: absolute; top: 0px; left: 50%; transform: translateX(-50%); width: 50vw; @media screen and (min-height: 600px) { width: 80vw; } @media screen and (min-height: 700px) { top: 20px; width: 90vw; } @media screen and (min-height: 800px) { top: 40px; width: 100vw; } z-index: 1000; } } @media screen and (min-width: 500px) { #title { content: url(../../assets/title.svg); position: absolute; bottom: 5px; right: 5px; width: 20vw; z-index: 1000; } } With these instructions, we assign title.svg to our title div and determine that depending on the size (and orientation) of a screen, our title will either be centered at the top… … or at the bottom right. Decoration Let’s make the DecorationComponent what its name promises: a decorative component. To do this, we copy the entire contents of decorations.svg and paste it into decoration.component.html. You wonder why you pack this SVG stuff into an HTML component? Well, then start our app and be amazed: And thanks to the wonderful features of SVGs, the whole thing looks great in the landscape view as well (and on a huge screen, too): Maze Last but not least, we fill the MazeComponent. Here we proceed exactly as we did with the DecorationComponent: We copy the entire contents of maze.svg into maze.component.html. That’s the result: Now our UI is ready. Looks great, right? You’ve discovered Santa Clause in the labyrinth and you’re curious what he’s looking for there? Then look forward to next Sunday, where we’ll get Santa up and running. Until then: Happy reading and coding! [...]
December 1, 2019TutorialI first had the idea to write a “Making of the Halloween Maze”. But Halloween is over and Christmas is coming. I think it’s better to update the topic and to start a little tutorial series “How to make an X-Maze” (a playable labyrinth with a Christmas look). It’s supposed to show the creative side of Ionic: graphics, animations, audio. The series at a glance: Part 1: Make an Ionic 4 app project Part 2: Make it look pretty Part 3: Make it move nicely Part 4: Make it sound x-massy Every Advent Sunday there will be a new part of the tutorial. For Christmas, I’ll give you the finished program code for download. Today is the first Advent! So let’s start with our first part … Part 1: Make an Ionic 4 app project Preparations Starting an Ionic project is not difficult once you have made all the necessary installations. If you are not so familiar with this topic, you’re in luck: check out the Look Inside for my new Ionic book on this website. In chapter 1.2 “Installations” you will learn how to do it. Create the project In console / terminal move to your dev folder of choice and type $ ionic start x-maze blank to create your Ionic project. Choose Angular as the framework you want to use to develop your app. After the Ionic CLI has created our project, we switch to its project folder and continue … Create components Our project will include a page called “Home” (which already exists). We’ll populate it with three components called “Arrow-Buttons”, “Decoration” and “Maze”, which we are now creating. In terminal type the following commands: $ ionic g component components/Arrow-Buttons $ ionic g component components/Decoration $ ionic g component components/Maze In a subfolder “components” you’ll now find the new components. Create a components module In order to be able to embed our components in our app page in an elegant way, let’s quickly write down a component module called components.module.ts, which we put in our “components” subfolder: import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IonicModule } from '@ionic/angular'; import { ArrowButtonsComponent } from './arrow-buttons/arrow-buttons.component'; import { DecorationComponent } from './decoration/decoration.component'; import { MazeComponent } from './maze/maze.component'; const PAGES_COMPONENTS = @NgModule({ declarations: , imports: , exports: }) export class ComponentsModule {} Import the components module into page module In home.module.ts we can now add: import { ComponentsModule } from './../components/components.module'; and at the imports: imports: , ... Now we are able to import and use the components in the home page. Build the UI structure of our app Now we build the UI structure for our app using the components in home.page.html: <ion-content> <div id="title"></div> <app-decoration></app-decoration> <app-maze></app-maze> <app-arrow-buttons></app-arrow-buttons> </ion-content> If we start our app with $ ionic serve and take a first look at what we’ve done so far, it looks like this: Of course, that doesn’t look that great yet. But the basic architecture of our app is done and and we’ll make it look pretty in the second part of our X-Maze tutorial. CU next Sunday, December 8, 2019. Until then: Happy reading and coding! [...]