• 4 Votes
    6 Posts
    240 Views
    Dimitrios KanellopoulosD
    @brechtvb no afaik. So for example its not so hard to read a service and sub to a characteristic etc
  • SuuntoPlus Apps Development

    SuuntoPlus™ Sports Apps
    55
    10 Votes
    55 Posts
    12k Views
    M
    Great news! It would be fantastic if these apps could also be launched directly in the future (not just during training). That would open up even more possibilities. But it’s a very good start
  • S+ parkrun

    SuuntoPlus™ Sports Apps
    39
    7 Votes
    39 Posts
    4k Views
    Nikolai SimonovN
    Hey guys! If you’ve run into the ‘to many apps’ error, could you try the updated version? I’ve made some optimizations to the QR generation algorithm, so it should consume less memory now. Please let me know if it works for you!
  • 2 Votes
    14 Posts
    403 Views
    Francesco PaganoF
    I know an indoor climbing app is already available, but after seeing how this works on a Garmin as a sport mode, I think it can be improved. For instance my friend was able to select the difficulty level before starting each attempt. Nothing interesting then showed on his Garmin Connect activity page, so I think a good app should also record each attempt and related details properly into Suunto App.
  • Watch Faces as part of new OpenSuuntoPlus

    Suunto Plus Development
    3
    4 Votes
    3 Posts
    120 Views
    S
    @dtsdts You did not miss anything! At launch we only provide the ability to create sports apps, not watch faces. Watch face creation – including an intuitive visual editor – is in development, but is not quite ready yet. You will know when it is widely available – we don’t intend to hide it
  • Live location S+ app

    SuuntoPlus™ Sports Apps
    268
    58 Votes
    268 Posts
    69k Views
    Nikolai SimonovN
    While we wait for the app stores to approve version 1.0.14, I want to share an early preview of a new feature I think you’ll find useful. Introducing Sensor Bridge! or sensor forwarding, decide later… You can use this during indoor sessions to share your heart rate, cadence, and power directly to apps like MyWhoosh or Zwift, or simply broadcast your HR to your bike computer, or other Suunto watch… Here is a quick video showing it in action: https://drive.proton.me/urls/STJ4XA016R#DVBaMPzj9cLt I also prepared an early Android build so you can try it yourself: https://artifact.fra1.cdn.digitaloceanspaces.com/live-t/apk/sensor-forward/live-t.apk Please give it a run and let me know if it works for you! I will release the iOS TestFlight build later this week. Well… To try this out in the Android build, simply follow these steps: Go to Settings -> Lab and turn Sensor Forwarding to ON. Start the service and connect your watch as usual. Once your watch connects, you can pair your other device to the sensor. Look for a Bluetooth device named SF-ATHL. It will broadcast your HR, Power, and Cadence/Speed data.
  • Next Big Update for Race 2 etc...

    Watches
    13
    1 Votes
    13 Posts
    629 Views
    2
    @cosme.costa said in Next Big Update for Race 2 etc...: auto re-routing using offline maps in the app. Thats interesting… I will surely test this…
  • Scare in Paris: SAFE MODE stuck in a LOOP!

    Suunto Race S
    4
    0 Votes
    4 Posts
    208 Views
    2
    @enriqueescoms I try to avoid USB ports of unknown source. That’s why I’m asking. Using the power outlet should be working, in my book. strange this…
  • 0 Votes
    1 Posts
    24 Views
    No one has replied
  • Wahoo Kickr Core 2 speed and distance

    Suunto Race S
    20
    0 Votes
    20 Posts
    254 Views
    D
    @raven said in Wahoo Kickr Core 2 speed and distance: @Dwartus said in Wahoo Kickr Core 2 speed and distance: Thank you for your proposal, for that topic when I run outside with my bike, I add a Suunto “Bike POD” which provide to the watch cadence ans speed. Also, I remember that during the run the average speed and speed is given by the “Bike POD”, but at the end of the actvity, GPS data are used to give average speed and distance. Nevertheless, you understood that I didn’t connect/used this Suunto “Bike POD” for Home Trainer activity (also save “Bike POD” battery because unmount). Thanks Oops, I missed this in my last reply. If you already have a speed/cadence sensor you use outdoors, then yes try that as an additional sensor for your indoor rides and it should work. Yes ! Thanks for the hint, I didn’t think about that because actually not installed because Home Trainer period As a conclusion, the Suunto watch Race/Race S (perhaps other models) are not able to catch all the data broadcasted by Wahoo trainer, only power meter can be read (but not as a Suunto “Power POD” because not possible to calibrate it), it’s too bad Thank you
  • Suunto Run & Treadmill

    Suunto Run
    1
    0 Votes
    1 Posts
    41 Views
    No one has replied
  • 8 Votes
    45 Posts
    4k Views
    Adrian.SA
    @aiv4r Unfortunately, after editing the elevation in Suunto app, it still isn’t re-calculated, which I reported in this thread: https://forum.suunto.com/topic/14776/new-feature-add-elevation-treadmill-training-adjust-elevation
  • 1 Votes
    2 Posts
    56 Views
    F
    @onghyr, yes, https://forum.suunto.com/topic/14749/bug-vertical-2-randomly-dropping-external-bt-sensors-and-not-reconnecting Unfortunately no response from the forum big guys (moderators, field testers).
  • 3 Votes
    2 Posts
    118 Views
    martintrailM
    @Marek-Machala Hlavni mesto Praha is still fine. I have Roznov pod Radhostem there, and it almost doesn’t fit in there I would gladly delete the entire position and leave only the weather, as you write.
  • 0 Votes
    3 Posts
    219 Views
    withManishW
    @matt-r, I have noticed that with this watch, HR and SpO2 readings are inconsistent and inaccurate, even after following the instructions during resting.
  • Stuck to Power Save Mode

    Suunto Race S
    3
    1
    0 Votes
    3 Posts
    155 Views
    withManishW
    @Horizontal_2 Thank you, the trick that worked.
  • 1 Votes
    1 Posts
    50 Views
    No one has replied
  • Undecided for a new bike computer

    Suunto app and other software services
    38
    0 Votes
    38 Posts
    1k Views
    Stoke80S
    @Dimitrios-Kanellopoulos Thank you. Just for my guide: How would I see the zones in the fit-File? Would there be an additional row, where they are shown?
  • Vertical 2 syncing of routes

    Suunto Vertical 2
    8
    0 Votes
    8 Posts
    185 Views
    W
    @maszop totally agree! Offline re-routing on downloaded maps is perhaps the greatest Achilles’s heel of the Suunto platform and, I think, the most important difference from Garmin’s system. As said in previous posts, this Achilles’s heel is further compounded by the limited number of routes one can offload onto the SV2. I believe that watch processor and storage space are not the issues. And even if they are, why, at least, not use Suunto app on mobile device for these two jobs for eventual BT transfer to watch of gpx file (like you did with a third party app)? It’s not ideal since you have to have a mobile device on activity/exercise but you will have peace of mind that you are covered for any eventuality.
  • Examples explained

    Pinned Suunto Plus Development
    2
    0 Votes
    2 Posts
    213 Views
    S
    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: Create an empty folder and open it in Visual Studio Code Create new files manifest.json, main.js and g.html Copy-paste the code snippets above to their respective files 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). 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!