October 24, 2012

JavaScript: Letterbox, pillowbox or crop an image to any container size

Very often, I've run across the situation of having images of an unspecified size that need to fit into a container of an unspecified size, either letterboxed, or cropped to fill the container. This is easily accomplished by scaling the image, positioning it within the container, and masking the container's contents. With the following methods, it's easy to crop or letterbox/pillowbox an image for your particular use case.

Here's a method that will give you the size and position of your image, based on its relation to the container's size. The method receives the container's dimensions, the image's original size, and a boolean to specify cropping or letterboxing. The method returns an array with the coordinates and dimensions to reposition and resize your image. If you don't know the size of your images beforehand, you can use this method to find out before using the following code.
var ImageUtil = ImageUtil || {};

ImageUtil.getOffsetAndSizeToCrop = function( containerW, containerH, imageW, imageH, cropFill ) {
  var ratioW = containerW / imageW;
  var ratioH = containerH / imageH;
  var shorterRatio = ratioW > ratioH ? ratioH : ratioW;
  var longerRatio = ratioW > ratioH ? ratioW : ratioH;
  var resizedW = cropFill ? Math.ceil(imageW * longerRatio) : Math.ceil(imageW * shorterRatio);
  var resizedH = cropFill ? Math.ceil(imageH * longerRatio) : Math.ceil(imageH * shorterRatio);
  var offsetX = Math.ceil((containerW - resizedW) * 0.5);
  var offsetY = Math.ceil((containerH - resizedH) * 0.5);
  return [offsetX, offsetY, resizedW, resizedH];
};

To wrap up the cropping functionality, you can use the following method to apply the css styles to your image element based on its size, the container's size, and the type of image resizing you'd like. I've included special cases to anchor the image to top or bottom of the container rather than vertically centering it, in case the images' content requires that type of positioning. Note that the method requires a raw html element reference for the image and container - this doesn't require jQuery or any such library.
ImageUtil.CROP = 'CROP';
ImageUtil.CROP_TOP = 'CROP_TOP';
ImageUtil.CROP_BOTTOM = 'CROP_BOTTOM';
ImageUtil.LETTERBOX = 'LETTERBOX';

ImageUtil.cropImage = function( containerEl, containerW, containerH, imageEl, imageW, imageH, cropType ) {
  var cropFill = ( cropType == ImageUtil.CROP || cropType == ImageUtil.CROP_TOP || cropType == ImageUtil.CROP_BOTTOM );
  var offsetAndSize = ImageUtil.getOffsetAndSizeToCrop(containerW, containerH, imageW, imageH, cropFill);

  // set outer container size
  containerEl.style.width = containerW+'px';
  containerEl.style.height = containerH+'px';

  // resize image
  imageEl.width = offsetAndSize[2];
  imageEl.height = offsetAndSize[3];
  imageEl.style.width = offsetAndSize[2]+'px';
  imageEl.style.height = offsetAndSize[3]+'px';

  // position image
  imageEl.style.left = offsetAndSize[0]+'px';
  imageEl.style.top = offsetAndSize[1]+'px';

  // special y-positioning 
  if( cropType == utensils.ImageUtil.CROP_TOP ) {
    imageEl.style.top = '0px';
    imageEl.style.bottom = '';
  } else if( cropType == utensils.ImageUtil.CROP_BOTTOM ) {
    imageEl.style.top = '';
    imageEl.style.bottom = '0px';
  }
};
The only default styles you'd need on your container and image are as follows:
.cropped-image {
  position: absolute;
}
.container {
  position: relative;
  overflow: hidden;
}

September 11, 2012

Obj-C: Center a view horizontally with autoresizingMask properties

Cocoa's UIView layout system gives the developer some nice tools to automatically resize and reposition views inside of each other. This is usually done in the nib/xib/storyboard editor with the Origin/Autoresizing View property inspectors, and honestly it's never made a ton of sense to me. I'm only an occasional obj-c dev, so bear with me. I wanted to horizontally center and fill some UIView elements to fit the width of different devices and orientations, and I knew there should be a simple way to accomplish this. I came up with several methods to help apply these settings to any number of outer containers that should fill the width of the parent view, and inner containers that should be centered within:
- (void)setViewCentered:(UIView*)view
{
    view.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin);
}

- (void)setContainerToParentWidth:(UIView*)view
{
    view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
}

- (int)getHorizCenterFromView:(UIView*)view
{
    return (self.view.bounds.size.width - view.frame.size.width)/2;
}

- (int)getHorizCenterFromInt:(int)width
{
    return (self.view.bounds.size.width - width)/2;
}
The key to centering a fixed-width view - this was the confusing part for me - is setting the x-position of the frame to 1/2 of the width of the parent container. The associated example call to center a view is here. From my researching, it seems that the autoresizingMask property should be changed after addSubview
    int viewW = 320;
    _controlsContainer = [[UIView alloc] initWithFrame:CGRectMake([self getHorizCenterFromInt:viewW], 0, viewW, controlsH)];
    [self.view addSubview:_controlsContainer];
    [self setViewCentered:_controlsContainer];
Note that self.view is the outer container that we're centering inside of. Then if you simply want to fill a container to the width of the parent, use something like this:
    _header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44)];
    [self.view addSubview:_header];
    [self setContainerToParentWidth:_header];
Happy centering without having to change a UIView's frame!

July 9, 2012

HTML5: Mobile web dev notes for games & fancy interfaces

Note: Use vendor prefixes for CSS3 styles mentioned here.
  • Bind to touchend events rather than click events for far more responsive click/tap handling. This is one of the most elementary steps to make a mobile site feel more app-like. Mobile boilerplate has a nice way to do this (see the fastButton methods).
  • Lock your window in place by canceling a touchmove event on document:
    var lockTouchScreen = function( locks ) {
      if( locks == true ) {
        document.ontouchmove = function( event ) {
          event.preventDefault();
        };
      } else {
        document.ontouchmove = null;
      }
    };
    
  • Hide the browser address bar to gain more screen real estate. There's another nice method for this in the Mobile boilerplate project (see hideUrlBar). An important note about this working properly: your content has to be tall enough to convince the browser it needs to hide the address bar. This may require manually setting the height property of an outer container to $(window).height() + 50 before calling hideUrlBar().
  • Use transform: translate3d(0,0,0) to hardware accelerate the movement and CSS animation of individual HTML elements. This is true for all version of iOS, and Android 4.0 and newer. However, there are some potential side-effects described below.
  • Using the above hardware acceleration trick will accelerate any CSS animations that are added to the same element, such as opacity, background-color, width, etc.
  • Sometimes z-index is ignored if you've used transform: translate3d(0,0,0) and placed a non-accelerated button above an accelerated element. The accelerated element can block clicks or touch events. (iOS)
  • Adding this acceleration to sub-elements inside an accelerated container can further improve performance. For example, a series of img elements inside a larger element that's being positioned with translate3d (iOS)
  • Swapping a background image with a class toggle on an element that's been hardware-accelerated can lead to the element disappearing. (Intermittent on iOS)
  • Android ignores z-position in translate3d positioning as of this posting
  • Android ignores background-size in 2.x versions, and possibly later versions, though Android 4.x seems to fix the problem.
  • iOS can get really chunky when it's loading images. For less chunky loading, use containers with smaller background images, and use webkit scaling to size them up to the right size.
  • Make sure you're loading @2x images for retina screens, or larger screens if you need to fill up a larger area. Also make sure you're not double-loading by using max and min pixel density media queries:
    (-webkit-min-device-pixel-ratio: 1.5)
    (-webkit-max-device-pixel-ratio: 1.5)
  • It really can't hurt to turn off backface-visibility: none - this is a common optimization in 3D programming, though I would expect browsers to handle it by default?
  • Holding a touch down on some versions of Android will reduce the image quality of images on the page, thus increasing framerate significantly for elements that are being animated or updated each frame. This fact could be exploited for gaming purposes...
  • Using scale and translate3d at the same time on an element will break on Android 2.x and maybe others. For better cross-platform positioning/sclaing, use the technique described in my previous post.
  • This Android transform bug is not only on the same element - if a container that's been scaled has a child element with a transform, animating transition or backface-visibility defined, the outer container will lose its scale. This is a MAJOR bug in Android 2.2 and 2.3, and there are probably other properties that will break transform: scale().
  • Detect pinch gesture capabilities with isEventSupported('gesturestart'), using the event support detection method by Kangax. Then use my GestureCallback class to perform the pinch detection, or fall back to another input style.
  • The default iOS menu that shows when an iOS user taps & holds an img element can be blocked if the img element is within a container that has a transform: translate3d(0,0,0); applied to it. Though, much like the button-blocking note above, by adding transform: translate3d(0,0,0); to the img, the issue is solved.
  • Likewise on Android, this same menu that lets you save or copy an img can be blocked when using the touchstart event-canceling trick that's needed to provide a draggable interface. I'm not sure what a good fix would be for this if you want to use the image menu inside a draggable element.
  • When detecting accelerometer support, it's not enough to check existence of the devicemotion event. You have to read the properties of the incoming event and check for real numbers. Something like: if( event.acceleration != undefined && event.acceleration.x != undefined ) var hasAccel = true;
  • iOS caches the previous page you've visited, so if you press the browser back button, you can end up in a broken state if your page requires a re-initialization that would've happened on $(document).ready(). In my case, I was using document.location = '/new-location';. To force a reload if the user comes back via the browser back button, I used the following code:
    document.location = '/new-location';
    // reload page if we end up back at a cached page
    setTimeout(function(){
        window.location.reload();
    },3000);
    The browser will execute the timeout the user comes back, and problem solved.
More to come...

June 19, 2012

Android: fix the Android 2.x CSS transform bug with translate3d() and scale()

There's a serious bug with the use of the CSS3 transform property in Android's webkit browser, specifically in Android versions 2.2 and 2.3. The problem is that the scale() property is discarded in the presence of translate3d() (and other applicable transform properties). I can confirm it doesn't happen in 2.1, and I'm not seeing it consistently in all versions/devices with 2.3.x. The bug is officially documented here. There is a workaround, unfortunate as the bug is, which is to separately apply the translate3d() property to an outer element, and scale() to an inner element, like so:

Markup:
<div class="outer">
  <div class="inner"></div>
</div>
CSS:
.outer {
  transform: translate3d(100px, 100px, 0px)
  -webkit-transform: translate3d(100px, 100px, 0px)
  /* add other vendor prefixes so all browsers handle your hack */
}

.inner {
  transform: scale(5)
  /* add vendor-prefixed property again */
}
This doesn't work with scale() on the outer element, and translate3d() on the inner element, for what it's worth.

May 27, 2012

CSS: Rotation animation with CSS3

Sometimes you want to continuously spin something on your web page. In my case I was spinning a .png image for a smooth loading indicator. The following CSS will spin any element:
/* rotation animation */
@-webkit-keyframes rotate {
  from { -webkit-transform:rotate(0deg); }
  to { -webkit-transform:rotate(360deg); }
}

@-moz-keyframes rotate {
  from { -moz-transform:rotate(0deg); }
  to { -moz-transform:rotate(360deg); }
}

@-ms-keyframes rotate {
  from { -ms-transform:rotate(0deg); }
  to { -ms-transform:rotate(360deg); }
}

@-o-keyframes rotate {
  from { -o-transform:rotate(0deg); }
  to { -o-transform:rotate(360deg); }
}

.rotating {
  -webkit-transform-origin: 50% 50%;
  -webkit-animation-name: rotate;
  -webkit-animation-duration: 1.5s;
  -webkit-animation-iteration-count: infinite;
  -webkit-animation-timing-function: linear;
  -moz-transform-origin: 50% 50%;
  -moz-animation-name: rotate;
  -moz-animation-duration: 1.5s;
  -moz-animation-iteration-count: infinite;
  -moz-animation-timing-function: linear;
  -ms-transform-origin: 50% 50%;
  -ms-animation-name: rotate;
  -ms-animation-duration: 1.5s;
  -ms-animation-iteration-count: infinite;
  -ms-animation-timing-function: linear;
  -o-transform-origin: 50% 50%;
  -o-animation-name: rotate;
  -o-animation-duration: 1.5s;
  -o-animation-iteration-count: infinite;
  -o-animation-timing-function: linear;
}
Just add/remove the .rotating class to an element to start/stop the animation.

April 23, 2012

Processing: Flip a PImage horizontally

I needed a mirror image of a PImage in my Processing project, so I came up with this little snippet:
public PImage getReversePImage( PImage image ) {
 PImage reverse = new PImage( image.width, image.height );
 for( int i=0; i < image.width; i++ ){
  for(int j=0; j < image.height; j++){
   reverse.set( image.width - 1 - i, j, image.get(i, j) );
  }
 }
 return reverse;
}

April 12, 2012

Javascript: Array.splice() is "broken" in IE 8 and below

It's not exactly broken, because all of the Array.splice() documentation that I've found says that the first 2 parameters - (start index and delete count) - are required. However, it seems that most modern browsers allow the 2nd parameter to be optional, and IE9 has made this the case as well. By leaving the 2nd parameter off, it assumes that you passed in the length of the array and clears everything past the start index. I found the issue when using array.splice(0) to empty an array, but it wasn't getting emptied in IE7 and IE8. My confusion came from my past life as an ActionScript developer, where the 2nd delete count parameter was optional. From now on, I'll be using at least the 2 required parameters for legacy IE support.

March 23, 2012

Javascript: Remove duplicate values from an array

I did a Google search for different methods of removing duplicate values from an array, and I found a bunch of crappy algorithms that all returned a new array, rather than paring down the original array. So I wrote this, which wouldn't work on identical complex objects, but works great for primitive data types and objects stored by reference.
function removeDuplicates(arr) {
  arr.sort();
  var i = arr.length - 1;
  while (i > 0) {
    if (arr[i] === arr[i - 1]) arr.splice(i, 1);
    i--;
  }
}

March 22, 2012

Javascript: Get the original size of an image

Sometimes you need to know the original dimensions of an image that you've loaded via javascript, or that's already loaded in the DOM. Here's a little class that will take the location of an image, and a callback function that receives the original width and height:
var getImageSizeWithCallback = function( src, callback ) {
  var image = new Image();
  image.onload = function () {
    if( callback ) callback( image.width, image.height );
    image.onload = image.onerror = null;
  };
  image.onerror = function () {
    if( callback ) callback( -1, -1 );
    image.onload = image.onerror = null;
  };
  // load it
  image.src = src;
};

Usage:
getImageSizeWithCallback( 'images/path/to/img.jpg', function( w, h ) {
  console.log( 'image size:', w, h );
});

March 6, 2012

Bookmarklet: Scrape a web page for YouTube videos and generate a playlist

I wrote this little script to capture about 50 YouTube videos from a music blog I was browsing. I figured it would be useful for others, so I turned it into a bookmarklet:
// get html source
var src = document.getElementsByTagName('html')[0].innerHTML;
// handle old youtube vids, and https
var vids = src.replace(/\"/g).replace(/http:\/\/www.youtube.com\/v\//gi,'http://www.youtube.com/embed/').replace(/https/gi,'http');
// parse the source for youtube embeds
var vidArr = vids.split('http://www.youtube.com/embed/')
var links = '';
// build string for playlist
var playlist = 'http://www.ytplaylist.com/?pl=';
// build links
if(vidArr.length > 1) {
// keep track of IDs so we don't get duplicates
var videos = [];
for(var i=1; i < vidArr.length; i++) {
// grab 11-character YouTube ID
var ytID = vidArr[i].substr(0,11);
if(videos.indexOf(ytID) == -1) {
// add link to video, and to playlist link
var link = 'http://www.youtube.com/watch?v='+ytID;
links += '<a target="_blank" href="'+link+'">'+link+'</a><br/>';
playlist += ytID+';';
videos.push(ytID);
}
}
playlist = playlist.substr(0,playlist.length-1);
// draw it to the page
var container = document.createElement('div');
container.innerHTML = 'Videos Found:<br/><br/>'+links+'<br/><br/>'+'<a target="_blank" href="'+playlist+'">View as Playlist</a><br/>';
container.style.position='fixed';
container.style.background='#fff';
container.style.border='5px solid black';
container.style.padding='10px';
container.style.top='0';
container.style.right='0';
container.style.zIndex='999';
document.body.appendChild(container);
} else {
alert('No YouTube videos found');
}
And here's the bookmarklet link text:
javascript:(function(){var%20src%20%3D%20document.getElementsByTagName('html')%5B0%5D.innerHTML%3B%0Avar%20vids%20%3D%20src.replace(%2F%5C%22%2Fg).replace(%2Fhttp%3A%5C%2F%5C%2Fwww.youtube.com%5C%2Fv%5C%2F%2Fgi%2C'http%3A%2F%2Fwww.youtube.com%2Fembed%2F').replace(%2Fhttps%2Fgi%2C'http')%3B%0Avar%20vidArr%20%3D%20vids.split('http%3A%2F%2Fwww.youtube.com%2Fembed%2F')%0Avar%20links%20%3D%20''%3B%20%0Avar%20playlist%20%3D%20'http%3A%2F%2Fwww.ytplaylist.com%2F%3Fpl%3D'%3B%20%0A%2F%2F%20build%20links%0Aif(vidArr.length%20%3E%201)%20%7B%0A%20%20var%20videos%20%3D%20%5B%5D%3B%0A%20%20for(var%20i%3D1%3B%20i%20%3C%20vidArr.length%3B%20i%2B%2B)%20%7B%20%0A%20%20%20%20var%20ytID%20%3D%20vidArr%5Bi%5D.substr(0%2C11)%3B%0A%20%20%20%20if(videos.indexOf(ytID)%20%3D%3D%20-1)%20%7B%0A%20%20%20%20%20%20var%20link%20%3D%20'http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D'%2BytID%3B%0A%20%20%20%20%20%20links%20%2B%3D%20'%3Ca%20target%3D%22_blank%22%20href%3D%22'%2Blink%2B'%22%3E'%2Blink%2B'%3C%2Fa%3E%3Cbr%2F%3E'%3B%0A%20%20%20%20%20%20playlist%20%2B%3D%20ytID%2B'%3B'%3B%0A%20%20%20%20%20%20videos.push(ytID)%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20playlist%20%3D%20playlist.substr(0%2Cplaylist.length-1)%3B%0A%20%20var%20container%20%3D%20document.createElement('div')%3B%0A%20%20container.innerHTML%20%3D%20'Videos%20Found%3A%3Cbr%2F%3E%3Cbr%2F%3E'%2Blinks%2B'%3Cbr%2F%3E%3Cbr%2F%3E'%2B'%3Ca%20target%3D%22_blank%22%20href%3D%22'%2Bplaylist%2B'%22%3EView%20as%20Playlist%3C%2Fa%3E%3Cbr%2F%3E'%3B%0A%20%20container.style.position%3D'fixed'%3B%0A%20%20container.style.background%3D'%23fff'%3B%0A%20%20container.style.border%3D'5px%20solid%20black'%3B%0A%20%20container.style.padding%3D'10px'%3B%0A%20%20container.style.top%3D'0'%3B%0A%20%20container.style.right%3D'0'%3B%0A%20%20container.style.zIndex%3D'999'%3B%0A%20%20document.body.appendChild(container)%3B%0A%7D%20else%20%7B%0A%20%20alert('No%20YouTube%20videos%20found')%3B%0A%7D}());

February 29, 2012

iPad bug: using CSS transitions on :hover prevents child element from showing

While building a CSS-powered drop-down navigation, I found a little iOS bug that prevented my main nav elements' sub-menus from showing on :hover. Consider the following markup and css, where a rollover on the outer <li> will reveal the inner <ul>.
<header class="admin_nav">
  <ul>
    <li>
      <a href="#" title="Users">
        <span class="label">Users</span>
      </a>
      <ul>
        <li>
          <a href="#" title="Map">Map</a>
        </li>
      </ul>
    </li>
  </ul>
</header>
header.admin_nav ul li ul {
  visibility:hidden;
  opacity:0;
  -webkit-transition: opacity 0.35s linear;
}

header.admin_nav ul li:hover ul {
  visibility:visible;
  opacity:1;
}
On desktop browsers, this is a nice way to fade in a sub-menu on :hover of the main menu item. But, having the -webkit-transition definition breaks the iPad's handling of this nicely-animated menu. Get rid of the transition, and it works just fine, albeit not quite as slickly.

January 5, 2012

Coffeescript: HTML5 <input> placeholder attribute fallback

You want to use the new HTML5 placeholder attribute on input fields, but you also want to support older browsers. Here's a little Coffeescript class that takes an HTML element in the constructor, and uses jQuery to bind the focus and blur events to swap out text like a modern browser would without the script.
# ## HTML5 placeholder feature fallback class
class PlaceholderFallback

  constructor: (el) ->
    @el = $(el)
    @initialize()
  
  # HTML5 <input> placeholder feature detection
  browserHasPlaceholder: =>
    "placeholder" of document.createElement("input")
  
  # Reads the placeholder attribute and uses it in a javascript fallback, if needed
  initialize: =>
    if @browserHasPlaceholder() == false
      placeholderText = @el.attr 'placeholder' 
      @el.removeAttr 'placeholder'
      @el.val(placeholderText)
      @el.focus (e) ->
        if this.value == placeholderText
          this.value = ''
      @el.blur (e) ->
        if this.value == ''
          this.value = placeholderText
    else
      @el = null

# Usage:
placeholderFallback = new PlaceholderFallback( element )