Medium-like Image Loading with Vue.js (part 2)
Louis Zawadzki5 min read
Quick summary of part 1
I’m quite fond of the way Medium displays its images while they’re loading.
At first they display a grey placeholder, then displays a small version of the image - something like 27x17 pixels.
The trick is that most browsers will blur a small image if it is streched out.
Finally, when the full-size image is downloaded, it replaces the small one. You can see a live demo of what I had done on this Codepen.
In this post I intend to make a component that is as close as possible to what Medium actually does, as it is explained on this excellent post by José M. Perez.
And I have also switched from Vue 1 to Vue 2 ;)
Adding a placeholder behind the images
Let’s add the first element which we wait for the images to be loaded: a grey placeholder.
In our template we have 3 elements:
- a grey placeholder that will be shown while the low-resolution version of the image is not loaded yet
- a low-resolution image that will be shown streched out while the high-resolution is not loaded yet
- a high-resolution image that will be displayed to the user
The javascript logic will set the sources of the images and display the right element depending on which images are already loaded.
To load the images we’ll use the mounted
function of the component.
To set the “state” of the component, we’ll use a data called currentSrc
that will be initialized to null
and will take the value of the source of the image that should be displayed.
This far, the Vue component should look like this:
<template>
<div v-show="currentSrc === null" class="placeholder"></div>
<img v-show="currentSrc === hiResSrc" :src="lowResSrc"></img>
<img v-show="currentSrc === hiResSrc" :src="hiResSrc"></img>
</template>
<style scoped>
img, .placeholder {
height: 600px;
width: 900px;
position: absolute;
}
.placeholder {
background-color: rgba(0,0,0,.05);
}
</style>
<script>
export default {
props: [
'hiResSrc',
'loResSrc'
],
data: function() {
return {
currentSrc: null // setting the attribute to null to display the placeholder
}
},
mounted: function () {
var loResImg, hiResImg, that, context;
loResImg = new Image();
hiResImg = new Image();
that = this;
loResImg.onload = function(){
that.currentSrc = that.loResSrc; // setting the attribute to loResSrc to display the lo-res image
}
hiResImg.onload = function(){
that.currentSrc = that.hiResSrc; // setting the attribute to hiResSrc to display the hi-res image
}
loResImg.src = that.loResSrc; // loading the lo-res image
hiResImg.src = that.hiResSrc; // loading the hi-res image
}
}
</script>
Adding transitions
Then we need to add some transitions when the value of currentSrc
changes.
To be more accurate, we want to fade-in/out every element as they appear/disappear.
Vue.js lets you handle CSS transitions in a pretty easy way by adding and removing classes.
As we have multiple elements to transition between we have to use a transition group:
<template>
<transition-group name="blur" tag="div">
<div v-show="currentSrc === null" key="placeholder" class="placeholder blur-transition"></div>
<img v-show="currentSrc === loResSrc" :src="loResSrc" key="lo-res" class="blur-transition"></canvas>
<img v-show="currentSrc === hiResSrc" :src="hiResSrc" key="hi-res" class="blur-transition"></img>
</transition-group>
</template>
Here is how Vue.js handles the transition when the value of currentSrc
changes from null
to loResSrc
:
- the ‘blur-leave’ class is added to the placeholder, thus triggering the transition for the placeholder
- the ‘blur-enter’ class is added to the low resolution image
- the placeholder is hidden and the image is shown
- on the next frame, the ‘blur-enter’ and ‘blur-leave’ classes are removed, thus triggering the transition for the image
Knowing this we can make the following changes in our style:
<style scoped>
img, .placeholder {
height: 600px;
width: 900px;
position: absolute;
}
.placeholder {
background-color: rgba(0,0,0,.05);
}
.blur-transition {
transition: opacity linear .4s 0s;
opacity: 1;
}
.blur-enter, .blur-leave {
opacity: 0;
}
</style>
That way the images and placeholders will fade in and out when they appear and disappear.
You can see a live demo here: http://codepen.io/zkilo/pen/wgdxWq.
Well, it looks good but there is a slight difference with what it actually looks like on Medium.
Can you spot it?
Using canvas
If you’ve looked well at the previous Codepen you may have found out a little issue.
When we change the opacity of the low resolution image, we can see the ugly pixels because browsers aren’t able to blur the image while its opacity changes.
But don’t worry, there’s an easy way to solve this!
We’re going to use canvas, because browsers can actually blur canvas while their opacity changes with a little trick!
So, let’s change our template:
<template>
<transition-group name="blur" tag="div">
<div v-show="currentSrc === null" class="placeholder blur-transition" key="placeholder"></div>
<canvas v-show="currentSrc === loResSrc" height="17" width="27" key="canvas" class="blur-transition"></canvas>
<img v-show="currentSrc === hiResSrc" :src="hiResSrc" key="image" class="blur-transition"></img>
</transition-group>
</template>
And our style:
<style scoped>
img, canvas, .placeholder {
height: 600px;
width: 900px;
position: absolute;
}
.placeholder {
background-color: rgba(0,0,0,.05);
}
canvas {
filter: blur(10px);
}
.blur-transition {
transition: opacity linear .4s 0s;
opacity: 1;
}
.blur-enter, .blur-leave {
opacity: 0;
}
</style>
You can see that we’ve set the height
and weight
attributes of our canvas in the template and streched it out in our style.
You might also have spotted the little trick: we can add a blur filter attribute on the canvas and it will still be here even if the opacity of our element changes!
I’ve set it to 10px empirically, but you can learn more about the canvas filter here.
Once we’ve done this, we need to draw our low resolution image inside the canvas once it is loaded:
<script>
export default {
props: [
'hiResSrc',
'loResSrc'
],
data: function() {
return {
currentSrc: null
}
},
mounted: function () {
var loResImg, hiResImg, that, context;
loResImg = new Image();
hiResImg = new Image();
that = this;
context = this.$el.getElementsByTagName('canvas')[0].getContext('2d'); // get the context of the canvas
loResImg.onload = function(){
context.drawImage(loResImg, 0, 0);
that.currentSrc = that.loResSrc;
}
hiResImg.onload = function(){
that.currentSrc = that.hiResSrc;
}
loResImg.src = that.loResSrc;
hiResImg.src = that.hiResSrc;
}
}
</script>
You can see the final result on this Codepen: http://codepen.io/zkilo/pen/ZLyweL.
And that’s it!
You can find the component as a .vue file on this Github repository.