Skip to content

Instantly share code, notes, and snippets.

@rmdort
Forked from Integralist/Description.md
Created August 7, 2013 17:57
Show Gist options
  • Save rmdort/6176656 to your computer and use it in GitHub Desktop.
Save rmdort/6176656 to your computer and use it in GitHub Desktop.

The BBC has a server-side image service which provides developers with multiple sized versions of any image they request. It works in a similar fashion to http://placehold.it/ but it also handles the image ratios returned (where as placehold.it doesn't).

The original BBC News process (and my re-working of the script) follows roughly these steps...

  • Create new instance of ImageEnhancer
  • Change any divs within the page (which have a class of delayed-image-load) into a transparent GIF using a Base64 encoded string.
    • We set the width & height HTML attributes of the image to the required size
    • We know what size the image needs to be because each div has custom data-attr set server-side to the size of the image
    • We then set a class of image-replace onto each newly created transparent image
  • We use a 250ms setTimeout to unblock the UI thread and which calls a function resizeImages which enhances the image-replace images so their source is now set to a URL where the returned image matches the requested dimensions in the URL path (in my example below I use requestAnimationFrame instead)
    • We also create an event listener for the resize event which fires a pubsub event of imageEnhancer:resize
  • We call resizeImages initially and then every time the pubsub event fires.
    • This function loops each image-replace image and changes the src attribute to the new URL with the best possible match dimensions specified within the URL path (based on the dimensions required for the image).
    • If the resize event fires then we check the dimensions of the page and determine if we need a new URL to be used.

The example code that follows is not production ready, you can't just drop it into your code base and expect it to work. It is a rough prototype I built in about 30 minutes to help demonstrate in a roundabout way how the BBC News responsive team were handling the enhancement of images. The code uses features only available to more modern browsers (e.g. I've tested it in Chrome 28 and nothing else so your mile may vary if you're using a different browser to look at the example).

I've also used the http://placehold.it/ service in place of the actual BBC News image provider. They don't work in quite the same way, but if you open the following HTML code in a modern browser (e.g. Chrome) and resize the browser window downwards you'll see the images change based on the current window dimensions. So rather than load a large image for smaller screens we swap out our images with images that are optimised for that screen size.

Notes about the rewritten example:

  • I should use live nodeList otherwise we'll miss any images added to the DOM after ImageEnhancer is initialised.
  • I might be able to work-around the use of the base64 image
  • Look at caching of image loads
<!doctype html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ImageEnhancer</title>
<style>
img {
max-width: 100%;
}
a {
display: block;
margin-top: 1em;
}
</style>
</head>
<body>
<p>Here is our main image, we load this regardless of JavaScript support.</p>
<img src="http://placehold.it/340">
<p>Below are three <code>div</code> elements that will lazy-load more images (as long as JavaScript is enabled).</p>
<div class="delayed-image-load" data-src="http://placehold.it/340" data-width="340"></div>
<div class="delayed-image-load" data-src="http://placehold.it/340" data-width="340"></div>
<div class="delayed-image-load" data-src="http://placehold.it/340" data-width="340"></div>
<script>
var pubsub = (function(){
var doc = document;
var topics = {};
var id = -1;
var pubsub = {};
pubsub.subscribe = function (topic, fn) {
if (!topics[topic]) {
topics[topic] = [];
}
var token = (++id).toString();
topics[topic].push({
token: token,
fn: fn
});
return token;
};
pubsub.unsubscribe = function (token) {
for (var m in topics) {
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) {
if (topics[m][i].token === token) {
topics[m].splice(i, 1);
return token;
}
}
}
}
return false;
};
pubsub.publish = function (topic, data) {
if (!topics[topic]) {
return false;
}
setTimeout(function(){
var subscribers = topics[topic],
len = topics[topic].length;
while (len--) {
subscribers[len].fn(topic, data);
}
}, 0);
return true;
};
return pubsub;
}());
function ImageEnhancer(){
pubsub.subscribe('imageEnhancer:resize', this.resizeImages.bind(this));
pubsub.subscribe('div:added', this.changeDivsToEmptyImages.bind(this));
pubsub.subscribe('div:changed', this.resizeImages.bind(this));
this.availableWidthsFromOurImageProviderService = [96, 130, 165, 200, 235, 270, 304, 340, 375, 410, 445, 485, 520, 555, 590, 625, 660, 695, 736];
this.divs = document.getElementsByClassName('delayed-image-load'); // we use `getElementsByClassName` so we get a live NodeList
this.changeDivsToEmptyImages();
window.requestAnimationFrame(this.init.bind(this));
}
ImageEnhancer.prototype = {
changeDivsToEmptyImages: function(){
var i = this.divs.length;
while (i--) {
var div = this.divs[i],
img = document.createElement('img');
img.src = 'data:image/gif;base64,R0lGODlhEAAJAIAAAP///wAAACH5BAEAAAAALAAAAAAQAAkAAAIKhI+py+0Po5yUFQA7';
img.className = 'image-replace';
img.width = div.getAttribute('data-width');
img.setAttribute('data-src', div.getAttribute('data-src'));
div.parentNode.replaceChild(img, div);
}
if (this.initialised) {
pubsub.publish('div:changed');
}
},
init: function(){
this.initialised = true;
this.resizeImages();
window.addEventListener('resize', function() {
pubsub.publish('imageEnhancer:resize');
}, false);
},
resizeImages: function(){
var imageList = Array.prototype.slice.call(document.querySelectorAll('.image-replace'));
if (!this.isResizing) {
this.isResizing = true;
imageList.forEach(function (img) {
img.src = this.calculateNewImageSrc(img);
}.bind(this));
this.isResizing = false;
}
},
calculateNewImageSrc: function (img) {
var imageSrc = img.getAttribute('data-src'),
imageWidth = img.clientWidth,
selectedWidth = this.availableWidthsFromOurImageProviderService[0];
this.availableWidthsFromOurImageProviderService.forEach(function (currentlyAvailableWidth, index) {
if (imageWidth > currentlyAvailableWidth) {
selectedWidth = this.availableWidthsFromOurImageProviderService[index + 1];
}
}.bind(this));
return imageSrc.replace(/^(.+\/)\d+$/i, function (match, captured) {
return captured + selectedWidth;
});
}
};
function createAnchor(){
var anchor = document.createElement('a');
anchor.href = '#my_new_element';
anchor.innerHTML = 'Click me to add a new image to the DOM after ImageEnhancer has already been instantiated'
document.body.appendChild(anchor);
anchor.onclick = createNewImage;
}
function createNewImage(){
var div = document.createElement('div');
div.className = 'delayed-image-load';
div.setAttribute('name', 'my_new_element');
div.setAttribute('data-src', 'http://placehold.it/340');
div.setAttribute('data-width', '340');
document.body.appendChild(div);
pubsub.publish('div:added');
}
createAnchor();
new ImageEnhancer();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment