captain_jim1
captain_jim1

Reputation: 1001

Apply effects to edge of transparent area on Canvas

I have two canvases - both of the same size. The MainCanvas is completely filled with a background image. On SecondaryCanvas I have a small image - a company logo that has some transparent areas (basically where all of the white pixels have had their alpha reduced to 0).

I want to apply some sort of bevel or innershadow to the SecondaryCanvas, but only where there is an actual image. Typically I see people stroke a path to do this, but it's impossible for me to trace the edges of the non-transparent area.

How can I do this? The thought just occurred to me that I could scan the entire SecondaryCanvas pixel-by-pixel and check to see if it's neighboring pixels are transparent or not (an attempt to find the 'edges')... and apply the appropriate colors to the pixel if it is... but that seems very CPU intensive.

Upvotes: 0

Views: 405

Answers (1)

Blindman67
Blindman67

Reputation: 54059

Here is the quickest I know of using convolution filters.

Copys from source image to destination. I have added some flags as well for this solution.

The function (sorry about the spelling).

embose(imageDataSource, imageDataDestination, method, emboseImageEdge, perceptualCorrect, edgeOnly)

  • imageDataSource: pixel data for source image
  • imageDataDestination: pixel data for destination. Must be different than source or results will be incorrect. Must be same size
  • method: (String) one of "3 pixel soft", "3 pixel", "1 pixel" // forgot default so if passed unknown method will crash. I am sure you can fix
  • emboseImageEdge: (Boolean) if true pixels at the edge of the image will be embossed.
  • perceptualCorrect: (Boolean) if true uses Human perception calcs for luminance. Slightly slow runtime.
  • edgeOnly: (Boolean) if true only embosses pixels near transparent edges. Else embosses all opaque pixels by there luminance.

Function returns true if processed or false if not. If false no pixels are changed.

Todo. I forgot the add Invert but that is easy to do just invert val = -val all the vals in the embos arrays then set the center value to 1 (must be one or will not look good). You can also rotate the direction of the light by rotating the negative/positive line in the embos array, but that is a little more complex. For softer embos create larger convolution embos arrays. Set size and halfSize to match. EG 3 pixel embos requires a 7 by 7 array with size=7 and halfSize = 3 with a similar set of values. The bigger the embos array the slower the function. For very large images this will block the page. If using this function for large images move this function to a web worker.

As it uses a convolution filter it can be adapted to do a host of other filters. Though this one only uses pixel luminance, it is easy to modify to a per colour channel filter. So filter type this can be adapted to. Gaussian blur, blur, sharpen, edge detect, and many more.

See bottom of code example how it is used.

Hope this does what you need or can be adapted to do so. Any question feel free to ask. Sorry the commenting is currently sparse but I am short on time. Will return to fix that soon.

var canvas = document.getElementById("canV");
var ctx = canvas.getContext("2d");

// Groover API log dependency replacement 
function log(data){
   // console.log(data); // just suck it up
}

// Groover Bitmaps API dependency replacement
// Extracted from Groover.Bitmaps
var createImage= function(w,h){ // create a image of requier size
    var image = document.createElement("canvas"); 
    image.width = w;
    image.height =h;
    image.ctx = image.getContext("2d");  // tack the context onto the image
    return image;
}    
    

function embose(imageDataSource,imageDataDestination, method, emboseImageEdge, perceptualCorrect,edgeOnly){
    "use strict";
    var dataS = imageDataSource.data;
    var dataD = imageDataDestination.data;
    var w = imageDataSource.width;
    var h = imageDataSource.height;
    if(dataS.length !== dataD.length){
        return false; // failed due to size mismatch
    }
    var embos,size,halfSize;
    var lMethod = method.toLowerCase(); // some JS engines flag reasignment of
                                        // arguments as unoptimised as this 
                                        // is computationally intensive create a
                                        // new var for lowercase

    if(lMethod === "2 pixel soft" ){
        embos = [  
            -0.25   ,  -0.5 ,  -1,  -1 , 0.5,
            -0.5   ,  -1 ,  -1,  1 , 1,
            -1   ,  -1 ,  1, 1 , 1,
            -1   ,  -1 , 1, 1 , 0.5,
            -0.5   ,  1, 1, 0.5 , 0.25 
        ];
        size = 5;
        halfSize = 2;
    }else
    if(lMethod === "2 pixel" ){
        embos = [  
            -1   ,  -1 ,  -1,  -1 , 1,
            -1   ,  -1 ,  -1,  1 , 1,
            -1   ,  -1 ,  1, 1 , 1,
            -1   ,  -1 , 1, 1 , 1,
            -1   ,  1, 1, 1 , 1 
        ];
        size = 5;
        halfSize = 2;
    }else
    if(lMethod === "1 pixel" ){
        embos = [  
            -1 , -1,  -1,
            -1,  1, 1,
            1 , 1,  1 
        ];
        size = 3;
        halfSize = 1;
    }
var deb = 0
    var x,y,l,pl,g,b,a,ind,scx,scy,cx,cy,cInd,nearEdge,pc;
    pc = perceptualCorrect; // just for readability
    for(y = 0; y < h; y++){
        for(x = 0; x < w; x++){
            ind = y*4*w+x*4;
            l = 0;
            nearEdge = false;
            if(dataS[ind+3] !== 0){ // ignor transparent pixels
                for (cy=0; cy<size; cy++) {
                    for (cx=0; cx<size; cx++) {
                        scy = y + cy - halfSize;
                        scx = x + cx - halfSize;
                        if (scy >= 0 && scy < h && scx >= 0 && scx < w) {
                            cInd = (scy*w+scx)*4;
                            if(dataS[cInd+3] === 0){
                                nearEdge = true;
                            }
                            l += pc?(embos[cy*size+cx] * 
                                        ((Math.pow(dataS[cInd++]*0.2126,2)+
                                         Math.pow(dataS[cInd++]*0.7152,2)+
                                         Math.pow(dataS[cInd++]*0.0722,2)
                                        )/3)):
                                    (embos[cy*size+cx] * 
                                        ((dataS[cInd++]+
                                         dataS[cInd++]+
                                         dataS[cInd++]
                                        )/3));
                                      
                        }else
                        if(emboseImageEdge){
                            nearEdge = true;
                        }
                    }
                }
                        
                if((nearEdge && edgeOnly) || ! edgeOnly){
                    if(pc){
                        pl = Math.sqrt((Math.pow(dataS[ind]*0.2126,2) + Math.pow(dataS[ind+1]*0.7152,2) + Math.pow(dataS[ind+2]*0.0722,2))/3);
                        l = Math.sqrt(Math.max(0,l));

                        if(pl > 0){
                            pl = l/pl;
                            dataD[ind] = Math.sqrt(dataS[ind]*dataS[ind++]*pl);
                            dataD[ind] = Math.sqrt(dataS[ind]*dataS[ind++]*pl);
                            dataD[ind] = Math.sqrt(dataS[ind]*dataS[ind++]*pl);
                            dataD[ind] = dataS[ind]; // alpha not effected
                        }else{ // black pixel 
                            dataD[ind++] = 0;
                            dataD[ind++] = 0;
                            dataD[ind++] = 0;
                            dataD[ind] = dataS[ind];
                        }
                    }else{
                        l = Math.max(0,l);
                        pl = (dataS[ind]+dataS[ind+1]+dataS[ind+2])/3;
                        if(pl > 0){
                            pl = l/pl;
                            dataD[ind] = dataS[ind++]*pl;
                            dataD[ind] = dataS[ind++]*pl;
                            dataD[ind] = dataS[ind++]*pl;
                            dataD[ind] = dataS[ind]; // alpha not effected
                            
                        }else{ // black pixel 
                            dataD[ind++] = 0;
                            dataD[ind++] = 0;
                            dataD[ind++] = 0;
                            dataD[ind] = dataS[ind];
                        }
                    }
                }else{  // if not edge then just copy image pixel to dest
                    dataD[ind] = dataS[ind++];
                    dataD[ind] = dataS[ind++];
                    dataD[ind] = dataS[ind++];
                    dataD[ind] = dataS[ind];
                }
            }else{  // set transparent pixel to zero
                dataD[ind+3] = 0;
            }
        }
 
    }
    // all done
    return true; // return success
}
var img = createImage(128,128);
img.ctx.font = "32px arial black";
img.ctx.textAlign = "center";
img.ctx.textBaseline = "middle";
img.ctx.lineCap = "round";
img.ctx.lineJoin = "round";
img.ctx.lineWidth = 4
img.ctx.strokeStyle = "#3AD";
img.ctx.fillStyle = "#334";
img.ctx.strokeText("LOGO!",64,64);
img.ctx.fillText("LOGO!",64,64);
ctx.drawImage(img,0,0);
var img1 = createImage(128,128);
var imgData = img.ctx.getImageData(0,0,128,128);
var imgData1 = img1.ctx.getImageData(0,0,128,128);
if(embose(imgData,imgData1,"2 pixel soft",false, true, false)){
    img1.ctx.putImageData(imgData1,0,0);
    log("ONe")
    ctx.drawImage(img1,128,0);
}
img.ctx.fillStyle = "#DA3";  // make is look better for the sell ;)
img.ctx.fillText("LOGO!",64,64);  
var imgData = img.ctx.getImageData(0,0,128,128);
var img1 = createImage(128,128);
var imgData1 = img1.ctx.getImageData(0,0,128,128);
if(embose(imgData,imgData1,"2 pixel",false, true, true)){
    img1.ctx.putImageData(imgData1,0,0);
    ctx.drawImage(img1,0,128);
}

var img1 = createImage(128,128);
var imgData1 = img1.ctx.getImageData(0,0,128,128);
if(embose(imgData,imgData1,"1 pixel",false, false, false)){
    img1.ctx.putImageData(imgData1,0,0);
    ctx.drawImage(img1,128,128);
}
    
.canC {
    width:256px;
    height:256px;
}
<canvas class="canC" id="canV" width=256 height=256></canvas>

Upvotes: 3

Related Questions