<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
html,
body {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
}
.canvas-box {
display: flex;
position: relative;
}
#canvas {
box-shadow: 0 0 0 1px #ccc;
}
.option {
padding-left: 12px;
}
.item {
display: flex;
align-items: center;
margin-bottom: 6px;
}
.btn-box {
width: 300px;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.cursor {
width: 20px;
height: 20px;
position: absolute;
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 0 2px #000 inset;
background-color: transparent;
pointer-events: none;
opacity: 0;
z-index: 99;
}
</style>
</head>
<body>
<div class="canvas-box">
<div class="cursor"></div>
<canvas id="canvas" style="background-color: rgba(0, 0, 0, 0)"></canvas>
</div>
<div class="option">
<div class="item">
画笔大小(2~50):
<input type="number" name="pencil" id="" min="2" max="50" value="20" />
</div>
<div class="item">
橡皮大小(2~50):
<input type="number" name="eraser" id="" min="2" max="50" value="20" />
</div>
<div class="item">
保存图片质量(0.4~1):
<input
type="number"
name="quality"
id=""
min="0.4"
max="1"
value="0.8"
step="0.1"
/>
</div>
<div class="item">
保存格式:
<input type="radio" data-type="imgType" name="png" id="" checked />png
<input type="radio" data-type="imgType" name="jpeg" id="" />jpeg
<input type="radio" data-type="imgType" name="webp" id="" />webp
</div>
<div class="item">
画布随上传图片缩放:
<input type="radio" data-type="imgRule" name="long" checked />长边
<input type="radio" data-type="imgRule" name="short" />短边
<input type="radio" data-type="imgRule" name="custom" />自适应
</div>
<div class="item">
水印文案:<input
type="text"
name="waterMark"
value="大吉大利,今晚吃鸡"
/>
</div>
<div class="item">
水印文案颜色:<input type="color" name="waterMarkColor" />
</div>
<div class="item">
水印透明度(0.1~1):
<input
type="number"
name="waterMarkOpacity"
id=""
min="0.1"
max="1"
value="0.15"
step="0.01"
/>
</div>
<div class="item">
水印文字大小(10~60):
<input
type="number"
name="fontSize"
id=""
min="12"
max="60"
value="14"
/>
</div>
<div class="item">
水印旋转角度(-360~360):
<input
type="number"
name="rotate"
id=""
min="-360"
max="360"
value="-25"
/>
</div>
<div class="item">
水印水平间距(50~500):
<input type="number" name="x" id="" min="20" max="500" value="100" />
</div>
<div class="item">
水印垂直间距(50~500):
<input type="number" name="y" id="" min="20" max="500" value="100" />
</div>
<div class="item">
水印X轴偏移量(50~500):
<input
type="number"
name="offsetX"
id=""
min="50"
max="300"
value="50"
/>
</div>
<div class="item">
水印Y轴偏移量(50~500):
<input
type="number"
name="offsetY"
id=""
min="50"
max="300"
value="50"
/>
</div>
<div class="item">
水印文案最大宽度(50~500):
<input
type="number"
name="maxWidth"
id=""
min="50"
max="300"
value="200"
/>
</div>
<div class="btn-box">
<button name="usePencil">使用画笔</button>
<button name="useEraser">使用橡皮</button>
<input type="color" name="color" id="" />
<button name="reset">清空画布</button>
<button name="undo">撤销</button>
<button name="redo">恢复</button>
<button name="save">保存</button>
<button name="upload">上传图片</button>
<button name="waterMark">使用水印</button>
<button name="cutout">抠图</button>
</div>
</div>
<input
type="file"
name="file"
accept="image/jpeg,image/png,image/webp"
style="display: none"
/>
<script>
const container = document.querySelector(".canvas-box");
const cursorDom = document.querySelector(".cursor");
const fileDom = document.querySelector('input[type="file"]');
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d", { willReadFrequently: true });
const cursor = document.querySelector(".cursor");
const usePencil = document.querySelector('button[name="usePencil"]');
const useEraser = document.querySelector('button[name="useEraser"]');
const pencilRange = document.querySelector('input[name="pencil"]');
const eraserRange = document.querySelector('input[name="eraser"]');
const qualityRange = document.querySelector('input[name="quality"]');
const colorPick = document.querySelector('input[name="color"]');
const cutout = document.querySelector('button[name="cutout"]');
const waterMarkColorPick = document.querySelector(
'input[name="waterMarkColor"]'
);
const waterMarkOpacityRange = document.querySelector(
'input[name="waterMarkOpacity"]'
);
const imgTypeRadios = document.querySelectorAll(
'input[data-type="imgType"]'
);
const imgRuleRadios = document.querySelectorAll(
'input[data-type="imgRule"]'
);
const fontSizeRange = document.querySelector('input[name="fontSize"]');
const xRange = document.querySelector('input[name="x"]');
const yRange = document.querySelector('input[name="y"]');
const rotateRange = document.querySelector('input[name="rotate"]');
const offsetRangeX = document.querySelector('input[name="offsetX"]');
const offsetRangeY = document.querySelector('input[name="offsetY"]');
const maxWidthRange = document.querySelector('input[name="maxWidth"]');
const reset = document.querySelector('button[name="reset"]');
const undo = document.querySelector('button[name="undo"]');
const redo = document.querySelector('button[name="redo"]');
const save = document.querySelector('button[name="save"]');
const upload = document.querySelector('button[name="upload"]');
const waterMark = document.querySelector('button[name="waterMark"]');
const canvasDefaultSize = 600;
canvas.width = canvasDefaultSize;
canvas.height = canvasDefaultSize;
let pencil = 20;
let eraser = 20;
let quality = 0.8;
let imgType = "png";
let imgRule = "long";
let isPencil = true;
let isEraser = false;
let isDrawingLine = false;
let colors = "#000";
let historyIdx = 0;
let history = [];
let canvasArea = [0, 0, canvas.width, canvas.height];
let text = "大吉大利,今晚吃鸡";
let rotate = -25;
let maxWidth = 200;
let offsetX = 50;
let offsetY = 50;
let gap = [100, 100];
let isCutout=false
const setCursorSize = (size) => {
cursor.style.width = size + "px";
cursor.style.height = size + "px";
};
cutout.onclick=(e)=>{
isCutout=!isCutout
if(isCutout){
cutout.innerHTML='取消抠图'
}else{
cutout.innerHTML='抠图'
}
}
pencilRange.oninput = (e) => {
if (!e.target.value) e.target.value = 20;
pencil = e.target.valueAsNumber;
isPencil && setCursorSize(pencil);
};
eraserRange.oninput = (e) => {
if (!e.target.value) e.target.value = 20;
eraser = e.target.valueAsNumber;
isEraser && setCursorSize(eraser);
};
qualityRange.oninput = (e) => {
if (!e.target.value) e.target.value = 0.8;
quality = e.target.valueAsNumber;
};
fontSizeRange.oninput = (e) => {
if (!e.target.value) e.target.value = 14;
ctx.font = `500 ${e.target.valueAsNumber}px sans-serif`;
};
xRange.oninput = (e) => {
if (!e.target.value) e.target.value = 100;
gap[0] = e.target.valueAsNumber;
};
yRange.oninput = (e) => {
if (!e.target.value) e.target.value = 100;
gap[1] = e.target.valueAsNumber;
};
rotateRange.oninput = (e) => {
if (Number.isNaN(e.target.valueAsNumber)) e.target.value = -25;
rotate = e.target.valueAsNumber;
};
offsetRangeX.oninput = (e) => {
if (Number.isNaN(e.target.valueAsNumber)) e.target.value = 50;
offsetX = e.target.valueAsNumber;
};
offsetRangeY.oninput = (e) => {
if (Number.isNaN(e.target.valueAsNumber)) e.target.value = 50;
offsetY = e.target.valueAsNumber;
};
maxWidthRange.oninput = (e) => {
if (Number.isNaN(e.target.valueAsNumber)) e.target.value = 200;
maxWidth = e.target.valueAsNumber;
};
waterMarkOpacityRange.oninput = (e) => {
const [r, g, b] = hex2Rgb(waterMarkColorPick.value);
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${waterMarkOpacityRange.valueAsNumber})`;
};
fileDom.oninput = (e) => {
const fileReader = new FileReader();
fileReader.readAsDataURL(e.target.files[0]);
fileReader.onload = (e) => {
const img = document.createElement("img");
img.src = e.target.result;
img.onload = () => {
let wh = [img.width, img.height];
let canvasWh = [canvasDefaultSize, canvasDefaultSize];
if (imgRule === "long") {
const ratio = canvasDefaultSize / Math.max(...wh);
wh = [img.width * ratio, img.height * ratio];
} else if (imgRule === "short") {
const ratio = canvasDefaultSize / Math.min(...wh);
wh = [img.width * ratio, img.height * ratio];
} else {
canvasWh = [img.width, img.height];
}
ctx.clearRect(...canvasArea);
canvasArea = [0, 0, ...canvasWh];
canvas.width = canvasWh[0];
canvas.height = canvasWh[1];
ctx.drawImage(img, 0, 0, ...wh);
};
};
};
imgTypeRadios.forEach((el) => {
el.onclick = () => {
imgTypeRadios.forEach((el2) => (el2.checked = false));
el.checked = true;
imgType = el.name;
};
});
imgRuleRadios.forEach((el) => {
el.onclick = () => {
imgRuleRadios.forEach((el2) => (el2.checked = false));
el.checked = true;
imgRule = el.name;
};
});
colorPick.oninput = (e) => (colors = e.target.value);
waterMarkColorPick.oninput = (e) => {
const [r, g, b] = hex2Rgb(e.target.value);
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${waterMarkOpacityRange.valueAsNumber})`;
};
// 撤销
undo.onclick = () => {
historyIdx--;
if (historyIdx <= -1) {
historyIdx = -1;
ctx.clearRect(...canvasArea);
} else {
ctx.putImageData(history[historyIdx], 0, 0);
}
};
// 恢复
redo.onclick = () => {
historyIdx++;
if (historyIdx > history.length - 1) historyIdx = history.length - 1;
else ctx.putImageData(history[historyIdx], 0, 0);
};
save.onclick = () => {
const a = document.createElement("a");
a.href = canvas.toDataURL(`image/${imgType}`, quality);
a.download = `save_${Date.now()}`;
document.body.append(a);
a.click();
a.remove();
};
upload.onclick = () => {
fileDom.value = "";
fileDom.click();
};
usePencil.onclick = () => {
isPencil = true;
isEraser = false;
setCursorSize(pencil);
};
useEraser.onclick = () => {
isPencil = false;
isEraser = true;
setCursorSize(eraser);
};
reset.onclick = () => {
history = [];
historyIdx = -1;
ctx.clearRect(...canvasArea);
canvas.width = canvasDefaultSize;
canvas.height = canvasDefaultSize;
canvasArea = [0, 0, canvas.width, canvas.height];
const [r, g, b] = hex2Rgb(waterMarkColorPick.value);
waterMarkOpacityRange.oninput();
ctx.font = `500 ${fontSizeRange.valueAsNumber}px sans-serif`;
};
waterMark.onclick = () => {
let [x, y] = gap;
let [, , w, h] = canvasArea;
const xLine = Math.ceil((w - offsetX) / x);
const yLine = Math.ceil((h - offsetY) / y);
waterMarkOpacityRange.oninput();
ctx.font = `500 ${fontSizeRange.valueAsNumber}px sans-serif`;
for (let i = 0; i <= xLine; i++) {
const x0 = x * i + offsetX;
for (let j = 0; j <= yLine; j++) {
drawWaterMark(x0, y * j + offsetY);
}
}
};
const drawWaterMark = (x, y) => {
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotate / 180) * Math.PI);
ctx.fillText(text, -ctx.measureText(text).width / 2, 0, maxWidth);
ctx.restore();
};
canvas.onmousedown = (e) => {
// 只允许左键
if (e.button) return;
if (!isPencil && !isEraser) return false;
isDrawingLine = true;
ctx.beginPath();
ctx.globalCompositeOperation = isEraser
? "destination-out"
: "source-over";
ctx.strokeStyle = isEraser ? "#fff" : colors;
if(isCutout){
ctx.globalCompositeOperation = 'destination-out';
ctx.strokeStyle=undefined
}
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = isEraser ? eraser : pencil;
ctx.moveTo(e.offsetX, e.offsetY);
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
canvas.onmousemove = composeCanvasMousemove;
};
canvas.onmouseup = () => {
if (!isPencil && !isEraser) return false;
isDrawingLine = false;
canvas.onmousemove = null;
// 使用了橡皮时,必须存在历史记录,否则不做任何事
if (isEraser && !history.length) return false;
addOneHistory(lineToAlpha());
};
container.onmouseenter = (e) => {
if (!container.contains(e.toElement)) return;
if (isPencil || isEraser) {
// 离开画布时,若正在使用划线功能,重新开启一个路径
if (isDrawingLine) {
ctx.beginPath();
ctx.moveTo(e.offsetX, e.offsetY);
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
}
canvas.style.cursor = "none";
cursorDom.style.opacity = "1";
cursorDom.style.left = e.offsetX + "px";
cursorDom.style.top = e.offsetY + "px";
cursorDom.style.boxShadow = `0 0 0 2px ${
isPencil ? colors : "#ccc"
} inset`;
} else canvas.style.cursor = "default";
};
container.onmousemove = (e) => {
if (isPencil || isEraser) {
cursorDom.style.opacity = "1";
cursorDom.style.left = e.offsetX + "px";
cursorDom.style.top = e.offsetY + "px";
return false;
}
};
container.onmouseout = (e) => {
if (!container.contains(e.fromElement)) return;
cursorDom.style.opacity = "0";
};
const composeCanvasMousemove = (e) => {
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
};
const lineToAlpha = () => {
const imageData = ctx.getImageData(...canvasArea);
return imageData;
};
// 添加一项历史记录
const addOneHistory = (json) => {
if (history.length - 1 !== historyIdx) {
history = history.slice(0, historyIdx + 1);
}
historyIdx = history.push(json) - 1;
};
// hex 2 rgb
const hex2Rgb = (hex) => {
const red = hex.substring(1, 3);
const green = hex.substring(3, 5);
const blue = hex.substring(5, 7);
return [parseInt(red, 16), parseInt(green, 16), parseInt(blue, 16)];
};
// 鼠标抬起时,发现不处于画布内,调用画笔结束
const handleLeaveCanvas = (e) => {
if (!isDrawingLine) return;
if (!container.contains(e.target)) canvas.onmouseup();
};
window.addEventListener("mouseup", handleLeaveCanvas);
setCursorSize(pencil);
</script>
</body>
</html>
标签:canvas,const,target,画笔,ctx,水印,querySelector,document From: https://www.cnblogs.com/ypSharing/p/18553242