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.