April 27, 2011

Webkit bug: translate3d positioning doesn't activate browser scrollbars

After some frustration in Safari and Mobile Safari, I found that by setting a -webkit-transform: translate3d() to position the y-coordinate of an html element beyond the browser window height, it will not cause the browser to activate its scrollbars. However, by switching to the web-standards css top style, this problem is alleviated. This seems like a browser bug to me.

Consider the following css, in both desktop and mobile Safari:
<html>
<head>
  <style>
    .notbroken {
      position:absolute;
      top:10000px;
    }
    .broken {
      position:absolute;
      -webkit-transform: translate3d(0px, 10000px, 0px);
    }
  </style>
</head>
<body>
  <div class="notbroken">Scrollbars, please.</div>
</body>
</html>
Both css classes position the div 10,000 pixels down the page, but if you use the .broken version, you won't get scrollbars, and you'll never be able to see the content.

April 19, 2011

Javascript: Handle touch gestures for pinch (scale) and rotation

I wrote a little class to handle pinch & rotate gestures on a web page in iOS. There's a bunch of event listener adding/removing, which can get a little messy, so this should help keep your code clean if you need to handle gestures.

Here's the class:
function GestureCallback( element, endCallback, changeCallback ) {
  this.element = element;
  this.end_callback = endCallback;
  this.change_callback = changeCallback;
  this.scale = 1;
  this.rotation = 0;
  this.init();
}

GestureCallback.prototype.init = function() {
  // scope functions for listener removal
  var self = this;
  this.gestureStart = function(e){ self.onGestureStart(e) };
  this.gestureChange = function(e){ self.onGestureChange(e) };
  this.gestureEnd = function(e){ self.onGestureEnd(e) };
  
  // really no need to check for IE stupidness, but maybe they'll support gestures someday? oy.
  if( this.element.attachEvent ) this.element.attachEvent( "touchstart", this.gestureStart ); else this.element.addEventListener( "touchstart", this.gestureStart, false );
  if( this.element.attachEvent ) this.element.attachEvent( "gesturestart", this.gestureStart ); else this.element.addEventListener( "gesturestart", this.gestureStart, false );
};

GestureCallback.prototype.onGestureStart = function ( e ) {
  if( this.element.attachEvent ) this.element.attachEvent( "gesturechange", this.gestureChange ); else this.element.addEventListener( "gesturechange", this.gestureChange, false );
  if( this.element.attachEvent ) this.element.attachEvent( "gestureend", this.gestureEnd ); else this.element.addEventListener( "gestureend", this.gestureEnd, false );
};

GestureCallback.prototype.onGestureChange = function ( e ) {
  this.scale = e.scale;
  this.rotation = e.rotation;
  if( this.change_callback ) this.change_callback( this.scale, this.rotation );
};

GestureCallback.prototype.onGestureEnd = function ( e ) {
  if( this.element.detachEvent ) this.element.detachEvent( "gesturechange", this.gestureChange ); else this.element.removeEventListener( "gesturechange", this.gestureChange, false );
  if( this.element.detachEvent ) this.element.detachEvent( "gestureend", this.gestureEnd ); else this.element.removeEventListener( "gestureend", this.gestureEnd, false );

  this.scale = e.scale;
  this.rotation = e.rotation;
  if( this.end_callback ) this.end_callback( this.scale, this.rotation );
};

GestureCallback.prototype.dispose = function() {
  if( this.element.attachEvent ) this.element.detachEvent( "touchstart", this.gestureStart ); else this.element.removeEventListener( "touchstart", this.gestureStart, false );
  if( this.element.attachEvent ) this.element.detachEvent( "gesturestart", this.gestureStart ); else this.element.removeEventListener( "gesturestart", this.gestureStart, false );
  
  this.element = null;
  this.end_callback = null;
  this.change_callback = null;
  
  this.gestureStart = null;
  this.gestureChange = null;
  this.gestureEnd = null;
};
And the instantiation:
var pinchCallback = new GestureCallback( yourElement, function( scale, rotation ){
  console.log('done: '+scale+','+rotation);
  if( scale < 1 ) {
    // leave the section, since we've pinched it closed
  }
}, function( scale, rotation ){
  console.log('changing: '+scale+','+rotation);
});
And when you're done with your gesture needs, collect your garbage:
pinchCallback.dispose();
pinchCallback = null;

April 8, 2011

Processing: MovieMaker gotchas in Eclipse

I haven't touched my Processing sketches for a while, and got lots of weird errors while trying to render a video using the built-in MovieMaker object.
Exception in thread "Animation Thread" java.lang.UnsatisfiedLinkError: quicktime.QTSession.Gestalt(I[I)S
Caused by: java.lang.UnsatisfiedLinkError: /System/Library/Java/Extensions/libQTJNative.jnilib:  no suitable image found.  Did find:  /System/Library/Java/Extensions/libQTJNative.jnilib: no matching architecture in universal wrapper
The above error was fixed by switching the JVM that Eclipse was using in the "Run..." configuration. On my Macbook, I simply switched to an alternate JVM 1.5, and it got rid of the problem:

If you get the following error, you need to delete the previous rendered movie, or use some sort of timestamp to automatically name each new movie:
The movie file already exists.  Please delete it first.
Here's a little timestamp code I wrote to help prevent file name conflicts:
_timestamp = "" + String.valueOf( p.year() ) + "-" + String.valueOf( p.month() ) + "-" + String.valueOf( p.day() ) + "-" + String.valueOf(p.hour()) + "-" + String.valueOf(p.minute()) + "-" + String.valueOf(p.second());
  
_mm = new MovieMaker( p, p.width, p.height, "output/render-"+_timestamp+".mov", _framesPerSecond, MovieMaker.ANIMATION, MovieMaker.HIGH );

A stupid user-error error message I got was due to accidentally starting 2 instances of MovieMaker. Upon calling MovieMaker.finish(), I got this error:
quicktime.std.StdQTException[QTJava:7.6.6g],-2014=invalidDuration,QT.vers:7668000

Some other issues I've run into:
* Certain codecs won't be installed on your machine. For example, I can't use MovieMaker.H264, but I can use MovieMaker.ANIMATION
* After rendering a movie, Quicktime can have trouble playing it back, especially with certain codecs. My movies look blank. A workaround is to open and export the video from Quicktime Pro in order to view it.

Finally, something I've been doing with all my Processing sketches is to add the following VM Arguments to the Run Configuration. This helps avoid running out of memory and to run in 32-bit mode (required for certain operations):
-d32
-Xmx1024M
-Xms1024M