跳到主要内容

为图像的非透明部分添加边框

如何在带 Alpha 通道的图像周围绘制描边?

此示例展示如何在 Konva 框架中使用自定义滤镜,创建一个沿带 Alpha 通道图像轮廓显示的边框。

精确跟踪轮廓是一项复杂任务,因此我们使用模糊阴影作为边框基础。滤镜使用所需的边框纯色替换透明或模糊像素。

**操作说明:**观察沿图像非透明部分显示的自定义边框。

import Konva from 'konva';

// Create stage
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});

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

// Define variables for our custom filter
let canvas = document.createElement('canvas');
let tempCanvas = document.createElement('canvas');

// Make all pixels opaque 100% (except pixels that are 100% transparent)
function removeTransparency(canvas) {
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const nPixels = imageData.data.length;

for (let 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;
}

// Define our custom Border filter
function Border(imageData) {
const nPixels = imageData.data.length;
const size = this.getAttr('borderSize') || 0;

// Set correct dimensions for canvases
canvas.width = imageData.width;
canvas.height = imageData.height;

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

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

// Remove alpha channel because it will affect shadow (transparent shapes have smaller shadow)
removeTransparency(tempCanvas);

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

// Use shadow as border
ctx.save();
ctx.shadowColor = color;
ctx.shadowBlur = size;
ctx.drawImage(tempCanvas, 0, 0);
ctx.restore();

// Get image data of [original image + shadow]
const tempImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

const SMOOTH_MIN_THRESHOLD = 3;
const SMOOTH_MAX_THRESHOLD = 10;

let val, hasValue;
const offset = 3;

for (let 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 resulting image (original + shadow without opacity) into canvas
ctx.putImageData(tempImageData, 0, 0);

// 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();

// Copy colored shadow into original imageData
const newImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

const indexesToProcess = [];
for (let i = 3; i < nPixels; i += 4) {
const hasTransparentOnTop = imageData.data[i - imageData.width * 4 * offset] === 0;
const hasTransparentOnTopRight = imageData.data[i - (imageData.width * 4 + 4) * offset] === 0;
const hasTransparentOnTopLeft = imageData.data[i - (imageData.width * 4 - 4) * offset] === 0;
const hasTransparentOnRight = imageData.data[i + 4 * offset] === 0;
const hasTransparentOnLeft = imageData.data[i - 4 * offset] === 0;
const hasTransparentOnBottom = imageData.data[i + imageData.width * 4 * offset] === 0;
const hasTransparentOnBottomRight = imageData.data[i + (imageData.width * 4 + 4) * offset] === 0;
const hasTransparentOnBottomLeft = imageData.data[i + (imageData.width * 4 - 4) * offset] === 0;

const hasTransparentAround =
hasTransparentOnTop ||
hasTransparentOnRight ||
hasTransparentOnLeft ||
hasTransparentOnBottom ||
hasTransparentOnTopRight ||
hasTransparentOnTopLeft ||
hasTransparentOnBottomRight ||
hasTransparentOnBottomLeft;

// Skip pixels presented in original image
if (imageData.data[i] === 255 || (imageData.data[i] && !hasTransparentAround)) {
continue;
}

if (!newImageData.data[i]) {
// Skip transparent pixels
continue;
}

indexesToProcess.push(i);
}

for (let index = 0; index < indexesToProcess.length; index += 1) {
const i = indexesToProcess[index];
const alpha = imageData.data[i] / 255;

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;
}
}

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

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