https://blog.csdn.net/m0_56069948/article/details/122285951
https://blog.csdn.net/m0_56069948/article/details/122285941
目录* Canvas学习
+ 一、 Canvas概述
- 1.1 Hello world
- 1.2 Canvas的像素化
- 1.3 Canvas的动画思想
- 1.4 面向对象思维实现canvas动画
+ 二、Canvas的绘制功能
- 2.1 绘制矩形
- 2.2 绘制路径
- 2.3 绘制圆弧
- 2.4 炫彩小球
- 2.5 透明度
- 2.6 小球连线
- 2.7 线型
* lineWidth
* lineCap
* lineJoin
* setLineDash
* lineDashOffset
- 2.8 文本
- 2.9 渐变 Gradients
- 2.10 阴影
+ 三、使用图片
+ 四、资源管理器
+ 4.1 获取对象中属性的长度
+ 4.2 管理器的实现
+ 五、变形
- 5.1 移动translate
- 5.2 旋转 rotate
- 5.3 缩放 scale
- 5.4 变形 transform
- 5.5 滚动的车轮案例
+ 六、合成与裁剪
- 6.1 globalCompositeOperation
- 6.2 裁剪路径
- 6.3 刮刮乐案例
+ 七、总结
canvas 读音 /ˈkænvəs/, 即kæn və s(看我死).
学习的目的主要是为了网状关系拓扑图形的绘制.
推荐文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial
canvas是用来绘制图形的.它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。
长久以来, web上的动画都是Flash. 比如动画广告\ 游戏等等, 基本都是Flash 实现的. Flash目前都被禁用了, 而且漏洞很多, 重量很大, 需要安装Adobe Flash Player, 而且也会卡顿和不流畅等等.
canvas是HTML5提出的新标签,彻底颠覆了Flash的主导地位。无论是广告、游戏都可以使用canvas实现。
Canvas 是一个轻量级的画布, 我们使用Canvas进行JS的编程,不需要增加额外的组件,性能也很好,不卡顿,在手机中也很流畅。
我们可以在页面中设置一个canvas标签
| |
canvas的标签属性只有两个,width和height,表示的是canvas画布的宽度和高度,不要用css来设置,而是用属性来设置,画布会失真变形。
标签的innerContent是用来提示低版本浏览器(IE6、7、8)并不能正常使用canvas,高版本的浏览器是看不到canvas标签内部的文字的。
| | // 得到canvas的画布 |
| | const myCanvas:HTMLCanvasElement = document.getElementById("main\_canvas") as HTMLCanvasElement// 返回某种类型的HTMLElement |
| | |
| | // 得到画布的上下文,上下文有两个,2d的上下文和3d的上下文 |
| | // 所有的图像绘制都是通过ctx属性或者是方法进行设置的,和canvas标签没有关系了 |
| | const ctx = myCanvas.getContext("2d") |
| | if(ctx !== null) { |
| | // 设置颜色 |
| | ctx.fillStyle = 'green' |
| | // 绘制矩形 |
| | ctx.fillRect(100, 100, 200, 50) |
| | } |
通过上面的代码我们发下canvas本质上就是利用代码在浏览器的页面上进行画画,比如上面的代码fillRect就代表在页面中绘制矩形,一共四个属性,前两个100,100代表(x, y), 即填充起始位置,200代表宽,50代表高,单位都是px。
我们用canvas绘制了一个图形,一旦绘制成功了,canvas就像素化了他们。canvas没有能力,从画布上再次得到这个图形,也就是我们没有能力去修改已经在画布上的内容,这个就是canvas比较轻量的原因,Flash重的原因之一就有它可以通过对应的api得到已经上“画布”的内容然后再次绘制
如果我们想要这个canvas图形移动,必须按照:清屏——更新——渲染的逻辑进行编程。总之,就是重新再画一次
要使用面向对象的思想来创建动画。
canvas上画布的元素,就被像素化了,所以不能通过style.left方法进行修改,而且必须要重新绘制。
| | // 得到画布 |
| | const myCanvas:HTMLCanvasElement = document.getElementById("main\_canvas") as HTMLCanvasElement |
| | |
| | // 获取上下文 |
| | const ctx = myCanvas.getContext("2d") |
| | |
| | if(ctx !== null) { |
| | // 设置颜色 |
| | ctx.fillStyle = "blue" |
| | // 初始信号量 |
| | let left:number = -200 |
| | // 动画过程 |
| | setInterval(() => { |
| | // 清除画布,0,0代表从什么位置开始,600,600代表清除的宽度和高度 |
| | ctx.clearRect(0,0,600,600) |
| | // 更新信号量 |
| | left++ |
| | // 如果已经走出画布,则更新信号量为初始位置 |
| | if(left > 600) { |
| | left = -200 |
| | } |
| | ctx.fillRect(left, 100, 200, 200) |
| | },10) |
| | } |
实际上,动画的生成就是相关静态画面连续播放,这个就是动画的过程。我们把每一次绘制的静态话面叫做一帧,时间的间隔(定时器的间隔)就是表示的是帧的间隔。
因为canvas不能得到已经上屏的对象,所以我们要维持对象的状态。在canvas动画重,我们都是用面向对象来进行编程,因为我们可以使用面向对象的方式来维持canvas需要的属性和状态;
| | // 得到画布 |
| | const myCanvas:HTMLCanvasElement = document.getElementById("main\_canvas") as HTMLCanvasElement |
| | |
| | // 获取上下文 |
| | const ctx = myCanvas.getContext("2d") |
| | |
| | class Rect { |
| | // 维护状态 |
| | constructor( |
| | public x: number, |
| | public y: number, |
| | public w: number, |
| | public h: number, |
| | public color: string |
| | ) { |
| | } |
| | // 更新的方法 |
| | update() { |
| | this.x++ |
| | if(this.x > 600) { |
| | this.x = -200 |
| | } |
| | } |
| | // 渲染 |
| | render(ctx: CanvasRenderingContext2D) { |
| | // 设置颜色 |
| | ctx.fillStyle = this.color |
| | // 渲染 |
| | ctx.fillRect(this.x, this.y, this.w, this.h) |
| | } |
| | } |
| | |
| | // 实例化 |
| | let myRect1: Rect = new Rect(-100, 200, 100, 100, 'purple') |
| | let myRect2: Rect = new Rect(-100, 400, 100, 100, 'pink') |
| | |
| | // 动画过程 |
| | |
| | // 更新的办法 |
| | setInterval(() => { |
| | // 清除画布,0,0代表从什么位置开始,600,600代表清除的宽度和高度 |
| | if(ctx !== null) { |
| | // 清屏 |
| | ctx.clearRect(0,0,600,600) |
| | // 更新方法 |
| | myRect1.update() |
| | myRect2.update() |
| | // 渲染方法 |
| | myRect1.render(ctx) |
| | myRect2.render(ctx) |
| | } |
| | },10) |
动画过程在主定时器重,每一帧都会调用实例的更新和渲染方法。
填充一个矩形:
| | if(ctx !== null) { |
| | // 设置颜色 |
| | ctx.fillStyle = 'green' |
| | // 填充矩形 |
| | ctx.fillRect(100, 100, 300, 50) |
| | } |
参数含义:分别代表填充坐标x、填充坐标y、矩形的高度和宽度。
绘制矩形边框,和填充不同的是绘制使用的是strokeRect, 和strokeStyle实现的
| | if (ctx !== null) { |
| | // 设置颜色 |
| | ctx.strokeStyle = 'red' |
| | // 绘制矩形 |
| | ctx.strokeRect(300, 100, 100, 100) |
| | } |
参数含义:分别代表绘制坐标x、绘制坐标y、矩形的高度和宽度。
清除画布,使用clearRect
| | // 擦除画布内容 |
| | btn3.onclick = () => { |
| | if (ctx !== null) { |
| | ctx.clearRect(0, 0, 600, 600) |
| | } |
| | } |
参数含义:分别代表擦除坐标x、擦除坐标y、擦除的高度和擦除的宽度。
绘制路径的作用是为了设置一个不规则的多边形状态
路径都是闭合的,使用路径进行绘制的时候需要既定的步骤:
| | // 创建一个路径 |
| | ctx.beginPath() |
| | // 1. 移动绘制点 |
| | ctx.moveTo(100, 100) |
| | // 2. 描述行进路径 |
| | ctx.lineTo(200, 200) |
| | ctx.lineTo(400, 180) |
| | ctx.lineTo(380, 50) |
| | // 3. 封闭路径 |
| | ctx.closePath(); |
| | |
| | // 4. 绘制这个不规则的图形 |
| | ctx.strokeStyle = 'red' |
| | ctx.stroke() |
| | ctx.fillStyle = 'orange' |
| | ctx.fill() |
总结我们要绘制一个图形,要按照顺序
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x, y)
ctx.lineTo(x, y)
ctx.closePath()
ctx.stroke()
ctx.fill()
此时我们发现之前我们在学习绘制矩形的时候使用的是fillRect
和strokeRect
,但是实际上fill
和stroke
也是具有绘制填充功能的
stroke()
: 通过线条来绘制图形轮廓。
fill()
: 通过填充路径的内容区域生成实心的图形。
我们在绘制路径的时候选择不关闭路径(closePath),这个时候会实现自封闭现象(只针对fill,stroke不生效)
arc(x, y, radius, startAngle, endAngle, anticlockwise)
画一个以(x, y)为圆心的以radius为半径的圆弧(圆), 从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针false, true为逆时针)来生成。
| | // 创建一个路径 |
| | ctx.beginPath() |
| | // 移动绘制点 |
| | // ctx.arc(200, 200, 100, 0, 2 * Math.PI, false) |
| | ctx.arc(200, 200, 100, 0, 2 * 3.14, false) |
| | |
| | ctx.stroke() |
圆弧也是绘制路径的一种,也需要beginPath和stroke.
参数的含义:200, 200代表的是起始x,y坐标,100代表的是圆心半径,0和1代表的是开始和结束位置,单位如果是数字,代表的是一个圆弧的弧度(一个圆的弧度是Math.PI * 2, 约等于7个弧度),所以在顺时针的情况下,如果如果两个参数的差为7,则代表绘制一个圆。
| | // 得到画布 |
| | const myCanvas: HTMLCanvasElement = document.getElementById("main\_canvas") as HTMLCanvasElement |
| | |
| | // 获取上下文 |
| | const ctx = myCanvas.getContext("2d") |
| | |
| | class Ball { |
| | color: string // 小球的颜色 |
| | r: number // 小球的半径 |
| | dx: number // 小球在x轴的运动速度/帧 |
| | dy: number // 小球在y轴的运动速度/帧 |
| | constructor(public x: number, public y: number) { |
| | // 设置随机颜色 |
| | this.color = this.getRandomColor() |
| | // 设置随机半径[1, 101) |
| | this.r = Math.floor(Math.random() * 100 + 1) |
| | // 设置x轴, y轴的运动速度(-5, 5) |
| | this.dx = Math.floor(Math.random() * 10) - 5 |
| | this.dy = Math.floor(Math.random() * 10) - 5 |
| | } |
| | // 随机颜色,最后返回的是类似'#3fe432' |
| | getRandomColor(): string { |
| | let allType = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f" |
| | let allTypeArr = allType.split(',') |
| | let color = '#' |
| | for (let i = 0; i < 6; i++) { |
| | let random = Math.floor(Math.random() * allTypeArr.length) |
| | color += allTypeArr[random] |
| | } |
| | return color |
| | } |
| | |
| | // 渲染小球 |
| | render(): void { |
| | if(ctx !== null) { |
| | ctx.beginPath() |
| | ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false) |
| | ctx.fillStyle=this.color |
| | ctx.fill() |
| | } |
| | } |
| | |
| | // 更新小球 |
| | update(): void { |
| | // 小球的运动 |
| | this.x += this.dx |
| | this.y += this.dy |
| | this.r -= 0.5 |
| | // 如果小球的半径小于0了,从数组中删除 |
| | if(this.r <= 0) { |
| | this.remove() |
| | } |
| | } |
| | |
| | // 移除小球 |
| | remove(): void { |
| | for(let i = 0; i < ballArr.length; i++) { |
| | if(ballArr[i] === this) { |
| | ballArr.splice(i, 1) |
| | } |
| | } |
| | } |
| | } |
| | // 维护小球的数组 |
| | let ballArr: Ball[] = [] |
| | |
| | // canvas设置鼠标监听 |
| | myCanvas.addEventListener("mousemove", (event)=> { |
| | ballArr.push(new Ball(event.offsetX, event.offsetY)) |
| | }) |
| | |
| | |
| | // 定时器进行动画渲染和更新 |
| | setInterval(function() { |
| | // 动画的逻辑,清屏-更新-渲染 |
| | if(ctx !== null) { |
| | ctx.clearRect(0, 0, myCanvas.width, myCanvas.height) |
| | } |
| | for(let i = 0; i < ballArr.length; i++) { |
| | // 小球的更新和渲染 |
| | ballArr[i].update() |
| | if(ballArr[i]) { |
| | ballArr[i].render() |
| | } |
| | |
| | } |
| | // 60 帧 |
| | }, 1000 / 60) |
透明度的值是0到1之间: (1是完全不透明,0是完全透明)
| | ctx.globalAlpha = 1 |
| | // 得到画布 |
| | const myCanvas: HTMLCanvasElement = document.getElementById("mycanvas") as HTMLCanvasElement |
| | |
| | // 获取上下文 |
| | const ctx = myCanvas.getContext("2d") |
| | |
| | // 设置画布的尺寸 |
| | myCanvas.width = document.documentElement.clientWidth - 30 |
| | myCanvas.height = document.documentElement.clientHeight - 30 |
| | |
| | class Ball { |
| | x: number = Math.floor(Math.random() * myCanvas.width) |
| | y: number = Math.floor(Math.random() * myCanvas.height) |
| | r: number = 10 |
| | color: string = 'gray' |
| | dx: number = Math.floor(Math.random() * 10) - 5 |
| | dy: number = Math.floor(Math.random() * 10) - 5 |
| | constructor() { |
| | ballArr.push(this) |
| | } |
| | |
| | // 小球的渲染 |
| | render() { |
| | if(ctx !== null) { |
| | ctx.beginPath() |
| | // 透明度 |
| | ctx.globalAlpha = 1 |
| | // 画小球 |
| | ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false) |
| | ctx.fillStyle = this.color |
| | ctx.fill() |
| | } |
| | } |
| | // 小球的更新 |
| | update() { |
| | // 更新x |
| | this.x += this.dx |
| | // 纠正x |
| | if(this.x <= this.r) { |
| | this.x = this.r |
| | } else if ( this.x >= myCanvas.width - this.r) { |
| | this.x = myCanvas.width - this.r |
| | } |
| | // 更新y |
| | this.y += this.dy |
| | // 纠正y |
| | if(this.y <= this.r) { |
| | this.y = this.r |
| | } else if ( this.y >= myCanvas.height - this.r) { |
| | this.y = myCanvas.height - this.r |
| | } |
| | // 碰壁返回 |
| | if(this.x + this.r >= myCanvas.width || this.x - this.r <= 0) { |
| | this.dx *= -1 |
| | } |
| | if(this.y + this.r >= myCanvas.height || this.y - this.r <= 0) { |
| | this.dy *= -1 |
| | } |
| | } |
| | |
| | } |
| | |
| | // 小球数组 |
| | let ballArr: Ball[] = [] |
| | |
| | // 创建20个小球 |
| | for(let i = 0; i < 20; i++) { |
| | new Ball() |
| | } |
| | |
| | // 定时器动画 |
| | setInterval(() => { |
| | // 清除画布 |
| | if(ctx !== null) { |
| | ctx.clearRect(0, 0, myCanvas.width, myCanvas.height) |
| | } |
| | // 小球渲染和更新 |
| | for(let i = 0; i < ballArr.length; i++) { |
| | ballArr[i].render() |
| | ballArr[i].update() |
| | } |
| | // 画线的逻辑 |
| | if(ctx !== null) { |
| | for(let i = 0; i < ballArr.length; i++) { |
| | for(let j = i + 1; j < ballArr.length; j++) { |
| | let distance = Math.sqrt(Math.pow((ballArr[i].x - ballArr[j].x), 2) + Math.pow((ballArr[i].y -ballArr[j].y), 2)) |
| | if( distance <= 150) { |
| | ctx.strokeStyle = '#000' |
| | ctx.beginPath() |
| | // 线的透明度,根据当前已经连线的小球的距离进行线的透明度设置 |
| | // 距离越近透明度越大,距离越远透明度越小 |
| | ctx.globalAlpha = 1 - distance / 150 |
| | ctx.moveTo(ballArr[i].x, ballArr[i].y) |
| | ctx.lineTo(ballArr[j].x, ballArr[j].y) |
| | ctx.closePath() |
| | ctx.stroke() |
| | } |
| | } |
| | } |
| | } |
| | }, 1000/60) |
我们可以利用lineWidth设置线的粗细,属性值为number型,默认为1,没有单位
| | ctx.lineWidth = 1.0 |
我们可以使用lineCap指定如何绘制每一条线段末端的属性:"butt" | "round" | "square"
, 其中butt
代表线段末端以方形结束,round
表示线段末端以圆形结束,square
线段末端以方形结束,但是增加了一个宽度和线段相同,高度是线段厚度一半的矩形区域,默认是butt
。
| | ctx.lineCap = "round"; |
该图是三种lineCapd的类型,从左到右依次为butt
、 round
和square
。
我们可以使用lineJoin来设置设置2个长度不为0的相连部分(线段,圆弧,曲线)如何连接在一起的属性(长度为0的变形部分,其指定的末端和控制点在同一位置,会被忽略):"bevel" | "round" | "miter"
。
| | ctx.lineJoin = "bevel"; |
round
表示通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。 圆角的半径是线段的宽度。bevel
表示在相连部分的末端填充一个额外的以三角形为底的区域, 每个部分都有各自独立的矩形拐角。mitter
表示通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域。我们可以使用setLineDash方法在填充线时使用虚线模式。
| | ctx.setLineDash(segments); |
segments
是一个Array
数组。一组描述交替绘制线段和间距(坐标空间单位)长度的数字。 如果数组元素的数量是奇数, 数组的元素会被复制并重复。例如, [5, 15, 25]
会变成 [5, 15, 25, 5, 15, 25]
。数组内部是虚线的交替状态
| | // 得到画布 |
| | const myCanvas: HTMLCanvasElement = document.getElementById("mycanvas") as HTMLCanvasElement |
| | |
| | // 获取上下文 |
| | const ctx = myCanvas.getContext("2d") |
| | |
| | // 画布的尺寸 |
| | myCanvas.width = document.documentElement.clientWidth - 30 |
| | myCanvas.height = document.documentElement.clientHeight - 30 |
| | |
| | if(ctx !== null) { |
| | ctx.setLineDash([15, 15]); |
| | ctx.strokeRect(50,50, 90, 90) |
| | ctx.setLineDash([15,10,2,10]) |
| | ctx.strokeRect(200,50, 90, 90) |
| | } |
我们可以使用lineDashOffset设置虚线偏移量的属性。设置的是起始偏移量,使线向左移动value。
| | ctx.lineDashOffset = value; |
value
偏移量是float精度的数字。 初始值为 0.0
。我们可以在画布上绘制文字:
| | ctx.font = "30px 微软雅黑" // 空格前为文字大小,空格后为字体类型 |
| | // 第一个参数为文字内容,第二和第三个参数为文字绘制坐标, |
| | // 第四个参数是可选参数,代表文字的最大宽度,如果字体宽度超过该值则压缩字体宽度 |
| | ctx.fillText("你好,世界!", 100, 100) |
我们可以使用textAlign
来设置文本的对齐选项。可选的值包括:start
, end
, left
, right
or center
。默认值是 start
。该对齐是基于fillText方法的x的值。
| | ctx.textAlign = "left" || "right" || "center" || "start" || "end"; |
left
: 文本左对齐。right
: 文本右对齐。center
: 文本居中对齐。start
: 文本对齐界线开始的地方 (左对齐指本地从左向右,右对齐指本地从右向左)。end
: 文本对齐界线结束的地方 (左对齐指本地从左向右,右对齐指本地从右向左)。提供两种渐变方式,一种是线性渐变,一种是径向渐变。
| | ctx.createLinearGradient(x1, y1, x2, y2) |
addColorStop内部接收两个参数,第一个参数是当前渐变的位置(0~1之间的小数),第二个参数是颜色。
| | let liner: CanvasGradient = ctx.createLinearGradient(0, 0, 100, 100) |
| | liner.addColorStop(0, 'red') |
| | liner.addColorStop(.5, 'blue') |
| | liner.addColorStop(.8, 'yellow') |
| | liner.addColorStop(1, 'green') |
| | ctx.fillStyle = liner |
| | ctx.fillRect(10, 10, 100,100) |
径向渐变:createRadialGradient方法接受 6 个参数,前三个定义一个以 (x1,y1) 为原点,半径为 r1 的开始圆形,后三个参数则定义另一个以 (x2,y2) 为原点,半径为 r2 的结束圆形。
| | let radial: CanvasGradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 100) |
| | radial.addColorStop(0, 'red') |
| | radial.addColorStop(1, 'purple') |
| | ctx.fillStyle = radial |
| | ctx.arc(100, 100, 100, 0, Math.PI *2, false) |
| | ctx.fill( |
我们可以在画布中设置画布的阴影的状态:
shadowOffsetX
: shadowOffsetX
和 shadowOffsetY
用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为 0
。shadowOffsetY
: shadowOffsetX
和 shadowOffsetY
用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为 0
。shadowBlur
: shadowBlur
用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响,默认为 0
。shadowColor
: shadowColor
是标准的 CSS 颜色值,用于设定阴影颜色效果,默认是全透明的黑色。
| | ctx.shadowOffsetX = 1 // 阴影左右偏离的距离 |
| | ctx.shadowOffsetY = 1 // 阴影上下偏离的距离 |
| | ctx.shadowBlur = 1 // 模糊状态 |
| | ctx.shadowColor = 'green' // 阴影颜色 |
| | ctx.font ='30px 宋体' |
| | ctx.fillText('你好,世界!', 100, 100) |
canvs中使用drawImage来绘制图片,主要是把外部的图片导入进来,绘制到画布上。
图片的渲染过程:
| | // 导入图片 |
| | if(ctx !== null) { |
| | // 第一步是创建一个image元素 |
| | let image:HTMLImageElement = new Image() |
| | // 用src设置图片的地址 |
| | image.src = "image/test1.png" |
| | // 必须要在onload函数内绘制图片,否则不会渲染 |
| | image.onload = function() { |
| | ctx.drawImage(image, 100, 100) |
| | } |
| | } |
如果我们在drawImage里设置的参数一共是两个(不包含第一个image参数),表示的是图片的加载位置。
| | ctx.drawImage(image, 100, 100) |
如果drawImage有四个参数,分别表示图片的绘制位置和图片的宽高。(注意,此时图像会被拉伸)
| | ctx.drawImage(image, 100, 100, 50, 50) |
还可以使用八个参数的drawImage, 前四个参数指的是你在图片中设置切片的宽度和高度,以及切片的位置,后四个参数指的是切片在画布上的位置和切片宽度高度。
| | // ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); |
| | ctx.drawImage(image, 100, 300, 200, 200) |
| | ctx.drawImage(image, 100, 100, 200, 200, 100, 100, 200, 200) |
image
的矩形(裁剪)选择框的左上角 X 轴坐标。image
的矩形(裁剪)选择框的左上角 Y 轴坐标。image
的矩形(裁剪)选择框的宽度。如果不说明,整个矩形(裁剪)从坐标的sx
和sy
开始,到image
的右下角结束。image
的矩形(裁剪)选择框的高度。image
的左上角在目标canvas上 X 轴坐标。image
的左上角在目标canvas上 Y 轴坐标。image
在目标canvas上绘制的宽度。 允许对绘制的image
进行缩放。 如果不说明, 在绘制时image
宽度不会缩放。image
在目标canvas上绘制的高度。 允许对绘制的image
进行缩放。 如果不说明, 在绘制时image
高度不会缩放。我们在开发游戏的时候,有一些静态资源是需要请求回来的,否则如果直接开始,某些静态资源没有,会报错,或者空白,比如我们的游戏被禁锢,如果没有请求回来就直接开始,页面会有空白现象。
资源管理器就是当游戏需要资源全部加载完毕的时候,再开始游戏
我们现在主要是图片的资源,所以我们要在canvas渲染过程中进行图片的资源加载。
有下面一个JSON(对象),此时我们想获取当前这个JSON属性数量
| | this.imgURL = { |
| | 'fengjing1':'./image/下载1.jpg', |
| | 'fengjing2':'./image/下载2.jpg', |
| | 'fengjing3':'./image/下载3.jpg', |
| | 'fengjing4':'./image/下载4.jpg', |
| | 'fengjing5':'./image/下载5.jpg', |
| | } |
此时我们使用this.imgURL.length
是得不到的,因为当前的this.imgURL.length
指的是获取imgURL对象的length
属性,而不是获取当前对象的属性个数,会返回undefined
正确答案是使用Object.keys()
来获取当前的属性key列表,然后通过这个列表获取长度。
| | Object.keys(this.imgURL).length |
| | interface StringOrImage { |
| | // 定义了一个接口,该接口要求对象的属性是string或者是HTMLImageElement类型 |
| | [index: string]: string | HTMLImageElement |
| | } |
| | |
| | class Game { |
| | dom: HTMLCanvasElement |
| | ctx: CanvasRenderingContext2D | null |
| | imgURL: StringOrImage |
| | constructor() { |
| | // 得到画布 |
| | this.dom = document.getElementById("mycanvas") as HTMLCanvasElement |
| | // 获取上下文 |
| | this.ctx = this.dom.getContext("2d") |
| | // 在属性中保存需要的图片地址 |
| | this.imgURL = { |
| | // 'fengjing1':'./image/下载1.jpg', |
| | // 'fengjing2':'./image/下载2.jpg', |
| | // 'fengjing3':'./image/下载3.jpg', |
| | // 'fengjing4':'./image/下载4.jpg', |
| | // 'fengjing5':'./image/下载5.jpg', |
| | 'fengjing1':'https://gimg2.baidu.com/image\_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F4k%2Fs%2F02%2F2109242332225H9-0-lp.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651933471&t=34b40d339ce3bc4177afb393e7785575', |
| | 'fengjing2':'https://gimg2.baidu.com/image\_search/src=http%3A%2F%2Ffile02.16sucai.com%2Fd%2Ffile%2F2014%2F0827%2Fc0c92bd51bb72e6d12d5b877dce338e8.jpg&refer=http%3A%2F%2Ffile02.16sucai.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651933483&t=453f28e751e0d54d70a2e3393e57b423', |
| | 'fengjing3':'https://gimg2.baidu.com/image\_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F1113%2F032120114622%2F200321114622-4-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651933493&t=e9017fa69deb525312e214d2583a76d4', |
| | 'fengjing4':'https://pic.rmb.bdstatic.com/1530971282b420d77bdfb6444d854f952fe31f0d1e.jpeg', |
| | 'fengjing5':'https://gimg2.baidu.com/image\_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2Ftp01%2F1ZZQ214233446-0-lp.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651933521&t=2cc050574824ec2761b539ab3a697522', |
| | } |
| | // 获取资源图片的总数 |
| | let imgCount = Object.keys(this.imgURL).length |
| | // 计数器,记录的是加载完毕的数量 |
| | let count = 0 |
| | // 遍历imgURL对象获取每一个路径地址 |
| | for(let key in this.imgURL) { |
| | // 备份每一张图片的地址 |
| | let src: string = this.imgURL[key] as string |
| | // 创建一个图片 |
| | this.imgURL[key] = new Image(); |
| | |
| | // 判断图片是否加载完成,如果完成了,记数,如果加载完毕的数量和总数量相同了,则说明资源加载完毕 |
| | // 第一种方法,将值提取出去做类型缩小 |
| | let value = this.imgURL[key] |
| | // 类型缩小成HTMLImageElement类型 |
| | if(typeof value !== 'string') { |
| | value.src = src |
| | value.onload = () => { |
| | // 增加计数器 |
| | count++ |
| | if(this.ctx !== null) { |
| | // 清屏 |
| | this.ctx.clearRect(0, 0, 600, 600) |
| | this.ctx.font = '16px Arial' |
| | this.ctx.fillText("图片已经加载:" + count +" / " + imgCount, 50, 50) |
| | // 判断图片是否加载完毕,如果加载完毕了再开始显示 |
| | if(count === imgCount) { |
| | this.start() |
| | } |
| | } |
| | } |
| | } |
| | |
| | // 第二种方法,使用as直接断言成HTMLImageElement |
| | //(this.imgURL[key] as HTMLImageElement).src = src |
| | |
| | } |
| | } |
| | start() { |
| | if(this.ctx !== null) { |
| | // 清屏 |
| | this.ctx.clearRect(0, 0, 600, 600) |
| | let startX = 0 |
| | let startY = 0 |
| | for(let key in this.imgURL) { |
| | this.ctx.drawImage(this.imgURL[key] as HTMLImageElement, startX, startY, 100, 100) |
| | startX += 100 |
| | startY += 100 |
| | } |
| | } |
| | } |
| | } |
| | |
| | new Game() |
canvas是可以进行变形的,但是变形的不是元素,而是ctx,ctx就是整个画布的渲染区域,整个画布在变形,我们需要在画布变形前进行保存和恢复:
save()
:保存画布(canvas)的所有状态。restore()
:save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。Canvas状态存储在栈中,每当save()
方法被调用后,当前的状态就被推送到栈中保存。一个绘画状态包括:
strokeStyle
, fillStyle
, globalAlpha
, lineWidth
, lineCap
, lineJoin
, miterLimit
, lineDashOffset
, shadowOffsetX
, shadowOffsetY
, shadowBlur
, shadowColor
, globalCompositeOperation
, font
, textAlign
, textBaseline
, direction
, imageSmoothingEnabled
你可以调用任意多次 save
方法。每一次调用 restore
方法,上一个保存的状态就从栈中弹出,所有设定都恢复。
以下的例子可以很好的印证这两个的用法:
| | // 得到画布 |
| | const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement |
| | // 获得上下文 |
| | const ctx = myCanvas.getContext('2d') |
| | |
| | if (ctx !== null) { |
| | ctx.fillRect(0, 0, 150, 150); // 使用默认设置绘制一个矩形 |
| | ctx.save(); // 保存默认状态 |
| | |
| | ctx.fillStyle = '#09F' // 在原有配置基础上对颜色做改变 |
| | ctx.fillRect(15, 15, 120, 120); // 使用新的设置绘制一个矩形 |
| | |
| | ctx.save(); // 保存当前状态 |
| | ctx.fillStyle = '#FFF' // 再次改变颜色配置 |
| | ctx.globalAlpha = 0.5; |
| | ctx.fillRect(30, 30, 90, 90); // 使用新的配置绘制一个矩形 |
| | |
| | ctx.restore(); // 重新加载之前的颜色状态 |
| | ctx.fillRect(45, 45, 60, 60); // 使用上一次的配置绘制一个矩形 |
| | |
| | ctx.restore(); // 加载默认颜色配置 |
| | ctx.fillRect(60, 60, 30, 30); // 使用加载的配置绘制一个矩形 |
| | } |
translate(x, y)
: translate
方法接受两个参数。x 是左右偏移量,y 是上下偏移量。
在做变形之前先保存状态是一个良好的习惯。大多数情况下,调用 restore 方法比手动恢复原先的状态要简单得多。又,如果你是在一个循环中做位移但没有保存和恢复 canvas 的状态,很可能到最后会发现怎么有些东西不见了,那是因为它很可能已经超出 canvas 范围以外了。
我们知道了变形实际上就是将整个画布进行的变形,所以如果一旦我们的变形操作变多了,画布将变得不可控。
所以如果我们使用到变形,一定记住下面的规律:变形之前要先备份,将世界和平的状态进行备份,然后再变形,变形完毕后再恢复到世界和平的样子,不要影响下一次的操作。
| | // 得到画布 |
| | const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement |
| | // 获得上下文 |
| | const ctx = myCanvas.getContext('2d') |
| | |
| | if (ctx !== null) { |
| | // 保存 |
| | ctx.save() |
| | ctx.translate(50, 50) |
| | ctx.fillRect(0, 0, 120, 120) |
| | // 恢复 |
| | ctx.restore() |
| | // 渲染位置是没有存档之前的位置 |
| | ctx.fillRect(120, 300, 120, 120) |
| | } |
rotate(angle)
这个方法只接受一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。
旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到 translate
方法。
scale(x, y)
: scale
方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子,如果比1小,会缩小图形, 如果比1大会放大图形。默认值为1, 为实际大小。
画布初始情况下, 是以左上角坐标为原点的第一象限。如果参数为负实数, 相当于以x 或 y轴作为对称轴镜像反转(例如, 使用translate(0,canvas.height); scale(1,-1);
以y轴作为对称轴镜像反转, 就可得到著名的笛卡尔坐标系,左下角为原点)。
默认情况下,canvas 的 1 个单位为 1 个像素。举例说,如果我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍。
transform(a, b, c, d, e, f)
a (m11)
: 水平方向的缩放;
b(m12)
: 竖直方向的倾斜偏移;
c(m21)
: 水平方向的倾斜偏移;
d(m22)
: 竖直方向的缩放;
e(dx)
: 水平方向的移动;
f(dy)
: 竖直方向的移动.
| | // 得到画布 |
| | const myCanvas: HTMLCanvasElement = document.getElementById('mycanvas') as HTMLCanvasElement |
| | // 获得上下文 |
| | const ctx = myCanvas.getContext('2d') |
| | |
| | if (ctx !== null) { |
| | // 保存 |
| | ctx.save() |
| | ctx.transform(0.5, 0, 1, 0.5, 100, 100) |
| | ctx.fillRect(0, 0, 100,100) |
| | // 恢复 |
| | ctx.restore() |
| | // 渲染位置是没有存档之前的位置 |
| | ctx.fillRect(0, 200, 100, 100) |
| | |
| | } |
| | html> |
| | |
| | |
| | |
| | |
| | |
| | |
| | canvas的变形-滚动的车轮title> |
| | |
| | |
| | head> |
| | |
| | |
| |