Introducing the Stash Avatar Picker

Recently in Stash we added Project Avatars. We wanted to have a really slick user experience for uploading, translating and cropping images to be used as avatars, so we built the Avatar Picker.

Our goal was to do as much as on the client side as possible, which in the case of modern browsers, is quite a lot. One of the decisions that allowed this to happen was to drop IE8 support for this feature, and to use a fallback in IE9 which allowed most of the functionality to be done client side and only using the server where absolutely necessary.

A key architectural decision was to build this feature out of a number of smaller components, which would allow us to combine them in different ways depending on browser support and to be extensible in the future (for example we could add an AJAX file upload component which utilises the existing drag and drop component to allow drag and drop attachments to pull request comments)

The component that binds all the other components together is ImageUploadAndCrop. Generally you will be dealing directly with it rather than with the other components. This is the point of configuration and the attachment points for handlers for the workflow of selecting an image. In Stash we have another component, the AvatarPickerDialog that provides the avatar specific configuration, triggers a dialog containing an ImageUploadAndCrop and handles the cropped avatar. The returned dataURI is added to a form POST which is sent to the server. On the server the dataURI is converted to an image file and saved to file system.

Lets take a look at some of the other components in more detail.

You can check out the source for these components at https://bitbucket.org/atlassianlabs/avatar-picker/src and there is a working demo hosted at http://atlassianlabs.bitbucket.org/avatar-picker

Many of the examples below are interactive demos that you can drag and drop images into and play around with the zooming and panning features of the image explorer.

DragDropFileTarget and UploadInterceptor

These take care of the “adding a file” aspect of choosing an avatar. When a user drops a file onto the target, or selects a file from their filesystem, these intercept those events and pass the selection on to one of the ClientFileHandlers for processing.

Live Demo
[iframe src=”http://atlassianlabs.bitbucket.org/avatar-picker/demo/drag-drop.html” width=”330″ height=”330″ style=”display: block; margin: -20px auto 0 auto;”]

ClientFileHandler, ClientFileReader, ClientIframeUploader

ClientFileHandler is the base component, it takes a collection of Files, filters them by any supplied criteria (file type, size and count) and passes them on to a callback. ClientFileReader extends ClientFileHandler to use the HTML5 FileReader to read the file data into something that can be used in the browser (by default it’s as a dataURI). ClientIframeUploader doesn’t extend ClientFileHandler, it replaces it with a fallback that uses a hidden iframe to post the selected image to the server and pass the location of the uploaded file to the callback.
If we wanted to implement the drag and drop attachment uploader mentioned above, we’d be able to create a ClientFileAJAXUploader that extended ClientFileHandler and turned Files into FormData that could be submitted via AJAX to the server (in modern browsers supporting XHR2)

ImageExplorer

Once we have the image as either a client side dataURI or as a link to image hosted on the remote server, we can make it the source for the ImageExplorer, which handles masking, drag panning and zooming the image.

Live Demo
[iframe src=”http://atlassianlabs.bitbucket.org/avatar-picker/demo/image-explorer.html” width=”330″ height=”370″ style=”display: block; margin: -20px auto 0 auto;”]

There’s a couple neat tricks at work in the ImageExplorer. Firstly the mask is done with CSS only (no images). It’s done by adding a very large box-shadow with no blur or spread and with an RGBA colour with an alpha channel set to < 1 to create the shadow. The image-explorer-container div has overflow: hidden so the visible area of the mask and the panned image are always contained within it.
You can see the code for this below (we use LESS with some mixins that are hopefully self explanatory)

image-explorer.less:86

[cc lang=’css’ line_numbers=’true’ escaped=’true’]

.image-explorer-mask {
.box-shadow(0 0 0 1000px rgba(0,0,0,0.5));
.centered(@mask-size);

&.circle-mask {
.circle(@mask-size);
}

&.square-mask {
.square(@mask-size);
}

&.rounded-square-mask {
.square(@mask-size);
.border-radius(5px);
}
}

[/cc]

As you can see, this makes it trivial to support different shaped masks and different shadow colours and levels of opacity.

ImageExplorer does some calculations based on the natural image size to dynamically set the slider scale. It has a number of supported scale modes. The upper bound is always 100% of the natural image size which can be overriden by setting the scaleMax option.

Exit mobile version