Canvas
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.pickerColor{
position: fixed;
left: 50%;
top: 10%;
transform: translate(-50%,0%);
height: 25px;
}
canvas{
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: 800px;
height: 500px;
background-color: #ccc;
}
.btnGroup{
position: fixed;
left: 60%;
top: 10%;
transform: translate(-50%,0%);
height: 25px;
}
/* .backAllbtn{
position: fixed;
left: 70%;
top: 10%;
transform: translate(-50%,0%);
height: 25px;
} */
#shapeOptions{
position: fixed;
left: 40%;
top: 10%;
transform: translate(-50%,0%);
height: 25px;
}
</style>
</head>
<body>
<p>因为是使用无限循环来绘制图形及通过endX,endY实时拖动位置,所以无法跟笔画共存</p>
<ul>
<li>要么就纯笔画</li>
<li>使用图形,笔画就会消失</li>
</ul>
<div>
<select id="shapeOptions" name="shape">
<option value="rect">矩形</option>
<option value="arc" >圆形</option>
<option value="square" >正方形</option>
<option value="write" selected>手写</option>
</select>
<input class="pickerColor" type="color">
<div class="btnGroup">
<button class="backbtn" disabled >回退</button>
<button class="backAllbtn" disabled >清除所有</button>
<button class="save" >保存图片</button>
</div>
</div>
<canvas></canvas>
</body>
</html>
<script>
let cvs = document.querySelector('canvas');
let ctx = cvs.getContext('2d');
let selectColor = document.querySelector('input')
let backbtn = document.querySelector('.backbtn')
let backAllbtn = document.querySelector('.backAllbtn')
let savebtn = document.querySelector('.save')
let shapeStyle = document.querySelector('#shapeOptions');
let isRender = false;
let isWrite = false;
const shapes = [];
const undoStack = [];
function init(){
cvs.width = 800 * devicePixelRatio;
cvs.height = 500 * devicePixelRatio;
}
init()
// 监听数组变化
function ShapeHandle(arr,even){
Object.defineProperty(arr,even, {
configurable: true,
enumerable: false,
writable: true,
value: function () {
switch (even) {
case 'push':
Array.prototype.push.apply(this, arguments);
break;
case 'pop':
Array.prototype.pop.apply(this, arguments);
break;
default:
break;
}
if(shapes.length > 0 || undoStack.length > 0 ){
backbtn.disabled = false;
backAllbtn.disabled = false;
}
else{
backbtn.disabled = true;
backAllbtn.disabled = true;
}
}
});
}
new ShapeHandle(shapes,'push')
new ShapeHandle(shapes,'pop')
new ShapeHandle(undoStack,'push')
new ShapeHandle(undoStack,'pop')
// 保存
savebtn.addEventListener('click',function(){
var dataURL = cvs.toDataURL('image/png');
var link = document.createElement('a');
link.download = 'myCanvas.png';
link.href = dataURL;
link.click();
})
// 回退
backbtn.addEventListener('click',function(){
let lastShape = shapes[shapes.length - 1];
if(lastShape.type && lastShape.type == 'write'){
if (undoStack.length > 1) {
if(undoStack.length == 1){
undoStack.length = 0;
}
else{
undoStack.pop()
}
ctx.putImageData(undoStack[undoStack.length - 1], 0, 0);
shapes.pop()
}
}
else{
shapes.pop()
isRender = true
requestAnimationFrame(()=>{
isRender = false
})
}
})
// 清除所有
backAllbtn.addEventListener('click',function(){
isRender = true
shapes.length = 0
requestAnimationFrame(()=>{
isRender = false
backbtn.disabled = true;
backAllbtn.disabled = true;
})
})
cvs.onmousedown = (e) =>{
// console.log(e)
// 获取当前点击的坐标 即 鼠标点击相对视口的距离 - canvas相对视口的距离
const rect = cvs.getBoundingClientRect()
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const shape = getShape(clickX,clickY)
isRender = true;
if(shape){
const { stratX,stratY,endX,endY } = shape;
window.onmousemove = (e)=>{
const disX = e.clientX - rect.left - clickX;
const disY = e.clientY - rect.top - clickY;
shape.stratX = stratX + disX;
shape.endX = endX + disX;
shape.stratY = stratY + disY;
shape.endY = endY + disY
}
}
else{
let shape = null;
if(shapeStyle.value == 'write'){
isWrite = true;
}
// 创建
switch (shapeStyle.value) {
case 'rect':
shape = new Rectangle(selectColor.value,clickX,clickY)
break;
case 'square':
shape = new Squareangle(selectColor.value,clickX,clickY)
break;
case 'arc':
shape = new Arcangle(selectColor.value,clickX,clickY)
break;
case 'write':
shape = new Writeangle(selectColor.value,clickX,clickY)
undoStack.push(ctx.getImageData(0, 0, cvs.width, cvs.height));
break;
default:
alert('还在开发中')
return;
break;
}
// if(shapeStyle.value != 'write') ;
shapes.push(shape)
// 监听鼠标移动事件和抬起
window.onmousemove = (e)=>{
shape.endX = e.clientX - rect.left;
shape.endY = e.clientY - rect.top;
if(shape.type && shape.type === 'write'){
shape.draw();
}
}
}
window.onmouseup = ()=>{
window.onmousemove = null;
window.onmouseup = null;
isRender = false;
isWrite = false;
}
}
// 矩形构造函数
class Rectangle{
constructor(color,stratX,stratY){
this.color = color;
this.stratX = stratX;
this.stratY = stratY;
// 一开始结束坐标和开始坐标是一样的,拖动的时候更改结束坐标
this.endX = stratX;
this.endY = stratY;
this.color = color;
}
get minX(){
return Math.min(this.stratX,this.endX)
}
get maxX(){
return Math.max(this.stratX,this.endX)
}
get minY(){
return Math.min(this.stratY,this.endY)
}
get maxY(){
return Math.max(this.stratY,this.endY)
}
draw(){
ctx.beginPath();
// 右下左上
ctx.moveTo(this.minX * devicePixelRatio,this.minY * devicePixelRatio);
ctx.lineTo(this.maxX * devicePixelRatio,this.minY * devicePixelRatio);
ctx.lineTo(this.maxX * devicePixelRatio,this.maxY * devicePixelRatio);
ctx.lineTo(this.minX * devicePixelRatio,this.maxY * devicePixelRatio);
ctx.lineTo(this.minX * devicePixelRatio,this.minY * devicePixelRatio);
ctx.fillStyle = this.color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineCap = 'round';
ctx.lineWidth = 3 * devicePixelRatio;
ctx.stroke()
}
// 点击是否是已创建的图形区域
inInside(x,y){
return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
}
}
// 正方形构造函数
class Squareangle{
constructor(color,stratX,stratY){
this.color = color;
this.stratX = stratX;
this.stratY = stratY;
// 一开始结束坐标和开始坐标是一样的,拖动的时候更改结束坐标
this.endX = stratX;
this.endY = stratY;
this.color = color;
}
get minX(){
return Math.min(this.stratX,this.endX)
}
get maxX(){
return Math.max(this.stratX,this.endX)
}
get minY(){
return Math.min(this.stratY,this.endY)
}
get maxY(){
return Math.max(this.stratY,this.endY)
}
get sameAsSide(){
return this.maxX - this.minX;
}
draw(){
ctx.beginPath();
// 右下左上
ctx.moveTo(this.minX * devicePixelRatio,this.minY * devicePixelRatio);
ctx.lineTo((this.minX + this.sameAsSide) * devicePixelRatio,this.minY * devicePixelRatio)
ctx.lineTo((this.minX + this.sameAsSide) * devicePixelRatio,(this.minY + this.sameAsSide) * devicePixelRatio)
ctx.lineTo((this.minX) * devicePixelRatio,(this.minY + this.sameAsSide) * devicePixelRatio)
ctx.lineTo((this.minX) * devicePixelRatio,(this.minY) * devicePixelRatio)
ctx.fillStyle = this.color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineCap = 'round';
ctx.lineWidth = 3 * devicePixelRatio;
ctx.stroke()
}
// 点击是否是已创建的图形区域
inInside(x,y){
return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
}
}
// 圆形构造函数
class Arcangle{
constructor(color,stratX,stratY){
this.color = color;
this.stratX = stratX;
this.stratY = stratY;
// 一开始结束坐标和开始坐标是一样的,拖动的时候更改结束坐标
this.endX = stratX;
this.endY = stratY;
this.color = color;
}
get minX(){
return Math.min(this.stratX,this.endX)
}
get maxX(){
return Math.max(this.stratX,this.endX)
}
get minY(){
return Math.min(this.stratY,this.endY)
}
get maxY(){
return Math.max(this.stratY,this.endY)
}
get sameAsSide(){
return this.maxX - this.minX;
}
draw(){
ctx.beginPath();
ctx.arc(this.minX * devicePixelRatio,this.minY * devicePixelRatio,this.sameAsSide * devicePixelRatio,0,2 * Math.PI,false)
ctx.fillStyle = this.color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineCap = 'round';
ctx.lineWidth = 3 * devicePixelRatio;
ctx.stroke()
}
// 点击是否是已创建的图形区域
inInside(x,y){
if (Math.sqrt((x - this.minX) ** 2 + (y - this.minY) ** 2) <= this.sameAsSide) {
return true;
} else {
return false;
}
}
}
// 手写
class Writeangle{
constructor(color,stratX,stratY){
this.color = color;
this.stratX = stratX;
this.stratY = stratY;
// 一开始结束坐标和开始坐标是一样的,拖动的时候更改结束坐标
this.endX = stratX;
this.endY = stratY;
this.color = color;
this.type = 'write'
}
get minX(){
return Math.min(this.stratX,this.endX)
}
get maxX(){
return Math.max(this.stratX,this.endX)
}
get minY(){
return Math.min(this.stratY,this.endY)
}
get maxY(){
return Math.max(this.stratY,this.endY)
}
draw(){
ctx.beginPath();
ctx.moveTo(this.stratX * devicePixelRatio,this.stratY * devicePixelRatio)
ctx.lineTo(this.endX * devicePixelRatio,this.endY * devicePixelRatio)
ctx.lineCap = 'round';
ctx.strokeStyle = this.color;
ctx.lineWidth = 3 * devicePixelRatio;
ctx.stroke();
this.stratX = this.endX;
this.stratY = this.endY;
}
// 点击是否是已创建的图形区域
inInside(x,y){
return false;
}
}
function getShape(x,y){
for(let i = shapes.length - 1; i >= 0 ;i--){
const s = shapes[i];
if(s.inInside(x,y)){
return s;
}
}
return null
}
// 不断的清空、创建
function draw(){
const drowAuto = requestAnimationFrame(draw); // 会无限调用该函数
if(isRender){
if(!isWrite){
ctx.clearRect(0,0,cvs.width,cvs.height);
}
try{
// 不能用这个重新绘制write
for(const s of shapes){
if(s.type == undefined && s.type != 'write'){
s.draw()
}
}
}
catch(e){
console.log(e)
}
}
// cancelAnimationFrame(drowAuto);
}
draw()
window.addEventListener('beforeunload',function(){
backbtn.removeEventListener('click')
})
</script>
简单的一个可以拖动和创建不同图形和手写板的效果,
但是手写板和图形不能共存,还请懂得小伙伴指教~!