Introduction
Youtube recently launched a new feature called Ambient Mode, it uses a lighting effect to make watching videos in Dark theme more immersive, by casting gentle colors from the video, into your screen’s background.
Youtube has made the switch to a darker shade of black for its dark theme in order to optimize the Ambient Mode effect for viewers.
open movie - Spring by blender studios.
How it works
- A canvas element is created and appended to the DOM
- The canvas element is resized to the size of the video and placed behind the video.
- The video is drawn on the canvas
- The canvas is blurred
Basic component setup
// AmbientVideo.tsx
import React, { useState, useRef } from 'react'
const AmbientVideo = () => {
const [isPlaying, setIsplaying] = useState(false)
const videoRef = useRef(null)
const canvasRef = useRef(null)
const ctx = canvasRef.current?.getContext('2d')
return (
<div id='cinematics' className='aspect-video relative inline-block '>
<canvas
ref={canvasRef}
id='canvas'
style={{
width: `${videoRef.current?.clientWidth}px`,
height: `${videoRef.current?.clientHeight}px`,
}}
className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 scale-110 opacity-80 blur-[40px]'
></canvas>
<video
poster='/assets/spring-poster.jpg'
preload='metadata'
ref={videoRef}
id='video'
src='/assets/spring.mp4'
className='relative h-full w-auto rounded-lg object-cover focus:outline-none'
controls
></video>
</div>
)
}Drawing the video on the canvas
useInterval hook by Dan Abramov is used to efficiently draw the video on to canvas. Read more about useInterval hook
//UseInterval hook
import React, { useState, useEffect, useRef } from 'react'
function useInterval(callback, delay) {
const savedCallback = useRef()
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current()
}
if (delay !== null) {
let id = setInterval(tick, delay)
return () => clearInterval(id)
}
}, [delay])
}// AmbientVideo.tsx
React.useEffect(() => {
const videoInstance = videoRef.current
// Add event listeners to the video element to update the isPlaying state, so that the canvas is only drawn when the video is playing.
videoInstance?.addEventListener('play', () => setIsplaying(true))
videoInstance?.addEventListener('ended', () => setIsplaying(false))
videoInstance?.addEventListener('pause', () => setIsplaying(false))
return () => {
videoInstance?.removeEventListener('play', () => setIsplaying(true))
videoInstance?.removeEventListener('ended', () => setIsplaying(false))
videoInstance?.removeEventListener('pause', () => setIsplaying(false))
}
}, [])
// useInterval hook
useInterval(
() => {
if (!videoRef.current || !canvasRef.current) {
return
}
ctx?.drawImage(videoRef.current, 0, 0, canvasRef.current?.width, canvasRef.current?.height)
},
isPlaying ? 1000 / 15 : null // runs at 15fps when video is playing, and stops when video is paused
)Stackblitz demo
Here is a demo of the component in action, you can play around with the code in the editor.
Next Steps
Goal of this project was just to implement a basic version of the feature, there are a lot of improvements that can be made to make it more performant and user friendly. Here are some ideas that I have in mind.
- Youtube generates ambient mode effects less frequently, as drawing onto canvas is a very expensive operation, they might even store the generated ambient mode images in the database and serve them when needed.
- Add a button to toggle the ambient mode on and off
- Adjust the canvas size on window resize
Share your thoughts or questions with me on my social media handles, I would love to hear from you.