Recreating YouTube’s Ambient Mode Glow Impact — Smashing Journal

0
2


I seen an enthralling impact on YouTube’s video participant whereas utilizing its darkish theme a while in the past. The background across the video would change because the video performed, making a lush glow across the video participant, making an in any other case bland background much more attention-grabbing.

Youtube ambient glow example
Discover the glow impact across the video participant. The CSS format has been edited in-browser to make the impact extra noticeable, which is why the video participant seems a bit off. (Massive preview)

This impact is named Ambient Mode. The characteristic was launched someday in 2022, and YouTube describes it like this:

“Ambient mode makes use of a lighting impact to make watching movies within the Darkish theme extra immersive by casting light colours from the video into your display screen’s background.”
— YouTube

It’s an extremely refined impact, particularly when the video’s colours are darkish and have much less distinction in opposition to the darkish theme’s background.

YouTube video with a purple glow emanating from behind
The background adjustments from prime to backside, matching the present body. The CSS format has been edited in-browser to make the impact extra noticeable, which is why the video participant seems a bit off. (Massive preview)

Curiosity hit me, and I got down to replicate the impact alone. After digging round YouTube’s convoluted DOM tree and supply code in DevTools, I hit an impediment: all of the magic was hidden behind the HTML <canvas> aspect and bundles of mangled and minified JavaScript code.

DevTools inspector with the canvas element highlighted
Even when the supply code didn’t give me an entire reply, trying into the HTML canvas may very well be a great start line. (Massive preview)

Regardless of having little or no to go on, I made a decision to reverse-engineer the code and share my course of for creating an ambient glow across the movies. I choose to maintain issues easy and accessible, so this text gained’t contain difficult colour sampling algorithms, though we are going to make the most of them by way of totally different strategies.

Earlier than we begin writing code, I feel it’s a good suggestion to revisit the HTML Canvas aspect and see why and the way it’s used for this little impact.

HTML Canvas

The HTML <canvas> aspect is a container aspect on which we will draw graphics with JavaScript utilizing its personal Canvas API and WebGL API. Out of the field, a <canvas> is empty — a clean canvas, if you’ll — and the aforementioned Canvas and WebGL APIs are used to fill the <canvas> with content material.

HTML <canvas> is just not restricted to presentation; we will additionally make interactive graphics with them that reply to plain mouse and keyboard occasions.

However SVG may also do most of that stuff, proper? That’s true, however <canvas> is extra performant than SVG as a result of it doesn’t require any extra DOM nodes for drawing paths and shapes the way in which SVG does. Additionally, <canvas> is simple to replace, which makes it very best for extra advanced and performance-heavy use instances, like YouTube’s Ambient Mode.

As you would possibly count on with many HTML components, <canvas> accepts attributes. For instance, we may give our drawing area a width and top:

<canvas width="10" top="6" id="js-canvas"></canvas>

Discover that <canvas> is just not a self-closing tag, like an <iframe> or <img>. We will add content material between the opening and shutting tags, which is rendered solely when the browser can not render the canvas. This will also be helpful for making the aspect extra accessible, which we’ll contact on later.

Returning to the width and top attributes, they outline the <canvas>’s coordinate system. Apparently, we will apply a responsive width utilizing relative models in CSS, however the <canvas> nonetheless respects the set coordinate system. We’re working with pixel graphics right here, so stretching a smaller canvas in a wider container leads to a blurry and pixelated picture.

Comparing the original video frame with the pixelated canvas image
A responsive canvas aspect utilizing a 10×6 pixel coordinate system stretched to 1280px width, leading to a pixelated picture. (Picture supply: Massive Buck Bunny video)(Massive preview)

The draw back of <canvas> is its accessibility. All the content material updates occur in JavaScript within the background because the DOM is just not up to date, so we have to put effort into making it accessible ourselves. One method (of many) is to create a Fallback DOM by putting customary HTML components inside the <canvas>, then manually updating them to mirror the present content material that’s displayed on the canvas.

YouTube ambient glow text
Any content material added to the canvas is just not mirrored within the DOM, together with easy textual content. Every part is hidden behind the canvas. (Massive preview)

Quite a few canvas frameworks — together with ZIM, Konva, and Cloth, to call just a few — are designed for advanced use instances that may simplify the method with a plethora of abstractions and utilities. ZIM’s framework has accessibility options constructed into its interactive elements, which makes growing accessible <canvas>-based experiences a bit simpler.

For this instance, we’ll use the Canvas API. We may even use the aspect for ornamental functions (i.e., it doesn’t introduce any new content material), so we gained’t have to fret about making it accessible, however quite safely disguise the <canvas> from assistive units.

That stated, we are going to nonetheless must disable — or reduce — the impact for individuals who have enabled lowered movement settings on the system or browser degree.

Extra after soar! Proceed studying under ↓

requestAnimationFrame

The <canvas> aspect can deal with the rendering a part of the issue, however we have to by some means maintain the <canvas> in sync with the taking part in <video>and guarantee that the <canvas> updates with every video body. We’ll additionally must cease the sync if the video is paused or has ended.

We might use setInterval in JavaScript and rig it to run at 60fps to match the video’s playback fee, however that method comes with some issues and caveats. Fortunately, there’s a higher means of dealing with a perform that should be known as on so typically.

That’s the place the requestAnimationFrame technique is available in. It instructs the browser to run a perform earlier than the subsequent repaint. That perform runs asynchronously and returns a quantity that represents the request ID. We will then use the ID with the cancelAnimationFrame perform to instruct the browser to cease operating the beforehand scheduled perform.

let requestId;

const loopStart = () => {
  /* ... */
  
  /* Initialize the infinite loop and maintain observe of the requestId */
  requestId = window.requestAnimationFrame(loopStart);
};

const loopCancel = () => {
  window.cancelAnimationFrame(requestId);
  requestId = undefined;
};

Now that we’ve all our bases coated by studying the way to maintain our replace loop and rendering performant, we will begin engaged on the Ambient Mode impact!

The Strategy

Let’s briefly define the steps we’ll take to create this impact.

First, we should render the displayed video body on a canvas and maintain all the things in sync. We’ll render the body onto a smaller canvas (leading to a pixelated picture). When a picture is downscaled, the necessary and most-dominant elements of a picture are preserved at the price of shedding small particulars. By decreasing the picture to a low decision, we’re decreasing it to probably the most dominant colours and particulars, successfully doing one thing much like colour sampling, albeit not as precisely.

Comparing original video with downscaled canvas
Evaluating unique video with downscaled canvas. (Massive preview)

Subsequent, we’ll blur the canvas, which blends the pixelated colours. We’ll place the canvas behind the video utilizing CSS absolute positioning.

Showing the blurred effect in the canvas element
Displaying the blurred impact within the canvas aspect. (Massive preview)

And eventually, we’ll apply extra CSS to make the glow impact a bit extra refined and as near YouTube’s impact as potential.

The blur effect with additional styling
The blur impact with extra styling. (Massive preview)

HTML Markup

First, let’s begin by establishing the markup. We’ll must wrap the <video> and <canvas> components in a mum or dad container as a result of that permits us to comprise absolutely the positioning we will likely be utilizing to place the <canvas> behind the <video>. However extra on that in a second.

Subsequent, we are going to set a hard and fast width and top on the <canvas>, though the aspect will stay responsive. By setting the width and top attributes, we outline the coordinate area in CSS pixels. The video’s body is 1920×720, so we are going to draw a picture that’s 10×6 pixels picture on the canvas. As we’ve seen within the earlier examples, we’ll get a pixelated picture with dominant colours considerably preserved.

<part class="wrapper">
  <video controls muted class="video" id="js-video" src="https://smashingmagazine.com/2023/07/recreating-youtube-ambient-mode-glow-effect/video.mp4"></video>
  <canvas width="10" top="6" aria-hidden="true" class="canvas" id="js-canvas"></canvas>
</part>

Syncing <canvas> And <video>

First, let’s begin by establishing our variables. We’d like the <canvas>’s rendering context to attract on it, so saving it as a variable is helpful, and we will do this by utilizing JavaScript’s getCanvasContext perform. We’ll additionally use a variable known as step to maintain observe of the request ID of the requestAnimationFrame technique.

const video = doc.getElementById("js-video");
const canvas = doc.getElementById("js-canvas");
const ctx = canvas.getContext("2nd");
    
let step; // Hold observe of requestAnimationFrame id

Subsequent, we’ll create the drawing and replace loop capabilities. We will truly draw the present video body on the <canvas> by passing the <video> aspect to the drawImage perform, which takes 4 values similar to the video’s beginning and ending factors within the <canvas> coordinate system, which, if you happen to keep in mind, is mapped to the width and top attributes within the markup. It’s that easy!

const draw = () => {
  ctx.drawImage(video, 0, 0, canvas.width, canvas.top);
};

Now, all we have to do is create the loop that calls the drawImage perform whereas the video is taking part in, in addition to a perform that cancels the loop.

const drawLoop = () => {
  draw();
  step = window.requestAnimationFrame(drawLoop);
};

const drawPause = () => {
  window.cancelAnimationFrame(step);
  step = undefined;
};

And eventually, we have to create two foremost capabilities that arrange and clear occasion listeners on web page load and unload, respectively. These are the entire video occasions we have to cowl:

  • loadeddata: This fires when the primary body of the video hundreds. On this case, we solely want to attract the present body onto the canvas.
  • seeked: This fires when the video finishes looking for and is able to play (i.e., the body has been up to date). On this case, we solely want to attract the present body onto the canvas.
  • play: This fires when the video begins taking part in. We have to begin the loop for this occasion.
  • pause: This fires when the video is paused. We have to cease the loop for this occasion.
  • ended: This fires when the video stops taking part in when it reaches its finish. We have to cease the loop for this occasion.
const init = () => {
  video.addEventListener("loadeddata", draw, false);
  video.addEventListener("seeked", draw, false);
  video.addEventListener("play", drawLoop, false);
  video.addEventListener("pause", drawPause, false);
  video.addEventListener("ended", drawPause, false);
};

const cleanup = () => {
  video.removeEventListener("loadeddata", draw);
  video.removeEventListener("seeked", draw);
  video.removeEventListener("play", drawLoop);
  video.removeEventListener("pause", drawPause);
  video.removeEventListener("ended", drawPause);
};

window.addEventListener("load", init);
window.addEventListener("unload", cleanup);

Let’s take a look at what we’ve achieved to date with the variables, capabilities, and occasion listeners we’ve configured.

See the Pen [Video + Canvas setup – dominant color [forked]](https://codepen.io/smashingmag/pen/gOQKGdN) by Adrian Bece.

See the Pen Video + Canvas setup – dominant colour [forked] by Adrian Bece.

That’s the arduous half! We’ve efficiently set this up in order that <canvas> updates in sync with what the <video> is taking part in. Discover the graceful efficiency!

Blurring And Styling

We will apply the blur() filter to the complete canvas proper after we seize the <canvas> aspect’s rendering context. Alternatively, we might apply blurring on to the <canvas> aspect with CSS, however I need to showcase how comparatively simple the Canvas API is for this.

const video = doc.getElementById("js-video");
const canvas = doc.getElementById("js-canvas");
const ctx = canvas.getContext("2nd");

let step;

/* Blur filter  */
ctx.filter = "blur(1px)";

/* ... */

Now all that’s left to do is add CSS that positions the <canvas> behind the <video>. One other factor we’ll do whereas we’re at it’s apply opacity to the <canvas> to make the glow extra refined, in addition to an inset shadow to the wrapper aspect to melt the sides. I’m deciding on the weather in CSS by their class names.

:root {
  --color-background: rgb(15, 15, 15);
}

* {
  box-sizing: border-box;
}

.wrapper {
  place: relative; /* Incorporates absolutely the positioning */
  box-shadow: inset 0 0 4rem 4.5rem var(--color-background);
}

.video,
.canvas {
  show: block;
  width: 100%;
  top: auto;
  margin: 0;
}

.canvas {
  place: absolute;
  prime: 0;
  left: 0;
  z-index: -1; /* Place the canvas in a decrease stacking degree */
  width: 100%;
  top: 100%;
  opacity: 0.4; /* Delicate transparency */
}

.video {
  padding: 7rem; /* Spacing to disclose the glow */
}

We’ve managed to provide an impact that appears fairly near YouTube’s implementation. The workforce at YouTube most likely went with a totally totally different method, maybe with a customized colour sampling algorithm or by including refined transitions. Both means, this can be a nice outcome that may be additional constructed upon in any case.

The blurred background canvas matches the current video frame
The blurred background canvas matches the present video body. (Massive preview)

Creating A Reusable Class

Let’s make this code reusable by changing it to an ES6 class in order that we will create a brand new occasion for any <video> and <canvas> pairing.

class VideoWithBackground {
  video;
  canvas;
  step;
  ctx;

  constructor(videoId, canvasId) {
    this.video = doc.getElementById(videoId);
    this.canvas = doc.getElementById(canvasId);

    window.addEventListener("load", this.init, false);
    window.addEventListener("unload", this.cleanup, false);
  }

  draw = () => {
    this.ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.top);
  };

  drawLoop = () => {
    this.draw();
    this.step = window.requestAnimationFrame(this.drawLoop);
  };

  drawPause = () => {
    window.cancelAnimationFrame(this.step);
    this.step = undefined;
  };

  init = () => {
    this.ctx = this.canvas.getContext("2nd");
    this.ctx.filter = "blur(1px)";

    this.video.addEventListener("loadeddata", this.draw, false);
    this.video.addEventListener("seeked", this.draw, false);
    this.video.addEventListener("play", this.drawLoop, false);
    this.video.addEventListener("pause", this.drawPause, false);
    this.video.addEventListener("ended", this.drawPause, false);
  };

  cleanup = () => {
    this.video.removeEventListener("loadeddata", this.draw);
    this.video.removeEventListener("seeked", this.draw);
    this.video.removeEventListener("play", this.drawLoop);
    this.video.removeEventListener("pause", this.drawPause);
    this.video.removeEventListener("ended", this.drawPause);
  };
    }

Now, we will create a brand new occasion by passing the id values for the <video> and <canvas> components right into a VideoWithBackground() class:

const el = new VideoWithBackground("js-video", "js-canvas");

Respecting Consumer Preferences

Earlier, we briefly mentioned that we would wish to disable or reduce the impact’s movement for customers preferring lowered movement. We’ve to contemplate that for ornamental thrives like this.

The straightforward means out? We will detect the person’s movement preferences with the prefers-reduced-motion media question and utterly disguise the ornamental canvas if lowered movement is the desire.

@media (prefers-reduced-motion: scale back) {
  .canvas {
    show: none !necessary;
  }
}

One other means we respect lowered movement preferences is to make use of JavaScript’s matchMedia perform to detect the person’s desire and forestall the mandatory occasion listeners from registering.

constructor(videoId, canvasId) {
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: scale back)");

  if (!mediaQuery.matches) {
    this.video = doc.getElementById(videoId);
    this.canvas = doc.getElementById(canvasId);

    window.addEventListener("load", this.init, false);
    window.addEventListener("unload", this.cleanup, false);
  }
}

Remaining Demo

We’ve created a reusable ES6 class that we will use to create new situations. Be at liberty to take a look at and mess around with the finished demo.

See the Pen [Youtube video glow effect – dominant color [forked]](https://codepen.io/smashingmag/pen/ZEmRXPy) by Adrian Bece.

See the Pen Youtube video glow impact – dominant colour [forked] by Adrian Bece.

Creating A React Part

Let’s migrate this code to the React library, as there are key variations within the implementation which can be value understanding if you happen to plan on utilizing this impact in a React undertaking.

Creating A Customized Hook

Let’s begin by making a customized React hook. As an alternative of utilizing the getElementById perform for choosing DOM components, we will entry them with a ref on the useRef hook and assign it to the <canvas> and <video> components.

We’ll additionally attain for the useEffect hook to initialize and clear the occasion listeners to make sure they solely run as soon as the entire mandatory components have mounted.

Our customized hook should return the ref values we have to connect to the <canvas> and <video> components, respectively.

import { useRef, useEffect } from "react";

export const useVideoBackground = () => {
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: scale back)");
  const canvasRef = useRef();
  const videoRef = useRef();
  
  const init = () => {
    const video = videoRef.present;
    const canvas = canvasRef.present;
    let step;
    
    if (mediaQuery.matches) {
      return;
    }
    
    const ctx = canvas.getContext("2nd");
    
    ctx.filter = "blur(1px)";
    
    const draw = () => {
      ctx.drawImage(video, 0, 0, canvas.width, canvas.top);
    };
    
    const drawLoop = () => {
      draw();
      step = window.requestAnimationFrame(drawLoop);
    };
    
    const drawPause = () => {
      window.cancelAnimationFrame(step);
      step = undefined;
    };
    
    // Initialize
    video.addEventListener("loadeddata", draw, false);
    video.addEventListener("seeked", draw, false);
    video.addEventListener("play", drawLoop, false);
    video.addEventListener("pause", drawPause, false);
    video.addEventListener("ended", drawPause, false);
    
    // Run cleanup on unmount occasion
    return () => {
      video.removeEventListener("loadeddata", draw);
      video.removeEventListener("seeked", draw);
      video.removeEventListener("play", drawLoop);
      video.removeEventListener("pause", drawPause);
      video.removeEventListener("ended", drawPause);
    };
  };
  
  useEffect(init, []);
  
  return {
    canvasRef,
    videoRef,
  };
};

Defining The Part

We’ll use comparable markup for the precise part, then name our customized hook and fix the ref values to their respective components. We’ll make the part configurable so we will cross any <video> aspect attribute as a prop, like src, for instance.

import React from "react";
import { useVideoBackground } from "../hooks/useVideoBackground";

import "./VideoWithBackground.css";

export const VideoWithBackground = (props) => {
  const { videoRef, canvasRef } = useVideoBackground();
  
  return (
    <part className="wrapper">
      <video ref={ videoRef } controls className="video" { ...props } />
      <canvas width="10" top="6" aria-hidden="true" className="canvas" ref={ canvasRef } />
    </part>
  );
};

All that’s left to do is to name the part and cross the video URL to it as a prop.

import { VideoWithBackground } from "../elements/VideoWithBackground";

perform App() {
  return (
    <VideoWithBackground src="http://commondatastorage.googleapis.com/gtv-videos-bucket/pattern/BigBuckBunny.mp4" />
  );
}

export default App;

Conclusion

We mixed the HTML <canvas> aspect and the corresponding Canvas API with JavaScript’s requestAnimationFrame technique to create the identical charming — however performance-intensive — visible impact that makes YouTube’s Ambient Mode characteristic. We discovered a means to attract the present <video> body on the <canvas>, maintain the 2 components in sync, and place them in order that the blurred <canvas> sits correctly behind the <video>.

We coated just a few different issues within the course of. For instance, we established the <canvas> as an ornamental picture that may be eliminated or hidden when a person’s system is about to a lowered movement desire. Additional, we thought of the maintainability of our work by establishing it as a reusable ES6 class that can be utilized so as to add extra situations on a web page. Lastly, we transformed the impact right into a part that can be utilized in a React undertaking.

Be at liberty to mess around with the completed demo. I encourage you to proceed constructing on prime of it and share your outcomes with me within the feedback, or, equally, you possibly can attain out to me on Twitter. I’d love to listen to your ideas and see what you can also make out of it!

References

Smashing Editorial
(gg, yk, il)



LEAVE A REPLY

Please enter your comment!
Please enter your name here