Border for Image around Non Transparent parts

How do draw a stroke around image with alpha channel?

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

For more information about custom filters take a look into Custom Filter Tutorial.

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/[email protected]/konva.min.js"></script>
<meta charset="utf-8" />
<title>Konva Border 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();
});

// now we will define our border filter

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

// make all pixels 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 size = this.getAttr('borderSize') || 0;

// - 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>
Enjoying Konva? Please consider to support the project.