Canvas之画图板

Canvas之画图板

js 复制代码
const canvas1 = document.createElement('canvas');
const ctx1 = canvas1.getContext('2d');
const canvas2 = document.createElement('canvas');
const ctx2 = canvas2.getContext('2d');

document.body.append(canvas1, canvas2);
canvas1.width = canvas2.width = 800;
canvas1.height = canvas2.height = 400;
let shapeType = 'line';
let color = '#000000';
let lineWidth = 1;

先设计好基本的画布,以及需要的 css 和 html 。

html 复制代码
<div id="toolbar">
    <ul>
        <li><button class="shapeType active" data="line" id="lineBtn">线条</button></li>
        <li><button class="shapeType" data="rect" id="rectBtn">矩形</button></li>
        <li><button class="shapeType" data="circle" id="circleBtn">圆形</button></li>
        <li><button class="shapeType" data="fill" id="fillBtn">填充</button></li>
        <li><button class="shapeType" data="rubber" id="rubberBtn">橡皮擦</button></li>
        <li>
            <select id="boldBtn">
                <option>1</option>
                <option>2</option>
                <option>4</option>
                <option>8</option>
            </select>
        </li>
        <li>
            <input id="colorBtn" type="color" />
        </li>
        <li><button id="clearBtn">清空画布</button></li>
        <li><button id="saveBtn">保存图片</button></li>
    </ul>
</div>
<div id="container"></div>
js 复制代码
function preBtnEvent(){
    document.querySelectorAll('.shapeType').forEach(btn=>{
        btn.onclick=function(){
            //如果当前按钮已经选中了,则返回
            if(this.classList.contains('active')){
                return;
            }
            //切换选中样式
            document.querySelector('.active').classList.remove('active');
            this.classList.add('active');
            //切换绘制类型
            shapeType = this.getAttribute('data');
        }
    })
    document.querySelector('#boldBtn').onchange=function(){ 
        lineWidth = this.value ;
    }
    document.querySelector('#colorBtn').onchange=function(){
        color = this.value ;
    }
    document.querySelector('#clearBtn').onclick=function(){
        ctx1.clearRect(0,0,canvas.width,canvas.height);
    }
    document.querySelector('#saveBtn').onclick=function(){
        const url = canvas1.toDataURL();
        console.log(url)
        const a = document.createElement('a');
        a.href = url;
        a.download = '保存图片';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
}
preBtnEvent()

接着就是根据 html 来添加相应的点击事件。前面几个可以合并为一个点击事件,只需要在切换属性时切换相关的自定义属性。在保存图片时,先创建 base64 图片数据,然后将锚点的链接指向图片数据,这时当模拟点击时就会触发下载。记得在下载完成后移除锚点。

js 复制代码
//定义基本的图形类
class Shape {
    constructor(type, x, y, ctx) {
        this.type = type;
        this.x = x;
        this.y = y;
        this.ctx = ctx;
        this.ex = x;
        this.ey = y;
        this.points = [];
    }
    draw() {
        switch (this.type) {
            case 'line': break;
            case 'rect': break;
            case 'circle': break;
            case 'rubber': break;
            case 'fill': break;
        }
    }
    drawLine() { }
    drawRect() { }
    drawCircle() { }
    drawRubber() { }
    drawFill() { }
}

//绘制过程
//避免在移出画布时无法绘制
const area = canvas2.getBoundingClientRect();
canvas2.onmousedown = function (e) {
    let x = e.clientX - area.left;
    let y = e.clientY - area.top;
    let shape = new Shape(shapeType, x, y, ctx2);
    //如果是填充不需要移动,直接在画布1绘制
    if (shapeType == 'fill') {
        shape.ctx = ctx1;
        shape.draw();
    } else {
        window.onmousemove = function (e) {
            let ex = e.clientX - area.left;
            let ey = e.clientY - area.top;
            switch (shapeType) {
                case 'line': break;
                case 'rect': break;
                case 'circle': break;
                case 'rubber': break;
            }
        };
        window.onmouseup = function () {
            window.onmousemove = null;
            window.onmouseup = null;
        };
    }

}

在设置基本的类的时候,需要设定类型,位置,画布,过程点,以及连续点的数组。在绘制时不同的点有不同的情况。

在绘制时,为了避免鼠标在移动时移出画布无法绘制的情况不能使用 e.offsetX ,e.offsetY ,只能使用视口相对于画布的坐标。如果类型为填充类型,则不需要移动,直接绘制在画布一上,剩下的情况需要分情况讨论。

如果鼠标抬起时结束相关事件。

js 复制代码
drawLine() { 
    this.ctx.save();
    this.ctx.beginPath();
    setStrokeStyle(this.ctx);
    this.ctx.moveTo(this.x,this.y)
    this.points.forEach((point,i)=>{
        this.ctx.lineTo(point.x,point.y);
    });
    this.ctx.stroke()
    this.ctx.moveTo(this.x,this.y);
}

//如果是填充不需要移动,直接在画布1绘制
if (shapeType == 'fill') {
    shape.draw();
} else {
    ctx2.save()
    setStrokeStyle(ctx2)
    ctx2.beginPath();
    ctx2.moveTo(x, y);
    window.onmousemove = function (e) {
        let ex = e.clientX - area.left;
        let ey = e.clientY - area.top;
        switch (shapeType) {
            case 'line':
                ctx2.lineTo(ex, ey);
                shape.points.push({ x: ex, y: ey });
                ctx2.stroke();
            break;
            case 'rect': break;
            case 'circle': break;
            case 'rubber': break;
        }
    };
    window.onmouseup = function () {
        window.onmousemove = null;
        window.onmouseup = null;
        ctx2.restore();
        shape.draw()
        ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
    };
}

然后可以在画布二上进行绘制。当鼠标移动时,不停绘制线条,并将所有线条的点保存起来,然后在 drawLine 里根据这些点进行画布一的绘制。在鼠标抬起时,进行 画布一 shape 的绘制,然后清空画布二。

这样做主要是为了在画布二绘制时不影响画布一的原来的图形,否则需要不停地重绘所有的图形。

js 复制代码
drawRect() {
    this.ctx.beginPath();
    this.ctx.save();
    setStrokeStyle(this.ctx);
    //x,y取最小的值
    const x = Math.min(this.x, this.ex);
    const y = Math.min(this.y, this.ey);
    //w,h取绝对值
    const w = Math.abs(this.x - this.ex);
    const h = Math.abs(this.y - this.ey);
    this.ctx.strokeRect(x, y, w, h)
    this.ctx.restore();
}
case 'rect':
    shape.ex = ex;
    shape.ey = ey;
    ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
    shape.draw();
    break;
//绘制在画布1上
shape.ctx = ctx1;
shape.draw()

绘制矩形需要在两个画布上绘制,绘制前清空前面的图形。绘制时,x,y 取最小值,w,h 取绝对值,直接生成一个相应的矩形。

js 复制代码
drawCircle(){
    this.ctx.beginPath();
    this.ctx.save();
    setStrokeStyle(this.ctx) ;
    //两个点横纵坐标的一般即为x轴半径 和 y轴半径。
    const r1 = Math.abs(this.x - this.ex) / 2 ;
    const r2 = Math.abs(this.y - this.ey) / 2 ;
    //找到最小的点, 分别加上各自轴的半径,即为圆心点。
    const x = Math.min(this.x , this.ex) + r1 ;
    const y = Math.min(this.y, this.ey) + r2 ;
    if(r1 == r2){
        //正圆
        this.ctx.arc(x,y,r1,0,Math.PI*2);
    }else{
        //椭圆
        this.ctx.ellipse(x,y,r1,r2,0,0,Math.PI*2) ;
    }
    this.ctx.stroke();
    this.ctx.restore();
}

绘制圆和前面类似,只是在绘制时需要加上两个半径,然后根据情况绘制正圆和椭圆。

在后面的 switch 部分需要进行 break 穿透,即两个 case 写在一起,当前面执行之后没有 break 时就会继续往后执行。这些图形除了 line 之外都是先在画布二上画完之后展示在画布一,而 line 不需要清空之前绘制的点。

js 复制代码
drawRubber() {
    this.ctx.beginPath();
    this.ctx.save();
    this.ctx.globalCompositeOperation = "destination-out";
    setStrokeStyle(this.ctx);
    this.ctx.moveTo(this.x, this.y)
    this.points.forEach((point, i) => {
        this.ctx.lineTo(point.x, point.y);
    });
    this.ctx.stroke()
    this.ctx.moveTo(this.x, this.y);
}
if (shapeType == 'rubber') {
    ctx1.save()
    setStrokeStyle(ctx1)
    ctx1.beginPath();
    ctx1.moveTo(x, y);
} else {
    ctx2.save()
    setStrokeStyle(ctx2)
    ctx2.beginPath();
    ctx2.moveTo(x, y);
}
case 'rubber':
    ctx1.globalCompositeOperation = "destination-out";
    ctx1.lineTo(ex, ey);
    shape.points.push({ x: ex, y: ey });
    ctx1.stroke();
    break;

使用橡皮擦和绘制线条类似,只是要直接绘制到画布一中,这样当擦除的时候可以直接显示出来。注意,使用 destination-out 合成效果,会使得保留前面和后面不重叠的部分,也就是实现了擦除的功能。

前面的部分也要分出适合 rubber 的画布。

js 复制代码
function change(x,y) {
    const stack = [[x, y]];
    while (stack.length > 0) {
        const [x, y] = stack.shift();
        if (x < 0 || y < 0 || x > canvas1.width || y > canvas1.height) {
            continue;
        }
        const i = point2Index(imageData, x, y)
        if (baseImageData.data[0] == imageData.data[i] && baseImageData.data[1] == imageData.data[i + 1] && baseImageData.data[2] == imageData.data[i + 2] && baseImageData.data[3] == imageData.data[i + 3]) {
            imageData.data[i] = 255;
            imageData.data[i + 1] = 0;
            imageData.data[i + 2] = 0;
            imageData.data[i + 3] = 255;
        } else {
            continue;
        }
        stack.push([x - 1, y]);
        stack.push([x + 1, y]);
        stack.push([x, y - 1]);
        stack.push([x, y + 1]);
    }
}

填充颜色时需要设置一个栈来保证在向四周扩撒的时候不会超出栈内存。因为四周的坐标始终在 stack 内存里添加。如果超出,就无法添加。注意判断边界条件以及判断四个通道值。

如果四个通道值与点击的点坐标通道值相同,则继续扩散,否则跳过这次循环。

js 复制代码
function hex2rgb(hex) {
    hex = hex.replace('#', '');
    return [
        parseInt(hex.substr(0, 2), 16),
        parseInt(hex.substr(2, 2), 16),
        parseInt(hex.substr(4, 2), 16)
    ];
}

将16进制的颜色转换为rgb形式的数字。

相关推荐
我是ed几秒前
# vue3 实现web网页不同分辨率适配
前端
ze_juejin1 分钟前
前端 SSR(Server-Side Rendering)框架汇总
前端
李大玄7 分钟前
一套通用的 JS 复制功能(保留/去掉换行,兼容 PC/移动端/微信)
前端·javascript·vue.js
Juchecar9 分钟前
Windows下手把手安装Node.js v22.18.0 (LTS) 配置开发环境教程
javascript
小高00716 分钟前
🔍浏览器隐藏的 API,90% 前端没用过,却能让页面飞起
前端·javascript·面试
泉城老铁19 分钟前
vue如何实现行编辑
前端·vue.js
好好好明天会更好19 分钟前
vue项目中pdfjs-dist实现在线浏览PDF文件
前端·vue.js
VisuperviReborn20 分钟前
react native 如何与webview通信
前端·架构·前端框架
然我22 分钟前
Canvas 竟能这么玩?从画张图到做动画,入门到上瘾只需这篇!
前端·javascript·html
三小河24 分钟前
什么是Lottie ,以及前端如何使用
前端