December 8, 2010

Javascript: Prototype.js - keep a timer running and maintain scope

If you create a Class with the Prototype.js class system, and you want to keep a timer running (for animation or other purposes), use this function to maintain the proper "this" scope:
runTimer: function() {
    // execute your own code here

    // keep timer running
    setTimeout(function(t) { 
        t.runTimer(); 
    }, 1000/30, this);
}

December 1, 2010

Javascript: Prototype.js Class system isolated into one file

I wanted to isolate the Class system from Prototype so I could use it with other libraries. I like the way it works, especially the inheritance features. You can copy and paste this into your own class.js file, and use the Prototype.js class system just like you would if you had the whole library included in your page. Check it out:
/* From object.js ------------------------------ */
Object.prototype.getType = function(o) {
  switch(o) {
    case null: return 'Null';
    case (void 0): return 'Undefined';
  }
  var type = typeof o;
  switch(type) {
    case 'boolean': return 'Boolean';
    case 'number':  return 'Number';
    case 'string':  return 'String';
  }
  return 'Object';
};

Object.prototype.keys = function(object) {
  if (this.getType(object) !== 'Object') { alert('type error'); }
  var results = [];
  for (var property in object) {
    if (object.hasOwnProperty(property)) {
      results.push(property);
    }
  }
  return results;
};

Object.prototype.isFunction = function(object) {
  return Object.prototype.toString.call(object) === '[object Function]';
};

Object.prototype.isUndefined = function(object) {
  return typeof object === "undefined";
};

Object.prototype.extend = function(destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
};

/* From array.js ------------------------------ */
function $A(iterable) {
  if (!iterable) return [];
  // Safari <2.0.4 crashes when accessing property of a node list with property accessor.
  // It nevertheless works fine with `in` operator, which is why we use it here
  if ('toArray' in Object(iterable)) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
};

/* From function.js ------------------------------ */
Function.prototype.argumentNames = function() {
  var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
    .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
    .replace(/\s+/g, '').split(',');
  return names.length == 1 && !names[0] ? [] : names;
};

// patched with inline helpers from function.js
Function.prototype.bind = function(context) {
  if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
  var __method = this, 
    args = Array.prototype.slice.call(arguments, 1);
  function update(array, args) {
    var arrayLength = array.length, length = args.length;
    while (length--) array[arrayLength + length] = args[length];
    return array;
  }
  function merge(array, args) {
    array = Array.prototype.slice.call(array, 0);
    return update(array, args);
  }
  return function() {
    var a = merge(args, arguments);
    return __method.apply(context, a);
  }
};

// patched with inline helper from function.js
Function.prototype.wrap = function(wrapper) {
  function update(array, args) {
    var arrayLength = array.length, length = args.length;
    while (length--) array[arrayLength + length] = args[length];
    return array;
  }
  var __method = this;
  return function() {
    var a = update([__method.bind(this)], arguments);
    return wrapper.apply(this, a);
  }
};

/* From class.js ------------------------------ */
/* Based on Alex Arnell's inheritance implementation. */
/* Refer to Prototype's web site for a [tutorial on classes and inheritance (http://prototypejs.org/learn/class-inheritance). */
var Class = (function() {
  
  // Some versions of JScript fail to enumerate over properties, names of which 
  // correspond to non-enumerable properties in the prototype chain
  var IS_DONTENUM_BUGGY = (function(){
    for (var p in { toString: 1 }) {
      // check actual property name, so that it works with augmented Object.prototype
      if (p === 'toString') return false;
    }
    return true;
  })();
  
  function subclass() {};
  function create() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

    function klass() {
      this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0, length = properties.length; i < length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    klass.prototype.constructor = klass;
    return klass;
  }

  function addMethods(source) {
    var ancestor   = this.superclass && this.superclass.prototype,
        properties = Object.keys(source);

    // IE6 doesn't enumerate `toString` and `valueOf` (among other built-in `Object.prototype`) properties,
    // Force copy if they're not Object.prototype ones.
    // Do not copy other Object.prototype.* for performance reasons
    if (IS_DONTENUM_BUGGY) {
      if (source.toString != Object.prototype.toString)
        properties.push("toString");
      if (source.valueOf != Object.prototype.valueOf)
        properties.push("valueOf");
    }

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], 
          value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames()[0] == "$super") {
        var method = value;
        value = (function(m) {
          return function() { return ancestor[m].apply(this, arguments); };
        })(property).wrap(method);

        value.valueOf = method.valueOf.bind(method);
        value.toString = method.toString.bind(method);
      }
      this.prototype[property] = value;
    }

    return this;
  }

  return {
    create: create,
    Methods: {
      addMethods: addMethods
    }
  };
})();

November 30, 2010

Android: Phonegap issue with HTML5 video

We've been trying to implement HTML5 video inside a WebView in an Android Phonegap app, and there's a big difference between the embedded web browser and the native web browser on the Samsung Galaxy Tab (and likely other Android devices). In the native browser, an HTML5 <video> player will pop the video into the native media player, and should play fine if you've used an appropriate codec, and jumped through the right hoops. But, in an embedded browser in a Phonegap app, the video won't play at all. We resorted to using an <a> link with a _blank target to pop you completely out of the app. This was the only solution we could come up with. It's a pretty sad story dealing with the "standards" of HTML5 video across all the platforms and browsers that supposedly support it. I'm not sure there's a good way to deal with all of the different platforms/scenarios...

November 29, 2010

Javascript: red plasma experiment

I ported an old Processing experiment of mine today, as an exercise to get myself back into coding after a couple weeks of vacation. Copy and paste the code into your own html file to try it out:
<!DOCTYPE html>
<html>
  <head>
    <title>Plasma</title>

    <script type="text/javascript">
        function initPlasma()
        {
            /* MATH FUNCTIONS ------------------------------ */
            
            function MathUtil() {}

            MathUtil.getDistance = function ( a, b ) {
                return Math.abs( Math.sqrt(a*a + b*b) );
            };
            
            MathUtil.randRangeDecimel = function ( min, max ) {  
                return Math.random() * ( max - min ) + min;
            };
            
            /* GRID CELL CLASS ------------------------------ */
            
            var Cell = function( x, y, w, h ) {
                this.x = x;
                this.y = y;
                this.w = w;
                this.h = h;
            }
            
            Cell.prototype.update = function( r, g, b ) {
                this.r = r;
                this.g = g;
                this.b = b;
                                
                this.draw();
            };
            
            Cell.prototype.draw = function() {
                if( !plasma ) return;
                
                // get color, based on distance 
                var ctrlPt1 = MathUtil.getDistance( this.x - plasma.controlPoints[0].x, this.y - plasma.controlPoints[0].y );
                var ctrlPt2 = MathUtil.getDistance( this.x - plasma.controlPoints[1].x, this.y - plasma.controlPoints[1].y );
                var ctrlPt3 = MathUtil.getDistance( this.x - plasma.controlPoints[2].x, this.y - plasma.controlPoints[2].y );
                
                var rVal = .5+.5*Math.sin(this.r) * Math.cos(ctrlPt1/100) * Math.cos(ctrlPt2/100) * Math.sin(ctrlPt3/100);
                var gVal = .2+.5*Math.sin(this.g) * Math.sin(ctrlPt1/100) * Math.sin(ctrlPt2/100) * Math.sin(ctrlPt3/100);
                var bVal = .2+.5*Math.cos(this.b) * Math.sin(ctrlPt1/100) * Math.cos(ctrlPt2/100) * Math.sin(ctrlPt3/100)
                
                // draw pixel to canvas
                plasma.context.fillStyle = "rgb("+ Math.round( 127 + rVal * 255 ) +","+ Math.round( 127 + gVal * 255 ) +","+ Math.round( 127 + bVal * 255 ) +")"; 
                plasma.context.fillRect ( this.x, this.y, this.w, this.h );  
            };
            
            
            /* CONTROL POINT CLASS ------------------------------ */
            
            var ControlPoint = function( canvasW, canvasH ) {
                // create random x,y starting point 
                this.incX = MathUtil.randRangeDecimel( 0, 2 * Math.PI );
                this.incY = MathUtil.randRangeDecimel( 0, 2 * Math.PI );
                // create random x,y oscillating speed 
                this.incXSpeed = MathUtil.randRangeDecimel( .01, .1 );
                this.incYSpeed = MathUtil.randRangeDecimel( .01, .1 );
                // store center point to oscillate around
                this.centerX = canvasW / 2;
                this.centerY = canvasH / 2;
            }
            
            ControlPoint.prototype.update = function() {
                // increment oscillating based on randomly-calculated speed
                this.incX += this.incXSpeed;
                this.incY += this.incYSpeed;
                // update coordinate
                this.x = this.centerX + this.centerX * Math.sin( this.incX );
                this.y = this.centerY + this.centerY * Math.sin( this.incY );
            };
            
            
            /* PLASMA CLASS ------------------------------ */
            
            var Plasma = function() {
                this.COLS = 50;
                this.ROWS = 50;
                this.CANVAS_W = 500;
                this.CANVAS_H = 500;
                this.FPS = 1000/30;
                this.NUM_CONTROL_POINTS = 3;
                
                this.startR = MathUtil.randRangeDecimel(0,2*Math.PI);
                this.startG = MathUtil.randRangeDecimel(0,2*Math.PI);
                this.startB = MathUtil.randRangeDecimel(0,2*Math.PI);
                this.startIncR = MathUtil.randRangeDecimel(.001,.05);
                this.startIncG = MathUtil.randRangeDecimel(.001,.05);
                this.startIncB = MathUtil.randRangeDecimel(.001,.05);
                this.incR = MathUtil.randRangeDecimel(.0001,.001);
                this.incG = MathUtil.randRangeDecimel(.0001,.001);
                this.incB = MathUtil.randRangeDecimel(.0001,.001);
                
                this.canvas;
                this.context;
                this.grid;
                
                this.buildStage();
                this.createGrid();
                this.createControlPoints();
                this.addSaveFunctionality();
                
                var self = this;
                setInterval( function(){ self.update(); }, this.FPS );
            };
        
            Plasma.prototype.buildStage = function() {
                // create and attach canvas element
                this.canvas = document.createElement('canvas');
                this.canvas.width = this.CANVAS_W;
                this.canvas.height = this.CANVAS_H;
                document.body.appendChild( this.canvas );
                
                // store graphical context
                this.context = this.canvas.getContext("2d");
            };
            
            Plasma.prototype.createGrid = function() {
                // calculate "pixel" size
                var boxW = this.CANVAS_W / this.COLS;
                var boxH = this.CANVAS_H / this.ROWS;
                
                // create 2D array of grid cells
                this.grid = new Array( this.COLS );
                for( var i = 0; i < this.COLS; i++ ) {
                    this.grid[ i ] = new Array( this.ROWS )
                    for( var j = 0; j < this.ROWS; j++ ) {
                        this.grid[ i ][ j ] = new Cell( i * boxW, j * boxH, boxW, boxH );
                    }
                }
            };
            
            Plasma.prototype.createControlPoints = function() {
                this.controlPoints = [];
                for ( var i = 0; i < this.NUM_CONTROL_POINTS; i++ ) {
                    this.controlPoints.push( new ControlPoint( this.CANVAS_W, this.CANVAS_H ) );
                }
            };
            
            Plasma.prototype.addSaveFunctionality = function() {
                var self = this;
                this.canvas.addEventListener("click", function(e) { 
                    window.open( self.canvas.toDataURL("image/jpeg") ); 
                }, false);
            };
            
            Plasma.prototype.update = function() {
                
                // increment the starting colors
                this.startR += this.startIncR;
                var curR = this.startR;
                this.startG += this.startIncG;
                var curG = this.startG;
                this.startB += this.startIncB;
                var curB = this.startB;
                
                // update control points
                for ( var i = 0; i < this.NUM_CONTROL_POINTS; i++ ) {
                    this.controlPoints[i].update();
                }
                
                // increment grid cells and draw to canvas
                for (var i = 0; i < this.COLS; i++) {
                    for (var j = 0; j < this.ROWS; j++) {
                        // send new base color to cells
                        this.grid[i][j].update( curR, curG, curB );
                        
                        // increment color as we traverse the grid
                        curR += this.incR;
                        curG += this.incG * 3;
                        curB += this.incB;
                    }
                }
            };
            
            
            // kick off the plasma controller
            var plasma = new Plasma();     
        }
    </script>
        
    <style>
        body, html {
            background-color:black;
        }
    </style>
  </head>
  <body onload="initPlasma();"></body>
</html>

And the result:

November 4, 2010

Android: Phonegap 0.9.2 doesn't work with Prototype.js 1.6.1

This morning I upgraded to Phonegap 0.9.2 on an HTML-based native app for Android, in order to use the new notification.confirm() function. After moving my project to this new version, I started getting this error:
Error initializing PhoneGap: JSON error
I was using Prototype.js 1.6.1, and after a bunch of investigation, I determined that it was Prototype that was causing the error. I replaced 1.6.1 with 1.7_rc3, and magically, everything works. I looked into why this might be happening, but decided to move on with my life and just go with 1.7_rc3 :)

Hopefully this post saves someone a little madness.

October 29, 2010

Android bug: "Miss a drag as we are waiting for WebCore's response for touch down."

I'm using Phonegap to port an HTML5 iPad app over to the Android platform. On my development HTC Incredible phone, every time I swipe my finger far enough, my app would freeze, and the adb debugging console would give me this error: "Miss a drag as we are waiting for WebCore's response for touch down." I researched a bunch and didn't find any solutions. I did find the Java code that logs this error in the core Android WebView.java class, but it didn't give me any clues to fix it.

I searched and hacked, and removed all my touch event listening code, and it would still break. Luckily the company work for has the resources to invest in development, and we went out and got a Samsung Galaxy S phone. I set the device up, published the app, and this phone did not have the cryptic issue! It did, however, show a range of other issues with fonts and the <canvas> object, which made me sad, as it's clear that building an HTML5-based app for Android isn't as easy as I hoped. The fragmentation of the Android platform is definitely an issue if you're attempting complex UI design and interaction with HTML/Javascript. I recommend keeping your HTML5 app very simple if you're targeting multiple Android platforms. Even though all new Android devices use Webkit, there are plenty of small, ugly differences.

[UPDATE]: The following code, when removed from my project, got rid of this weird error:
document.ontouchmove = function(event) {
    event.preventDefault();
};

Another update... Check out this post, and try out the demo code to get a bit more idea about how to handle preventDefault() on touch events in the Android browser: http://code.google.com/p/android/issues/detail?id=4549. It still crashes on my HTC device, but works great on the Samsung device.

October 22, 2010

Android browser bug: -webkit-transform scaling discrepency

[UPDATE]: this only happens on my HTC Incredible device, but not my Samsung Galaxy S device. yeesh.

I'm porting an HTML5 app we built for the iPad over to Android for the upcoming Samsung tablet. With the small difference in aspect ratio, I'm scaling down the entire site to avoid rebuilding everything. It turns out, that if you scale a container with CSS like so:
-webkit-transform-origin: 0 0;
-webkit-transform : scale(0.78125);
That works fine. However, I wanted to scale it dynamically with javascript, based on the device size, like so:
var globalScale = window.innerWidth / 768;
element.style.webkitTransformOrigin = '0 0';
element.style.webkitTransform = 'scale(' + globalScale + ')';
The result looks the same, but now clicking on anything in the scaled container is completely broken. :(

September 14, 2010

Javascript: strip HTML tags from a string

Here's a super simple RegEx to use when you want to be sure all html tags are removed from a string.
theString.replace(/<.*?>/g, '');

September 10, 2010

HTML/CSS on Android: rotation CSS difference between Android 2.1 and 2.2

Well, the splinternet is getting more interesting. As many developers settle on html as the most cross-functional platform, we're faced with ever more browsers and small differences between them. One that I just found is a difference in CSS positioning and rotation between the browsers on Android 2.1 and Android 2.2.

On my current project, I have a fancy UI that has an element constantly changing rotation and position using webkit transform CSS built with javascript. On Android 2.1, it worked fine as a 1-liner:
element.style.webkitTransform = "translate3d(" + xPos + "px, " + yPos + "px, 0px) rotate(" + rotation + "deg)";
But, on Android 2.2, the rotation stopped working. It seems that you can't have the translate3d and the rotate properties all set in the style.webkitTransform property. To fix the issue, I positioned using traditional absolute coordinates with the top and left CSS properties, and then used the webkitTransform property to do the rotation. There were a ton of special browser cases in my project to handle different things. Check out my platform detection class below to see how I handled a lot of special cases in one place.
PlatformHelper = function ()
{
    this.webkit_css_enabled = false;
    this.animations_enabled = false;
    this.is_android = false;
    this.is_android21 = false;
    this.is_android22 = false;
    this.is_idevice = false;
    this.is_touchscreen = false;
    this.is_msie = false;
    this.is_msie6 = false;
    this.is_msie8 = false;
    this.is_firefox = false;
    return this;
};

PlatformHelper.prototype.init = function ()
{
    // check for webkit positioning capability
    if( navigator.userAgent.match(/iPhone/i) ) this.webkit_css_enabled = true;
    else if( navigator.userAgent.match(/iPod/i) ) this.webkit_css_enabled = true;
    else if( navigator.userAgent.match(/iPad/i) ) this.webkit_css_enabled = true;
    else if( navigator.userAgent.match(/Chrome/i) ) this.webkit_css_enabled = true;
    else if( navigator.userAgent.match(/Safari/i) ) this.webkit_css_enabled = true;
    
    // check for certain platforms
    if( navigator.userAgent.match(/Android/i) ) this.is_android = true;
    if( navigator.userAgent.match(/Android 2.1/i) ) this.is_android21 = true;
    if( navigator.userAgent.match(/Android 2.2/i) ) this.is_android22 = true;
    if( navigator.userAgent.match(/MSIE/i) ) this.is_msie = true;
    if( navigator.userAgent.match(/MSIE 6/i) ) this.is_msie6 = true;
    if( navigator.userAgent.match(/MSIE 8/i) ) this.is_msie8 = true;
    if( navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPod/i) || navigator.userAgent.match(/iPad/i) ) this.is_idevice = true;
    if( navigator.userAgent.match(/Firefox/i) ) this.is_firefox = true;
    
    // special cases for touchscreens
    if( this.is_android == true || this.is_idevice == true ) this.is_touchscreen = true;
    
    // decide who sees animations
    if( this.is_msie == true ) this.animations_enabled = false;
    else this.animations_enabled = true;
};

PlatformHelper.prototype.updatePosition = function ( element, xPos, yPos, rotation )
{
    if( !this.webkit_css_enabled || this.is_android22 )
    {
        element.style.left = xPos + 'px';
        element.style.top = yPos + 'px';
        element.style.MozTransform = 'rotate(' + rotation + 'deg)';
        element.style.webkitTransform = 'rotate(' + rotation + 'deg)';
    }
    else
    {
        var new_transform = "translate3d(" + xPos + "px, " + yPos + "px, 0px) rotate(" + rotation + "deg)";
        if( element.style.webkitTransform != new_transform )    // only apply style if not already in position
         element.style.webkitTransform = new_transform;
    }
};

September 8, 2010

Javascript / CSS animated text fire effect

A coworker sent me a funny example of a text-shadow CSS fire effect. I had a little time to kill, so I took the example and created an animated version using javascript. It's not very realistic, but it is highly silly:


Copy and paste the code into an html file to try it out:
<!DOCTYPE html>
<html>
  <head>
    <title>Fire</title>

    <script type="text/javascript">
        function fireText()
        {
            var FireColorStop = function( xPos, yPos, blur, color )
            {
                this.x = xPos;
                this.y = yPos;
                this.blur = blur;
                this.color = color;
                this.oscSpeed = Math.random() * Math.abs( yPos ) / 75;
                this.oscIncrement = 0;
                this.xOffset = 0;
                this.yOffset = 0;
                this.blurOffset = 0;
            };
        
            FireColorStop.prototype.oscillate = function() 
            {
                this.oscIncrement += this.oscSpeed;
                this.xOffset = Math.sin(this.oscIncrement) * this.blur / 3;
                this.yOffset = Math.sin(this.oscIncrement) * 1;
                this.blurOsc = this.blur + 10 + Math.sin(this.oscIncrement) * 3;
            };
        
            FireColorStop.prototype.getCSS = function() 
            {
                return ( this.x + this.xOffset ) + 'px ' + ( this.y + this.yOffset ) + 'px ' + this.blurOsc + 'px ' + this.color; 
            };
            
            // create objects for each color stop for independent animation
            var fireColors = [  new FireColorStop(0,  0,  4,  '#FFFFFF'),
                                new FireColorStop(0, -5,  4,  '#FFFF33'),
                                new FireColorStop(2, -10, 6,  '#FFDD33'),
                                new FireColorStop(-2,-15, 11, '#FF8800'),
                                new FireColorStop(2, -25, 18, '#FF2200')
                                ];
        
            var fps = 1000/30;
            var text = document.getElementById('fireText');
            
            // oscillate color stops and rebuild fire css
            setInterval( function(){ 
                var shadowCSS = '';
                for( var i = 0; i < fireColors.length; i++ )
                {
                    fireColors[i].oscillate();
                
                    shadowCSS += fireColors[i].getCSS();
                    if( i < fireColors.length - 1 )
                        shadowCSS += ', ';
                }
                text.style.textShadow = shadowCSS;
            }, fps );
        }
    </script>
        
    <style>
        body, html {
            background-color:black;
        }
        #fireText {
            background-color:black;
            position:absolute;
            display:block;
            width:100%;
            height:300px;
            line-height:300px;
            color:white;
            font-family: Arial, Verdana, sans-serif;
            font-size:50px;
            font-weight:bold;
            text-align:center;
        }
    </style>
    
  </head>
  <body>
    <div id="fireText">
        Yeah Dude.
    </div>
    <script type="text/javascript">
        fireText();
    </script>
  </body>
</html>

July 27, 2010

Android browser bug: pinch/zoom kills setTimeout()

I'm working on some cross-platform/mobile touch/mouse code for a fancy html/js UI, and everything's been working great, but when I pinch/zoom the web page in an Android browser, my setTimeout() calls stop running. To be safe, I recommend using setInterval() instead.
// before:
setTimeout( function() { runTimer(); } , 1000/30 );
function runTimer() {
    // update graphics here
    setTimeout( function() { runTimer(); } , 1000/30 );
}

// after:
setInterval( function(){ runTimer(); }, 1000/30 );
function runTimer() {
    // update graphics here
}
I initially thought that my touch events (touchstart, touchmove, touchend) were randomly failing after zooming, because my custom motion code would completely break after running at a solid 30+ fps. It appears that this is a known bug in pre-2.2 (Froyo) Android web browsers: http://code.google.com/p/android/issues/detail?id=8566

July 25, 2010

PHPFlicker: Get images by user's tag

I started using phpFlickr (http://phpflickr.com/), so I could use Flickr as a query-able backend for some of my images. I'm tagging images that I want to show up on certain pages of my site, but there wasn't an example for using tags on the phpFlickr examples page. So here's an example:
<?php
 $api_key = "YOUR_API_KEY";
 $user_id = "YOUR_USER_ID";
 $secret = "YOUR_SECRET";
 
 require_once("./php/flickr/phpFlickr.php");
 $f = new phpFlickr($api_key, $secret);

 $photos = $f->photos_search(array( "api_key"=>$api_key, "user_id"=>$user_id, "tags"=>"promo", "tag_mode"=>"any", "extras"=>"original_format,tags,url_o,description") );
 
 // Loop through the photos and output the html
 foreach( (array)$photos['photo'] as $photo ) 
 {
  // get original, or large if no original
  if( isset( $photo['url_o'] ) ) 
   echo '<a rel="lightbox[flickr]" title="'. $photo['title'].' - '. $photo['description'].'" href="'. $photo['url_o'] .'">';
  else
   echo '<a rel="lightbox[flickr]" title="'. $photo['title'].' - '. $photo['description'].'" href="'. $f->buildPhotoURL($photo, "large") .'">';

  echo '<img border="0" alt="'.$photo[title].' - '. $photo['description'].'" title="'.$photo[title].' - '. $photo['description'].'" src="' . $f->buildPhotoURL($photo, "square") . '" />';

  echo "</a>";
 }
?>

Just replace YOUR_API_KEY, YOUR_USER_ID (something like: 38845956@N05), and YOUR_SECRET (something like 7fc67607bd5abc59). This type of search is "secure", meaning that you have to acquire a secret key via the Flickr API.

This script will display square thumbnails that link out to the largest available image size. This can easily by styled, augmented with a lightbox javascript, or customized via the phpFlickr search options.

Enjoy.

July 5, 2010

Javascript: iPad orientation class with Prototype.js

I'm writing an iPad app in "HTML5", and I wanted to keep track of the device orientation. I wrote this little class to send notifications when it changes, and to always have simple access to the current state. This requires Prototype.js, but could easily be ported to another OOP style.
var AppState = Class.create({
 PORTRAIT: 0,
 LANDSCAPE: 1,
 orientation: -1,    
 initialize: function() {
  this.orientation = this.PORTRAIT; // default for desktop browser
  this.setUpOrientationListener();
 },
 setUpOrientationListener : function() {
  // add listener to window if it's orientation-capable
  if( window.orientation !== undefined )
  {
   var self = this; // handles scope
   window.onorientationchange = function (event)
   {
    if ( Math.abs( window.orientation ) % 180 == 90 )
    {
     self.orientation = self.LANDSCAPE;
    }
    else
    {
     self.orientation = self.PORTRAIT;
    }
    // send out custom event
    var containerNode = $$('body');
    containerNode[0].fire("app:orientationchange", { orientation: self.orientation });
   }
   // make sure local flag is set right away
   window.onorientationchange(null);
  }
 }
});


/*
// example code for listening to custom event that fires on orientation change
document.observe("app:orientationchange", function(event) {
 console.log( "Tag " + event.target.tagName + " with id of " + event.target.id + " says the orientation is now " + event.memo.orientation + ".");
});
*/

/*
// class initialization
var app_state = new AppState(),
*/

May 27, 2010

iPhone: UISegmentedControl with custom colors

[UPDATE]: We had an app approved with this code. Keep in mind that Apple has very inconsistent policies regarding app approval, but it's looking good for now.

Tasked with building a UISegmentedControl with different colors for selected/unselected buttons, I created a subclass that accomplishes this by digging through the subviews of the segmented control. I can't verify that this will get approved by the mysterious app store process, nor can I say it will function properly if you're using all of the built-in functions of UISegmentedControl (adding/removing segments dynamically will break this code). But for a simple case, it's working well for my purposes. Here we go:

CustomSegmentedControl.h :
#import 


@interface CustomSegmentedControl : UISegmentedControl {
 
 UIColor *offColor;
 UIColor *onColor;
 
 BOOL hasSetSelectedIndexOnce;
}

-(id)initWithItems:(NSArray *)items offColor:(UIColor*)offcolor onColor:(UIColor*)oncolor;
-(void)setInitialMode;
-(void)setToggleHiliteColors;

@end

CustomSegmentedControl.m :
#import "CustomSegmentedControl.h"


@implementation CustomSegmentedControl


-(id)initWithItems:(NSArray *)items offColor:(UIColor*)offcolor onColor:(UIColor*)oncolor {
 if (self = [super initWithItems:items]) {
        // Initialization code
  offColor = [offcolor retain];
  onColor = [oncolor retain];
  hasSetSelectedIndexOnce = NO;
  [self setInitialMode];
  [self setSelectedSegmentIndex:0];  // default to first button, or the coloring gets all whacked out :(
    }
    return self;
}

-(void)setInitialMode
{
 // set essential properties
 [self setBackgroundColor:[UIColor clearColor]];
 [self setSegmentedControlStyle:UISegmentedControlStyleBar];
 
 // loop through children and set initial tint
 for( int i = 0; i < [self.subviews count]; i++ )
 {
  [[self.subviews objectAtIndex:i] setTintColor:nil];
  [[self.subviews objectAtIndex:i] setTintColor:offColor];
 }
 
 // listen for updates
 [self addTarget:self action:@selector(setToggleHiliteColors) forControlEvents:UIControlEventValueChanged];
}

-(void)setToggleHiliteColors
{
 // get current toggle nav index
 int index = self.selectedSegmentIndex;
 int numSegments = [self.subviews count];
 
 for( int i = 0; i < numSegments; i++ )
 {
  // reset color
  [[self.subviews objectAtIndex:i] setTintColor:nil];
  [[self.subviews objectAtIndex:i] setTintColor:offColor];
 }
 
 if( hasSetSelectedIndexOnce )
 {
  // this is super weird - the subviews array is backwards... so deal with it like that
  [[self.subviews objectAtIndex: numSegments - 1 - index] setTintColor:onColor];
 }
 else
 {
  // ...but the very first time, they're the expected order :-/
  [[self.subviews objectAtIndex: index] setTintColor:onColor];
  hasSetSelectedIndexOnce = YES;
 }

}


@end 
And to initialize :
NSArray *toggleItems = [[NSArray alloc] initWithObjects:@"One",@"Two",@"Three",nil];
CustomSegmentedControl *toggleNav = [[CustomSegmentedControl alloc] initWithItems:toggleItems offColor:[UIColor blackColor] onColor:[UIColor redColor] ];
[toggleNav addTarget:self action:@selector(handleToggleNav:) forControlEvents:UIControlEventValueChanged];
[toggleNav setFrame:CGRectMake(52, 8, 211, 25)]; 
[self.view addSubview:toggleNav];
[toggleNav release];

Otherwise, follow the documentation for a UISegmentedControl, and enjoy.

iPhone: Bug in UITextField component - setTextColor not working

I'm working on a form for an iPhone app that does real-time form validation to let the user know if they're trying to create an account with a username that already exists. To let the user know that a username is already taken, I set the UITextField textColor property to red:
#define kColorRedError [UIColor colorWithRed:1 green:0 blue:0 alpha:1.0]
//... UITextField *username = [[UITextField alloc] initWithFrame:CGRectMake(12, 14, 296, 30)];
[username setTextColor:kColorRedError];
This wouldn't update until I typed another letter into the UITextField, so quite often it would display an error when it shouldn't, and vice versa. I tried using setNeedsDisplay and some other bits of code to try to force a display update after setting the text color. Nothing worked, until I tried this:
[password setTextColor:kColorRedError];
username.text = username.text;
...Quite absurd, but forces a redraw on the component. Gotta love those Apple components ;)

April 21, 2010

iPhone: Subviews in UIButtons block the touch, unless...

If you add a UIView or other UIView subclass to a UIButton, the UIView object will block touches/clicks on the UIButton. There's a simple fix to ensure that subviews in your UIButton don't interfere. In the following case, I have a subclass of UIView that loads an image from the web (WebImage), that's created inside a subclass of UIButton:
WebImage *thumb = [[WebImage alloc] initWithFrame:CGRectMake(1, 1, 94, 94) andImageUrl:@"http://mysite.com/thumbnail.png"];
[self addSubview:thumb];
thumb.userInteractionEnabled = NO;
thumb.exclusiveTouch = NO;
[thumb release]; 
All you need to do is set userInteractionEnabled and exclusiveTouch to NO or FALSE, and the subview will no longer block your button.

April 19, 2010

iPhone: You can't have a UIView subclass named "WebView"

Good lord, this wasted half a day. I was creating a UIView subclass to hold a UIWebView, and named it "WebView". Not too abstract, right? I kept getting the following cryptic error:
*** -[WebView _isAncestorOfFirstResponder]: unrecognized selector sent to instance 0x463a7b0
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[WebView _isAncestorOfFirstResponder]: unrecognized selector sent to instance 0x463a7b0'
I renamed my class "WebViewView" and no more errors. Thanks for the shitty error messages Apple!

April 14, 2010

iPhone/iTouch/iPad: Check for the ability to make a phone call

I came across a situation where I wanted to lay out different buttons in case the iDevice (iPod Touch or iPad) can run my app, but can't make a phone call. The iPhone will display a "Call Store" button, but other devices won't. For a simple check, look at the following code, which checks for the device name, and looks for "iPhone" at the beginning of the string. It's a very simple implementation, but I didn't need further detection functionality for this project.
// check iDevice model for calling capability and initially assume it's an iPhone
NSString *model= [[UIDevice currentDevice] model];
BOOL canMakeCalls = YES;
//model = @"iTouch"; // manual test for non-iPhones in simulator
if ( ![model hasPrefix:@"iPhone"] ) canMakeCalls = NO;
[model release];
Then you can conditionally display or disable UI elements depending on whether it's for the iPhone only. This can obviously be customized further and more robustly, but for something quick and easy, I hope you find it useful. Check out this blog for a much more detailed detection class.

March 19, 2010

Actionscript 3: Masking in Native 3D Issue in FP10

Today I tried to mask a Sprite that was in a container that had native 3d rotation applied - i.e. a parent Sprite had some rotationX and rotationY. The masking wasn't working, but it turns out that it was only not working because another Sprite within the same parent container had a 3D "z" value set. In order to get the masking to behave properly, I had to nest the Sprite and its mask in another Sprite, so that no sibling clips had z-positioning within the same immediate parent clip. Hopefully this little note saves someone the time it took for me to realize what the issue was.

March 12, 2010

iPhone: Get the class name of an object

Sometimes you want to know what type of object something is, when pulling it out of an array. In Obj-c there's a simple way to do that:

NSLog(@"object type = %@", [[myObject class] className]);
or
NSLog(@"object type = %@", [[myObject class] description]);
or!
if( [[myArray objectAtIndex:i] isKindOfClass:NSClassFromString(@"MyCustomClass")] ) { NSLog(@"it's a MyCustomClass"); }

Easy.