Before we start...
When building this website, I found that the page of articles
always leaves a lot of
blank space while loading those image covers.
It's very boring and crude to leave those spaces,
so I was thinking about adding some interesting effects while waiting for the loading.
I came up with the blur effect immediately,
cause I have seen a similar effect in Nextjs <Image>
.
So, I decided to apply the same effect to my article's covers.
After hours of searching and coding, I finally achieved this effect.
Briefly, there will be two layers:
- The blur image layer, which could be loaded immediately
- The image layer, which might take some time to download
The blur layer's opacity is 100 while downloading the image, As long as the image is completely downloaded, the blur layer's opacity will transit to 0 and reveal the image. You could hover over the picture below👇, it shows what I mean
Now, let's build this <ImageBg>
with blur effect step by step!
1. A div with the image background
At the first, create a new component called ImageBg.vue
.
It will contain a blur layer and an image layer.
Then let's build a div
with a simple image background in ImageBg.vue
.
<template>
<div class="container">
</div>
</template>
<style scoped>
.container {
position: relative;
width: 300px;
height: 200px;
background-image: url(/img/article_cover/blur_cover.jpg);
}
</style>
Now, you will have a div, whose width is 300px and height is 200px, with an image background.
But there is a little bug, the image is not fully displayed. It keeps the original size and only shows its left top corner. Let's fix this by adding some background image settings.
2. Center and resize the background image
<template>
...
</template>
<style scoped>
.container {
...
background-image: url(/img/article_cover/blur_cover.jpg);
background-position: center;
background-size: cover;
}
</style>
Add some settings in CSS
, and we could see the image is displayed properly.
- background-position: means the relative position between the background image and div. We pass 'center' to it means that the image would be placed at the center of the div.
- background-size: 'cover' means the image will try to fill the whole div and keep its aspect ratio. The image won't be deformed.
Now, the image layer is fully prepared. In the next step, we will learn how to generate a blurred image and make it a component.
3. Blur an image with Blurhash
Blurhash provides an algorithm to convert an image to a <canvas>
.
The process of an image transfer to a string is called 'encode',
and the process of a string converting to an image is called 'decode'.
Now, please upload one image you like and keep the
LIA_9M%3L~-.yYRO8{OGuPX8ITo#
Woltapp, the creator of the Blurhash, has provided the SDK in many languages, with those tools, you could automate the 'encode' process easily. But in this tutorial, we would just simply drag the image to its website, and get the Blurhash string.
To decode this string we need to use Blurhash's SDK in JS language. Let's install it.
# use npm
npm install blurhash
# or use yarn
yarn add blurhash
4. Build the skeleton of <ImageBlur>
Firstly, we will create a new component called <ImageBlur>
only has the skeleton.
<template>
<div class="wrapper">
<canvas class="canvas" width=32 height=32 />
</div>
</template>
<script setup lang='ts'>
</script>
<style scoped>
.wrapper {
position: relative;
height: 0;
}
.canvas {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
z-index: 10;
}
</style>
Here are only HTML and CSS, and it won't display anything, so don't be worried. Let's break it down little by little.
In <template>
, there are two elements nested.
The outer is wrapper
, it is a div
without any height
because we will use padding-bottom
to control its verticle space occupation in the next step.
Also position: relative
tells the child element that
their position should be relative to the left-top corner of the wrapper
.
Here is a useful link that tells how the position
works: mdn-css-postion
The child element, canvas
, is where we draw the pixels.
Its CSS attribute, position: absolute
, tells that it will be placed according to its parent element,
which has the position: relative
attribute, in our case, it would be wrapper
.
The following top
, bottom
, left
, and right
tells
the canvas would be fixed at the left-top corner of the wrapper.
Finally, to make the canvas a cover, we use z-index
to set it at the front.
5. Finish <ImageBlur>
component
In this step, we will define the properties of the component, so that we don't need to hardcode any Blurhash string in our component. Also, we will learn how to decode a Blurhash string and draw it on the canvas. Here is the code.
<template>
<div class="wrapper" :style="`padding-bottom: ${props.aspectRatio}%`">
<canvas class="canvas" ref="canvas" width=32 height=32 />
</div>
</template>
<script setup lang='ts'>
import { decode } from "blurhash"
// Define the props of this component
// hash: the blurhash string from Blurhash, with default value "LVKmwwp{krRj8_xsg4Six]xtWCt6"
// aspectRatio: height/width * 100
const props = defineProps({
hash: { type: String, default: "LIA_9M%3L~-.yYRO8{OGuPX8ITo#" },
aspectRatio: { type: Number, default: 56.25 },
})
// use ref to get the canvase element
const canvas = ref<HTMLCanvasElement | null>(null)
const pixels = decode(props.hash, 32, 32)
onMounted(() => {
if (!canvas.value) {
// ensure to get the ref of the canvase element
console.log("Canvase is not ready")
return;
}
const ctx = canvas.value.getContext("2d")
if (ctx) {
// ensure ctx is not null
const imageData = ctx.createImageData(32, 32)
imageData.data.set(pixels)
ctx.putImageData(imageData, 0, 0)
}
})
</script>
<style scoped>
/* ... */
</style>
Let's have a look at the script part.
We import the decode
function for decoding the hash to pixels.
Then we define two props hash
and aspectRatio
. The details are written in the comment.
We use ref
to get the DOM element in HTML, just like getElementById()
in Vanilla JS.
After that, we use the decode function and store the pixel information in pixels
.
As long as the component is mounted, we draw the pixel on the canvas.
Place it in a div and we got this.
<template>
<div style="width: 300px; height: 200px;">
<ImageBlur>
</div>
</template>
6. Composition
Now, let's put everything together!
<template>
<div ref="wrapper" class="container">
<ImageBlur class="blur" :hash="props.hash" :style="{ opacity: onLoaded ? 0 : 100 }" />
</div>
</template>
<script setup lang='ts'>
const props = defineProps<{
src: string
hash: string
}>()
const wrapper = ref(null)
const onLoaded = ref(false)
onMounted(() => {
// Load the bg-image as Image firstly
const bgImage = new Image()
bgImage.src = props.src
// when image full loaded, pass it to bg-image
bgImage.onload = () => {
wrapper.value.style.backgroundImage = `url(${props.src})`
onLoaded.value = true
}
})
</script>
<style scoped>
.container {
/** ... */
}
.blur {
min-width: 100%;
min-height: 100%;
object-fit: cover;
transition: opacity 500ms;
}
</style>
To control the opacity of the blur layer, we define a variable, onLoaded
, with an initialized value of false
,
which means the image is not loaded at the beginning.
After the image is loaded, it will change to true
,
as we defined this action in bgImage.onload()
callback function.
In the style definition of <ImageBlur>
, we use the Conditional operator
to manipulate the opacity.
Furthermore, we could define the opacity transition in 500ms, to have a smooth transition from blur to clear.
Finally, you have your ImageBg with blur effect. Voila 🎉
Thanks for reading!
border-radius
and overflow
to the container's CSS<template>
<!-- ... -->
</template>
<script setup lang='ts'>...</script>
<style scoped>
.container {
/* ... */
border-radius: 2rem;
overflow: hidden;
}
.blur {
/* ... */
}
</style>