Welcome to this new tutorial. I did it using vanilla JS because it's more simple to adapt it to various framework. I did it previously on Svelte, but obviously is possible also on React, Angular, Vue, etc. with some little modification.
What is the effect?
The effect we want is the light turning on from the place where we have the light switch, it's also responsive.
In this example, switch is only a checkbox, but as you can see below you can use something prettier, like a svg bulb.
Let's start!
I assume you already know basics of HTML, CSS and JS. We are going fast on simple declarations, concentrating on Canvas functions.
HTML
<html>
<body>
<canvas id="canvas-bg"></canvas>
<main id="main">
<h1 id="title" class="sans-serif">Changing color title</h1>
<div class="sans-serif"><input type="checkbox" id="switch" onclick="handleClick(event)"/>Switch Bulb</div>
</main>
</body>
</html>
HTML code is pretty simple, we are declaring 2 main containers, the canvas (background) and the main with the page content.
We are declaring a checkbox which now will do the light switch, you can also use something else.
CSS
:root {
--dark-color: #040020;
--light-color: #fff;
}
body {
background-color: var(--dark-color);
margin: 0;
}
main {
display: flex;
justify-content: center;
padding: 10px;
}
#canvas-bg {
position: fixed;
z-index: -1;
width: 100%;
height: 100%;
}
.sans-serif {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
h1 {
transition: all .5s;
}
.dark *{
color: var(--dark-color);
}
.light *{
color: var(--light-color);
}
CSS is simple, too. Let's concentrate on colors, I created dark and light classes, which give the color to all the children.
The #canvas-bg which is the canvas object, is positioned fixed, which is relative to viewport, so it will remain there also if scrolling. Z-index is used to put the background behind the other elements.
Javascript
Ok, now we are seeing Javascript in pieces, to explain what I did and why.
const dark_color = "#040020", light_color = "#fff";
let animationCount = 0;
const speed = 10;
const clickPosition = {x: 0, y: 0};
const switchBulb = document.getElementById("switch");
let lightOn = switchBulb.checked;
let canvas = document.getElementById("canvas-bg");
let ctx = canvas.getContext("2d");
We are declaring some variables, starting from colors. Then we will find out what are animationCount, speed and clickPosition variables.
We are binding switchBulb element with the checkbox and lightOn will be the value which is telling us if the light is on or off.
After that we declare the canvas and we get the context from it.
Now let's go to functions.
handleClick(e)
function handleClick(e) {
lightOn = switchBulb.checked;
clickPosition.x = e.x;
clickPosition.y = e.y;
if(lightOn) turnOn();
else turnOff();
changeContent();
}
What do we do here? We are handling the click. So first we are assigning to lightOn the value of checkbox.
Then we get from the event what is the click position based on the document, so we set it in the object we created before. That will be the starting point of the animation.
Then we call one of the two functions, if lights is set to on we obviously call turnOn and viceversa.
After that we call the changeContent function, which is explained below.
resizeCanvas()
function resizeCanvas(){
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
ctx.fillStyle = lightOn ? light_color : dark_color;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
};
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
What are we doing here? This is the function dedicated to responsive design.
We are resizing the width and height of canvas and filling it with a rect with the same size for background with the correct color, based on light state.
Then we call the function to adapt at first time the canvas and adding the listener on windows resize.
changeContent()
function changeContent(){
let main = document.getElementById("main");
main.classList.add(lightOn ? "dark" : "light");
main.classList.remove(lightOn ? "light" : "dark");
}
changeContent();
This is simple, we are adding the class we created before, to change the content color, based on light state.
We are also calling it for the first time to adapt the content color.
In frameworks, this and other functions are useless, because you can set class directly on html based on js vars.
turnOn()
Ok, this is the start of "difficult" part. Let's see the functions which turn on the lights divided into parts.
What do we need? We need to create a circle, starting from zero pixels, till the maximum size. What will be the maximum size? It's calculated, we will see how.
function turnOn() {
if(animationCount === 0) switchBulb.disabled = true;
let pixelRadius = animationCount * speed;
If we are at the start of animation, we are disabling the switch, to prevent some bugs on turning on and off.
Then we are calculating the radius of the circle in pixels, which will be the animationCount (starting from zero) multiplied by speed, which is a default multiplier specified at the start.
ctx.fillStyle = dark_color;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.beginPath();
ctx.arc(clickPosition.x, clickPosition.y, pixelRadius, 0, 2 * Math.PI, true);
ctx.fillStyle = light_color;
ctx.fill();
What are we doing here? We are drawing. Starting with filling with dark background color the canvas.
Then we are drawing the circle, starting from click position, with the pixel radius declared before, we fill it and we confirm the drawing. This is the first step of the animation, then?
animationCount++;
if(pixelRadius < (Math.sqrt(Math.pow(ctx.canvas.width,2) + Math.pow(ctx.canvas.height,2))+ 200)){
setTimeout(turnOn, 1);
} else {
animationCount = 0;
switchBulb.disabled = false;
}
}
This part is important, we are raising animationCount value by one.
Then we are checking if pixelRadius is at the size we want the animation ends. What is it? That is Pythagorean theorem to calculate the diagonal between screen width and height. Then we add 200px to be sure the circle is out of the screen.
So, if the circle reached the end, the animationCount returns to zero and switch is enabled, otherwise, this function will be relaunched asynchronously in 1 ms.
turnOff()
Ok, this is the last important function.
What do we need for the turnOff function? We need that the light circle, starts from maximum size and goes to zero, to switch off the light.
function turnOff() {
let pixelRadius = animationCount * speed;
if(animationCount === 0) {
switchBulb.disabled = true;
pixelRadius = (Math.sqrt(Math.pow(ctx.canvas.width,2) + Math.pow(ctx.canvas.height,2))+ 200);
animationCount = Math.ceil(pixelRadius / speed);
}
We are declaring the pixelRadius, like before. Obviously it can't works with animationCount to zero, so we check it.
If animationCount is zero, we are disabling the switch and we are calculating the maximum size of the circle like on the function above. After the calculation we divide it by speed to get the starting animationCount value.
ctx.fillStyle = dark_color;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.beginPath();
ctx.arc(clickPosition.x, clickPosition.y, pixelRadius, 0, 2 * Math.PI, true);
ctx.fillStyle = light_color;
ctx.fill();
Like on the turnOn function, we are setting the background in dark color, and we are creating the circle with pixelRadius size.
animationCount--;
if(animationCount > 0) setTimeout(turnOff, 1);
else {
ctx.fillStyle = dark_color;
ctx.fillRect(0, 0, ctx.canvas.width, canvas.height);
switchBulb.disabled = false;
}
So we are lowering animationCount value, then we check, if it is greater than zero, we relaunch the function asynchronously in 1 ms.
If animationCount is zero or less, we are filling the background just to be sure, and then we are enabling the switch checkbox.
Conclusion
I hope this could be useful for you. I know I could put turnOn and turnOff together in some way, but now it's easier to explain.
If you are using it in some project let me know.
You can find the complete code here:
CodePen
You can find me on:
igorzanella.dev
hello@igorzanella.dev
Twitter
Github
Top comments (2)
Wow, man! Nice job!
Thank you!😉