<!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实现抠图,画笔,水印等功能的更多相关文章

  1. 【javascript】html5中使用canvas编写头像上传截取功能

    [javascript]html5中使用canvas编写头像上传截取功能 本人对canvas很是喜欢,于是想仿照新浪微博头像上传功能(前端使用canvas) 本程序目前在谷歌浏览器和火狐浏览器测试可用 ...

  2. 导出HTML5 Canvas图片并上传服务器功能

    这篇文章主要介绍了导出HTML5 Canvas图片并上传服务器功能,文中通过实例代码给大家介绍了HTML5 Canvas转化成图片后上传服务器,代码简单易懂非常不错,具有一定的参考借鉴价值,需要的朋友 ...

  3. canvas实现平铺水印

    欲实现的水印平铺的效果图如下: 从图上看,应该做到以下几点: 文字在X和Y方向上进行平铺: 文字进行了一定的角度的旋转: 水印作为背景,其z-index位置应位于页面内容底部, 即不能覆盖页面主内容: ...

  4. (H5)canvas实现裁剪图片和马赛克功能,以及又拍云上传图片

    1.核心功能 此组件功能包含: 图片裁剪(裁剪框拖动,裁剪框改变大小): 图片马赛克(绘制马赛克,清除马赛克): 图片预览.图片还原(返回原图.返回处理图): 图片上传(获取签名.上传图片). 2.核 ...

  5. canvas与html5实现视频截图功能

    这段时间一直在研究canvas,突发奇想想做一个可以截屏视频的功能,然后把图片拉去做表情包,哈哈哈哈哈哈~~ 制作方法: 1.在页面中加载视频 在使用canvas制作这个截图功能时,首先必须保证页面上 ...

  6. 本图片处理类功能非常之强大可以实现几乎所有WEB开发中对图像的处理功能都集成了,包括有缩放图像、切割图像、图像类型转换、彩色转黑白、文字水印、图片水印等功能

    import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Font; import java.awt.Graphic ...

  7. canvas给图片加水印

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. Android 在Canvas中实现画笔效果(一)--钢笔

    如题: 公司要求做一个涂鸦板,要有钢笔.毛笔等画笔效果,网上搜了很多,可是效果不怎么好,决定自己研究下.废话不多说,进入正题. 首先,赛贝尔曲线弄明白了,在画曲线的过程中就是一条条的向量. 第二,曲线 ...

  9. tp5文件上传实现缩略图+水印的功能(参考)

    public function AddNews(){ $data = Request::instance()->param(); //接收文件 $file = request()->fil ...

  10. canvas刮刮乐和画笔

    这周有点迷茫,不知道干嘛了,一天天就过去了!我在博客右侧公告栏加了qq交流,各位有好的主题,或者有趣的技术,欢迎交流!今天突发奇想,就写了2个h5 canvas的demo玩玩! demo一:刮刮乐 舍 ...

随机推荐

  1. Protues中51单片机按键无法复位(已解决)

    前言 昨晚用 Protues 搭建了 51 的最小系统电路,在实物中好用的复位电路,到仿真里不能正常复位了. 51 单片机是高电平复位,所以在运行时 RST 引脚应该是低电平,但在仿真中 RST 引脚 ...

  2. 《大话设计模式》java实现:第一章-简单工厂模式

    在<大话设计模式>中,示例代码使用C#实现,所以我这里使用Java实现一遍书中的设计模式. 第一章是使用简单工厂实现计算器. 遇到了一个问题:在Operation父类中,我们可以定义两个操 ...

  3. 你还用ES存请求日志?ClickHouse+Vector打造最强Grafana日志分析看板

    为什么要做NGINX日志分析看板 Grafana官网的dashboards有NGINX日志采集到ES数据源的展示看板,也有采集到LOKI数据源的展示看板,唯独没有采集到ClickHouse数据源的展示 ...

  4. Chrome 130 版本新特性& Chrome 130 版本发行说明

    Chrome 130 版本新特性& Chrome 130 版本发行说明 一.Chrome 130 版本浏览器更新 1. 新的桌面提示 Chrome 130 引入了一种新的 Toast 样式,用 ...

  5. “应用程序无法正常启动(0xc000007)”处理办法

    前几天使用非静态方式编译了一个程序,在部分系统上运行提示缺少msvcp140.dll,就从VS2019安装目录找了一个同名文件放在了程序同级目录,程序也可以正常运行了.今天重新打开虚拟机,突然就报了这 ...

  6. 适合才最美:Shiro安全框架使用心得

    大家好,我是 V 哥.Apache Shiro 是一个强大且灵活的 Java 安全框架,专注于提供认证.授权.会话管理和加密功能.它常用于保护 Java 应用的访问控制,特别是在 Web 应用中.相比 ...

  7. Docker启动的centos容器使用systemctl功能

    Failed to get D-Bus connection: Operation not permitted 错误的解决办法 # 启动的时候用如下命令 docker run --privileged ...

  8. html代码新手教学

    HTML 是超文本标记语言(HyperText Markup Language)的缩写,是用来描述网页结构的标记语言.在这篇教学中,我们将介绍一些 HTML 基础知识,帮助新手快速学习并掌握如何编写简 ...

  9. 2个月搞定计算机二级C语言——真题(12)解析

    1. 前言 本篇我们讲解2个月搞定计算机二级C语言--真题12 2. 程序填空题 2.1 题目要求 2.2 提供的代码 #include <stdio.h> #define N 3 int ...

  10. MySQL8.0新特性之增强版逻辑备份恢复

    前言关于MySQL库表逻辑备份恢复,我们主要有以下几种常用的工具: 1.mysqldump:MySQL原生自带的逻辑备份恢复工具,支持整个实例.单个数据库.单张表等的备份与恢复,对于1-10个G的数据 ...