Questions about "mobile":

Human-interface guidelines (HIG) and UI frameworks are great!

Goal: pit of success!
Problems of emulating native UI on the web:

Detailed study trying to do this for iOS: read the article on cheeaun.com.

Tweak layout with media queries:
@media screen and (max-width: 1000px) {
#sidebar { display: none; }
}

window.matchMedia lets you evaluate arbitrary media queries:
var result = window.matchMedia('(min-width: 500px)');
result.matches // True iff width > 500px.
You can also listen for changes to match:
function onOrientationChange(mql) {
var isLandscape = mql.matches;
}
var mql = window.matchMedia("(orientation: landscape)");
mql.addListener(onOrientationChange);
function detectFormFactor() {
var device = DESKTOP;
if (hasTouch()) {
device = isSmall() ? PHONE : TABLET;
}
return device;
}
function hasTouch() {
return Modernizr.touch; // Soon: (hover: 0) and (pointer: coarse)
}
function isSmall() {
return window.matchMedia('(min-device-width: ???)').matches;
}

'*' denotes double-density devices
devpx = window.devicePixelRatio * csspx
How about landscape mode?
<head>
<link rel="alternate" href="http://foo.com" id="desktop"
media="only screen and (hover: 1) and (pointer: fine)">
<link rel="alternate" href="http://m.foo.com" id="phone"
media="only screen and (max-device-width: 650px)">
<link rel="alternate" href="http://tablet.foo.com" id="tablet"
media="only screen and (min-device-width: 651px)">
</head>
Read these Google Webmaster smartphone guidelines.
Use it to automatically redirect to the correct version based on
<link rel="alternate"> declarations.
Built-in version forcing, and intelligent redirection.
For details, visit github.com/borismus/device.js.
All we have is the User-Agent string.
Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19
...and the user agent string was a complete mess, and near useless, and everyone pretended to be everyone else, and confusion abounded." - Aaron Andersen (How the User Agent got its String)
Solutions that make dealing with User-Agent strings easier (standalone and as a service API).
Drawbacks:

<meta name="viewport" content="...">. At minimum, set width=device-width.

Goal: serve page at the right size, eliminating the need to pinch-zoom.
Use initial-scale=1, minimum-scale=1 and maximum-scale=1:
<head>
<meta name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1">
</head>
Avoid user-scalable=no, since it's not well supported.
Use commas, not semicolons;
Limit of human perception: 60 FPS
Bottom line: utilize hardware acceleration as much as possible.
Example: use position: fixed to keep things fixed to viewport.
For in-element scrolling, overflow: auto;
-webkit-overflow-scrolling: touch; forces hardware acceleration.
Goal: animations at 60 FPS.
This is slow: position: absolute; left: 100px; top: 100px;.
This is usually hardware accelerated → fast:
transform: translate3d(100px, 100px, 0);
Others: rotate, scale...
3D transformations, and perspective!
matrix3d(m00, m01, m02, m03,
m10, m11, m12, m13,
m20, m21, m22, m23,
m30, m31, m31, m33)
CSS interpolates for you: transition: transform 1s ease-in-out;.
Note:
transition,transformstill need vendor prefixes!
events: touchstart, touchmove, touchend
properties: touches, targetTouches, changedTouches
There's a specification!
Browser has built-in touch behavior (scrolling, tab switching).
How to override?
var el = document.querySelector('#my .selector');
el.addEventListener('touchmove', function(event) {
event.preventDefault();
});
In IE:
#my .selector {
-ms-touch-action: none;
}
Some things can't be overridden, such as bevel swipe to switch tabs.
Beware click delays!
The click and mouse* events are delayed by 300ms.
Use touchend for faster buttons.
Multi-touch events come in faster than 60 FPS. Decouple input and draw:
var touches = [];
canvas.addEventListener('touchmove', function(event) {
touches = event.touches;
}, false);
function draw() {
// Touch rendering code goes here...
}
// Setup a 60 FPS timer.
var timer = setInterval(function() {
draw();
}, 1000/60);
In Chrome, use window.webkitRequestAnimationFrame instead (see
shim).
function draw(time) {
// Touch rendering code goes here...
requestId = window.requestAnimationFrame(draw);
}
function start() {
requestId = window.requestAnimationFrame(draw);
}
function stop() {
window.cancelAnimationFrame(requestId);
}
start();
What happens if you want to support both?
el.addEventListener('touchstart', function(e) {
var touch = e.changedTouches[0];
onDown(touch.pageX, touch.pageY);
});
el.addEventListener('mousedown', function(e) {
onDown(e.pageX, e.pageY);
});
function onDown(x, y) { /* ... */ }
Unnecessary boilerplate!
Unfortunately gesture* events aren't widely implemented.
Robust pinch-zoom based on touch* events:
/**
* Gesture recognizer for compound multi-touch transformations.
*
* 1. pinch/zoom/scale gesture.
* 2. rotate gesture.
*/
function TransformRecognizer(element) {
// Reference positions for the start of the transformation.
this.referencePair = null;
// Bind touch event handlers to this element.
element.addEventListener('touchstart', this.touchStartHandler.bind(this));
element.addEventListener('touchmove', this.touchMoveHandler.bind(this));
element.addEventListener('touchend', this.touchEndHandler.bind(this));
this.element = element;
// Object of callbacks this function provides.
this.callbacks = {
rotate: null,
scale: null
};
// Define gesture states.
this.Gestures = {
NONE: 0,
ROTATE: 1,
SCALE: 2
};
// Define thresholds for gestures.
this.Thresholds = {
SCALE: 0.2, // percentage difference.
ROTATION: 5 // degrees.
};
// The current gesture of this transformation.
this.currentGesture = this.Gestures.NONE;
}
/**
* Touch event handlers.
*/
TransformRecognizer.prototype.touchStartHandler = function(e) {
var touches = e.touches;
for (var i = 0; i < touches.length; i++) {
this.log('identifier: ' + touches[i].identifier);
}
// If there are now exactly 2 touches, this is the initial position.
if (touches.length == 2) {
// Save these two points as the reference.
this.referencePair = new TouchPair(touches);
}
};
TransformRecognizer.prototype.touchMoveHandler = function(e) {
// Prevent default behavior of scrolling.
e.preventDefault();
console.log('current gesture', this.currentGesture);
var touches = e.touches;
// Check if there are exactly 2 fingers touching this element.
if (touches.length == 2) {
// Get the current touches as a TouchPair.
var currentPair = new TouchPair(touches);
// Compute angle and scale differences WRT reference position.
var angle = currentPair.angleSince(this.referencePair);
var scale = currentPair.scaleSince(this.referencePair);
// Check if we're already in a gesture locked state.
if (this.currentGesture == this.Gestures.NONE) {
if (angle > this.Thresholds.ROTATION ||
-angle > this.Thresholds.ROTATION) {
// If rotated enough, start a rotation.
this.currentGesture = this.Gestures.ROTATE;
} else if (scale > 1 + this.Thresholds.SCALE ||
scale < 1 - this.Thresholds.SCALE) {
// Otherwise if scaled enough, start a scaling gesture.
this.currentGesture = this.Gestures.SCALE;
}
}
var center = currentPair.center()
// Handle known gestures.
if (this.currentGesture == this.Gestures.ROTATE) {
// If we're already rotating, callback with the rotation amount.
this.callbacks.rotate({
rotation: angle,
x: center.x,
y: center.y
});
}
if (this.currentGesture == this.Gestures.SCALE) {
// If already scaling, callback with scale amount.
this.callbacks.scale({
scale: scale,
x: center.x,
y: center.y
});
}
}
};
TransformRecognizer.prototype.touchEndHandler = function(e) {
var touches = e.touches;
// If there are less than 2 fingers, reset current gesture.
if (touches.length < 2) {
this.currentGesture = this.Gestures.NONE;
}
};
/**
* Registers a callback to fire when a pinch occurs.
*/
TransformRecognizer.prototype.onScale = function(callback) {
this.callbacks.scale = callback;
};
/**
* Registers a callback to fire when a rotate occurs.
*/
TransformRecognizer.prototype.onRotate = function(callback) {
this.callbacks.rotate = callback;
};
TransformRecognizer.prototype.log = function(msg) {
this.element.innerHTML += msg + '
';
};
/**
* Represents a pair of fingers touching the screen.
*/
function TouchPair(touchList) {
// Grab the first two touches from the list.
this.t1 = new Touch(touchList[0].pageX, touchList[0].pageY);
this.t2 = new Touch(touchList[1].pageX, touchList[1].pageY);
}
/**
* Given a reference position, calculate how much rotation happened.
*/
TouchPair.prototype.angleSince = function(referencePair) {
// TODO: handle the edge case of going between 0 and 360.
// eg. the difference between 355 and 0 is 5.
return this.angle() - referencePair.angle();
};
/**
* Given a reference position, calculate the scale multiplier.
*/
TouchPair.prototype.scaleSince = function(referencePair) {
return this.span() / referencePair.span();
};
/**
* Calculate the center of this transformation.
*/
TouchPair.prototype.center = function() {
var x = (this.t1.x + this.t2.x) / 2
var y = (this.t1.y + this.t2.y) / 2
return new Touch(x, y);
};
/**
* Calculate the distance between the two touch points.
*/
TouchPair.prototype.span = function() {
var dx = this.t1.x - this.t2.x;
var dy = this.t1.y - this.t2.y;
return Math.sqrt(dx*dx + dy*dy);
};
/**
* Calculate the angle (in degrees, 0 < a < 360) between the touch points.
*/
TouchPair.prototype.angle = function() {
var dx = this.t1.x - this.t2.x;
var dy = this.t1.y - this.t2.y;
return Math.atan2(dy, dx) * 180 / Math.PI;
};
function Touch(x, y) {
this.x = x;
this.y = y;
}
Idea: consolidated input model.
Problems:
mouse*, touch*, and
MSPointer*)Idea: consolidate input model across browsers (pointerdown,
pointermove, pointerup).
var el = document.querySelector(mySelector);
el.addEventListener('pointerdown', function(event) {
// ...
});
Basically, abstraction layer around mouse*, touch* and MSPointer*
events.
Get original event source was (touch or mouse) via event.pointerType.
Pointer.js also provides gesture events built on top of pointer events.
var el = document.querySelector(mySelector);
el.addEventListener('gesturescale', function(e) {
console.log('scale: ' + e.scale);
});
For details, visit github.com/borismus/pointer.js.
Easiest path: emulate mobile on desktop.
<script> and <object> tags.For details, visit github.com/borismus/magictouch.
Ultimately, you need to test on the device. More info.
android and google-chrome