HTML5 Canvas Custom Filter Tutorial - Border for Image around Non Transparent parts

How do draw border around image with alpha channel?

This demo demonstrate how to use custom filters with Konva framework.

Filter is a function that have canvas ImageData as input and it should mutate it.

function Filter(imageData) {
// do something with image data
imageData.data[0] = 0;
}

In this demo we will create custom filter that will draw solid border around image following its contour.
Since following contour is a complex thing we will use a hack. So we will use blurred shadow as a border foundation.
We will replace transparent/blurred pixels with our solid color that we want for border.

For all available filters go to Filters Documentation.

Konva Custom Filter Image Demoview raw
<!DOCTYPE html>
<html>

<head>
<script src="https://unpkg.com/konva@2.4.2/konva.min.js"></script>
<meta charset="utf-8">
<title>Konva Blur Image Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #F0F0F0;
}
</style>
</head>

<body>
<div id="container"></div>
<script>
var stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});

var layer = new Konva.Layer();
stage.add(layer);

Konva.Image.fromURL('/assets/lion.png', function (image) {
layer.add(image);
image.setAttrs({
x: 80,
y: 30,
borderSize: 5,
borderColor: 'red'
});

image.filters([Border]);
image.cache();

layer.draw();
});


// now we will define our border filter

var canvas = document.createElement('canvas');
var tempCanvas = document.createElement('canvas');

// make all pixells opaque 100% (except pixels that 100% transparent)
function removeTransparency(canvas) {
var ctx = canvas.getContext('2d');

var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var nPixels = imageData.data.length;
for (var i = 3; i < nPixels; i += 4) {
if (imageData.data[i] > 0) {
imageData.data[i] = 255;
}
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.putImageData(imageData, 0, 0);
return canvas;
}

function Border(imageData) {
var nPixels = imageData.data.length;

var ratio = this._cache.canvas.scene.pixelRatio;
var size = (this.getAttr('borderSize') || 0) * ratio;

// - first set correct dimensions for canvases
canvas.width = imageData.width;
canvas.height = imageData.height;

tempCanvas.width = imageData.width;
tempCanvas.height = imageData.height;

// - the draw original shape into temp canvas
tempCanvas.getContext('2d').putImageData(imageData, 0, 0);

// - then we need to remove alpha chanel, because it will affect shadow (transparent shapes has smaller shadow)
removeTransparency(tempCanvas);

var ctx = canvas.getContext('2d');
var color = this.getAttr('borderColor') || 'black';


// 3. we will use shadow as border
// so we just need apply shadow on the original image
ctx.save();
ctx.shadowColor = color;
ctx.shadowBlur = size;
ctx.drawImage(tempCanvas, 0, 0);
ctx.restore();


// - Then we will dive in into image data of [original image + shadow]
// and remove transparency from shadow
var tempImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

var SMOOTH_MIN_THRESHOLD = 3;
var SMOOTH_MAX_THRESHOLD = 10;

let val, hasValue;

var offset = 3;

for (var i = 3; i < nPixels; i += 4) {
// skip opaque pixels
if (imageData.data[i] === 255) {
continue;
}

val = tempImageData.data[i];
hasValue = val !== 0;
if (!hasValue) {
continue;
}
if (val > SMOOTH_MAX_THRESHOLD) {
val = 255;
} else if (val < SMOOTH_MIN_THRESHOLD) {
val = 0
} else {
val = (val - SMOOTH_MIN_THRESHOLD) / (SMOOTH_MAX_THRESHOLD - SMOOTH_MIN_THRESHOLD) * 255;
}
tempImageData.data[i] = val;
}


// draw resulted image (original + shadow without opacity) into canvas
ctx.putImageData(tempImageData, 0, 0);

// then fill whole image with color (after that shadow is colored)
ctx.save();
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();


// then we need to copy colored shadow into original imageData
var newImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

var indexesToProcess = [];
for (var i = 3; i < nPixels; i += 4) {

var hasTransparentOnTop = imageData.data[i - (imageData.width * 4) * offset] === 0;
var hasTransparentOnTopRight = imageData.data[i - (imageData.width * 4 + 4) * offset] === 0;
var hasTransparentOnTopLeft = imageData.data[i - (imageData.width * 4 - 4) * offset] === 0;
var hasTransparentOnRight = imageData.data[i + 4 * offset] === 0;
var hasTransparentOnLeft = imageData.data[i - 4 * offset] === 0;
var hasTransparentOnBottom = imageData.data[i + (imageData.width * 4) * offset] === 0;
var hasTransparentOnBottomRight = imageData.data[i + (imageData.width * 4 + 4) * offset] === 0;
var hasTransparentOnBottomLeft = imageData.data[i + (imageData.width * 4 - 4) * offset] === 0;
var hasTransparentAround =
hasTransparentOnTop || hasTransparentOnRight || hasTransparentOnLeft || hasTransparentOnBottom ||
hasTransparentOnTopRight || hasTransparentOnTopLeft || hasTransparentOnBottomRight || hasTransparentOnBottomLeft;

// if pixel presented in original image - skip it
// because we need to change only shadow area
if ((imageData.data[i] === 255) || imageData.data[i] && !hasTransparentAround) {
continue;
}
if (!newImageData.data[i]) { // skip transparent pixels
continue;
}
indexesToProcess.push(i);
}

for (var index = 0; index < indexesToProcess.length; index += 1) {
var i = indexesToProcess[index];

var alpha = imageData.data[i] / 255;

if ((alpha > 0) && (alpha < 1)) {
var aa = 1 + 1;
}
imageData.data[i] = newImageData.data[i];
imageData.data[i - 1] = newImageData.data[i - 1] * (1 - alpha) + imageData.data[i - 1] * alpha;
imageData.data[i - 2] = newImageData.data[i - 2] * (1 - alpha) + imageData.data[i - 2] * alpha;
imageData.data[i - 3] = newImageData.data[i - 3] * (1 - alpha) + imageData.data[i - 3] * alpha;

if ((newImageData.data[i] < 255) && (alpha > 0)) {
var bb = 1 + 1;
}
}
}
</script>

</body>

</html>