Examples explained
-
Max Pacer
Max Pacer is an app that monitors your pace and alerts if you start going too fast. Is demonstrates notifications/view changes, vibrations and using graphs and sensor data. This post explains in more detail the implementation of it. The Code for Max Pacer can be found by running SuuntoPlus: Open Examples from VSCode Command Palette.
manifest.json
Starting with the manifest.json. There are a few mandatory field that need to be filled: name, version, author, description, type (feature/device), usage (workout), modificationTime and template. But more importantly there are in, out and settings.mainfest.json ... "in": [ { "name": "Speed", "source": "/Activity/Move/-1/Speed/Current", "type": "subscribe" } ], "out": [ { "name": "pace" } ], ... "settings": [ { "shownName": "Maximum speed before warning", "path": "appSettings.maxPace", "type": "int", "min": 1, "max": 20, "inputType": "slider"}, { "shownName": "How often is pace calculated (s)", "path": "appSettings.timelapse", "type": "int", "min": 1, "max": 10, "inputType": "slider"} ] }In defines a input variable Speed. Because we use subscribe as the type everytime we use input. Speed in main.js we get the current speed as a value.
Out defines a output variables to make a link between main.js and t.html. Using output.pace we can modify the variable in main.js and show the value in t.html.
Lastly we define two settings for users to control the SuuntoPlus App from the mobile App. The settings are written in an data.json file and the path is given for setting in manifest.json. To use the settings we also need a function in main.js, I will show that part a little further down the text. Declearing variables that stay in watch memory work similarly and these are both covered in more detail in SuuntoPlus Reference under Sports app settings.
data.json { "appSettings": { "maxPace": 11.00, "timelapse": 5 } }main.js
Next we move onto main.js where most of the functionality is done. First lines are defining variables that are needed for the full scope of the code.
Next there are helper functions. Those should be written as external functions for best performance:var myFunction = function() { }ChangeTemplate is a handy function that changes the current template to the given one. The most important part about that is the unload(‘_cm’) which refreshes the view.
main.js var changeTemplate = function(template) { if (currentTemplate == template) { return; } switch (template) { case "t": recentlyChanged = true currentTemplate = "t"; pTimer = 0 unload('_cm'); break; case "p": currentTemplate = "p"; playIndication("Interval"); unload('_cm'); break; } }LoadSetting is important for the use of settings. It loads the settings from localstorage, and if there is not any, loads the defaults. If you wanted to change the settings for the next exercise you could use localStorage.setObject(“appVariables”, settings), but it is not required for this app.
main.js var loadSettings = function(input, output) { settings = localStorage.getObject("appSettings"); if (settings == null) { settings = { maxPace: MAXPACE, timelapse: ARRAYLENGTH, } } }Then we get to Suunto spesific functions:
OnLoad happens when the exercise starts and is a good place to initialise all variables so that there would be any uninitialised variables at runtime.main.js function onLoad(input, output) { loadSettings(input, output); currentTemplate = "t" input.Speed = 0; paces = new Array(settings.timelapse); output.pace = 0.0; pTimer = 0; spacer = 0; counter = 0; isPaused = false; }OnExerciseStart and -Pause are used so that the app would calculating pace when it is paused.
main.js function onExerciseStart(input, output) { isPaused = false; } function onExercisePause(input, output) { isPaused = true; }Evaluate is the most important function of file. It executes every second and monitors if current pace is too fast. If so, it changes the template to p which is like a notifitation.
main.js function evaluate(input, output) { if (!isPaused) { paces[counter] = input.Speed; if (counter == 0) { output.pace = average(paces); } if (mpsToPace(output.pace) <= settings.maxPace && !recentlyChanged) { changeTemplate("p"); } if (pTimer == NOTIFICATIONTIME) { changeTemplate("t") pTimer = 0; } // implemented earlier in file tick(); } }OnEvent handles any events triggered in html files. In this app eventId 1 means that user has pressed the middle button on notification view to change back to normal view.
main.js function onEvent(_input, output, eventId) { switch (eventId) { case 1: changeTemplate("t"); break; } }And lastly getUserInterface that defines what html the watch should show, but the main logic is written changeTemplate function.
main.js function getUserInterface() { return { template: currentTemplate }; }t.html
Next we’ll look into t.html:
The html starts with uiView and consist of multiple divs. The divs have classes to get the desired look and format and style is used to put them to correct places. The meanings of different classes should be looked up from SuuntoPlus Reference under HTML templates under CSS.The graph is drawn with graph element that uses speed as an input. A key thing to note here is that we wanted pace and not speed. That is accomplished with valueFormat="Pace_Fourdigits. The different formats are also found in the Suunto Reference.
t.html <div id="graph" style="width:48%;height:50%;top:calc(75% - 100%e);left:calc(28% - 50%e);"> <graph style="position:absolute; left:0px; top:0px; width:100%; height:100%; box-sizing: border-box; padding-right:90px; padding-top:50px; padding-bottom:50px; fill-color:rgba(255, 0, 0, 0.7);" valueFormat="Pace_Fourdigits" type="line" grid="three lines" inputType="subscribe" input="/Activity/Move/-1/Speed/Current" min="0"/> </div>Then we have eval element. That is used to display watch data or output data.
t.html <eval input="/Zapp/{zapp_index}/Output/pace" outputFormat="Pace_Fourdigits" default="--"/> ... <eval input="Activity/Activity/-1/Distance/Current" outputFormat="Distance_Accumulated" default="--"/>The file has also an icon which can be displayd using “f-ico”, “f-ico-m”, “f-ico-l”, “f-ico-xl” as the class and the code as text.
t.html <div class="f-ico" style="top:calc(85% - 100%e);left:calc(40% - 50%e);"></div>The p.html has similar components but it also declares userinput. If there are multiple userinputs you must give the unique numbers to distinguish them in main.js onEvent funtion.
p.html <userInput> <pushButton name="next" type="lock" onClick="$.put('/Zapp/{zapp_index}/Event', 1, null, 'int32');" /> </userInput>And that was the whole app. If you have any questions about Max Pacer don’t hesitate to ask!
-
FowlPlay
FowlPlay is a quick, light‑hearted minigame perfect for killing time while waiting during any sport. Master the slingshot, tweak your trajectory and send your bird soaring toward its waiting pig friend.
manifest.json
The manifest for FowlPlay only defines the strictly necessary fields:name(that app’s name),description(a short description of the app; preferably under 22 characters),version(a unique short identifier; must be changed when reuploading to ApiZone),author(credit where credit is due),modificationTime(a Epoch Unix Timestamp in whole seconds representing modification time),type(the literal string “feature” for sport apps),usage(the literal string “workout”) andtemplate(an array of html templates used by this app).manifest.json:
{ "name": "FowlPlay", "description": "Unite with the pig!!", "version": "1.0", "author": "Birdy B.", "modificationTime": 1770000000, "type": "feature", "usage": "workout", "template": [{"name": "g.html"}] }
main.js
For this app, all rendering and game logic live directly inside the HTML template (see below). Every app still needs amain.jsfile and must implement the globalgetUserInterfacefunction that tells the watch which template to load. The function should return an object that includes at least atemplatefield with template’s name. The file extension (.html) should be dropped here, even though the manifest lists full file names for templates. Here’s our minimalmain.jsfile:main.js:
function getUserInterface() { return { template: 'g' }; }
g.html
Finally, the HTML template! The file name should match the declaration inmanifest.jsonand value returned bygetUserInterfaceinmain.js. For your convenience the entire code with comments is listed here and explained below:g.html:
<uiView onLoad=" // Set constants for world gravity (g) and bird/enemy radius(r) var g = .2, r = 5; // Set the current state (one of 'sling', 'launched', 'won' or 'reset') and initialize sleep timer (s) var state = 'sling', s = 0; // Set variables for the launch force (f), launch angle (a) and current direction (d; angle) var f = 2, a = 0, d = 0; // Define the position and velocities for the bird and enemy var bird = {x: 20, y: 61.5, dx: 0, dy: 0}; var enemy = {x: 80, y: 80 - r, dx: 0, dy: 0}; /** @param { CanvasRenderingContext2D } ctx */ function renderGame(ctx) { // Viewport width and height from canvas width and height var vw = ctx.width / 100; var vh = ctx.height / 100; // Draw sky ctx.fillStyle = '#CCEEFF'; ctx.fillRect(0, 0, 100 * vw, 100 * vh); // Draw ground ctx.fillStyle = '#14B814'; ctx.fillRect(0, 80 * vh, 100 * vw, 20 * vh); // Draw slingshot ctx.strokeStyle = '#8A380F'; ctx.fillStyle = '#8A380F'; ctx.beginPath(); ctx.lineWidth = 3 * vh; ctx.lineCap = 'round' ctx.arc(20 * vw, 61.5 * vh, 7.5 * vh, 0, 3.15); ctx.stroke(); ctx.fillRect(18 * vw, 70 * vh, 4 * vw, 10 * vh); // Draw bird circle(ctx, '#F2DF0D', bird.x * vw, bird.y * vh, r * vh); // Calculate current direction d = (state == 'sling') ? a : (!!bird.dx ? (Math.atan(bird.dy / bird.dx) + .8) / -.2 : 0); // Draw beak ctx.fillStyle = '#F97706'; ctx.beginPath(); ctx.moveTo((bird.x + Math.cos(-d * .2 - 1 ) * r * .45) * vw, (bird.y + Math.sin(-d * .2 - 1 ) * r * .45) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - .4) * r * .95) * vw, (bird.y + Math.sin(-d * .2 - .4) * r * .95) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 + .2) * r * .45) * vw, (bird.y + Math.sin(-d * .2 + .2) * r * .45) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - .4) * r * .2 ) * vw, (bird.y + Math.sin(-d * .2 - .4) * r * .2 ) * vh); ctx.closePath(); ctx.fill(); // Draw eyes ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.moveTo((bird.x + Math.cos(-d * .2 - 2.1) * r * .25) * vw, (bird.y + Math.sin(-d * .2 - 2.1) * r * .25) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - 1.4) * r * .5 ) * vw, (bird.y + Math.sin(-d * .2 - 1.4) * r * .5 ) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - 1.5) * r * .75) * vw, (bird.y + Math.sin(-d * .2 - 1.5) * r * .75) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - 1.2) * r * .75) * vw, (bird.y + Math.sin(-d * .2 - 1.2) * r * .75) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - 1.3) * r * .15) * vw, (bird.y + Math.sin(-d * .2 - 1.3) * r * .15) * vh); ctx.fill(); // Draw enemy circle(ctx, '#FF80D4', enemy.x * vw, enemy.y * vh, r * vh); // Eyes circle(ctx, '#ffffff', (enemy.x - 2) * vw, enemy.y * vh, (r - 3.5) * vh); circle(ctx, '#ffffff', (enemy.x + 2) * vw, enemy.y * vh, (r - 3.5) * vh); circle(ctx, '#000000', (enemy.x - 2) * vw, (enemy.y - .25) * vh, .25 * vh); circle(ctx, '#000000', (enemy.x + 2) * vw, (enemy.y - .25) * vh, .25 * vh); //Snout circle(ctx, '#FFCCEE', enemy.x * vw, (enemy.y + 1) * vh, (r - 3) * vh); circle(ctx, '#330022', (enemy.x - .75) * vw, (enemy.y + .5) * vh, .65 * vh); circle(ctx, '#330022', (enemy.x + .75) * vw, (enemy.y + .75) * vh, .45 * vh); // Draw predictions var pred = nextPos({x: bird.x, y: bird.y, dx: bird.dx, dy: bird.dy}); if (state == 'sling') for (var i = 0; i <= 5; ++i) { pred = nextPos(nextPos(pred)); circle(ctx, '#47b4eb', pred.x * vw, pred.y * vh, 1 * vh); } // Game info ctx.fillStyle = '#0d5173'; cText(ctx, 'FowlPlay', 50 * vw, 15 * vh); if (state == 'sling') cText(ctx, 'Force: ' + f, 50 * vw, 25 * vh); if (state == 'won') cText(ctx, 'Congrats, you win!', 50 * vw, 25 * vh); // Move bird and enemy if (state == 'launched') bird = nextPos(bird); enemy = nextPos(enemy); // Calculate new direction if (state == 'sling') { bird.dx = Math.cos(-a * .2 - .8) * f; bird.dy = Math.sin(-a * .2 - .8) * f; } // Check if bird and enemy overlap if ((bird.x - enemy.x) * (bird.x - enemy.x) + (bird.y - enemy.y) * (bird.y - enemy.y) < 4 * r * r) state = 'won'; // Next attempt if bird is still for 30 frames or game is reset if ((Math.abs(bird.dx) < 0.1 && Math.abs(bird.dy) < 0.1) || state == 'won') { ++s; } else { s = 0; } if ((s > 30 && state != 'sling')|| state == 'reset') { // Reset game state, bird position and get random location for enemy from current time state = 'sling'; bird = {x: 20, y: 61.5, dx: 0, dy: 0}; $.get('/Dev/Time/LocalTime', function(v){ enemy.x = parseInt(formatValue(v, 'time s')) % 11 * 4 + 40; }) } } // Helper function to center text function cText(ctx, text, x, y) { ctx.fillText(text, x - ctx.measureText(text).width / 2, y); } // Helper function to draw a circle function circle(ctx, color, x, y, rd) { ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x, y, rd, 0, 6.3); ctx.closePath(); ctx.fill(); } // Helper method to calculate next position function nextPos(obj) { obj.dy += g; obj.x += obj.dx; obj.y += obj.dy; // Snap to ground if below ground surface after movement for simplicity if (obj.y >= 80 - r) { obj.y = 80 - r; obj.dy *= -.9; obj.dx *= .9; } return obj; } // Helper function to handle push button input function handleGameIO(v) { // Up and down button-click changes the launch angle if (v == 10 && state == 'sling') ++a; if (v == 20 && state == 'sling') --a; // Up-button-longpress changes the force of the slingshot if (v == 11 && state == 'sling') f = (f % 4) + 1; // Down-button-longpress launches the bird if (v == 21 && state == 'sling') state = 'launched'; if (v == 20 && state == 'launched') state = 'reset'; } " onActivate = "$.subscribe('/Dev/Time/Tick10hz', function(){ control('#cnv', 'REFRESH'); })" > <div id="suuntoplus"> <uiViewSet id="view"> <div id="welcome"> <div style="width: 100%; height: 50%; top: 0%; left: 0%;"><img src="btn-shape-top.png" class="c-yellow"></div> <div class="f-b-s cm-fgc" style="top: 13%; left: calc(79% - 100%e);" >CHANGE FORCE</div> <div style="width: 50px; height: 50px; top: calc(30% - 50%e); left: calc(90% - 50%e);"><img class="cm-bgc" src="hint-btn-top.png" /></div> <div class="p-m"> <span class="f-m">FowlPlay</span><br/> Click buttons to aim<br /> Longpress for action<br /> Launch away! </div> <div style="width: 100%; height: 50%; top: calc(100% - 100%e); left: 0%;"><img src="btn-shape-btm.png" class="c-yellow p-b"></div> <div class="f-b-s cm-fgc" style="top: calc(87% - 100%e); left: calc(79% - 100%e);" >LAUNCH BIRD</div> <div style="width: 50px; height: 50px; top: calc(70% - 50%e); left: calc(90% - 50%e);"><img class="cm-bgc" src="hint-btn-bottom.png" /></div> <userInput> <pushButton name="down" onLongPress="navigate('#view', 'game')"> </userInput> </div> <div id="game"> <object id="cnv" type="canvas" build="ctx => renderGame(ctx)" style="width: 100%; height: 100%; top: 0%; left: 0%;"/> <userInput> <pushButton name="up" onClick="handleGameIO(10)" onLongPressStart="handleGameIO(11)" /> <pushButton name="down" onClick="handleGameIO(20)" onLongPressStart="handleGameIO(21)" /> </userInput> </div> </uiViewSet> </div> </uiView>If you have the SuuntoPlusEditor-plugin installed in Visual Studio Code and your Suunto watch connected to your laptop you are now ready to test the app:
- Create an empty folder and open it in Visual Studio Code
- Create new files
manifest.json,main.jsandg.html - Copy-paste the code snippets above to their respective files
- Navigate to the bottom of the
Explorerview where these files are listed. Under theSuuntoPlus Appssection hover the mouse over the newly created app and click on the watch icon (Deploy to Watch). - Wait for the building and uploading to finish; the app is now available on your watch under
SuuntoPlusonce you start a new exercise.
Please note that the app does not work in the simulator.
In this example, the
g.htmlfile handles everything from layout and user interactions (watch buttons) to game logic and rendering. Let’s remove the game-related code and focus on the layout and user interactions first.<uiView> <div id="suuntoplus"> <uiViewSet id="view"> <div id="welcome"> <div style="width: 100%; height: 50%; top: 0%; left: 0%;"><img src="btn-shape-top.png" class="c-yellow"></div> <div class="f-b-s cm-fgc" style="top: 13%; left: calc(79% - 100%e);" >CHANGE FORCE</div> <div style="width: 50px; height: 50px; top: calc(30% - 50%e); left: calc(90% - 50%e);"><img class="cm-bgc" src="hint-btn-top.png" /></div> <div class="p-m"> <span class="f-m">FowlPlay</span><br/> Click buttons to aim<br /> Longpress for action<br /> Launch away! </div> <div style="width: 100%; height: 50%; top: calc(100% - 100%e); left: 0%;"><img src="btn-shape-btm.png" class="c-yellow p-b"></div> <div class="f-b-s cm-fgc" style="top: calc(87% - 100%e); left: calc(79% - 100%e);" >LAUNCH BIRD</div> <div style="width: 50px; height: 50px; top: calc(70% - 50%e); left: calc(90% - 50%e);"><img class="cm-bgc" src="hint-btn-bottom.png" /></div> <userInput> <pushButton name="down" onLongPress="navigate('#view', 'game')"> </userInput> </div> <div id="game"> <!-- The game logic will be added here --> </div> </uiViewSet> </div> </uiView>Every sport app template has a
<uiView>element representing the Suunto watch as their root element. It is good practice to wrap the content inside<uiView>in a<div>(you can think of these as the<html>and<body>elements of HTML).This app has two different screens: a welcome screen and the actual game. This can be achieved with a
<uiViewSet>element containing two<div>elements. All three elements have anidattribute for easy referencing in the code. This can be used for example to change which of the<div>elements is displayed. This is done with:<userInput> <pushButton name="down" onLongPress="navigate('#view', 'game')"> </userInput>The
<userInput>element can contain several<pushButton>elements. A<pushButton>has anameattribute to identify the button (here set to “down” for the bottom-right button) and event listeners for different types of user actions (here we listen to a longpress withonLongPress). Because the<userInput>element is defined inside the welcome screen’s<div>element, the interactions are captured only when the welcome screen is visible.navigateis a global built-in function of the watch that takes two parameters here: a query string to identify the targeted<uiViewSet>element and theidattribute of the<div>element to display.The rest of the
<div>elements are used to set the position and dimensions (setting CSS styles (top/left/width/height) with thestyleattribute;%represents a percentage of the parent element’s width/height while%erepresents a percentage of the current element’s width/height) as well as the font and color (through class names with theclassattribute) of texts and images on the welcome screen. The class names and images used here are part of the watch and can be used without explicit definition or importing. The first letter of the class name typically defines its purpose:cis for colors (fg= foreground;bg= background;cm= current (light/dark) mode),fis for font styles andpis for positioning.The game logic is defined in an event listener attached to the
<uiView>. Here we use theonLoadattribute (notice the uppercaseLcompared to the HTML standard) that behaves similarly to the onLoad function that could be defined inmain.jsexcept the symbols (functions and variables) defined here are in scope for (and thus can be used in) the entire HTML template. We take advantage of this for rendering. First we define a drawing surface in our layout using:<object id="cnv" type="canvas" build="ctx => renderGame(ctx)" style="width: 100%; height: 100%; top: 0%; left: 0%;"/>An
<object>element with atypeattribute of “canvas” works similarly to a HTML5<canvas>. The content will be rendered using the anonymous function defined by thebuildattribute. Here we capture the argument (that is a modified version of aCanvasRenderingContext2Dfrom the JavaScript Canvas API) and pass it onto the functionrenderGame. By setting theidandstyleattributes we ensure the canvas can be easily referenced later and takes up the entire screen respectively. This setup draws our game just once, but for a smooth gaming experience the screen needs to be updated continuously. This is achieved by adding the following attribute to the<uiView>element:onActivate = "$.subscribe('/Dev/Time/Tick10hz', function(){ control('#cnv', 'REFRESH'); })"Here the
$object allows us to access the devices resources. Here we use thesubscribemethod to attach a change listener to a resource. The'/Dev/Time/Tick10hz'resource will trigger the callback function approximately 10 times per second (this essentially works likesetIntervalin JavaScript).controlis a global built-in function of the watch that takes two parameters: a query string to identify the target element and the control signal to send it. Here this causes the<canvas>to be refreshed (=rerendered). The actual rendering is handled by the renderGame function defined in theonLoadattribute of the<uiView>:function renderGame(ctx) { // Viewport width and height from canvas width and height var vw = ctx.width / 100; var vh = ctx.height / 100; // Draw sky ctx.fillStyle = '#CCEEFF'; ctx.fillRect(0, 0, 100 * vw, 100 * vh); // Draw ground ctx.fillStyle = '#14B814'; ctx.fillRect(0, 80 * vh, 100 * vw, 20 * vh); // Draw slingshot ctx.strokeStyle = '#8A380F'; ctx.fillStyle = '#8A380F'; ctx.beginPath(); ctx.lineWidth = 3 * vh; ctx.lineCap = 'round' ctx.arc(20 * vw, 61.5 * vh, 7.5 * vh, 0, 3.15); ctx.stroke(); ctx.fillRect(18 * vw, 70 * vh, 4 * vw, 10 * vh); // Draw bird circle(ctx, '#F2DF0D', bird.x * vw, bird.y * vh, r * vh); // Calculate current direction d = (state == 'sling') ? a : (!!bird.dx ? (Math.atan(bird.dy / bird.dx) + .8) / -.2 : 0); // Draw beak ctx.fillStyle = '#F97706'; ctx.beginPath(); ctx.moveTo((bird.x + Math.cos(-d * .2 - 1 ) * r * .45) * vw, (bird.y + Math.sin(-d * .2 - 1 ) * r * .45) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - .4) * r * .95) * vw, (bird.y + Math.sin(-d * .2 - .4) * r * .95) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 + .2) * r * .45) * vw, (bird.y + Math.sin(-d * .2 + .2) * r * .45) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - .4) * r * .2 ) * vw, (bird.y + Math.sin(-d * .2 - .4) * r * .2 ) * vh); ctx.closePath(); ctx.fill(); // Draw eyes ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.moveTo((bird.x + Math.cos(-d * .2 - 2.1) * r * .25) * vw, (bird.y + Math.sin(-d * .2 - 2.1) * r * .25) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - 1.4) * r * .5 ) * vw, (bird.y + Math.sin(-d * .2 - 1.4) * r * .5 ) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - 1.5) * r * .75) * vw, (bird.y + Math.sin(-d * .2 - 1.5) * r * .75) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - 1.2) * r * .75) * vw, (bird.y + Math.sin(-d * .2 - 1.2) * r * .75) * vh); ctx.lineTo((bird.x + Math.cos(-d * .2 - 1.3) * r * .15) * vw, (bird.y + Math.sin(-d * .2 - 1.3) * r * .15) * vh); ctx.fill(); // Draw enemy circle(ctx, '#FF80D4', enemy.x * vw, enemy.y * vh, r * vh); // Eyes circle(ctx, '#ffffff', (enemy.x - 2) * vw, enemy.y * vh, (r - 3.5) * vh); circle(ctx, '#ffffff', (enemy.x + 2) * vw, enemy.y * vh, (r - 3.5) * vh); circle(ctx, '#000000', (enemy.x - 2) * vw, (enemy.y - .25) * vh, .25 * vh); circle(ctx, '#000000', (enemy.x + 2) * vw, (enemy.y - .25) * vh, .25 * vh); //Snout circle(ctx, '#FFCCEE', enemy.x * vw, (enemy.y + 1) * vh, (r - 3) * vh); circle(ctx, '#330022', (enemy.x - .75) * vw, (enemy.y + .5) * vh, .65 * vh); circle(ctx, '#330022', (enemy.x + .75) * vw, (enemy.y + .75) * vh, .45 * vh); // Draw predictions var pred = nextPos({x: bird.x, y: bird.y, dx: bird.dx, dy: bird.dy}); if (state == 'sling') for (var i = 0; i <= 5; ++i) { pred = nextPos(nextPos(pred)); circle(ctx, '#47b4eb', pred.x * vw, pred.y * vh, 1 * vh); } // Game info ctx.fillStyle = '#0d5173'; cText(ctx, 'FowlPlay', 50 * vw, 15 * vh); if (state == 'sling') cText(ctx, 'Force: ' + f, 50 * vw, 25 * vh); if (state == 'won') cText(ctx, 'Congrats, you win!', 50 * vw, 25 * vh); // Move bird and enemy if (state == 'launched') bird = nextPos(bird); enemy = nextPos(enemy); // Calculate new direction if (state == 'sling') { bird.dx = Math.cos(-a * .2 - .8) * f; bird.dy = Math.sin(-a * .2 - .8) * f; } // Check if bird and enemy overlap if ((bird.x - enemy.x) * (bird.x - enemy.x) + (bird.y - enemy.y) * (bird.y - enemy.y) < 4 * r * r) state = 'won'; // Next attempt if bird is still for 30 frames or game is reset if ((Math.abs(bird.dx) < 0.1 && Math.abs(bird.dy) < 0.1) || state == 'won') { ++s; } else { s = 0; } if ((s > 30 && state != 'sling')|| state == 'reset') { // Reset game state, bird position and get random location for enemy from current time state = 'sling'; bird = {x: 20, y: 61.5, dx: 0, dy: 0}; $.get('/Dev/Time/LocalTime', function(v){ enemy.x = parseInt(formatValue(v, 'time s')) % 11 * 4 + 40; }) } }The drawing is achieved by using methods familiar from the JavaScript Canvas API such as
fillRect,beginPath,arc,moveTo,lineTo,fillText,measureText,strokeandfillas well as reading and modifying properties such aswidth,height,fillStyle,strokeStyle,lineWidthandlineCap. It is notable that the drawing context can be passed on to other function as is done in this example with the following helper functions::// Helper function to center text function cText(ctx, text, x, y) { ctx.fillText(text, x - ctx.measureText(text).width / 2, y); }// Helper function to draw a circle function circle(ctx, color, x, y, rd) { ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x, y, rd, 0, 6.3); ctx.closePath(); ctx.fill(); }We have also defined some constants and variables to manage the game’s physics and state (and once again defined a helper function to move the bird):
// Set constants for world gravity (g) and bird/enemy radius(r) var g = .2, r = 5; // Set the current state (one of 'sling', 'launched', 'won' or 'reset') and initialize sleep timer (s) var state = 'sling', s = 0; // Set variables for the launch force (f), launch angle (a) and current direction (d; angle) var f = 2, a = 0, d = 0; // Define the position and velocities for the bird and enemy var bird = {x: 20, y: 61.5, dx: 0, dy: 0}; var enemy = {x: 80, y: 80 - r, dx: 0, dy: 0};// Helper method to calculate next position function nextPos(obj) { obj.dy += g; obj.x += obj.dx; obj.y += obj.dy; // Snap to ground if below ground surface after movement for simplicity if (obj.y >= 80 - r) { obj.y = 80 - r; obj.dy *= -.9; obj.dx *= .9; } return obj; }Here a game object (the bird or pig) is defined by its x and y coordinates as well as its horizontal and vertical velocity (dx and dy).
One quirk of the code is how the new pseudo-random location for the pig is calculated.
$.get('/Dev/Time/LocalTime', function(v){ enemy.x = parseInt(formatValue(v, 'time s')) % 11 * 4 + 40; })Here we fetch (the
getmethod from the$object works similarly tosubscribemethod except this is only executed once) the current time, treat the value as a time and format it to only include the current second (using the built-in functionformatValue). Then we convert the seconds to a number and clamp it to fit our game world.The final step to complete the game is to attach event listeners to user input. This is similar to before except this time we use a helper function to handle the mapping of the users actions to correct changes to the game depending on the game’s state.
<userInput> <pushButton name="up" onClick="handleGameIO(10)" onLongPressStart="handleGameIO(11)" /> <pushButton name="down" onClick="handleGameIO(20)" onLongPressStart="handleGameIO(21)" /> </userInput>// Helper function to handle push button input function handleGameIO(v) { // Up and down button-click changes the launch angle if (v == 10 && state == 'sling') ++a; if (v == 20 && state == 'sling') --a; // Up-button-longpress changes the force of the slingshot if (v == 11 && state == 'sling') f = (f % 4) + 1; // Down-button-longpress launches the bird if (v == 21 && state == 'sling') state = 'launched'; if (v == 20 && state == 'launched') state = 'reset'; }
The app combines many different capabilities of Suunto watches. Feel free to ask any questions you might have about this example app. Hopefully it inspired you to create some awesome. Happy launching; may your idle moments forever be fowl‑filled!
-
D Dimitrios Kanellopoulos pinned this topic