跳到主要内容

如何使用 JavaScript Canvas 构建姓名牌和徽章制作器

姓名牌、徽章和标签随处可见,从会议和活动到产品包装和学校项目。使用 Konva,你可以构建完整的交互式徽章制作器,其中包含可拖动文本、图形模板、自定义颜色和一键导出功能。

操作说明: 单击徽章模板开始制作。编辑文本字段,拖动元素以调整位置,更改颜色,然后单击“导出为 PNG”保存徽章。

import Konva from 'konva';

// --- Controls ---
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:8px;align-items:center;margin-bottom:4px;flex-wrap:wrap;';

const bgLabel = document.createElement('label');
bgLabel.textContent = 'Badge Color: ';
const bgColor = document.createElement('input');
bgColor.type = 'color';
bgColor.value = '#3b82f6';
bgLabel.appendChild(bgColor);

const textColorLabel = document.createElement('label');
textColorLabel.textContent = 'Text Color: ';
const textColor = document.createElement('input');
textColor.type = 'color';
textColor.value = '#ffffff';
textColorLabel.appendChild(textColor);

const exportBtn = document.createElement('button');
exportBtn.textContent = 'Export as PNG';
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Reset';

controls.appendChild(bgLabel);
controls.appendChild(textColorLabel);
controls.appendChild(exportBtn);
controls.appendChild(clearBtn);
const container = document.getElementById('container');
container.parentNode.insertBefore(controls, container);

// --- Stage ---
const stageWidth = window.innerWidth;
const stageHeight = window.innerHeight - 40;

const stage = new Konva.Stage({
container: 'container',
width: stageWidth,
height: stageHeight,
});

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

// Badge dimensions
const badgeW = 340;
const badgeH = 220;
const badgeX = (stageWidth - badgeW) / 2;
const badgeY = (stageHeight - badgeH) / 2;

// Badge background
const badge = new Konva.Rect({
x: badgeX,
y: badgeY,
width: badgeW,
height: badgeH,
fill: bgColor.value,
cornerRadius: 16,
shadowColor: 'rgba(0,0,0,0.15)',
shadowBlur: 12,
shadowOffset: { x: 0, y: 4 },
});
layer.add(badge);

// Decorative top stripe
const stripe = new Konva.Rect({
x: badgeX,
y: badgeY,
width: badgeW,
height: 50,
fill: 'rgba(0,0,0,0.15)',
cornerRadius: [16, 16, 0, 0],
});
layer.add(stripe);

// "HELLO" header
const helloText = new Konva.Text({
x: badgeX,
y: badgeY + 8,
width: badgeW,
text: 'HELLO',
fontSize: 14,
fontFamily: 'Arial',
fontStyle: 'bold',
fill: 'rgba(255,255,255,0.8)',
align: 'center',
letterSpacing: 4,
});
layer.add(helloText);

// "my name is" subtitle
const subtitleText = new Konva.Text({
x: badgeX,
y: badgeY + 26,
width: badgeW,
text: 'my name is',
fontSize: 12,
fontFamily: 'Arial',
fill: 'rgba(255,255,255,0.6)',
align: 'center',
});
layer.add(subtitleText);

// Editable name
const nameText = new Konva.Text({
x: badgeX + 20,
y: badgeY + 70,
width: badgeW - 40,
text: 'Your Name',
fontSize: 36,
fontFamily: 'Arial',
fontStyle: 'bold',
fill: textColor.value,
align: 'center',
draggable: true,
});
layer.add(nameText);

// Editable title/role
const roleText = new Konva.Text({
x: badgeX + 20,
y: badgeY + 120,
width: badgeW - 40,
text: 'Job Title',
fontSize: 18,
fontFamily: 'Arial',
fill: 'rgba(255,255,255,0.75)',
align: 'center',
draggable: true,
});
layer.add(roleText);

// Editable company
const companyText = new Konva.Text({
x: badgeX + 20,
y: badgeY + 150,
width: badgeW - 40,
text: 'Company',
fontSize: 16,
fontFamily: 'Arial',
fill: 'rgba(255,255,255,0.55)',
align: 'center',
draggable: true,
});
layer.add(companyText);

// Small circle decoration
const circle = new Konva.Circle({
x: badgeX + badgeW - 30,
y: badgeY + badgeH - 30,
radius: 14,
fill: 'rgba(255,255,255,0.2)',
});
layer.add(circle);

// Transformer for selection
const tr = new Konva.Transformer({
rotateEnabled: false,
borderStrokeWidth: 1,
anchorSize: 8,
});
layer.add(tr);

// Select on click
stage.on('click tap', function (e) {
const target = e.target;
if (target === stage || target === badge || target === stripe || target === circle) {
tr.nodes([]);
return;
}
if (target.draggable()) {
tr.nodes([target]);
}
});

// Double-click to edit text
function enableTextEdit(textNode) {
textNode.on('dblclick dbltap', () => {
textNode.hide();
tr.hide();
const textPosition = textNode.absolutePosition();
const stageBox = stage.container().getBoundingClientRect();
const input = document.createElement('input');
input.type = 'text';
input.value = textNode.text();
input.style.position = 'absolute';
input.style.top = stageBox.top + textPosition.y + 'px';
input.style.left = stageBox.left + textPosition.x + 'px';
input.style.width = textNode.width() + 'px';
input.style.fontSize = textNode.fontSize() + 'px';
input.style.fontFamily = textNode.fontFamily();
input.style.textAlign = textNode.align();
input.style.border = '2px solid #3b82f6';
input.style.borderRadius = '4px';
input.style.padding = '2px 4px';
input.style.outline = 'none';
input.style.background = '#fff';
input.style.zIndex = '1000';
document.body.appendChild(input);
input.focus();
input.select();

function finish() {
textNode.text(input.value);
textNode.show();
tr.show();
tr.forceUpdate();
document.body.removeChild(input);
}
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') finish();
});
input.addEventListener('blur', finish);
});
}

enableTextEdit(nameText);
enableTextEdit(roleText);
enableTextEdit(companyText);

// Controls
bgColor.addEventListener('input', () => {
badge.fill(bgColor.value);
});

textColor.addEventListener('input', () => {
nameText.fill(textColor.value);
});

exportBtn.addEventListener('click', () => {
// hide transformer for clean export
tr.nodes([]);
const dataURL = stage.toDataURL({
x: badgeX - 4,
y: badgeY - 4,
width: badgeW + 8,
height: badgeH + 8,
pixelRatio: 3,
});
const link = document.createElement('a');
link.download = 'badge.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});

clearBtn.addEventListener('click', () => {
nameText.text('Your Name');
roleText.text('Job Title');
companyText.text('Company');
bgColor.value = '#3b82f6';
badge.fill('#3b82f6');
textColor.value = '#ffffff';
nameText.fill('#ffffff');
tr.nodes([]);
});