Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EaselJS Alpha Mask Filter

I'm fairly new to Canvas. I've been trying to get the images reversed in this EaselJS Alpha Mask example so that the initial image is clear, and what you paint is blurry; basically, the reverse of the demo.

I've been playing around with it for hours, applying filters to the bitmap var and removing them from the blur var. Everything I do just doesn't work. Seems like it would be an easy fix of just switching things around but that doesn't seem to be the case. Not for me anyway.

Does anybody have an example of this, or know what to do? I could provide code examples of what I did, but it's basically just playing around with stuff like a monkey on a typewriter.

Here's the code on Github

Here's the relevant code from their example.

<script id="editable">
    var stage;
    var isDrawing;
    var drawingCanvas;
    var oldPt;
    var oldMidPt;
    var displayCanvas;
    var image;
    var bitmap;
    var maskFilter;
    var cursor;
    var text;
    var blur;

    function init() {
        examples.showDistractor();

        image = new Image();
        image.onload = handleComplete;
        image.src = "../_assets/art/flowers.jpg";

        stage = new createjs.Stage("testCanvas");
        //text = new createjs.Text("Loading...", "20px Arial", "#FFF");
        //text.set({x: stage.canvas.width / 2, y: stage.canvas.height - 40});
        //text.textAlign = "center";
    }

    function handleComplete() {
        examples.hideDistractor();
        createjs.Touch.enable(stage);
        stage.enableMouseOver();

        stage.addEventListener("stagemousedown", handleMouseDown);
        stage.addEventListener("stagemouseup", handleMouseUp);
        stage.addEventListener("stagemousemove", handleMouseMove);
        drawingCanvas = new createjs.Shape();
        bitmap = new createjs.Bitmap(image);

        blur = new createjs.Bitmap(image);
        blur.filters = [new createjs.BlurFilter(24, 24, 2), new createjs.ColorMatrixFilter(new createjs.ColorMatrix(60))];
        blur.cache(0, 0, 960, 400);

        //text.text = "Click and Drag to Reveal the Image.";

        stage.addChild(blur, text, bitmap);
        updateCacheImage(false);

        cursor = new createjs.Shape(new createjs.Graphics().beginFill("#FFFFFF").drawCircle(0, 0, 25));
        cursor.cursor = "pointer";

        stage.addChild(cursor);
    }

    function handleMouseDown(event) {
        oldPt = new createjs.Point(stage.mouseX, stage.mouseY);
        oldMidPt = oldPt;
        isDrawing = true;
    }

    function handleMouseMove(event) {
        cursor.x = stage.mouseX;
        cursor.y = stage.mouseY;

        if (!isDrawing) {
            stage.update();
            return;
        }

        var midPoint = new createjs.Point(oldPt.x + stage.mouseX >> 1, oldPt.y + stage.mouseY >> 1);

        drawingCanvas.graphics.setStrokeStyle(40, "round", "round")
                .beginStroke("rgba(0,0,0,0.2)")
                .moveTo(midPoint.x, midPoint.y)
                .curveTo(oldPt.x, oldPt.y, oldMidPt.x, oldMidPt.y);

        oldPt.x = stage.mouseX;
        oldPt.y = stage.mouseY;

        oldMidPt.x = midPoint.x;
        oldMidPt.y = midPoint.y;

        updateCacheImage(true);
    }

    function handleMouseUp(event) {
        updateCacheImage(true);
        isDrawing = false;
    }

    function updateCacheImage(update) {
        if (update) {
            drawingCanvas.updateCache();
        } else {
            drawingCanvas.cache(0, 0, image.width, image.height);
        }

        maskFilter = new createjs.AlphaMaskFilter(drawingCanvas.cacheCanvas);

        bitmap.filters = [maskFilter];
        if (update) {
            bitmap.updateCache(0, 0, image.width, image.height);
        } else {
            bitmap.cache(0, 0, image.width, image.height);
        }

        stage.update();
    }
</script>
like image 501
timgavin Avatar asked Dec 08 '15 01:12

timgavin


2 Answers

The pure Javascript way using the Canvas 2D context API.

You will need to create a canvas, load the image, create a mask image, and a blur image. I have blurred the image already as I did not want to write a blur.

The following functions in the object imageTools create the canvas/images, and loads images. Note that the canvas and images are interchangeable. The canvas does not have a src, and an image can not be drawn on appart from that they are the same. I convert all images to canvas and attach the context to them. I also call them images.

/** ImageTools.js begin **/
var imageTools = (function () {
    var tools = {
        canvas : function (width, height) {  // create a blank image (canvas)
            var c = document.createElement("canvas");
            c.width = width;
            c.height = height;
            return c;
        },
        createImage : function (width, height) {
            var image = this.canvas(width, height);
            image.ctx = image.getContext("2d");
            return image;
        },
        loadImage : function (url, callback) {
            var image = new Image();
            image.src = url;
            image.addEventListener('load', callback);
            image.addEventListener('error', callback);
            return image;
        }
    };
    return tools;
})();

Then I use imageTools to load the images I need and create a mask, when I have the image size as I am matching the mask resolution to the image resolution

// load the images and create the mask
var imageLoadedCount = 0;
var error = false;
var maskImage;
var flowerImage = imageTools.loadImage("http://www.createjs.com/demos/_assets/art/flowers.jpg", function (event) {
    if (event.type === "load") {
        imageLoadedCount += 1;
    } else {
        error = true;
    }
});
var flowerImageBlur = imageTools.loadImage("http://i.stack.imgur.com/3S5m8.jpg", function () {
    if (event.type === "load") {
        maskImage = imageTools.createImage(this.width, this.height);
        imageLoadedCount += 1;
    } else {
        error = true;
    }
});

I use requestAnimationFrame to create a 60FPS canvas drawing function that waits for the images to load and then displays the 3 layers onto the canvas

   // ctx is the main canvas context.
   // drawImageCentered scales the image to fit. See Demo for code.

   // draw the unblured image that will appear at the top
   ctx.globalCompositeOperation = "source-over";
   drawImageCentered(ctx, flowerImage, cw, ch);
   drawText(ctx, "Click drag to blur the image via mask", 40 + Math.sin(time / 100), cw, ch - 30, "White");

   // Mask out the parts when the mask image has pixels
   ctx.globalCompositeOperation = "destination-out";
   drawImageCentered(ctx, maskImage, cw, ch);

   // draw the blured image only where the destination has been masked
   ctx.globalCompositeOperation = "destination-atop";
   drawImageCentered(ctx, flowerImageBlur, cw, ch);

It first draws the image that appears if no masked pixels are visible. Then it draw some text for instructions.

Next comes the mask that uses destination-out. This means that for pixels in the mask that have an alpha > 0 remove from the destination that amount of alpha. So if a mask pixel has an alpha of 50 and the destination (canvas) has an alpha of 255 then the result of that pixel after rendering the mask with destination-out will be 255 - 50 = 205. This effectively has put holes on the canvas where ever there are pixels on the mask.

Now we can fill the holes with the blurred image and render it using destination-atop which means only draw pixels from the source (blurred image) where the destination alpha is less that 255

That is the layered masking done, all we need is to draw on the mask. For that we just listen to the mouse events and if the button is down draw a circle on the mask where the mouse is. My example has scaled the images so there is a little extra work there but the basics are as follows,

// draws circle with gradient
function drawCircle(ctx, x, y, r) {
    var gr = ctx.createRadialGradient(x, y, 0, x, y, r)
        gr.addColorStop(1, "rgba(0,0,0,0)")
        gr.addColorStop(0.5, "rgba(0,0,0,0.08)")
        gr.addColorStop(0, "rgba(0,0,0,0.1)")
        ctx.fillStyle = gr;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fill();
}
// draw a circle on the mask where the mouse is.
drawCircle(maskImage.ctx, mouse.x, mouse.y, 20);

For the demo there is a little more code to make it all work nicely but you can pick out the bits you need.

var imageLoadedCount = 0;
var error = false;
var maskImage;
var flowerImage;
var flowerImageBlur;
/** ImageTools.js begin **/
var imageTools = (function () {
    var tools = {
        canvas : function (width, height) {  // create a blank image (canvas)
            var c = document.createElement("canvas");
            c.width = width;
            c.height = height;
            return c;
        },
        createImage : function (width, height) {
            var image = this.canvas(width, height);
            image.ctx = image.getContext("2d");
            return image;
        },
        loadImage : function (url, callback) {
            var image = new Image();
            image.src = url;
            image.addEventListener('load', callback);
            image.addEventListener('error', callback);
            return image;
        }
    };
    return tools;
})();




var mouse;
var demo = function(){
    /** fullScreenCanvas.js begin **/
    var canvas = (function(){
        var canvas = document.getElementById("canv");
        if(canvas !== null){
            document.body.removeChild(canvas);
        }
        // creates a blank image with 2d context
        canvas = document.createElement("canvas"); 
        canvas.id = "canv";    
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight; 
        canvas.style.position = "absolute";
        canvas.style.top = "0px";
        canvas.style.left = "0px";
        canvas.style.zIndex = 1000;
        canvas.ctx = canvas.getContext("2d"); 
        document.body.appendChild(canvas);
        return canvas;
    })();
    var ctx = canvas.ctx;
    
    /** fullScreenCanvas.js end **/
    /** MouseFull.js begin **/
    if(typeof mouse !== "undefined"){  // if the mouse exists 
        if( mouse.removeMouse !== undefined){
            mouse.removeMouse(); // remove previouse events
        }
    }else{
        var mouse;
    }
    var canvasMouseCallBack = undefined;  // if needed
    mouse = (function(){
        var mouse = {
            x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
            interfaceId : 0, buttonLastRaw : 0,  buttonRaw : 0,
            over : false,  // mouse is over the element
            bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
            getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
            startMouse:undefined,
            mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
        };
        function mouseMove(e) {
            var t = e.type, m = mouse;
            m.x = e.offsetX; m.y = e.offsetY;
            if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
            m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
            if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
            } else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
            } else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
            } else if (t === "mouseover") { m.over = true;
            } else if (t === "mousewheel") { m.w = e.wheelDelta;
            } else if (t === "DOMMouseScroll") { m.w = -e.detail;}
            if (canvasMouseCallBack) { canvasMouseCallBack(mouse); }
            e.preventDefault();
        }
        function startMouse(element){
            if(element === undefined){
                element = document;
            }
            mouse.element = element;
            mouse.mouseEvents.forEach(
                function(n){
                    element.addEventListener(n, mouseMove);
                }
            );
            element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
        }
        mouse.removeMouse = function(){
            if(mouse.element !== undefined){
                mouse.mouseEvents.forEach(
                    function(n){
                        mouse.element.removeEventListener(n, mouseMove);
                    }
                );
                canvasMouseCallBack = undefined;
            }
        }
        mouse.mouseStart = startMouse;
        return mouse;
    })();
    if(typeof canvas !== "undefined"){
        mouse.mouseStart(canvas);
    }else{
        mouse.mouseStart();
    }
    /** MouseFull.js end **/
    
    // load the images and create the mask
    if(imageLoadedCount === 0){
        imageLoadedCount = 0;
        error = false;
        maskImage;
        flowerImage =    imageTools.loadImage("http://www.createjs.com/demos/_assets/art/flowers.jpg", function (event) {
            if (event.type === "load") {
                imageLoadedCount += 1;
            } else {
                error = true;
            }
        })
        flowerImageBlur = imageTools.loadImage("http://i.stack.imgur.com/3S5m8.jpg", function () {
            if (event.type === "load") {
                maskImage = imageTools.createImage(this.width, this.height);
                imageLoadedCount += 1;
            } else {
                error = true;
            }
        })
    }
    // set up the canvas 
    var w = canvas.width;
    var h = canvas.height;
    var cw = w / 2;
    var ch = h / 2;


    // calculate time to download image using the MS algorithum. As this code is a highly gaurded secret I have obsficated it for your personal safty.
    var calculateTimeToGo= (function(){var b="# SecondQMinuteQHourQDayQWeekQMonthQMomentQTick@.,Some time soon,Maybe Tomorrow.".replace(/Q/g,"@.,# ").split(","),r=Math.random,f=Math.floor,lc=0,pc=0,lt=0,lp=0;var cttg=function(a){if(lc===0){lc=100+r(r()*60);lt=f(r()*40);if(pc===0||r()<(lp/b.length)-0.2){lp=f(r()*b.length);pc=1+f(r()*10)}else{pc-=1}}else{lc-=1}a=lt;if(lp===0){a=lt;if(r()<0.01){lt-=1}}var s=b[lp].replace("#",a);if(a===1){s=s.replace("@","")}else{s=s.replace("@","s")}return s};return cttg})();    

    // draws circle with gradient
    function drawCircle(ctx, x, y, r) {
        var gr = ctx.createRadialGradient(x, y, 0, x, y, r)
            gr.addColorStop(1, "rgba(0,0,0,0)")
            gr.addColorStop(0.5, "rgba(0,0,0,0.08)")
            gr.addColorStop(0, "rgba(0,0,0,0.1)")
            ctx.fillStyle = gr;
        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2);
        ctx.fill();
    }
    // draw text
    function drawText(ctx, text, size, x, y, c) {
        ctx.fillStyle = c;
        ctx.strokeStyle = "black";
        ctx.lineWidth = 5;
        ctx.lineJoin = "round";
        ctx.font = size + "px Arial Black";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        if (c !== "black") {
            ctx.strokeText(text, x, y + 1);
        }
        ctx.fillText(text, x, y);
    }
    // draw the image to fit the current canvas size
    function drawImageCentered(ctx, image, x, y) {
        var scale = Math.min(w / image.width, h / image.height);
        ctx.setTransform(scale, 0, 0, scale, cw, ch);
        ctx.drawImage(image, -image.width / 2, -image.height / 2);
        ctx.setTransform(1, 0, 0, 1, 0, 0);
    }
    // points for filling gaps between mouse moves.
    var lastMX,lastMY;
    // update function will try 60fps but setting will slow this down.    
    function update(time){
        ctx.setTransform(1, 0, 0, 1, 0, 0); // restore transform
        ctx.clearRect(0, 0, w, h); // clear rhe canvas
        // have the images loaded???
        if (imageLoadedCount === 2) {
            // draw the unblured image that will appear at the top
            ctx.globalCompositeOperation = "source-over";
            drawImageCentered(ctx, flowerImage, cw, ch);
            drawText(ctx, "Click drag to blur the image via mask", 20 + Math.sin(time / 100), cw, ch - 30, "White");
            // Mask out the parts when the mask image has pixels
            ctx.globalCompositeOperation = "destination-out";
            drawImageCentered(ctx, maskImage, cw, ch);
            // draw the blured image only where the destination has been masked
            ctx.globalCompositeOperation = "destination-atop";
            drawImageCentered(ctx, flowerImageBlur, cw, ch);

            // is the mouse down
            if (mouse.buttonRaw === 1) {
                // because image has been scaled need to get mouse coords on image
                var scale = Math.min(w / flowerImage.width, h / flowerImage.height);
                var x = (mouse.x - (cw - (maskImage.width / 2) * scale)) / scale;
                var y = (mouse.y - (ch - (maskImage.height / 2) * scale)) / scale;
                // draw circle on mask
                drawCircle(maskImage.ctx, x, y, 20);
                // if mouse is draging then draw some points between to fill the gaps
                if (lastMX !== undefined) {
                    drawCircle(maskImage.ctx, ((x + lastMX) / 2 + x) / 2, ((y + lastMY) / 2 + y) / 2, 20);
                    drawCircle(maskImage.ctx, (x + lastMX) / 2, (y + lastMY) / 2, 20);
                    drawCircle(maskImage.ctx, ((x + lastMX) / 2 + lastMX) / 2, ((y + lastMY) / 2 + lastMY) / 2, 20);
                }
                // save las mouse pos on image
                lastMX = x;
                lastMY = y;
            } else {
                // undefined last mouse pos
                lastMX = undefined;

            }
        } else {
            // Laoding images so please wait.
            drawText(ctx, "Please wait.", 40 + Math.sin(time / 100), cw, ch - 30, "White");
            drawText(ctx, "loading images... ", 12, cw, ch, "black")
            drawText(ctx, "ETA " + calculateTimeToGo(time), 14, cw, ch + 20, "black")
        }
        
        // if not restart the request animation frame
        if(!STOP){
            requestAnimationFrame(update);
        }else{
            var can = document.getElementById("canv");
            if(can !== null){
                document.body.removeChild(can);
            }        
            STOP = false;
            
        }
    }

    update();
   
}
var STOP = false; // flag to tell demo app to stop
function resizeEvent() {
    var waitForStopped = function () {
        if (!STOP) { // wait for stop to return to false
            demo();
            return;
        }
        setTimeout(waitForStopped, 200);
    }
    STOP = true;
    setTimeout(waitForStopped, 100);
}
window.addEventListener("resize", resizeEvent);
demo();
/** FrameUpdate.js end **/
like image 114
Blindman67 Avatar answered Nov 19 '22 06:11

Blindman67


There are a few steps to do this. Most of them you have probably already done:

1) Change the order you add the items to stage. Since you want to reveal the blur instead, add them in the reverse order. This puts the blur on top.

stage.addChild(bitmap, text, blur);

2) Change what is cached or updateCached in the updateCacheImage method:

if (update) {
    blur.updateCache(0, 0, image.width, image.height);
} else {
    blur.cache(0, 0, image.width, image.height);
}

This is where you probably got tripped up. If you set the filters on the blurImage to just the maskFilter, it will not appear to work. The maskFilter IS working, but will have removed the blur and color filters that were applied. To add the maskFilter, you have to put it in the array with the current filters. This is my approach, which ensures the original 2 filters are intact, and the maskFilter is just added once:

blur.filters.length = 2; // Truncate the array to 2
blur.filters.push(maskFilter); // add the new filter

In my opinion, this effect isn't as obvious - so you might want to increase the opacity of the brush:

drawingCanvas.graphics.setStrokeStyle(40, "round", "round")
    .beginStroke("rgba(0,0,0,0.5)"); // From 0.2

I was the author of the original AlphaMaskFilter demo in EaselJS - glad you found it useful and/or interesting!

like image 3
Lanny Avatar answered Nov 19 '22 08:11

Lanny