Suunto app Forum Suunto Community Forum
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Register
    • Login

    Examples explained

    Scheduled Pinned Locked Moved Suunto Plus Development
    2 Posts 1 Posters 110 Views 1 Watching
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • S Online
      SuuntoPartnerTeam
      last edited by SuuntoPartnerTeam

      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);">&#xF102;</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!

      S 1 Reply Last reply Reply Quote 0
      • S Online
        SuuntoPartnerTeam @SuuntoPartnerTeam
        last edited by

        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”) and template (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 a main.js file and must implement the global getUserInterface function that tells the watch which template to load. The function should return an object that includes at least a template field 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 minimal main.js file:

        main.js:

        function getUserInterface() { return { template: 'g' }; }
        

        g.html
        Finally, the HTML template! The file name should match the declaration in manifest.json and value returned by getUserInterface in main.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:

        1. Create an empty folder and open it in Visual Studio Code
        2. Create new files manifest.json, main.js and g.html
        3. Copy-paste the code snippets above to their respective files
        4. Navigate to the bottom of the Explorer view where these files are listed. Under the SuuntoPlus Apps section hover the mouse over the newly created app and click on the watch icon (Deploy to Watch).
        5. Wait for the building and uploading to finish; the app is now available on your watch under SuuntoPlus once you start a new exercise.

        Please note that the app does not work in the simulator.

        In this example, the g.html file 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 an id attribute 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 a name attribute 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 with onLongPress). Because the <userInput> element is defined inside the welcome screen’s <div> element, the interactions are captured only when the welcome screen is visible. navigate is a global built-in function of the watch that takes two parameters here: a query string to identify the targeted <uiViewSet> element and the id attribute 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 the style attribute; % represents a percentage of the parent element’s width/height while %e represents a percentage of the current element’s width/height) as well as the font and color (through class names with the class attribute) 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: c is for colors (fg = foreground; bg = background; cm = current (light/dark) mode), f is for font styles and p is for positioning.

        The game logic is defined in an event listener attached to the <uiView>. Here we use the onLoad attribute (notice the uppercase L compared to the HTML standard) that behaves similarly to the onLoad function that could be defined in main.js except 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 a type attribute of “canvas” works similarly to a HTML5 <canvas>. The content will be rendered using the anonymous function defined by the build attribute. Here we capture the argument (that is a modified version of a CanvasRenderingContext2D from the JavaScript Canvas API) and pass it onto the function renderGame. By setting the id and style attributes 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 the subscribe method 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 like setInterval in JavaScript). control is 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 the onLoad attribute 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, stroke and fill as well as reading and modifying properties such as width, height, fillStyle, strokeStyle, lineWidth and lineCap. 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 get method from the $ object works similarly to subscribe method 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 function formatValue). 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!

        1 Reply Last reply Reply Quote 2
        • Dimitrios KanellopoulosD Dimitrios Kanellopoulos pinned this topic
        • First post
          Last post

        Suunto Terms | Privacy Policy