Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fade canvas video from greyscale to color

I have 2 elements - video and canvas. On video play event, a functions draws the same video on canvas only greyscale. Then I have a button which is supposed to fade canvas video from greyscale back to color. So far I've managed to get back colors on button click, but I need it to fade - from greyscale to color, not just instantly show color.

Any ideas on how could I accomplish that? Or.. is it even possible?

Here's the code:

function grey() {
    if (!stop) {
    bgContext.drawImage(video, 0, 0, w, h);
    var pixelData = bgContext.getImageData(0, 0, w, h);
    for (var i = 0; i < pixelData.data.length; i += 4 ) {
        var r = pixelData.data[i];
        var g = pixelData.data[i+1];
        var b = pixelData.data[i+2];
        var averageColour = (r + g + b) / 3;
        pixelData.data[i] = averageColour;
        pixelData.data[i+1] = averageColour;
        pixelData.data[i+2] = averageColour;
    }
    context.putImageData(pixelData, 0, 0);
    }
}

function color() {
    bgContext.drawImage(video, 0, 0, w, h);
    var pixelData = bgContext.getImageData(0, 0, w, h);
    for (var i = 0; i < pixelData.data.length; i += 4 ) {
        var r = pixelData.data[i];
        var g = pixelData.data[i+1];
        var b = pixelData.data[i+2];
        pixelData.data[i] = r;
        pixelData.data[i+1] = g;
        pixelData.data[i+2] = b;
    }
    context.putImageData(pixelData, 0, 0);
}

video.addEventListener('play', function() {
    setInterval("grey()", 0);
}, false);

button.addEventListener('click', function() {
    stop = true;
    setInterval("color()", 0);
}, false);
like image 305
destripet Avatar asked Jan 05 '23 12:01

destripet


2 Answers

Canvas Filters for real-time Video/Animation.

Black & White filter

To do a black and white filter is easy.

 // mixAmount is a value from 0 - 1 0 = no mix 1 = full FX
 // video is the video
 ctx.drawImage(video,0,0); // draw the video
 // set up filter
 ctx.fillStyle = "#888"; // gray colour
 ctx.globalAlpha = mixAmount;   // amount of FX
 ctx.globalCompositeOperation = "color";  // The comp setting to do BLACK/WHITE
 ctx.fillRect(0,0,video.width,video.height);  // Draw gray over the video
 ctx.globalAlpha = 1;  // reset alpha
 ctx.globalCompositeOperation = "source-over";  // reset comp

Or you can render the video over itself to get other FX, the demo shows the Black and White filter and several more by just using the above code and a few extra layers.

More info

For more on displaying a video see Display video inside canvas


Demo

The demo show how to do Black and white and some other FX while I am at it.

How to use.

See video title for attribution. FX on left from top to bottom "Lighten", "Black & white", "Sepia", "Saturate", and "Negative"

See video title for attribution. FX on left from top to bottom "Lighten", "Black & white", "Sepia", "Saturate", and "Negative".

The Demo has the following FX Lighter, Darken, Black/White, Negative, Saturate, Sepia, B&W negative, and more.

The Javascript

The code relating to the question is all at the top and marked. The rest is UI loading etc..

Each FX is a function that will call either addMix or addOverlay to apply the filter as shown in the snippet above. The addMix function is slightly different as it draws the video over the video to get the FX rather than a fill.

Instructions are on the demo.

Please note not all browser support all comp modes (WHY?? who knows!! :( ) Nor is there a way to be 100% sure if a browser supports a mode or not. The safe bet is Firefox, Chrome, and Edge for all other browsers best of luck..

//==========================================================================
// All the mix function are in this section
var FXMix = 1;
var addOverlay = function(type, repeat = 1){
    if(FXMix > 0){
        ctx.globalCompositeOperation = type; 
        ctx.globalAlpha = FXMix;
        while (repeat-- > 0) {
            ctx.fillRect(0, 0, canvas.width, canvas.height);
        }
        ctx.globalAlpha = 1;
        ctx.globalCompositeOperation = "source-over"; 
    }
}
var addMix = function(type,video, repeat = 1){
    if(FXMix > 0){
        ctx.globalCompositeOperation = type; 
        ctx.globalAlpha = FXMix;
        while (repeat-- > 0) {
            ctx.drawImage(video,0, 0, canvas.width, canvas.height);
        }
        ctx.globalAlpha = 1;
        ctx.globalCompositeOperation = "source-over"; 
    }
}
var fill = function(style){
    ctx.globalAlpha = FXMix;
    ctx.fillStyle = style;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.globalAlpha = 1;
}
var FX = {
}
var FXList = [];
var currentFX = "";
var addFX = function(name,func){
    FXList.push(name);
    FX[name] = func;
    currentFX = name;
}
// multiply,screen,overlay,color-dodge,color-burn,hard-light,soft-light,difference,exclusion,hue,saturation,color,luminosity
addFX("Ligher",(vid)=>{ addMix("lighter",vid);} );
addFX("BlackWhite",(vid)=>{ ctx.fillStyle = "#888"; addOverlay("color");} );
addFX("Negative",(vid)=>{ ctx.fillStyle = "#FFF"; addOverlay("difference");} );
addFX("Sepia",(vid)=>{ fill("#F94"); addMix("luminosity",vid); ;} );
addFX("B&W Negative",(vid)=>{ ctx.fillStyle = "#FFF";   addOverlay("difference");ctx.fillStyle = "#888"; addOverlay("color");} );
addFX("Ligher+",(vid)=>{   addMix("lighter",vid);addMix("lighter",vid);addMix("lighter",vid);} );
addFX("B&W Lighten",(vid)=>{  addMix("lighter",vid);ctx.fillStyle = "#888"; addOverlay("color");} );
addFX("Darken+",(vid)=>{  addMix("multiply",vid);addMix("multiply",vid);addMix("multiply",vid);} );
addFX("Darken",(vid)=>{  addMix("multiply",vid);} );
addFX("Saturate",()=>{ ctx.fillStyle = "#F00";addOverlay("saturation");});
addFX("Movement",(vid) => {
  const keepMix = FXMix;
  FXMix = 1;
  addMix("difference",can1);
  addMix("lighter",ctx.canvas,2);
  addMix("multiply",vid,1);
  FXMix = keepMix * 0.95;
  addMix("screen",can2,1);
  can2.ctx.drawImage(ctx.canvas,0,0,canvas.width, canvas.height);
  FXMix = 1;
  addMix("lighter",ctx.canvas,1);
  FXMix = keepMix;
  var scale = videoContainer.scale;
  var vidH = vid.videoHeight;
  var vidW = vid.videoWidth;
  var top = canvas.height / 2 - (vidH /2 ) * scale;
  var left = canvas.width / 2 - (vidW /2 ) * scale;
  if(can1.counting === undefined) { can1.counting = 0 }
  else { can1.counting ++ }
  if(can1.counting % 2 === 0) {
     can1.ctx.drawImage(vid, left, top, vidW * scale, vidH * scale);  
  }
});
addFX("None",()=>{});



// end of FX mixing
//==========================================================================

var mediaSource = "http://video.webmfiles.org/big-buck-bunny_trailer.webm";
var mediaSource = "http://upload.wikimedia.org/wikipedia/commons/7/79/Big_Buck_Bunny_small.ogv";
var muted = true;
var canvas = document.getElementById("myCanvas"); // get the canvas from the page
var ctx = canvas.getContext("2d");
const can1 = document.createElement("canvas");
can1.width = canvas.width;
can1.height = canvas.height;
can1.ctx = can1.getContext("2d");
const can2 = document.createElement("canvas");
can2.width = canvas.width;
can2.height = canvas.height;
can2.ctx = can2.getContext("2d");
var videoContainer; // object to hold video and associated info
var video = document.createElement("video"); // create a video element
video.src = mediaSource;
// the video will now begin to load.
// As some additional info is needed we will place the video in a
// containing object for convenience
video.autoPlay = false; // ensure that the video does not auto play
video.loop = true; // set the video to loop.
video.muted = muted;
videoContainer = {  // we will add properties as needed
     video : video,
     ready : false,   
};
// To handle errors. This is not part of the example at the moment. Just fixing for Edge that did not like the ogv format video
video.onerror = function(e){
    document.body.removeChild(canvas);
    document.body.innerHTML += "<h2>There is a problem loading the video</h2><br>";
    document.body.innerHTML += "Users of IE9+ , the browser does not support WebM videos used by this demo";
    document.body.innerHTML += "<br><a href='https://tools.google.com/dlpage/webmmf/'> Download IE9+ WebM support</a> from tools.google.com<br> this includes Edge and Windows 10";
    
 }
video.oncanplay = readyToPlayVideo; // set the event to the play function that 
                                  // can be found below
function readyToPlayVideo(event){ // this is a referance to the video
    // the video may not match the canvas size so find a scale to fit
    videoContainer.scale = Math.min(
                         canvas.width / this.videoWidth, 
                         canvas.height / this.videoHeight); 
    videoContainer.ready = true;
    // the video can be played so hand it off to the display function
    requestAnimationFrame(updateCanvas);
    // add instruction
    document.getElementById("playPause").textContent = "Click video to play/pause.";
    document.querySelector(".mute").textContent = "Mute";
}
var playClick = false;
function updateCanvas(){
    ctx.clearRect(0,0,canvas.width,canvas.height); 
    // only draw if loaded and ready
    if(videoContainer !== undefined && videoContainer.ready){ 
        // find the top left of the video on the canvas
        video.muted = muted;
        var scale = videoContainer.scale;
        var vidH = videoContainer.video.videoHeight;
        var vidW = videoContainer.video.videoWidth;
        var top = canvas.height / 2 - (vidH /2 ) * scale;
        var left = canvas.width / 2 - (vidW /2 ) * scale;
        // now just draw the video the correct size
        ctx.drawImage(videoContainer.video, left, top, vidW * scale, vidH * scale);

        FX[currentFX](videoContainer.video);
        if(videoContainer.video.paused){ // if not playing show the paused screen 
            drawPayIcon();
        }
        overUI = false;
        cursor = "default";
        drawSlider();
        drawList();
        if(mouse.over){
            if(!overUI){
                if((mouse.button&1)===1){ // bit field
                    playClick = true;
                }
                if((mouse.button&1)===0 && playClick){ // bit field
                    playClick = false;
                    playPauseClick();
                }
                cursor = "pointer";
            }
        }
        if(showFXName > 0){
            showFXName = Math.max(0,showFXName - 0.05);
            ctx.globalAlpha = Math.min(1,showFXName);
            ctx.font = "32px Arial";
            ctx.textAlign = "center";
            ctx.textbaseLine = "middle";
            ctx.fillStyle = "white";
            ctx.strokeStyle = "black";
            ctx.lineJoin = "round"
            ctx.strokeText(currentFX,canvas.width/2,canvas.height/2);
            ctx.fillText(currentFX,canvas.width/2,canvas.height/2);
            ctx.globalAlpha = 1;
        }
        
        canvas.style.cursor = cursor;
    }
    // all done for display 
    // request the next frame in 1/60th of a second
    requestAnimationFrame(updateCanvas);
}
var showFXName = 0;
var cursor = "default";
var overUI = false;
var sliderAlpha = 1;
var listAlpha = 1;
var dragging = false;
var listWidth = null;
function getMaxListWidth(){
    ctx.font = "12px arial";
    FXList.forEach(text => {listWidth = Math.max(listWidth,ctx.measureText(text).width)})
    
}

function drawList(){
    if(listWidth === null){
        getMaxListWidth();
        listWidth += 10;
    }

    if(!overUI && mouse.over && mouse.x > canvas.width - listWidth){
        listAlpha = 1;
        overUI = true;
        
    }else{
        listAlpha = Math.max(0,listAlpha - 0.05);
    }
    if(listAlpha > 0){
        ctx.font = "12px arial";
        var textH = 14;
        var border = 10;
        ctx.textAlign = "right";
        ctx.textBaseline = "middle";
        ctx.globalAlpha = listAlpha;
         ctx.fillStyle = "black";
         ctx.strokeStyle = "white";        
        var len = FXList.length;
        var h = len * textH;
        var y = canvas.height / 2 - h/2;
        var x = canvas.width - border * 2;
        ctx.fillRect(x - listWidth,y - border, listWidth+border,h + border );
        ctx.strokeRect(x - listWidth,y - border, listWidth + border,h + border );
        ctx.fillStyle = "white"
        for(var i = 0; i < len; i ++){
            var yy = y + i * textH;
            if(FXList[i] === currentFX){
                ctx.fillStyle = "#0FF";
                ctx.fillText(FXList[i],x,yy);
                ctx.fillStyle = "white"
            }else
            if(mouse.x > canvas.width - listWidth && mouse.y > yy - textH/2 && mouse.y < yy + textH /2){
                ctx.fillStyle = "#0F0";
                ctx.fillText(FXList[i],x,yy);
                ctx.fillStyle = "white"
                cursor = "pointer";
                if((mouse.button & 1) === 1){
                    currentFX =FXList[i];
                    showFXName = 4;
                }
            }else{
                ctx.fillText(FXList[i],x,yy);
            }
            
            
        }
        ctx.globalAlpha = 1;

        
        
    }
    
    
}

function drawSlider(){
    if(currentFX === "None"){
        sliderAlpha = 0;
        return;
    }

    var cw = canvas.width;
    var ch = canvas.height;
    var handle = 5;
    var inset = 10
    var x = inset;
    var w = cw - inset*2;
    var h = 20;
    var y = ch - inset - h;
    var pos =  FXMix * w + x;;
    if(mouse.y > y - h* 2){
        cursor = "e-resize";
        overUI = true;
        if((mouse.button&1) && !dragging){  // bit field
            dragging = true;
        }
    }else{
        cursor = "pointer";

    }
    if(dragging){
        overUI = true;
        cursor = "e-resize";
        sliderAlpha = 1;
        pos = mouse.x - x;
        FXMix = Math.min(1,Math.max(0,pos / w));
        if( (mouse.button&1) === 0 ){ //bit field
            dragging = false;

        }
    }else{
        
    }
    if(!dragging && mouse.y > y-h*2 && mouse.over){
        sliderAlpha = 1;
    }else{
        if(sliderAlpha > 0){
            sliderAlpha = Math.max(0,sliderAlpha- 0.05);
        }
    }
    if(sliderAlpha === 0){
        return;
    }
    ctx.globalAlpha =  sliderAlpha;
    ctx.font = "18px arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    var amount = FXMix;
    ctx.fillStyle = "black";
    ctx.strokeStyle = "white";
    ctx.fillRect(x,y,w,h);
    ctx.strokeRect(x,y,w,h);
    ctx.fillStyle = "white";
    ctx.fillText(currentFX + " "+ (FXMix * 100).toFixed(0)+"%",w/2,y + h / 2);
    pos = amount * w + x;
    ctx.fillStyle = "white";
    ctx.strokeStyle = "black";
    ctx.fillRect(pos-handle*2,y-handle,handle* 4,h + handle * 2);
    ctx.strokeRect(pos-handle*2,y-handle,handle* 4,h + handle * 2);
    ctx.strokeRect(pos-1,y-handle * 0.5,2,h + handle);
    ctx.globalAlpha =  1;
    
    
    
}
function drawPayIcon(){
    // ctx.fillStyle = "black";  // darken display
    // ctx.globalAlpha = 0.5;
    // ctx.fillRect(0,0,canvas.width,canvas.height);
     ctx.fillStyle = "#DDD"; // colour of play icon
     ctx.globalAlpha = 0.75; // partly transparent
     ctx.beginPath(); // create the path for the icon
     var size = (canvas.height / 2) * 0.5;  // the size of the icon
     ctx.moveTo(canvas.width/2 + size/2, canvas.height / 2); // start at the pointy end
     ctx.lineTo(canvas.width/2 - size/2, canvas.height / 2 + size);
     ctx.lineTo(canvas.width/2 - size/2, canvas.height / 2 - size);
     ctx.closePath();
     ctx.fill();
     ctx.globalAlpha = 1; // restore alpha
}    


mouse = (function(){
    var mouse = {
        x : 0, y : 0, w : 0, 
        button : 0,
        over : false,
        bm : [1, 2, 4, 6, 5, 3], 
        active : false,
        bounds : null, 
        border : {top : 10, left : 10},
        mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,contextmenu".split(",")
    };
    var m = mouse;
    function mouseMove(e) {
        var t = e.type;
        m.bounds = m.element.getBoundingClientRect();
        m.x = e.clientX - m.bounds.left - m.border.left; 
        m.y = e.clientY - m.bounds.top - m.border.top;
        
        if (t === "mousedown") { 
            m.button |= m.bm[e.which-1]; 
        } else if (t === "mouseup") { 
            m.button &= m.bm[e.which + 2]; 
        }else if (t === "mouseout") { 
            m.button = 0; 
            m.over = false; 
        }else if (t === "mouseover") { 
            m.over = true; 
        }
        e.preventDefault();
    }
    m.start = function (element) {
        m.element = element;
        m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
        m.active = true;
        //m.border.top = Number(element.style.borderTopWidth.replace(/[a-zA-Z]/g,""));
        //m.border.left = Number(element.style.borderLeftWidth.replace(/[a-zA-Z]/g,""));
    }
    m.remove = function () {
        if (m.element !== undefined) {
            m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
            m.active = false;
            m.element = undefined;
        }
    }
    return mouse;
})();




function playPauseClick(){
     if(videoContainer !== undefined && videoContainer.ready){
          if(videoContainer.video.paused){                                 
                videoContainer.video.play();
          }else{
                videoContainer.video.pause();
          }
     }
}
function videoMute(){
    muted = !muted;
	if(muted){
         document.querySelector(".mute").textContent = "Mute";
    }else{
         document.querySelector(".mute").textContent= "Sound on";
    }


}
// register the event
//canvas.addEventListener("click",playPauseClick);
document.querySelector(".mute").addEventListener("click",videoMute)
setTimeout(()=>{mouse.start(canvas)},100);
body {
    font :14px  arial;
    text-align : center;
    background : #36A;
}
h2 {
    color : white;
}
canvas {
    border : 10px white solid;
    cursor : pointer;
}
a {
  color : #F93;
}
.mute {
    cursor : pointer;
    display: initial;   
}
<h2>Simple video FX via canvas "globalCompositeOperation"</h2>
<p>This example show how to use the 2d context "globalCompositeOperation" property to create a variety of FX. Video may take a few moment to load.
</p>
<p>Play pause video with click. Move to bottom of video to see FX mix slider (Not available if filter None). Move to right to get filter selection and select the filter example. Happy filtering</p>
<canvas id="myCanvas" width = "532" height ="300" ></canvas><br>
<h3><div id = "playPause">Loading content.</div></h3>
<div class="mute"></div><br>

Update 30 Sep 2019

Add filter "Movement" that highlight the change per frame (movement). Slider changes the persistence of the highlighted changes.

like image 62
Blindman67 Avatar answered Jan 08 '23 01:01

Blindman67


The simplest is to set a greyscale css filter on the canvas.

var video = document.getElementById("myCanvas");
var button = document.getElementById("myButton");

function grey() {
  video.className += " greyscale";
}

function color() {
  classGreyscale = video.className.indexOf("greyscale");
  if (classGreyscale > 0)
    video.className = video.className.substring(0, video.className.length - 10)
}

button.addEventListener('click', function() {
  color();
});

grey();
.greyscale {
  -webkit-filter: grayscale(100%);
  filter: grayscale(100%);
}

.transition {
  transition: all 1s;
  -webkit-transition: all 1s;
}
<div>
  <img src="https://upload.wikimedia.org/wikipedia/en/2/24/Lenna.png" id="myCanvas" class="transition" />
  <br/>
  <button id="myButton">to Colour</button>
</div>

So what we do here is add a transition class for our canvas, so it can animate the changes. Then when we add a grayscale class to it and this changes a css filter along with the transition so it fades in. When we want to make it colorful again we remove the greyscale class.

I made the example with an image but it will work with everything.

This way you fade in the class with 1s transition on all parameters (greyscale here). It is better to use this, because you don't have to count every pixel in grayscale, it is cleaner.

Note that you could use jQuery addClass removeClass for simpler and cleaner soultion.

Also note that you should average r,g,b with weights: https://en.wikipedia.org/wiki/Grayscale#Colorimetric_.28luminance-preserving.29_conversion_to_grayscale

like image 29
godzsa Avatar answered Jan 08 '23 03:01

godzsa