commit 0b1c7ff386c26baa5687819f68938ea702fd2779 Author: Ryan McGrath Date: Sun Jul 10 03:15:27 2011 -0400 Initial push; huzzah. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cb6996c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2009 - 2011 Ryan McGrath + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/build.py b/build.py new file mode 100644 index 0000000..a868ce5 --- /dev/null +++ b/build.py @@ -0,0 +1,64 @@ +#!/usr/bin/python + +""" + Build-script that brings together all the JS files for wii.js and + takes care of minifying it down to a decent size. + + Before you even ask, no, we're not using uglify.js or Closure. Both + of them are simply too aggressive for their own good; while normally this + is an excellent feature to have, the JS engine that the Opera version on the Wii + uses has a really odd set of quirks when you start trying to get smart with code, + and YUI is the only minifier that doesn't appear to totally destroy it all through + obfuscation. We'll forego a few bytes to have the thing working. ;P + + Uglify also requires Node.js. While I *love* Node, I'll wait until it's more readily + available on Windows - there are a large amount of Windows-based programmers that I'm + not keen on keeping out of development. +""" + +import os +from subprocess import call + +currdir = os.getcwd() + +# The names of our JS files (minus the .js) that we want to build into the +# final distribution. +DEPENDENCIES = [ + 'wii', + 'util', + 'remote', +] + +# What we're going to end up injecting as our core build, into core.js. +inject_build = '' + +def minify_code(filename, final_filename): + """ + Dips out to Java/YUI compressor and runs the minifier on the specified file, dumping the + output into the specified final_filename. + """ + #cmd = 'java -jar %s/utilities/yuicompressor-2.4.2.jar %s -o %s' % (currdir, filename, final_filename) + call(['java', '-jar', '%s/utilities/yuicompressor-2.4.2.jar' % currdir, filename, '-o', final_filename]) + +# Run through each dependency, read it into "inject build", then do a simple split on the +# contents of core.js and wrap it all up. +for dependency in DEPENDENCIES: + f = open('%s/js/src/%s.js' % (currdir, dependency), 'r') + inject_build += f.read() + f.close() + +# Open core.js, split it on our build spot, wrap junks. +f = open('%s/js/src/core.js' % currdir, 'r') +core = f.read() +f.close() +core = core.split('/*{{inject_build}}*/') + +# Write out a non-minified build. +f = open('%s/js/wii.js' % currdir, 'w') +f.write(core[0]) +f.write(inject_build) +f.write(core[1]) +f.close() + +# Write out a minified build. +minify_code('%s/js/wii.js' % currdir, '%s/js/wii.min.js' % currdir) diff --git a/css/demo.css b/css/demo.css new file mode 100644 index 0000000..3194363 --- /dev/null +++ b/css/demo.css @@ -0,0 +1,25 @@ +/** + * Wii Demo CSS + * + * Uses a basic CSS reset, and then some very rudimentary styles to make stuff easy + * on the eyes with the Wii console. + */ + +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, font, img, kbd, q, s, samp, +small, strike, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { +margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } +:focus { outline: 0; } +body { line-height: 1; } +ol, ul { list-style: none; } +table { border-collapse: separate; border-spacing: 0; } +caption, th, td { text-align: left; font-weight: normal; } +blockquote:before, blockquote:after, q:before, q:after { content: ""; } +blockquote, q { quotes: "" ""; } + +html { + color: #333; + font: normal 30px/34px helvetica, arial, sans-serif; + padding: 0px; + text-align: center; + background: #f9f9f9; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..8716280 --- /dev/null +++ b/index.html @@ -0,0 +1,99 @@ + + + + + Wii.js Demo + + + +

Wii.js Demo

+

+ This is a demo page meant to show how easy it is to interact with the Nintendo Wii remotes using Javascript. It's + powered by wii-js, a Javascript library that abstracts the differences + and pain points associated with using the Wii remotes. +

+ +

+ This demo is really simple, and works best with two Wii remotes (Wiimotes). Hold either Wiimote #1 or #2 horizontal (sideways), and use + the directional pad to move the box floating in the bottom right corner around. Wiimote #1 will turn the box red and fill it with a "1", + and Wiimote #2 will turn the box green and fill it with a "2". Have fun! +

+ +

+ If you find bugs or want to get in touch, come find me at the project home on GitHub! You + can also find me on Twitter (@ryanmcgrath), or my personal website. + I'm always interested in what others are building! +

+ +

+ - Ryan McGrath +

+ +
x
+ + + + + + \ No newline at end of file diff --git a/js/src/core.js b/js/src/core.js new file mode 100644 index 0000000..5a9144e --- /dev/null +++ b/js/src/core.js @@ -0,0 +1,27 @@ +/** + * core.js + * + * This is the main template file for bringing together + * the various libraries that power this entire little eco-system. + * + * Building the releases requires Python (2.5+); simply run... + * + * python build.py + * + * ...from the /js/ directory. + * + * @Author: Ryan McGrath + * @Requires: Nothing, top-level file. + */ + +;(function(running_inside_wii_browser) { + /** + * If we're not running inside the Nintendo Wii browser, bail out. + * In the future, branch here for touch-enabled devices...? + */ + if(!running_inside_wii_browser) return false; + + /*{{inject_build}}*/ + + window.Wii = Wii; +})(window.opera && opera.wiiremote); diff --git a/js/src/remote.js b/js/src/remote.js new file mode 100644 index 0000000..3c721a9 --- /dev/null +++ b/js/src/remote.js @@ -0,0 +1,120 @@ +/** + * remote.js + * + * Handles the subscribing to events portion of a Wii remote. It's best to think of this + * as a "request" object; it asks to be notified of events, and the actual events are + * dispatched from the main wii.js file. + * + * @Author: Ryan McGrath + * @Requires: wii.js, util.js + */ + +/** + * var wii_remote = new Wii.Remote(1, {...}); + * + * Instantiates a Wii Remote object. Events can be set on each of these objects, + * and the internal game loop will fire events based on the properties subscribed + * to here. + * + * @param remote_id - Number, required. 1 - 4, dictates which Wiimote this object + * relates to. + * @param opts - Object, optional. Allows you to override internal settings and such, + * should you want different behavior. + * @returns Wii.Remote instance. + */ +Wii.Remote = function(remote_id, opts) { + this.remote_id = remote_id; + this.opts = opts; + + /** + * If this is the "main" wii_remote, then the bitwise checks will fail + * because it's treated more as a "browsing" device. For these events, + * we'll just store the current wii_remote that's denoted as the "browsing" + * device and let the normal event/key delegation take care of things. + * + * The rest of the wii_remotes will go through the DISPATCHER checks that + * they've subscribed to. + */ + var startupStatus = this.isEnabled(); + if(startupStatus) { + if(!startupStatus.isBrowsing) { + Wii.extraRemotes.push(this); + } else { + Wii.currentBrowsingRemote = this; + } + } +}; + +Wii.Remote.prototype = { + opts: { + /** + * We default the controller to be in the vertical orientation; if + * it's overridden as "horizontal" (false), we'll catch it for the different key + * events and fire accordingly (e.g, the "up" button is different depending on + * how the player is holding the controller). + */ + horizontal: false + }, + + /** + * A hash of events that this Wii remote is interested in. Each + * entry should be a key (evtName) with a value (response). + * "evtName" is the event name that corresponds with boolean functions + * in the DISPATCHER above, and the response is a remote-bound function + * to call on that event. + */ + evtsInterestedIn: undefined, + + /** + * Wiimote.isEnabled() + * + * Determines the status of the wii_remote this object is curious about. Will return + * an updated kPadStatus object if so, false if it's not responding or the data + * is sent back as "invalid". This makes it so we don't bother sending events + * where they're not applicable. + * + * @returns object or boolean. + */ + isEnabled: function() { + var remote = opera.wiiremote.update(this.remote_id - 1); + return (remote.isEnabled && remote.isDataValid ? remote : false); + }, + + /** + * Wiimote.on(evtName, fn) + * + * Allows you to listen for an event on a given Wiimote. Call this on an instantiated + * Wiimote; "this" will be scoped to the Wiimote object. ;) + * + * @param evtName - String, a supported wii.js (DISPATCHER) event name. + * @param fn - Function, callback function to be executed on the event firing. Will be scoped to the Wiimote. + * @returns object or undefined - instantiated object this was called on to begin with, undefined if evtName is not supported. + */ + when: function(evtName, fn) { + if(typeof Wii.DISPATCHER[evtName] !== 'undefined') { + + /** + * THIS IS INCREDIBLY IMPORTANT, DO NOT REMOVE THIS!. + * + * The Wii's browser has an (odd...?) bug wherein if you have a prototype chain + * set up for a given object, and you default keys on the prototype chain to a blank object ("{}", for instance), + * it will _NOT_ make this a unique object, it keeps pointers to the original object that was created by the system + * for the first instantiated object. + * + * This is, needless to say, unlike just about any other JS environment and threw me for a loop for a good 6 hours. + * This line ensures that the first time this property is ever referenced, we get a fresh _CORRECTLY ALLOCATED_ chunk + * of memory to play with and store things in. + * + * Note that this also happens with Array structures, and conceivably anything else that would be using a copy-by-reference + * technique instead of a full clone. We want an object for this case, though, so we're not doing iterations on event dispatch, + * just a 1:1 lookup instead. + */ + if(this.evtsInterestedIn === undefined) this.evtsInterestedIn = {}; + + this.evtsInterestedIn[evtName] = Wii.util.bind(this, fn); + return this; + } + + return undefined; + } +}; diff --git a/js/src/util.js b/js/src/util.js new file mode 100644 index 0000000..3ddb1fb --- /dev/null +++ b/js/src/util.js @@ -0,0 +1,66 @@ +/** + * util.js + * + * A basic utility wrapper; anything extra that's often re-used should + * find its way here (e.g, debuggerDiv, bind, etc). + * + * @Author: Ryan McGrath + * @Requires: wii.js + */ + +Wii.util = { + /** + * Wii.util.debug(err); + * + * The Wii has... such little options for debugging, but we can try and make this a bit nicer. + * This accepts a stack trace (see example code below) and then outputs it to the screen. + * + * try { ... } catch(e) { Wii.util.debug(e); } + */ + debug: function(err) { + if(Wii.debuggerDiv === null) { + Wii.debuggerDiv = document.createElement('div'); + + Wii.debuggerDiv.style.cssText = [ + 'width: 90%;', + 'height: 90%;', + 'padding: 10px;', + 'font-size: 26px;', + 'font-family: monospace;', + 'overflow: scroll', + 'position: absolute;', + 'top: 10px;', + 'left: 10px;', + 'color: #f9f9f9;', + 'background-color: #010101;' + ].join(''); + + document.body.appendChild(Wii.debuggerDiv); + } + + if(typeof err === 'string') { + Wii.debuggerDiv.innerHTML = err; + } else { + var msg = ''; + for(var e in err) { msg += e + '=' + err[e] + '
'; } + Wii.debuggerDiv.innerHTML = msg; + } + + Wii.debuggerDiv.style.display = 'block'; + }, + + /** + * Wiimote.util.bind(bindReference, fn) + * + * Takes a reference (an object to scope to "this" at a later runtime) and binds it to a function (fn). + * + * @param bindReference - An object to set as the "this" reference for a later function call. + * @param fn - A function to bind the "this" object for. + * @returns fn - A new function to pass around, wherein it's all scoped as you want it. + */ + bind: function(bindReference, fn) { + return function() { + return fn.apply(bindReference, arguments); + }; + } +}; diff --git a/js/src/wii.js b/js/src/wii.js new file mode 100644 index 0000000..f220666 --- /dev/null +++ b/js/src/wii.js @@ -0,0 +1,224 @@ +/** + * wii.js + * + * Provides a sane, event-based documented wrapper around the Wiimote controls + * and API inside the Opera-based Nintendo Wii web browser. + * + * This is not produced by nor endorsed by Nintendo or Opera. I've written it + * on my own because I see a device that's sitting in millions of living rooms + * but being sorely neglected because a company couldn't get their act together + * in regards to third party development. ;) + * + * @Author: Ryan McGrath + * @Requires: Nothing. + */ + +/* Wii + * + * Top level namespace. Contains information about the main event loop, etc. + */ +var Wii = { + /** + * A "global" reference to the Wiimotes we're currently watching. Lets us run + * through on each loop/iteration and poll for a new status on it. + */ + extraRemotes: [], + currentBrowsingRemote: null, + setKeyListeners: false, + debuggerDiv: null +}; + +/** + * Install some basic low-level event listeners to monitor how + * the primary wii_remote is interacting with the browser; it's treated + * differently than the other wii_remotes, more as a "browsing" tool than + * a controller. Doesn't mean we can't try and mend the gap... + */ +Wii.installKeyListeners = function() { + document.addEventListener('mouseup', Wii.parsePrimaryWiimote, false); + document.addEventListener('keydown', Wii.parsePrimaryWiimote, false); + document.addEventListener('mousedown', Wii.parsePrimaryWiimote, false); + document.addEventListener('keyup', Wii.parsePrimaryWiimote, false); + + /** + * Some keys, like the directional ones, get... multiple events? + * In this case, just shut. down. everything. + * + * ...and let the programmer deal with it. + */ + document.addEventListener('keypress', Wii.parsePrimaryWiimote, false); + + return true; +}; + +/** + * Wii.listen() + * + * The main game loop. This must stay very performant; try to keep things as absolutely + * low-level as possible here. + */ +Wii.listen = function() { + if(!Wii.setKeyListeners) Wii.setKeyListeners = Wii.installKeyListeners(); + + var i = Wii.extraRemotes.length; + + while(i--) { + /* Check if it's enabled; returns a kPadStatus object if so. */ + var wii_remote = Wii.extraRemotes[i], + wii_remoteCurrStatus = wii_remote.isEnabled(); + + /* If it's enabled, huzzah, do some checks and fire appropriate events. */ + if(wii_remoteCurrStatus) { + /** + * Do this check here as well, as the primary wiimote might've changed... + * Note that we don't remove it from the internal remote tracking Array; this is because + * if the remote that _was_ the primary one comes back online, it'll take over as the + * primary one again as it's the lowest ID in terms of all remotes. This check here will + * ensure that whatever remote is the current primary one will default to using other + * dispatched events instead of bitwise checks, but should all default back if another one + * comes online. + */ + if(wii_remoteCurrStatus.isBrowsing()) { + Wii.currentBrowsingRemote = wii_remote; + } else { + for(var evt in wii_remote.evtsInterestedIn) { + var evtHappened = Wii.DISPATCHER[evt](wii_remote, wii_remoteCurrStatus); + if(evtHappened) wii_remote.evtsInterestedIn[evt](wii_remote, wii_remoteCurrStatus); + } + } + } + } + + /** + * This is a better choice than working with intervals; it keeps the amount of "spasm" responses + * that one would normally get on the Wii to a bare minimum. This is due to how the two types of + * timers in JS work - intervals will queue up no matter what, and if there's a backlog, rapidly + * fire through all of them. Timeouts are guaranteed to always have their delay, even though at points + * it may end up being more (or less) than 100ms. + * + * Note that this is set to 100ms - any lower, and the Wii becomes very unresponsive for some reason. The + * web browser is... odd, not sure what the deal is. 100ms should be enough for most cases though... + */ + return setTimeout(Wii.listen, 100); +}; + +/** + * Wii.parsePrimaryWiimote(e) + * + * The Wii browser environment is... quite oddball at times. You see, the + * primary Wii remote is treated differently than the other Wiimotes; it uses + * browser-based events (keydown, mouseup, etc) to communicate which buttons have + * been pressed. + * + * The "primary" Wiimote can also change at any given time (loss of battery in the main, etc). + * + * Luckily, it's not -impossible- to catch this internally. Our main library event loop catches + * if a given Wiimote is marked as the primary one, and will not attempt bitwise operations on it, + * merely wait for standard DOM events to trickle up and handle firing them appropriately. + * + * ...ugh. + * + * This method is a callback for any DOM-event listeners; accepts an event as its argument. + */ +Wii.parsePrimaryWiimote = function(e) { + /* Cancel whatever the default was, because we're going to try and normalize everything. */ + e.preventDefault(); + + var wii_remote = Wii.currentBrowsingRemote, + wii_remoteCurrStatus = wii_remote.isEnabled(), + buttonPressed = Wii.PRIMARY_CONTROLLER_DISPATCHER[wii_remote.opts.horizontal ? 'horizontal' : 'vertical'][e.keyCode]; + + /** + * Filter down and figure out which "event" we're really looking at based on code + * matchups; this gets messy pretty quickly... + */ + if(typeof buttonPressed !== 'undefined' && wii_remote.evtsInterestedIn[buttonPressed] !== 'undefined') { + wii_remote.evtsInterestedIn[buttonPressed](wii_remote, wii_remoteCurrStatus); + } + + /* Doing this in conjunction with preventDefault() halts an odd clicking bug or two. */ + return false; +}; + +/** + * Wii.PRIMARY_CONTROLLER_DISPATCHER + * + * In order to keep things as performant as possible, we want DOM events (for the primary controller) + * to also be a 1:1 hash map lookup. This is PRIMARILY for the primary ("browsing") controller; all other + * controllers get their operations routed through the DISPATCHER below. + */ +Wii.PRIMARY_CONTROLLER_DISPATCHER = { + vertical: { + 0: 'pressed_a', + 13: 'pressed_a', /* Older versions of the Wii Browser...? */ + 170: 'pressed_minus', + 171: 'pressed_b', + 172: 'pressed_1', + 173: 'pressed_2', + 174: 'pressed_plus', + 175: 'pressed_up', + 176: 'pressed_down', + 177: 'pressed_right', + 178: 'pressed_left' + }, + horizontal: { + 0: 'pressed_a', + 13: 'pressed_a', /* Older versions of the Wii Browser...? */ + 170: 'pressed_minus', + 171: 'pressed_b', + 172: 'pressed_1', + 173: 'pressed_2', + 174: 'pressed_plus', + 175: 'pressed_left', + 176: 'pressed_right', + 177: 'pressed_up', + 178: 'pressed_down' + } +}; + +/** + * Wii.DISPATCHER + * + * A table of the supported events that we watch for in our game loop, then fire off for respective + * Wiimotes. Each index is a function that does a check and returns true or false. + * + * Many of these functions use bitwise comparisons. Read up on it if you're not familiar. Note that + * we also take into account the orientation of the device here! + */ +Wii.DISPATCHER = { + /** + * These functions depend on whether or not the controller is meant to be in horizontal mode + * or not. Quite... different. + */ + 'pressed_up': function(wii_remote, wii_remoteStatus) { + if(wii_remote.opts.horizontal) return wii_remoteStatus.hold & 2; + return wii_remoteStatus.hold & 8; + }, + 'pressed_right': function(wii_remote, wii_remoteStatus) { + if(wii_remote.opts.horizontal) return wii_remoteStatus.hold & 4; + return wii_remoteStatus.hold & 2; + }, + 'pressed_down': function(wii_remote, wii_remoteStatus) { + if(wii_remote.opts.horizontal) return wii_remoteStatus.hold & 1; + return wii_remoteStatus.hold & 4; + }, + 'pressed_left': function(wii_remote, wii_remoteStatus) { + if(wii_remote.opts.horizontal) return wii_remoteStatus.hold & 8; + return wii_remoteStatus.hold & 1; + }, + + 'pressed_plus': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 16; }, + 'pressed_minus': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 4096; }, + 'pressed_2': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 256; }, + 'pressed_1': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 512; }, + 'pressed_b': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 1024; }, + 'pressed_a': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 2048; }, + + /** + * I'm keeping these noted here for legacy reasons, but by and large it's just not even + * worth trying to use the Nunchuk with anything in the browser; the primary controller + * can never read them, so there's a large chunk of functionality missing... + */ + 'pressed_z': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 8192; }, + 'pressed_c': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 16384; } +}; diff --git a/js/wii.js b/js/wii.js new file mode 100644 index 0000000..84fe39d --- /dev/null +++ b/js/wii.js @@ -0,0 +1,437 @@ +/** + * core.js + * + * This is the main template file for bringing together + * the various libraries that power this entire little eco-system. + * + * Building the releases requires Python (2.5+); simply run... + * + * python build.py + * + * ...from the /js/ directory. + * + * @Author: Ryan McGrath + * @Requires: Nothing, top-level file. + */ + +;(function(running_inside_wii_browser) { + /** + * If we're not running inside the Nintendo Wii browser, bail out. + * In the future, branch here for touch-enabled devices...? + */ + if(!running_inside_wii_browser) return false; + + /** + * wii.js + * + * Provides a sane, event-based documented wrapper around the Wiimote controls + * and API inside the Opera-based Nintendo Wii web browser. + * + * This is not produced by nor endorsed by Nintendo or Opera. I've written it + * on my own because I see a device that's sitting in millions of living rooms + * but being sorely neglected because a company couldn't get their act together + * in regards to third party development. ;) + * + * @Author: Ryan McGrath + * @Requires: Nothing. + */ + +/* Wii + * + * Top level namespace. Contains information about the main event loop, etc. + */ +var Wii = { + /** + * A "global" reference to the Wiimotes we're currently watching. Lets us run + * through on each loop/iteration and poll for a new status on it. + */ + extraRemotes: [], + currentBrowsingRemote: null, + setKeyListeners: false, + debuggerDiv: null +}; + +/** + * Install some basic low-level event listeners to monitor how + * the primary wii_remote is interacting with the browser; it's treated + * differently than the other wii_remotes, more as a "browsing" tool than + * a controller. Doesn't mean we can't try and mend the gap... + */ +Wii.installKeyListeners = function() { + document.addEventListener('mouseup', Wii.parsePrimaryWiimote, false); + document.addEventListener('keydown', Wii.parsePrimaryWiimote, false); + document.addEventListener('mousedown', Wii.parsePrimaryWiimote, false); + document.addEventListener('keyup', Wii.parsePrimaryWiimote, false); + + /** + * Some keys, like the directional ones, get... multiple events? + * In this case, just shut. down. everything. + * + * ...and let the programmer deal with it. + */ + document.addEventListener('keypress', Wii.parsePrimaryWiimote, false); + + return true; +}; + +/** + * Wii.listen() + * + * The main game loop. This must stay very performant; try to keep things as absolutely + * low-level as possible here. + */ +Wii.listen = function() { + if(!Wii.setKeyListeners) Wii.setKeyListeners = Wii.installKeyListeners(); + + var i = Wii.extraRemotes.length; + + while(i--) { + /* Check if it's enabled; returns a kPadStatus object if so. */ + var wii_remote = Wii.extraRemotes[i], + wii_remoteCurrStatus = wii_remote.isEnabled(); + + /* If it's enabled, huzzah, do some checks and fire appropriate events. */ + if(wii_remoteCurrStatus) { + /** + * Do this check here as well, as the primary wiimote might've changed... + * Note that we don't remove it from the internal remote tracking Array; this is because + * if the remote that _was_ the primary one comes back online, it'll take over as the + * primary one again as it's the lowest ID in terms of all remotes. This check here will + * ensure that whatever remote is the current primary one will default to using other + * dispatched events instead of bitwise checks, but should all default back if another one + * comes online. + */ + if(wii_remoteCurrStatus.isBrowsing()) { + Wii.currentBrowsingRemote = wii_remote; + } else { + for(var evt in wii_remote.evtsInterestedIn) { + var evtHappened = Wii.DISPATCHER[evt](wii_remote, wii_remoteCurrStatus); + if(evtHappened) wii_remote.evtsInterestedIn[evt](wii_remote, wii_remoteCurrStatus); + } + } + } + } + + /** + * This is a better choice than working with intervals; it keeps the amount of "spasm" responses + * that one would normally get on the Wii to a bare minimum. This is due to how the two types of + * timers in JS work - intervals will queue up no matter what, and if there's a backlog, rapidly + * fire through all of them. Timeouts are guaranteed to always have their delay, even though at points + * it may end up being more (or less) than 100ms. + * + * Note that this is set to 100ms - any lower, and the Wii becomes very unresponsive for some reason. The + * web browser is... odd, not sure what the deal is. 100ms should be enough for most cases though... + */ + return setTimeout(Wii.listen, 100); +}; + +/** + * Wii.parsePrimaryWiimote(e) + * + * The Wii browser environment is... quite oddball at times. You see, the + * primary Wii remote is treated differently than the other Wiimotes; it uses + * browser-based events (keydown, mouseup, etc) to communicate which buttons have + * been pressed. + * + * The "primary" Wiimote can also change at any given time (loss of battery in the main, etc). + * + * Luckily, it's not -impossible- to catch this internally. Our main library event loop catches + * if a given Wiimote is marked as the primary one, and will not attempt bitwise operations on it, + * merely wait for standard DOM events to trickle up and handle firing them appropriately. + * + * ...ugh. + * + * This method is a callback for any DOM-event listeners; accepts an event as its argument. + */ +Wii.parsePrimaryWiimote = function(e) { + /* Cancel whatever the default was, because we're going to try and normalize everything. */ + e.preventDefault(); + + var wii_remote = Wii.currentBrowsingRemote, + wii_remoteCurrStatus = wii_remote.isEnabled(), + buttonPressed = Wii.PRIMARY_CONTROLLER_DISPATCHER[wii_remote.opts.horizontal ? 'horizontal' : 'vertical'][e.keyCode]; + + /** + * Filter down and figure out which "event" we're really looking at based on code + * matchups; this gets messy pretty quickly... + */ + if(typeof buttonPressed !== 'undefined' && wii_remote.evtsInterestedIn[buttonPressed] !== 'undefined') { + wii_remote.evtsInterestedIn[buttonPressed](wii_remote, wii_remoteCurrStatus); + } + + /* Doing this in conjunction with preventDefault() halts an odd clicking bug or two. */ + return false; +}; + +/** + * Wii.PRIMARY_CONTROLLER_DISPATCHER + * + * In order to keep things as performant as possible, we want DOM events (for the primary controller) + * to also be a 1:1 hash map lookup. This is PRIMARILY for the primary ("browsing") controller; all other + * controllers get their operations routed through the DISPATCHER below. + */ +Wii.PRIMARY_CONTROLLER_DISPATCHER = { + vertical: { + 0: 'pressed_a', + 13: 'pressed_a', /* Older versions of the Wii Browser...? */ + 170: 'pressed_minus', + 171: 'pressed_b', + 172: 'pressed_1', + 173: 'pressed_2', + 174: 'pressed_plus', + 175: 'pressed_up', + 176: 'pressed_down', + 177: 'pressed_right', + 178: 'pressed_left' + }, + horizontal: { + 0: 'pressed_a', + 13: 'pressed_a', /* Older versions of the Wii Browser...? */ + 170: 'pressed_minus', + 171: 'pressed_b', + 172: 'pressed_1', + 173: 'pressed_2', + 174: 'pressed_plus', + 175: 'pressed_left', + 176: 'pressed_right', + 177: 'pressed_up', + 178: 'pressed_down' + } +}; + +/** + * Wii.DISPATCHER + * + * A table of the supported events that we watch for in our game loop, then fire off for respective + * Wiimotes. Each index is a function that does a check and returns true or false. + * + * Many of these functions use bitwise comparisons. Read up on it if you're not familiar. Note that + * we also take into account the orientation of the device here! + */ +Wii.DISPATCHER = { + /** + * These functions depend on whether or not the controller is meant to be in horizontal mode + * or not. Quite... different. + */ + 'pressed_up': function(wii_remote, wii_remoteStatus) { + if(wii_remote.opts.horizontal) return wii_remoteStatus.hold & 2; + return wii_remoteStatus.hold & 8; + }, + 'pressed_right': function(wii_remote, wii_remoteStatus) { + if(wii_remote.opts.horizontal) return wii_remoteStatus.hold & 4; + return wii_remoteStatus.hold & 2; + }, + 'pressed_down': function(wii_remote, wii_remoteStatus) { + if(wii_remote.opts.horizontal) return wii_remoteStatus.hold & 1; + return wii_remoteStatus.hold & 4; + }, + 'pressed_left': function(wii_remote, wii_remoteStatus) { + if(wii_remote.opts.horizontal) return wii_remoteStatus.hold & 8; + return wii_remoteStatus.hold & 1; + }, + + 'pressed_plus': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 16; }, + 'pressed_minus': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 4096; }, + 'pressed_2': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 256; }, + 'pressed_1': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 512; }, + 'pressed_b': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 1024; }, + 'pressed_a': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 2048; }, + + /** + * I'm keeping these noted here for legacy reasons, but by and large it's just not even + * worth trying to use the Nunchuk with anything in the browser; the primary controller + * can never read them, so there's a large chunk of functionality missing... + */ + 'pressed_z': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 8192; }, + 'pressed_c': function(wii_remote, wii_remoteStatus) { return wii_remoteStatus.hold & 16384; } +}; +/** + * util.js + * + * A basic utility wrapper; anything extra that's often re-used should + * find its way here (e.g, debuggerDiv, bind, etc). + * + * @Author: Ryan McGrath + * @Requires: wii.js + */ + +Wii.util = { + /** + * Wii.util.debug(err); + * + * The Wii has... such little options for debugging, but we can try and make this a bit nicer. + * This accepts a stack trace (see example code below) and then outputs it to the screen. + * + * try { ... } catch(e) { Wii.util.debug(e); } + */ + debug: function(err) { + if(Wii.debuggerDiv === null) { + Wii.debuggerDiv = document.createElement('div'); + + Wii.debuggerDiv.style.cssText = [ + 'width: 90%;', + 'height: 90%;', + 'padding: 10px;', + 'font-size: 26px;', + 'font-family: monospace;', + 'overflow: scroll', + 'position: absolute;', + 'top: 10px;', + 'left: 10px;', + 'color: #f9f9f9;', + 'background-color: #010101;' + ].join(''); + + document.body.appendChild(Wii.debuggerDiv); + } + + if(typeof err === 'string') { + Wii.debuggerDiv.innerHTML = err; + } else { + var msg = ''; + for(var e in err) { msg += e + '=' + err[e] + '
'; } + Wii.debuggerDiv.innerHTML = msg; + } + + Wii.debuggerDiv.style.display = 'block'; + }, + + /** + * Wiimote.util.bind(bindReference, fn) + * + * Takes a reference (an object to scope to "this" at a later runtime) and binds it to a function (fn). + * + * @param bindReference - An object to set as the "this" reference for a later function call. + * @param fn - A function to bind the "this" object for. + * @returns fn - A new function to pass around, wherein it's all scoped as you want it. + */ + bind: function(bindReference, fn) { + return function() { + return fn.apply(bindReference, arguments); + }; + } +}; +/** + * remote.js + * + * Handles the subscribing to events portion of a Wii remote. It's best to think of this + * as a "request" object; it asks to be notified of events, and the actual events are + * dispatched from the main wii.js file. + * + * @Author: Ryan McGrath + * @Requires: wii.js, util.js + */ + +/** + * var wii_remote = new Wii.Remote(1, {...}); + * + * Instantiates a Wii Remote object. Events can be set on each of these objects, + * and the internal game loop will fire events based on the properties subscribed + * to here. + * + * @param remote_id - Number, required. 1 - 4, dictates which Wiimote this object + * relates to. + * @param opts - Object, optional. Allows you to override internal settings and such, + * should you want different behavior. + * @returns Wii.Remote instance. + */ +Wii.Remote = function(remote_id, opts) { + this.remote_id = remote_id; + this.opts = opts; + + /** + * If this is the "main" wii_remote, then the bitwise checks will fail + * because it's treated more as a "browsing" device. For these events, + * we'll just store the current wii_remote that's denoted as the "browsing" + * device and let the normal event/key delegation take care of things. + * + * The rest of the wii_remotes will go through the DISPATCHER checks that + * they've subscribed to. + */ + var startupStatus = this.isEnabled(); + if(startupStatus) { + if(!startupStatus.isBrowsing) { + Wii.extraRemotes.push(this); + } else { + Wii.currentBrowsingRemote = this; + } + } +}; + +Wii.Remote.prototype = { + opts: { + /** + * We default the controller to be in the vertical orientation; if + * it's overridden as "horizontal" (false), we'll catch it for the different key + * events and fire accordingly (e.g, the "up" button is different depending on + * how the player is holding the controller). + */ + horizontal: false + }, + + /** + * A hash of events that this Wii remote is interested in. Each + * entry should be a key (evtName) with a value (response). + * "evtName" is the event name that corresponds with boolean functions + * in the DISPATCHER above, and the response is a remote-bound function + * to call on that event. + */ + evtsInterestedIn: undefined, + + /** + * Wiimote.isEnabled() + * + * Determines the status of the wii_remote this object is curious about. Will return + * an updated kPadStatus object if so, false if it's not responding or the data + * is sent back as "invalid". This makes it so we don't bother sending events + * where they're not applicable. + * + * @returns object or boolean. + */ + isEnabled: function() { + var remote = opera.wiiremote.update(this.remote_id - 1); + return (remote.isEnabled && remote.isDataValid ? remote : false); + }, + + /** + * Wiimote.on(evtName, fn) + * + * Allows you to listen for an event on a given Wiimote. Call this on an instantiated + * Wiimote; "this" will be scoped to the Wiimote object. ;) + * + * @param evtName - String, a supported wii.js (DISPATCHER) event name. + * @param fn - Function, callback function to be executed on the event firing. Will be scoped to the Wiimote. + * @returns object or undefined - instantiated object this was called on to begin with, undefined if evtName is not supported. + */ + when: function(evtName, fn) { + if(typeof Wii.DISPATCHER[evtName] !== 'undefined') { + + /** + * THIS IS INCREDIBLY IMPORTANT, DO NOT REMOVE THIS!. + * + * The Wii's browser has an (odd...?) bug wherein if you have a prototype chain + * set up for a given object, and you default keys on the prototype chain to a blank object ("{}", for instance), + * it will _NOT_ make this a unique object, it keeps pointers to the original object that was created by the system + * for the first instantiated object. + * + * This is, needless to say, unlike just about any other JS environment and threw me for a loop for a good 6 hours. + * This line ensures that the first time this property is ever referenced, we get a fresh _CORRECTLY ALLOCATED_ chunk + * of memory to play with and store things in. + * + * Note that this also happens with Array structures, and conceivably anything else that would be using a copy-by-reference + * technique instead of a full clone. We want an object for this case, though, so we're not doing iterations on event dispatch, + * just a 1:1 lookup instead. + */ + if(this.evtsInterestedIn === undefined) this.evtsInterestedIn = {}; + + this.evtsInterestedIn[evtName] = Wii.util.bind(this, fn); + return this; + } + + return undefined; + } +}; + + + window.Wii = Wii; +})(window.opera && opera.wiiremote); diff --git a/js/wii.min.js b/js/wii.min.js new file mode 100644 index 0000000..50c2308 --- /dev/null +++ b/js/wii.min.js @@ -0,0 +1 @@ +(function(a){if(!a){return false}var b={extraRemotes:[],currentBrowsingRemote:null,setKeyListeners:false,debuggerDiv:null};b.installKeyListeners=function(){document.addEventListener("mouseup",b.parsePrimaryWiimote,false);document.addEventListener("keydown",b.parsePrimaryWiimote,false);document.addEventListener("mousedown",b.parsePrimaryWiimote,false);document.addEventListener("keyup",b.parsePrimaryWiimote,false);document.addEventListener("keypress",b.parsePrimaryWiimote,false);return true};b.listen=function(){if(!b.setKeyListeners){b.setKeyListeners=b.installKeyListeners()}var e=b.extraRemotes.length;while(e--){var g=b.extraRemotes[e],f=g.isEnabled();if(f){if(f.isBrowsing()){b.currentBrowsingRemote=g}else{for(var c in g.evtsInterestedIn){var d=b.DISPATCHER[c](g,f);if(d){g.evtsInterestedIn[c](g,f)}}}}}return setTimeout(b.listen,100)};b.parsePrimaryWiimote=function(g){g.preventDefault();var f=b.currentBrowsingRemote,d=f.isEnabled(),c=b.PRIMARY_CONTROLLER_DISPATCHER[f.opts.horizontal?"horizontal":"vertical"][g.keyCode];if(typeof c!=="undefined"&&f.evtsInterestedIn[c]!=="undefined"){f.evtsInterestedIn[c](f,d)}return false};b.PRIMARY_CONTROLLER_DISPATCHER={vertical:{0:"pressed_a",13:"pressed_a",170:"pressed_minus",171:"pressed_b",172:"pressed_1",173:"pressed_2",174:"pressed_plus",175:"pressed_up",176:"pressed_down",177:"pressed_right",178:"pressed_left"},horizontal:{0:"pressed_a",13:"pressed_a",170:"pressed_minus",171:"pressed_b",172:"pressed_1",173:"pressed_2",174:"pressed_plus",175:"pressed_left",176:"pressed_right",177:"pressed_up",178:"pressed_down"}};b.DISPATCHER={pressed_up:function(d,c){if(d.opts.horizontal){return c.hold&2}return c.hold&8},pressed_right:function(d,c){if(d.opts.horizontal){return c.hold&4}return c.hold&2},pressed_down:function(d,c){if(d.opts.horizontal){return c.hold&1}return c.hold&4},pressed_left:function(d,c){if(d.opts.horizontal){return c.hold&8}return c.hold&1},pressed_plus:function(d,c){return c.hold&16},pressed_minus:function(d,c){return c.hold&4096},pressed_2:function(d,c){return c.hold&256},pressed_1:function(d,c){return c.hold&512},pressed_b:function(d,c){return c.hold&1024},pressed_a:function(d,c){return c.hold&2048},pressed_z:function(d,c){return c.hold&8192},pressed_c:function(d,c){return c.hold&16384}};b.util={debug:function(c){if(b.debuggerDiv===null){b.debuggerDiv=document.createElement("div");b.debuggerDiv.style.cssText=["width: 90%;","height: 90%;","padding: 10px;","font-size: 26px;","font-family: monospace;","overflow: scroll","position: absolute;","top: 10px;","left: 10px;","color: #f9f9f9;","background-color: #010101;"].join("");document.body.appendChild(b.debuggerDiv)}if(typeof c==="string"){b.debuggerDiv.innerHTML=c}else{var f="";for(var d in c){f+=d+"="+c[d]+"
"}b.debuggerDiv.innerHTML=f}b.debuggerDiv.style.display="block"},bind:function(c,d){return function(){return d.apply(c,arguments)}}};b.Remote=function(e,d){this.remote_id=e;this.opts=d;var c=this.isEnabled();if(c){if(!c.isBrowsing){b.extraRemotes.push(this)}else{b.currentBrowsingRemote=this}}};b.Remote.prototype={opts:{horizontal:false},evtsInterestedIn:undefined,isEnabled:function(){var c=opera.wiiremote.update(this.remote_id-1);return(c.isEnabled&&c.isDataValid?c:false)},when:function(d,c){if(typeof b.DISPATCHER[d]!=="undefined"){if(this.evtsInterestedIn===undefined){this.evtsInterestedIn={}}this.evtsInterestedIn[d]=b.util.bind(this,c);return this}return undefined}};window.Wii=b})(window.opera&&opera.wiiremote); \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..66cf980 --- /dev/null +++ b/readme.md @@ -0,0 +1,104 @@ +wii-js +============================================================================================== +The Nintendo Wii is an entertainment system with an utterly _massive_ install base, and when +you couple it with the fact that it's got a web browser (mostly) built in, there's a lot of +potential for third party development. Sadly, few have opted to do any sort of development for +it. While it doesn't help that Nintendo pretty much dropped the ball on this opportunity, the +experience of browsing the web on the Wii isn't actually that compelling to begin with. + +That said, I think this can serve one other purpose: it's an ideal environment to teach children +how to program! I created this library to sanitize Wii interaction with webpages in the browser, +as it's notoriously crippled. It aims to offer a solid, documented, performant API that's easy to +understand and pick up. With this library, you can have up to 4 Wii-motes interacting with your +webpage at once, a dynamic not found in other web browsing mediums. + +Questions, comments, criticism and praise can be directed to me at the following outlets: + +- You can email me at **ryan [at] venodesigns (dot) net**. +- You can hit me up on Twitter: [@ryanmcgrath](http://twitter.com/ryanmcgrath/) +- Contact me through **[my website](http://venodesigns.net)** +- **Technical issues can be filed on the [wii-js GitHub Issues Tracker](https://github.com/ryanmcgrath/wii-js/issues)** + +Example Usage +---------------------------------------------------------------------------------------------- +``` javascript +var wiimote = new Wii.Remote(1, {horizontal: true}), + wiimote2 = new Wii.Remote(2, {horizontal: true}); + +wiimote.when('pressed_a', function() { + alert('Wiimote #1 pressed the A Button!'); +}); + +wiimote2.when('pressed_a', function() { + alert('Wiimote #2 pressed the A Button!'); +}); +``` + +Technical Documentation +---------------------------------------------------------------------------------------------- +The largest issue with making interactive pages that work with the Wii has been that the API has +been nothing short of a black hole. When you actually begin to dig in and figure out what's going on, +it gets even uglier to see - the primary wiimote, for instance, has a totally different set of signals +than the other three. + +wii-js abstracts away most of these differences and/or problems, and works on a very simple event-dispatch +system. What this means is that you create an instance of a Wii Remote, subscribe to events, and provide a +function to get called when that event has occurred. The following syntax should explain this: + +``` javascript +wiimote.when('event_name_here', function() { /* My callback function */ }); +``` + +When instantiating a Wii Remote instance, you can choose to have the library interpret directional pad controls +in horizontal or vertical mode. You can change this at any point, if you want, by simply swapping the property. + +``` javascript +var wiimote = new Wii.Remote(1, {horizontal: true}); // Horizontal controls +var wiimote = new Wii.Remote(1, {horizontal: false}); // Vertical controls + +wiimote.opts.horizontal = true; // Change to horizontal scheme. +``` + +You can listen for the following events on all controllers: + +- **pressed_up** - The up button was pressed. +- **pressed_down** - The down button was pressed. +- **pressed_left** - The left button was pressed. +- **pressed_right** - The right button was pressed. +- **pressed_a** - The A button was pressed. +- **pressed_1** - The 1 button was pressed. (_Note: On controller 1, this triggers a menu - work in progress..._) +- **pressed_2** - The 2 button was pressed. +- **pressed_plus** - The plus (+) button was pressed. +- **pressed_minus** - The minus (-) button was pressed. +- **roll_change** - The roll of the controller (balance) changed. You can get the current roll in radians with _"this.roll"_; positive is upright, negative is the other. +- **distance_change** - The distance of the wiimote (in meters) from the TV/sensor bar has changed. This event isn't totally reliable, but should work for most cases. + +You can listen for the following events on _extra controllers_, but not the primary controller. + +- **pressed_b** - The B button was pressed. +- **pressed_c** - The C button (on the Nunchuk) was pressed. +- **pressed_z** - The Z button (on the Nunchuk) was pressed. + +You can also get the following properties from any Wii Remote instance; they will return "undefined" if the remote +isn't able to see the TV/sensor bar, so be sure to check this! + +- **x** - The x coordinate where the Wii Remote is pointing to on the screen. Generally between 0 and 800, but can be more on wide pages. +- **y** - The y coordinate where the Wii Remote is pointing to on the screen. Odd one; can be found as low as -48, as high as the height +of the current webpage + toolbar height, if enabled. Tinker with this one for your purposes. + +Why the button limitations? +------------------------------------------------------------------------------------------------------------------ +The Nintendo Wii treats the primary controller differently than the other ones, and doesn't report any action +from the Nunchuk, nor does it report when someone has pressed the "B" button on the primary controller (as it's used +for scrolling a page). + +The Wii Browser also doesn't report data for Gamecube controllers, the Classic controller, or any other accessories. + +It's a work in progress to see what can be done about these, but it's impossible to guarantee anything will come out +of it unless Nintendo and/or Opera can come out with something new. + + +Licensing, etc +------------------------------------------------------------------------------------------------------------------- +wii-js is released under an MIT license. Just provide credit where need be if you choose to use this, it's taken quite +a bit of my time to decipher the utterly random pieces and intricacies of this Javascript engine. ;) diff --git a/utilities/yuicompressor-2.4.2.jar b/utilities/yuicompressor-2.4.2.jar new file mode 100755 index 0000000..c29470b Binary files /dev/null and b/utilities/yuicompressor-2.4.2.jar differ