概述

使用canvas做一个画板,代码里涵盖了一些canvas绘图的基本思想,各种工具的类也可以分别提出来用

详细

一、准备工作

1、如果不需要任何修改的话,直接使用dist文件夹内的文件即可

2、如果需要修改,需要安装node

3、打包js运行webpack 打包css运行gulp css 使用dist/index.html预览

4、学习es6与canvas基础知识,api

二、程序实现

文件结构:

retina屏兼容

retina屏会使用多个物理像素渲染一个独立像素,导致一倍图在retina屏幕上模糊,canvas也是这样,所以我们应该把canvas画布的大小设为canvas元素大小的2或3倍。元素大小在css中设置

const canvas = selector('#canvas')
const ctx = canvas.getContext('2d')
const RATIO = 3
const canvasOffset = canvas.getBoundingClientRect()
canvas.width = canvasOffset.width * RATIO
canvas.height = canvasOffset.height * RATIO

坐标系转化

把相对于浏览器窗口的坐标转化为canvas坐标,需要注意的是,如果兼容了retina,需要乘上devicePixelRatio。后面所有出现的坐标,都要通过这个函数转化

function windowToCanvas (x, y) {  return {
x: (x - canvasOffset.left) * RATIO,
y: (y - canvasOffset.top) * RATIO
}
}

不得不提的是,《HTML5 Canvas核心技术》有一个相同的函数,但是书上那个是错的(也有可能我看的那本是假书)

获取touch点的坐标

function getTouchPosition (e) {
let touch = e.changedTouches[0]
return windowToCanvas(touch.clientX, touch.clientY)
}

画布状态的储存和恢复

进行绘图操作时,我们会频繁设置canvas绘图环境的属性(线宽,颜色等),大多数情况下我们只是临时设置,比如画蓝色的线段,又要画一个红色的正方形,为了不影响两个绘图操作,我们需要在每次绘制时,先保存环境属性(save),绘图完毕后恢复(restore)

ctx.save()
ctx.fillStyle = "#333"
ctx.strokeStyle = "#666"
ctx.restore()

绘制表面的储存与恢复

主要用于临时性的绘图操作,比如用手指拖出一个方形时,首先要在touchstart事件里储存拖动开始时的绘制表面(getImageData),touchmove的事件函数中,首先要先恢复touch开始时的绘图表面(putImageData),再根据当前的坐标值画出一个方形,继续拖动时,刚才画出的方形会被事件函数的恢复绘图表面覆盖掉,在重新绘制一个方形,所以无论怎么拖动,我们看到的只是画了一个方形,下面是画板demo中方形工具的类

// 工具基础 宽度,颜色,是否在绘画中,是否被选中
class Basic {
constructor (width = RATIO, color = '#000') {
this.width = width
this.color = color
this.drawing = false
this.isSelect = false
}
}
class Rect extends Basic {
constructor (width = RATIO, color = '#000') {
super(width, color) this.startPosition = {
x: 0,
y: 0
}
this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight)
}
begin (loc) {
this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) //在这里储存绘图表面
saveImageData(this.firstDot)
Object.assign(this.startPosition, loc)
ctx.save() // 储存画布状态
ctx.lineWidth = this.width
ctx.strokeStyle = this.color
}
draw (loc) {
ctx.putImageData(this.firstDot, 0, 0) //恢复绘图表面,并开始绘制方形
const rect = {
x: this.startPosition.x <= loc.x ? this.startPosition.x : loc.x,
y: this.startPosition.y <= loc.y ? this.startPosition.y : loc.y,
width: Math.abs(this.startPosition.x - loc.x),
height: Math.abs(this.startPosition.y - loc.y)
}
ctx.beginPath()
ctx.rect(rect.x, rect.y, rect.width, rect.height)
ctx.stroke()
}
end (loc) {
ctx.putImageData(this.firstDot, 0, 0)
const rect = {
x: this.startPosition.x <= loc.x ? this.startPosition.x : loc.x,
y: this.startPosition.y <= loc.y ? this.startPosition.y : loc.y,
width: Math.abs(this.startPosition.x - loc.x),
height: Math.abs(this.startPosition.y - loc.y)
}
ctx.beginPath()
ctx.rect(rect.x, rect.y, rect.width, rect.height)
ctx.stroke()
ctx.restore() //恢复画布状态
}
bindEvent () {
canvas.addEventListener('touchstart', (e) => {
e.preventDefault()
if (!this.isSelect) {
return false
}
this.drawing = true
let loc = getTouchPosition(e)
this.begin(loc)
})
canvas.addEventListener('touchmove', (e) => {
e.preventDefault()
if (!this.isSelect) {
return false
}
if (this.drawing) {
let loc = getTouchPosition(e)
this.draw(loc)
}
})
canvas.addEventListener('touchend', (e) => {
e.preventDefault()
if (!this.isSelect) {
return false
}
let loc = getTouchPosition(e)
this.end(loc)
this.drawing = false
})
}
}

椭圆的绘制方法(均匀压缩法)

原理是在压缩过的坐标系中绘制一个圆形,那看起来就是一个椭圆了。因为是通过拖动绘制椭圆,所以在我们拖动时,必然拖出了一个方形,那其实就是以方形的中心为圆心,较长边的一半为半径画圆,这个圆要画在压缩过的坐标系中,压缩比例就是较窄边与较长边的比,圆心的坐标也要根据压缩比例做坐标变换,圆形工具类代码如下

class Round extends Basic{
constructor (width = RATIO, color = '#000') {
super(width, color) this.startPosition = {
x: 0,
y: 0
}
this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight)
}
drawCalculate (loc) {
ctx.save()
ctx.lineWidth = this.width
ctx.strokeStyle = this.color
ctx.putImageData(this.firstDot, 0, 0) //恢复绘图表面
const rect = {
width: loc.x - this.startPosition.x,
height: loc.y - this.startPosition.y
} // 计算方形的宽高(带有正负值)
const rMax = Math.max(Math.abs(rect.width), Math.abs(rect.height)) // 选出较长边
rect.x = this.startPosition.x + rect.width / 2 // 计算压缩前的圆心坐标
rect.y = this.startPosition.y + rect.height / 2
rect.scale = {
x: Math.abs(rect.width) / rMax,
y: Math.abs(rect.height) / rMax
} // 计算压缩比例
ctx.scale(rect.scale.x, rect.scale.y)
ctx.beginPath()
ctx.arc(rect.x / rect.scale.x, rect.y / rect.scale.y, rMax / 2, 0, Math.PI * 2)
ctx.stroke()
ctx.restore()
}
begin (loc) {
this.firstDot = ctx.getImageData(0, 0, canvasWidth, canvasHeight) //储存绘图表面
saveImageData(this.firstDot)
Object.assign(this.startPosition, loc)
}
draw (loc) {
this.drawCalculate(loc)
}
end (loc) {
this.drawCalculate(loc)
}
bindEvent () {
canvas.addEventListener('touchstart', (e) => {
e.preventDefault()
if (!this.isSelect) {
return false
}
this.drawing = true
let loc = getTouchPosition(e)
this.begin(loc)
})
canvas.addEventListener('touchmove', (e) => {
e.preventDefault()
if (!this.isSelect) {
return false
}
if (this.drawing) {
let loc = getTouchPosition(e)
this.draw(loc)
}
})
canvas.addEventListener('touchend', (e) => {
e.preventDefault()
if (!this.isSelect) {
return false
}
let loc = getTouchPosition(e)
this.end(loc)
this.drawing = false
})
}
}

撤销操作

上述例子中都有个 saveImageData() 函数,这个函数是把当前绘图表面储存在一个数组中,点击撤销的时候用于恢复上一步的绘图表面

const lastImageData = []
function saveImageData (data) {
(lastImageData.length == 5) && (lastImageData.shift()) // 上限为储存5步,太多了怕挂掉
lastImageData.push(data)
}
document.getElementById("cancel").addEventListener('click', () => {
if(lastImageData.length < 1) return false
ctx.putImageData(lastImageData[lastImageData.length - 1], 0, 0)
lastImageData.pop()
})

三、运行效果

点击目录里index.html

四、其他补充

还有一些简单地工具如线宽选择,调色板就不叙述了,有问题欢迎评论

注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权

使用canvas制作一个移动端画板的更多相关文章

  1. 使用canvas制作的移动端color picker

    使用canvas制作的移动端color picker 项目演示地址(用手机或者手机模式打开) 我在另一个中demo,需要用到color picker,但是找不到我需要的移动端color picker, ...

  2. 怎样用HTML5 Canvas制作一个简单的游戏

    原文连接: How To Make A Simple HTML5 Canvas Game 自从我制作了一些HTML5游戏(例如Crypt Run)后,我收到了很多建议,要求我写一篇关于怎样利用HTML ...

  3. [译]怎样用HTML5 Canvas制作一个简单的游戏

    这是我翻译自LostDecadeGames主页的一篇文章,原文地址:How To Make A Simple HTML5 Canvas Game. 下面是正文: 自从我制作了一些HTML5游戏(例如C ...

  4. 用canvas画一个的小画板(PC端移动端都能用)

    前言 本篇的内容主要包括: canvas标签简介 画板的功能简介 画板的JS部分(包括:1.获取画布 2.使画板全屏幕显示且自适应 3.如何绘制直线 4.绘画时的三种状态(鼠标点击.移动.离开)5.画 ...

  5. HTML5 canvas制作童年的回忆大风车

    今天看到一篇CSS3写的大风车http://www.cnblogs.com/yaojaa/archive/2013/01/30/2882521.html,感觉CSS3太神奇了,这在以前用CSS是想都不 ...

  6. canvas画画板,canvas画五角星,canvas制作钟表、Konva写钟表

    制作一个画画板,有清屏有橡皮擦有画笔可以换颜色 style样式 <head> <meta charset="UTF-8"> <title>画画板 ...

  7. 使用canvas制作在线画板

    canvas绘图的强大功能,让人前仆后继的去研究它.代码全部加起来不足百行.还用到了h5中的<input type="color"/>和<input type=& ...

  8. 用Phaser来制作一个html5游戏——flappy bird (一)

    Phaser是一个简单易用且功能强大的html5游戏框架,利用它可以很轻松的开发出一个html5游戏.在这篇文章中我就教大家如何用Phaser来制作一个前段时间很火爆的游戏:Flappy Bird,希 ...

  9. Canvas制作的下雨动画

    简介 在codepen上看到一个Canvas做的下雨效果动画,感觉蛮有意思的.就研究了下,这里来分享下,实现技巧.效果可以见下面的链接. 霓虹雨: http://codepen.io/natewile ...

随机推荐

  1. 阿里云云盾抗下全球最大DDoS攻击(5亿次请求,95万QPS HTTPS CC攻击) ,阿里百万级QPS资源调度系统,一般的服务器qps多少? QPS/TPS/并发量/系统吞吐量

    阿里云云盾抗下全球最大DDoS攻击(5亿次请求,95万QPS HTTPS CC攻击) 作者:用户 来源:互联网 时间:2016-03-30 13:32:40 安全流量事件https互联网资源 摘要:  ...

  2. Linux C Socket编程发送结构体、文件详解及实例

    利用Socket发送文件.结构体.数字等,是在Socket编程中经常需要用到的.由于Socket只能发送字符串,所以可以使用发送字符串的方式发送文件.结构体.数字等等. 本文:http://www.c ...

  3. iOS开发-UI基础Demo

    现在更多的学习资料都是xCode4.X的,发现xCode6.1还是很多东西,如果有正在学习iOS开发的可以通过Demo简单了解下iOS的UI开发~ 1.新建单视图文件: 2.新建项目名称,语言选择OC ...

  4. 让两个DIV的高度隐式同步

    以前遇到两个相临近的块,高度要一样,但是内容多少又不定时,我都是通过把这两块封装在TD里面实现,但今天在CSDN上面看到有人要通过JS来实现这个,我尝试了一下.http://topic.csdn.ne ...

  5. JDBC具体解释(2)

    1.载入驱动程序. 注冊驱动程序有多方法,Class.forName();是一种显式地载入.当一个驱动程序类被Classloader装载后,在溶解的过程中,DriverManager会注冊这个驱动类的 ...

  6. 以log(n)的时间求矩形内的点

    设想这么一个简单的问题,在一个平面上有n个点,给定一个矩形,问位于矩形内的点有哪些. 这个问题的简单思路非常简单,每次遍历所有点,看其是否在给定的矩形中.时间复杂度呢?单次查询的时间就是一次遍历的时间 ...

  7. Cognos两种建模工具对于复杂日期维度的处理比较(上)

    众所周知,在数据仓库中,日期维度是相当重要的.对数据分析的过程中可以从不同的角度去分析,比如按照下面的日期层次去分析数据. 年-季度-月-日 年-月-日 年-周-日 本示例将利用简单的商品销售分析的d ...

  8. [Node.js]27. Level 5: URL Building & Doing the Request

    Let's create a page which calls the twitter search API and displays the last few results for Code Sc ...

  9. JVM总结-内存监视手段及各区域内存溢出解决

    转载:https://blog.csdn.net/xuqu_volition/article/details/53786096 引言 本文仅关注一些常见的虚拟机内存监视手段,以及JVM运行时数据区各个 ...

  10. SqlServer日常积累(一)

    1. 将一个表的数据插入另一个表 情况一:目标表已存在 (1)如果2张表的字段一致,并且希望插入全部数据,可以用这种方法: Insert Into 目标表 Select * From 来源表; --例 ...