Canvas学习记录

news2024/11/18 17:49:09

Canvas学习

      • 基本用法
        • 1.首先得有canvas元素
        • 2.获取渲染上下文
        • 3.检查浏览器支持性
        • 4.例子
      • 绘制图形
        • 1.矩形
        • 2. 绘制路径
        • 3. 移动笔触
        • 4. 绘制直线
        • 5. 圆弧
        • 二次贝塞尔曲线与三次贝塞尔曲线
        • Path2D 对象
          • 创建Path2D对象
          • Path2D.addPath()
      • 使用样式和颜色
        • 色彩Colors
        • 透明度 Transparency
        • 线型 Line styles
        • 渐变 Gradients
        • 图案样式 Patterns
        • 阴影 Shadows
        • Canvas 填充规则
      • 绘制文本
        • 绘制文本
        • 有样式的文本
        • 预测量文本宽度
      • 使用图像 Using Images
        • 获得需要绘制的图片
          • 使用相同页面内的图片
          • 使用其他域名下的图片
          • 使用其他canvas元素
          • 由零开始创建图像
          • 通过data:url 方式嵌入图像
          • 使用视频帧
        • 绘制图片
        • 缩放 Scaling
        • 切片 Slicing
        • 控制图像的缩放行为 Controlling image scaling behavior
      • 变形 Transformations
        • 状态的保存和恢复 Saving and restoring state
        • 移动 Translating
        • 旋转 Rotating
        • 缩放 Scaling
        • 变形 Transforms
      • 组合 Compositing
        • globalCompositeOperation
        • 剪切路径
      • 基本动画
        • 动画的基本步骤
        • 操控动画 Controlling an animation
        • 计划更新 Scheduled updates
      • 高级动画
        • 绘制小球
        • 添加速率
        • 边界
        • 加速度
        • 长尾效果
        • 添加鼠标控制
        • 最终效果
      • 像素操作
        • ImageData对象
        • 创建一个ImageData对象
        • 得到场景像素数据
        • 在场景中写入像素数据
        • 图片灰度和反相颜色
        • 缩放和反锯齿
        • 保存图片
      • canvas优化
        • 1.在离屏canvas上预渲染相似的图形或重复的对象
        • 2.避免浮点数的坐标点,用整数取而代之
        • 3.不要在用drawImage时缩放图像
        • 4.使用多层画布去画一个复杂的场景
          • 5.用css设置大的背景图
          • 6.用css transforms特性缩放画布
          • 7.关闭透明度
          • 8.更多贴士
      • 终曲

MDN官网Canvas2D

基本用法

1.首先得有canvas元素

<canvas id="tutorial" width="150" height="150"></canvas>

如果你绘制出来的图像是扭曲的,尝试用 width 和 height 属性为明确规定宽高,而不是使用 CSS。


2.获取渲染上下文

初始canvas是空白的,脚本要渲染图形就需要先找到渲染的上下文,可以通过getContext()这个方法获取渲染上下文和它的绘画功能。其接收一个参数,对于2D图形可以传入‘2d’来获取一个CanvasRenderingContext2D 实例。

let canvas = document.getElementById('tutorial');
let ctx = canvas.getContext('2d');

3.检查浏览器支持性

var canvas = document.getElementById('tutorial');

if (canvas.getContext){
  var ctx = canvas.getContext('2d');
  // drawing code here
} else {
  // canvas-unsupported code here
}

4.例子

<html>
 <head>
  <script type="application/javascript">
    function draw() {
      var canvas = document.getElementById("canvas");
      if (canvas.getContext) {
        var ctx = canvas.getContext("2d");

        ctx.fillStyle = "rgb(200,0,0)";
        ctx.fillRect (10, 10, 55, 50);

        ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
        ctx.fillRect (30, 30, 55, 50);
      }
    }
  </script>
 </head>
 <body onload="draw();">
   <canvas id="canvas" width="150" height="150"></canvas>
 </body>
</html>

绘制图形

画布左上角为坐标原点(0,0)

1.矩形

fillRect(x,y,width,height) // 起点坐标,宽高 填充

strokeRect(x,y,width,height) // 起点坐标,宽高 边框

clearRect(x,y,width,height) // 起点坐标,宽高 清除(区域变透明)


2. 绘制路径

二维空间(面)由一维线构成,在canvas画布中,图形也是由一条条(路径)线构成。于是有了以下几点。

  • 首先需要创建路径 beginPath()
  • 使用画图命令去画路径 moveTo()起点、lineTo()连线(联结上一个点到下一个点)等等
  • 封闭路径(非必需) closePath()
  • 通过描边或填充路径区域来渲染图形 fill()填充 / stroke()描边

1. 当前路径为空,即调用 beginPath() 之后,或者 canvas 刚建的时候,第一条路径构造命令通常被视为是 moveTo(),无论实际上是什么。出于这个原因,你几乎总是要在设置路径之后专门指定你的起始位置。

2. 当你调用 fill() 函数时,所有没有闭合的形状都会自动闭合,所以你不需要调用 closePath() 函数。但是调用 stroke() 时不会自动闭合。


3. 移动笔触

moveTo(x,y)

将笔触移动到指定的坐标 (x, y) 上。

当 canvas 初始化或者beginPath()调用后,你通常会使用moveTo()函数设置起点。我们也能够使用moveTo()绘制一些不连续的路径。就像拿着画笔在白纸上画画,每次落笔到纸上时,都有一个点是最先接触到笔的,然后才是画线,所以每次画完一条线(提起笔),再画另一条线时都需要使用moveTo(都有一个点最先接触笔)。moveTo完成的功能可以看作是从上一个终点提笔,到新的点落笔。如下面的笑脸


4. 绘制直线

lineTo(x,y)

绘制一条从当前位置到(x,y)的直线。

lineTo可以连续使用,这时下一段线的开始点就是上一段线的结束点,可以使用moveTo改变开始点


5. 圆弧

绘制圆弧或者圆,我们使用arc()方法。当然可以使用arcTo(),不过这个的实现并不是那么的可靠,所以我们这里不作介绍。

arc(x,y,radius,startAngle,endAngle,anticlockwise)

以x,y为圆心radius为半径画圆弧(圆),startAngle与endAngle分部代表起始弧度与结束弧度,anticlockwise是一个boolean,true:逆时针,false:顺时针

arcTo(x1, y1, x2, y2, radius)
根据给定的控制点和半径画一段圆弧,再以直线连接两个控制点。
在这里插入图片描述

弧度=(Math.PI/180)*角度。

二次贝塞尔曲线与三次贝塞尔曲线

quadraticCurveTo(cp1x, cp1y, x, y)
绘制二次贝塞尔曲线,cp1x,cp1y 为一个控制点,x,y 为结束点。

    ctx.beginPath();
    ctx.moveTo(75, 25);
    ctx.quadraticCurveTo(25, 25, 25, 62.5);
    ctx.quadraticCurveTo(25, 100, 50, 100);
    ctx.quadraticCurveTo(50, 120, 30, 125);
    ctx.quadraticCurveTo(60, 120, 65, 100);
    ctx.quadraticCurveTo(125, 100, 125, 62.5);
    ctx.quadraticCurveTo(125, 25, 75, 25);
    ctx.stroke();

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点。

    ctx.beginPath();
    ctx.moveTo(75, 40);
    ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
    ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
    ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
    ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
    ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
    ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
    ctx.fill();

Path2D 对象

正如我们在前面例子中看到的,你可以使用一系列的路径和绘画命令来把对象“画”在画布上。为了简化代码和提高性能,Path2D对象已可以在较新版本的浏览器中使用,用来缓存或记录绘画命令,这样你将能快速地回顾路径。

创建Path2D对象
new Path2D();     // 创建一个path2D对象
new Path2D(path); // 创建一个path对象的拷贝
new Path2D(d);    // d为svg path数据构成的字符串,根据svg命令创建路径

// 例1:
    var path1 = new Path2D();
    path1.rect(10, 10, 100,100);
    
    var path2 = new Path2D(path1);
    path2.moveTo(220, 60);
    path2.arc(170, 60, 50, 0, 2 * Math.PI);
    
    ctx.stroke(path2);
  
// 例2:
    var rectangle = new Path2D();
    rectangle.rect(10, 10, 50, 50);

    var circle = new Path2D();
    circle.moveTo(125, 35);
    circle.arc(100, 35, 25, 0, 2 * Math.PI);

    ctx.stroke(rectangle);
    ctx.fill(circle);
    
// 例3:
    var p = new Path2D("M10 10 h 80 v 80 h -80 Z");
    ctx.fill(p);

Path2D.addPath()

Path2D.addPath() 是 Canvas 2D API 根据指定路径变量添加路径的方法

语法

void path.addPath(path [, transform]);

path: 需要添加的 Path2D路径

transform(可选):SVGMatrix作为新增路径的变换矩阵。(已弃用,可用DOMMatrix与DOMMatrixReadOnly接口来代替,但是某些浏览器并不支持,并且未可能重新修订标准)


var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

// Create a new path with a rect
var p1 = new Path2D();
p1.rect(0,0,100,100);

// Create another path with a rect
var p2 = new Path2D();
p2.rect(0,0,100,100);

// Create transformation matrix that moves vertically 300 points to the right
var m = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix();
m.a = 1; m.b = 0;
m.c = 0; m.d = 1;
m.e = 300; m.f = 0;

// add the second path to the first path
p1.addPath(p2, m);

// Finally, fill the first path onto the canvas
ctx.fill(p1);

使用样式和颜色

色彩Colors

fillStyle = color/gradient/pattern 设置图形的填充颜色
strokeStyle = color/gradient/pattern 设置图形轮廓的颜色

  • DOMString字符串,可以转化为CSS颜色值 (‘red’,#fdfdfd,rgb(0,0,244)等)

  • CanvasGradient对象,(线性渐变或放射性渐变)更多见下文“渐变 Gradients"模块

  • CanvasPattern对象(可重复的图片、视频等)见下文“图案样式 Patterns”模块

    for (var i=0;i<6;i++){
        for (var j=0;j<6;j++){
            ctx.strokeStyle = 'rgb(0,' + Math.floor(255-42.5*i) + ',' +
                            Math.floor(255-42.5*j) + ')';
            ctx.fillStyle = 'rgb(0,' + Math.floor(255-42.5*i) + ',' +
                            Math.floor(255-42.5*j) + ')';
            ctx.beginPath();
            ctx.arc(12.5+j*25,12.5+i*25,10,0,Math.PI*2,true);
            ctx.stroke();
            ctx.fill();
        }
    }
    ctx.strokeStyle = "red";
    ctx.strokeRect(0, 0, 150, 150);

在基于 WebKit- 和 Blink- 的浏览器中,还有两个非标准且不赞成使用的方法,可能在之后的版本中删除

ctx.setStrokeColor()、ctx.setFillColor()


透明度 Transparency

除了可以绘制实色图形,我们还可以用 canvas 来绘制半透明的图形。通过设置 globalAlpha 属性或者使用一个半透明颜色作为轮廓或填充的样式

globalAlpha = transparencyValue

  1. globalAlpha = 0.5 (0.0~1.0,默认1.0)
  2. ctx.strokeSytle = "rgba(255,0,0,.5)";ctx.fillSytle = "rgba(255,0,0,.5)";
ctx.globalAlpha = 0.2;
ctx.fillStyle = "blue";
ctx.fillRect(10, 10, 100, 100);
//ctx.globalAlpha = 0.8
ctx.fillStyle = "rgba(255,0,0,.5)";
ctx.fillRect(50, 50, 100, 100);

globalAlpha 属性在需要绘制大量拥有相同透明度的图形时候相当高效。其他情况使用第二种更灵活。

两种例子:

//1. globalAlpha 
  var ctx = document.getElementById('canvas').getContext('2d');
  // 画背景
  ctx.fillStyle = '#FD0';
  ctx.fillRect(0,0,75,75);
  ctx.fillStyle = '#6C0';
  ctx.fillRect(75,0,75,75);
  ctx.fillStyle = '#09F';
  ctx.fillRect(0,75,75,75);
  ctx.fillStyle = '#F30';
  ctx.fillRect(75,75,75,75);
  ctx.fillStyle = '#FFF';

  // 设置透明度值
  ctx.globalAlpha = 0.2;

  // 画半透明圆
  for (var i=0;i<7;i++){
      ctx.beginPath();
      ctx.arc(75,75,10+10*i,0,Math.PI*2,true);
      ctx.fill();
  }
//2.rgba  
  ctx.fillStyle = 'rgb(255,221,0)';
  ctx.fillRect(0,0,150,37.5);
  ctx.fillStyle = 'rgb(102,204,0)';
  ctx.fillRect(0,37.5,150,37.5);
  ctx.fillStyle = 'rgb(0,153,255)';
  ctx.fillRect(0,75,150,37.5);
  ctx.fillStyle = 'rgb(255,51,0)';
  ctx.fillRect(0,112.5,150,37.5);

  // 画半透明矩形
  for (var i=0;i<10;i++){
    ctx.fillStyle = 'rgba(255,255,255,'+(i+1)/10+')';
    for (var j=0;j<4;j++){
      ctx.fillRect(5+i*14,5+j*37.5,14,27.5)
    }
  }

线型 Line styles

lineWidth = value 设置线宽度(仅限正数,0、负数、Infinity、NaN会被忽略)

ctx.lineWidth = 15;

ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(130, 130);
ctx.rect(40, 40, 70, 70);
ctx.stroke();

lineCap = type 设置线的末端属性 butt:方形 | round:以lineWidth/2为半径的半圆结束 | square:以lineWidth/2为长度的方形结尾

ctx.lineWidth = 15;
ctx.beginPath();

ctx.moveTo(30,0);
ctx.lineCap = "square";
ctx.lineTo(30, 100);
ctx.stroke();

ctx.moveTo(60,0);
ctx.lineCap = "round";
ctx.lineTo(60, 100);
ctx.stroke();

ctx.moveTo(90,0);
ctx.lineCap = "butt";
ctx.lineTo(90, 100);
ctx.stroke();

lineJoin = value 设定线条与线条间接合处的样式 bevel:连接处外角为直线 | round:连接处外角为圆弧 | miter(默认):连接处外角为尖角

注意:如果 2 个相连部分在同一方向,那么 lineJoin 不会产生任何效果,因为在那种情况下不会出现连接区域。

ctx.lineWidth = 50;
ctx.lineJoin = "miter"; // bevel  round
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(200, 100);
ctx.lineTo(500,0);
ctx.stroke();

miterLimit = value 斜接面限制比例的的数字(仅限正数,0、负数、Infinity、NaN会被忽略)

此属性只有在ctx.lineJoin 为 ‘miter’时才会生效,生效范围内连接处的外角以bevel的形式“】”显示。(此属性存在的原因是,miter模式下,边角内角越小,外角长度就会越长,这时就可以设置边角长度的“阈值”)

例子:可以发现,随着内角越来越小,斜接(外角伸出部分)就越长,超过“阈值”就变为bevel格式了。

    canvas.width = 5000;
    canvas.height = 5000;
    var ctx = canvas.getContext('2d')
    ctx.beginPath();
    ctx.lineWidth = 20;
    ctx.lineJoin = 'miter'
    ctx.miterLimit = 20
    ctx.moveTo(0,400);
    for (var i=0;i<30;i++){
        if(i%2==0){
            ctx.lineTo((i+1)*40,400-(i+1)*20)
        }else{
            ctx.lineTo((i+1)*40,400+(i+1)*20)
        }
        
        ctx.stroke();
    }

setLineDash(segments) 设置填充线时使用虚线模式。参数为一组来指定描述模式的线和间隙的交替长度的Array数组,数组的元素如果是奇数个,那么会复制并重复元素。如[1,2,3] 会变成 [1,2,3,1,2,3]

  ctx.setLineDash([]);  // 实线
  ctx.setLineDash([1,1])  // 实1虚1   重复
  ctx.setLineDash([1,2,3]) // 实1虚2实3虚1实2虚3    重复

getLineDash() 获取当前填充线模式的格式数组

lineDashOffset = value 设置虚线偏移量的属性。可以实现蚂蚁线效果。value是一个float精度的值。

var offset = 0;

function draw() {
  ctx.clearRect(0,0, canvas.width, canvas.height);
  ctx.setLineDash([4, 2]);
  ctx.lineDashOffset = -offset;
  ctx.strokeRect(10,10, 100, 100);
}

function march() {
  offset++;
  if (offset > 16) {
    offset = 0;
  }
  draw();
  setTimeout(march, 20);
}

march();

渐变 Gradients

像一般的绘图软件一样,我们可以用线性或者径向的渐变来填充或描边。我们用下面的方法新建一个 canvasGradient 对象,并且赋给图形的 fillStyle 或 strokeStyle 属性。

使用ctx.createLinearGradient()或者ctx.createRadialGradient()创建

let gradient = ctx.createLinearGradient(x0,y0,x1,y1) 线性 x0,y0,开始点的坐标;x1,y1结束点的坐标

let gradient = ctx.createRadialGradient(x0,y0,r0,x1,y1,r1) 放射性 x0,y0,r0 开始圆的坐标、半径;x1,y1,r1 结束圆的坐标、半径

gradient.addColorStop(offset, color) 方法,添加一个由偏移值和颜色值指定的断点到渐变。 offset偏移值[0,1],color颜色

// 线型
var gradient = ctx.createLinearGradient(0,0,200,0);
gradient.addColorStop(0,"green");
gradient.addColorStop(1,"white");
ctx.fillStyle = gradient;
ctx.fillRect(10,10,200,100);

// 放射型
var gradient = ctx.createRadialGradient(100,100,100,100,100,10);
gradient.addColorStop(0,"white");
gradient.addColorStop(0.5,"green");
gradient.addColorStop(1,"yellow");
ctx.fillStyle = gradient;
ctx.fillRect(0,0,200,200);

图案样式 Patterns

使用ctx.createPattern()创建

CanvasPattern ctx.createPattern(image, repetition); image是图像源(包括img,video,canvas,ctx,ImageBitmap,ImageData,Blob),repetition是指定图像如何重复取值:repeat,repeat-x,repeat-y,no-repeat(‘’,null默认repeat)

用 canvas 对象作为 Image 参数在 Firefox 1.5 (Gecko 1.8) 中是无效的。

与 drawImage 有点不同,你需要确认 image 对象已经装载完毕,否则图案可能效果不对的。

var img = new Image();
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
img.onload = function() {
  var pattern = ctx.createPattern(img, 'repeat');
  ctx.fillStyle = pattern;
  ctx.fillRect(0,0,400,400);
};

阴影 Shadows

shadowOffsetX = float shadowOffsetY = float

shadowOffsetX 和 shadowOffsetY 用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为 0

shadowBlur = float

shadowBlur 用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响,默认为 0。

shadowColor = float

shadowColor 是标准的 CSS 颜色值,用于设定阴影颜色效果,默认是全透明的黑色。

  ctx.shadowOffsetX = 2;
  ctx.shadowOffsetY = 2;
  ctx.shadowBlur = 2;
  ctx.shadowColor = "rgba(0, 0, 0, 0.5)";

  ctx.font = "20px Times New Roman";
  ctx.fillStyle = "Black";
  ctx.fillText("Sample String", 5, 30);

Canvas 填充规则

ctx.fill、ctx.clip、ctx.isPointinPath 都可以选择一个填充规则。该填充规则根据某处在路径的外面或者里面来决定该处是否被填充,这对于自己与自己路径相交或者路径被嵌套的时候是有用的。

nonzero 非零环绕规则 若环绕数非零则表示在多边形内部,否则在外部

多边形的边变为矢量(有方向)。从点的位置发射一条射线,定一个多边形线条方向为正(向左),则另一个方向为负(向右),射线穿过正线+1,穿过负线-1,计算完射线穿过的所有边最终和为0 则表示在外部,非零表示在内部。

evenodd 奇偶规则 奇数表示在多边形内部,偶数表示在多边形外部。

从点的位置发射一条射线,穿过多边形所有边,如果穿过的此时是奇数则表示在内部,偶数表示在外部。

以上两个规则都是描述平面中点与(自相交)多边形之间的内外关系。传送门

在这里插入图片描述


  ctx.beginPath();
  ctx.arc(50, 50, 30, 0, Math.PI*2, true);
  ctx.arc(50, 50, 15, 0, Math.PI*2, true);
  ctx.fill("evenodd");

绘制文本

绘制文本

fillText(text, x, y [, maxWidth]) 更多 在(x,y)处填充text内容,最大宽度maxWidth可选

strokeText(text, x, y [, maxWidth]) 更多 在(x,y)处绘制text边框,最大宽度maxWidth可选

ctx.font = "50px serif";
ctx.strokeText("Hello world", 50, 50);
ctx.fillText("Hello world", 50, 100);

有样式的文本

font = value 字符串使用 与 css font 属性相同的语法,默认为:10px sans-serif

`ctx.font = "bold 48px serif"`

textAlign = value 文本对齐选项 [start, end, left, right, center] 默认 start

textBaseline = value 基线对齐选项 [top, hanging, middle, alphabetic, ideographic, bottom] 默认 alphabetic

direction = value 文本方向 [ltr, rtl, inherit] 默认是inherit

direction 属性会对 textAlign 属性产生影响。如果 direction 属性设置为 ltr,则 left 和 start 的效果相同,right 和 end 的效果相同;如果 direction 属性设置为 rtl,则 left 和 end 的效果相同,right 和 start 的效果相同。


预测量文本宽度

measureText( text ) 传入一个文本,返回一个对象(包涵宽度、所在像素等等文本的属性)


使用图像 Using Images

获得需要绘制的图片

HTMLImageElement 这些图片是由Image()函数构造出来的,或者任何元素

HTMVideoElement 使用html 元素作为图片源,从视频中抽取帧画图。

HTMLCanvas 使用另一个 元素作为图片源

ImageBitmap 高性能位图,可以低延迟地绘制,可以从上面的源或其他源中生成

以上源统一由 CanvasImageSource 类型来引用


使用相同页面内的图片
  • document.images集合
  • document.getElementsByTagName()方法
  • document.getElementById()获得(图片指定了ID)

使用其他域名下的图片

在HTMLImageElement 上使用 crossOrigin属性(取值:anonymous | use-credentials | “”,“”与anonymous相同),默认情况下crossOrigin不会启用。

如果图片的服务器允许跨域访问这个图片,那么你可以使用这个图片而不污染 canvas,否则,使用这个图片将会污染 canvas。


使用其他canvas元素

和引用页面内的图片类似地,用 document.getElementsByTagName 或 document.getElementById 方法来获取其它 canvas 元素。但你引入的应该是已经准备好的 canvas。

一个常用的应用就是将第二个 canvas 作为另一个大的 canvas 的缩略图。


由零开始创建图像

脚本中使用Image() 函数可以很方便的创建HTMLImageElement对象

var img = new Image();   // 创建一个<img>元素
img.src = 'myImage.png'; // 设置图片源地址

当脚本执行后,图片开始装载。

若调用drawImage时,图片还没装载完,那么什么都不会发生(一些就浏览器会抛出异常)。因此应该使用load事件来保证不会在加载完毕之前使用这个图片

var img = new Image();   // 创建 img 元素
img.onload = function(){
  // 执行 drawImage 语句
}
img.src = 'myImage.png'; // 设置图片源地址

如果多张图片,要是用一些预加载策略。

var imgData = ['src1','src2','src3']
var promiseAll = imgData.map(function (item, index) {
    return new Promise(function (resolve, reject) {
      var img = new Image();
      img.onload = function () {
        img.onload = null;
        resolve(img);
      };
      img.error = function () {
        reject('图片加载失败');
      };
      img.src = item;
    });
  });
  Promise.all(promiseAll).then(
    function () {
      // 图片全部加载完成,进行下一步
      // todo
    },
    function (err) {
      console.log(err);
    }
  );

/*作者:代码星空
链接:https://juejin.cn/post/6844903760695656455
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。*/

通过data:url 方式嵌入图像

我们还可以通过 data:url 方式来引用图像。Data urls 允许用一串 Base64 编码的字符串的方式来定义一个图片。

img.src = 'data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==';

其优点就是图片内容即时可用,无须再到服务器兜一圈。(还有一个优点是,可以将 CSS,JavaScript,HTML 和 图片全部封装在一起,迁移起来十分方便。)缺点就是图像没法缓存,图片大的话内嵌的 url 数据会相当的长:


使用视频帧

你还可以使用 中的视频帧(即便视频是不可见的)。例如,如果你有一个 ID 为“myvideo”的 元素,你可以这样做:

function getMyVideo() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    return document.getElementById('myvideo');
  }
}

绘制图片

一旦获得了源图对象,我们就可以使用 drawImage 方法将它渲染到 canvas 里。drawImage 方法有三种形态,下面是最基础的一种。

drawImage(image, x, y) 其中 image 是 image 或者 canvas 对象,x 和 y 是其在目标 canvas 里的起始坐标。

== SVG 图像必须在 根指定元素的宽度和高度。==

  var ctx = document.getElementById('canvas').getContext('2d');
    var img = new Image();
    img.onload = function(){
      ctx.drawImage(img,0,0);
      ctx.beginPath();
      ctx.moveTo(30,96);
      ctx.lineTo(70,66);
      ctx.lineTo(103,76);
      ctx.lineTo(170,15);
      ctx.stroke();
    }
    img.src = 'https://mdn.mozillademos.org/files/5395/backdrop.png';

缩放 Scaling

drawImage 方法的又一变种是增加了两个用于控制图像在 canvas 中缩放的参数。

drawImage(image, x, y, width, height) 这个方法多了 2 个参数:width 和 height,这两个参数用来控制 当向 canvas 画入时应该缩放的大小

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  var img = new Image();
  img.onload = function(){
    for (var i=0;i<4;i++){
      for (var j=0;j<3;j++){
        ctx.drawImage(img,j*50,i*38,50,38);
      }
    }
  };
  img.src = 'https://mdn.mozillademos.org/files/5397/rhino.jpg';
}

注意:如果图片中有文字最好不要缩放


切片 Slicing

drawImage 方法的第三个也是最后一个变种有 8 个新参数,用于控制做切片显示的。

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

第一个参数和其它的是相同的,都是一个图像或者另一个 canvas 的引用。其它 8 个参数最好是参照右边的图解,前 4 个是定义图像源的切片位置和大小,后 4 个则是定义切片的目标显示位置和大小。

切片是个做图像合成的强大工具

<html>
 <body onload="draw();">
   <canvas id="canvas" width="150" height="150"></canvas>
   <div style="display:none;">
     <img id="source" src="https://mdn.mozillademos.org/files/5397/rhino.jpg" width="300" height="227">
     <img id="frame" src="https://mdn.mozillademos.org/files/242/Canvas_picture_frame.png" width="132" height="150">
   </div>
 </body>
</html>
function draw() {
  var canvas = document.getElementById('canvas');
  var ctx = canvas.getContext('2d');

  // Draw slice
  ctx.drawImage(document.getElementById('source'),
                33,71,104,124,21,20,87,104);

  // Draw frame 
  ctx.drawImage(document.getElementById('frame'),0,0);
}

官网给的例子,frame图片是中空的,所以放在后面,如不是,则需要frame打底,否则小的图会被大的图遮住。


控制图像的缩放行为 Controlling image scaling behavior

如同前文所述,过度缩放图像可能会导致图像模糊或像素化。您可以通过使用绘图环境的imageSmoothingEnabled属性来控制是否在缩放图像时使用平滑算法。默认值为true,即启用平滑缩放。您也可以像这样禁用此功能:

ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;

变形 Transformations

变形是一种更强大的方法,可以将原点移动到另一点、对画布网格进行旋转和缩放。

状态的保存和恢复 Saving and restoring state

save() 保存画布的所有状态(快照)

  • 当前的变换矩阵。
  • 当前的剪切区域。
  • 当前的虚线列表。
  • 以下属性当前的值: strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.

restore() 弹出栈顶保存的状态

  • 当前应用的变形(即移动,旋转和缩放,见下)
  • 以及下面这些属性:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled
  • 当前的裁切路径(clipping path),会在下面介绍

Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。状态存储在栈中,每当save()方法被调用后,当前的状态就被推送到栈中保存。可以调用任意多次 save方法。每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。

  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);   // 使用加载的配置绘制一个矩形

移动 Translating

translate(x, y) 移动原点到(x,y)处

在做变形之前先保存状态是一个良好的习惯。

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  for (var i = 0; i < 3; i++) {
    for (var j = 0; j < 3; j++) {
      ctx.save();
      ctx.fillStyle = 'rgb(' + (51 * i) + ', ' + (255 - 51 * i) + ', 255)';
      ctx.translate(10 + j * 50, 10 + i * 50);
      ctx.fillRect(0, 0, 25, 25);
      ctx.restore();
    }
  }
}

旋转 Rotating

rotate(angle) 旋转angle角度。单位是弧度,正顺负逆

旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到 translate方法。

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.translate(75,75);

  for (var i=1;i<6;i++){ // Loop through rings (from inside to out)
    ctx.save();
    ctx.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';

    for (var j=0;j<i*6;j++){ // draw individual dots
      ctx.rotate(Math.PI*2/(i*6));
      ctx.beginPath();
      ctx.arc(0,i*12.5,5,0,Math.PI*2,true);
      ctx.fill();
    }

    ctx.restore();
  }
}

缩放 Scaling

scale(x, y) 缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,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 倍。

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');

  // draw a simple rectangle, but scale it.
  ctx.save();
  ctx.scale(10, 3);
  ctx.fillRect(1, 10, 10, 10);
  ctx.restore();

  // mirror horizontally
  ctx.scale(-1, 1);
  ctx.font = '48px serif';
  ctx.fillText('MDN', -135, 120);
}

变形 Transforms

transform(a, b, c, d, e, f) 这个方法是将当前的变形矩阵乘上一个基于自身参数的矩阵

变换矩阵的描述: [ a c e b d f 0 0 1 ] \begin{bmatrix} a & c & e \\ b & d & f \\ 0 & 0 & 1\\ \end{bmatrix} ab0cd0ef1

如果任意一个参数是Infinity,变形矩阵也必须被标记为无限大,否则会抛出异常。

a(m11) 水平缩放 b(m12) 垂直倾斜 c(m21) 水平倾斜

d(m21) 垂直缩放 e(dx) 水平移动 f(dy) 垂直移动

ctx.transform(1,0.5,-0.5,1,100,20);
ctx.fillRect(0,0,100,100);
ctx.setTransform();

transform 重复使用会相对上次的transform进行变换

setTransform 重复使用只会相对于同一个原型变换(默认坐标原点在canvas左上角)进行变换

setTransform(a, b, c, d, e, f)

这个方法会将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用 transform方法。如果任意一个参数是无限大,那么变形矩阵也必须被标记为无限大,否则会抛出异常。从根本上来说,该方法是取消了当前变形,然后设置为指定的变形,一步完成。

resetTransform() 重置当前变形为单位矩阵,功能等同:ctx.setTransform(1, 0, 0, 1, 0, 0);

transform/setTransform 例子

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');

  var sin = Math.sin(Math.PI/6);
  var cos = Math.cos(Math.PI/6);
  ctx.translate(100, 100);
  var c = 0;
  for (var i = 0; i <= 12; i++) {
        c = Math.floor(234 / 12 * i);
        ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
        ctx.fillRect(0, 0, 100, 10);
        ctx.transform(cos, sin, -sin, cos, 0, 0); // 相对上一次的transform变换进行变换
        // ctx.setTransform(1, 0, 0, 1, 10*i, 10*i);  // 相对左上角原型变换
    }
    
    ctx.setTransform(-1, 0, 0, 1, 100, 100);
    ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
    ctx.fillRect(0, 50, 100, 100);
}

组合 Compositing

在之前的例子里面,我们总是将一个图形画在另一个之上,对于其他更多的情况,仅仅这样是远远不够的。比如,对合成的图形来说,绘制顺序会有限制。不过,我们可以利用 globalCompositeOperation 属性来改变这种状况。此外,clip属性允许我们隐藏不想看到的部分图形。

globalCompositeOperation

我们不仅可以在已有图形后面再画新图形,还可以用来遮盖指定区域,清除画布中的某些部分(清除区域不仅限于矩形,像clearRect()方法做的那样)以及更多其他操作。

globalCompositeOperation = type 这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识 12 种遮盖方式的字符串。(注意看左上方红蓝结合的方块)

  • source-over

    这是默认设置,并在现有画布上下文之上绘制新图形。
    在这里插入图片描述

  • source-in

    新图形只在新图形和目标画布重叠的地方绘制。其他的都是透明的。
    在这里插入图片描述

  • source-out

    在不与现有画布内容重叠的地方绘制新图形。
    在这里插入图片描述

  • source-atop

    新图形只在与现有画布内容重叠的地方绘制。

  • destination-over

    在现有的画布内容后面绘制新的图形。

  • destination-in

    现有的画布内容保持在新图形和现有画布内容重叠的位置。其他的都是透明的

  • destination-out

    现有内容保持在新图形不重叠的地方。

  • destination-atop

    现有的画布只保留与新图形重叠的部分,新的图形是在画布内容后面绘制的。

  • lighter

    两个重叠图形的颜色是通过颜色值相加来确定的。

  • copy

    只显示新图形。

  • xor

    图像中,那些重叠和正常绘制之外的其他地方是透明的。

  • multiply

    将顶层像素与底层相应像素相乘,结果是一幅更黑暗的图片。

  • screen

    像素被倒转,相乘,再倒转,结果是一幅更明亮的图片。

  • overlay

    multiply 和 screen 的结合,原本暗的地方更暗,原本亮的地方更亮。

  • darken

    保留两个图层中最暗的像素。

  • lighten

    保留两个图层中最亮的像素。
    在这里插入图片描述

  • color-dodge

    将底层除以顶层的反置。

  • color-burn

    将反置的底层除以顶层,然后将结果反过来。

  • hard-light

    屏幕相乘(A combination of multiply and screen)类似于叠加,但上下图层互换了。

  • soft-light

    用顶层减去底层或者相反来得到一个正值。

  • difference

    一个柔和版本的强光(hard-light)。纯黑或纯白不会导致纯黑或纯白。

  • exclusion

    和 difference 相似,但对比度较低。

  • hue

    保留了底层的亮度(luma)和色度(chroma),同时采用了顶层的色调(hue)

  • stauration

    保留底层的亮度(luma)和色调(hue),同时采用顶层的色度(chroma)。

  • color

    保留了底层的亮度(luma),同时采用了顶层的色调 (hue) 和色度 (chroma)。

  • luminosity

    保持底层的色调(hue)和色度(chroma),同时采用顶层的亮度(luma)。


剪切路径

裁切路径和普通的 canvas 图形差不多,不同的是它的作用是遮罩,用来隐藏不需要的部分。如右图所示。红边五角星就是裁切路径,所有在路径以外的部分都不会在 canvas 上绘制出来。

如果和上面介绍的 globalCompositeOperation 属性作一比较,它可以实现与 source-in 和 source-atop差不多的效果。最重要的区别是裁切路径不会在 canvas 上绘制东西,而且它永远不受新图形的影响。这些特性使得它在特定区域里绘制图形时相当好用。

在 绘制图形 一节中,介绍了 stroke 和 fill 方法,这里介绍第三个方法clip。

clip() 将当前正在构建的路径转换为当前的裁剪路径

ctx.arc(100, 100, 75, 0, Math.PI*2, false);
ctx.clip();
ctx.fillRect(0, 0, 100,100);


基本动画

动画的基本步骤

  1. 清空canvas,除非接下来画的内容会充满canvas(如背景图),否则需要清空所有。最简单就是使用clearRect()。
  2. 保存canvas状态,如果要改变一些会改变canvas状态的设置(样式、变形之类的),又要在每一帧之时都是原始状态的话,需要先保存一下状态。
  3. 绘制动画图形(animated shapes)这一步才是重绘动画帧。
  4. 恢复canvas状态,如果已经保存了canvas的状态,可以先恢复它,然后重绘下一帧。

操控动画 Controlling an animation

在canvas上绘制内容是用canvas提供的或自定义的方法,而通常绘制结果都在脚本执行完之后,所以使用for循环来完成动画是不太可能的。

因此为了实现动画,我们需要一些可以定时执行重绘的方法。 首先可以通过setIntervalsetTimeout方法来控制在设定的时间点上执行重绘。

更多动画见官网


计划更新 Scheduled updates

首先,可以用window.setInterval(),window.setTimeout(),和window.requestAnimationFrame()来设定定期执行一个指定函数。

setInterval(function, delay) function 更具设置的时间(delay) 定期执行(宏任务)

setTimeout(function, delay) function 在设置的时间(delay)之后执行(宏任务)

requestAnimationFrame(callback) 跟随浏览器的刷新帧率,在下次浏览器重绘时调用回调函数更新动画(如果需要形成动画,需要在回调函数中再次调用window.requestAnimationFrame()。)


高级动画

基本动画以及逐步了解了让物件移动的方法。在这一部分,我们将会对运动有更深的了解并学会添加一些符合物理的运动以让我们的动画更加高级。

绘制小球

我们将会画一个小球用于动画学习,所以首先在画布上画一个球。下面的代码帮助我们建立画布。

<canvas id="canvas" width="600" height="300"></canvas>

跟平常一样,我们需要先画一个 context(画布场景)。为了画出这个球,我们又会创建一个包含一些相关属性以及 draw() 函数的 ball 对象,来完成绘制。

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

var ball = {
  x: 100,
  y: 100,
  radius: 25,
  color: 'blue',
  draw: function() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
};

ball.draw();

这里并没有什么特别的。小球实际上是一个简单的圆形,在arc() 函数的帮助下画出。


添加速率

现在我们有了一个小球,正准备添加一些基本动画,正如我们上一章所学的。又是这样,window.requestAnimationFrame() 再一次帮助我们控制动画。小球依旧依靠添加速率矢量进行移动。在每一帧里面,我们依旧用clear 清理掉之前帧里旧的圆形。

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var raf;

var ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 2,
  radius: 25,
  color: 'blue',
  draw: function() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
};

function draw() {
  ctx.clearRect(0,0, canvas.width, canvas.height);
  ball.draw();
  ball.x += ball.vx;
  ball.y += ball.vy;
  raf = window.requestAnimationFrame(draw);
}

canvas.addEventListener('mouseover', function(e){
  raf = window.requestAnimationFrame(draw);
});

canvas.addEventListener('mouseout', function(e){
  window.cancelAnimationFrame(raf);
});

ball.draw();

边界

若没有任何的碰撞检测,我们的小球很快就会超出画布。我们需要检查小球的 x 和 y 位置是否已经超出画布的尺寸以及是否需要将速度矢量反转。为了这么做,我们把下面的检查代码添加进 draw 函数:

if (ball.y + ball.vy + ball.radius > canvas.height || ball.y + ball.vy < ball.radius) {
            ball.vy = -ball.vy;
        }
        if (ball.x + ball.vx + ball.radius > canvas.width || ball.x + ball.vx < ball.radius) {
            ball.vx = -ball.vx;
        }

加速度

为了让动作更真实,你可以像这样处理速度,例如:

ball.vy *= .99;
ball.vy += .25;

这会逐帧减少垂直方向的速度,所以小球最终将只会在地板上弹跳。

长尾效果

现在,我们使用的是 clearRect 函数帮我们清除前一帧动画。若用一个半透明的 fillRect 函数取代之,就可轻松制作长尾效果。

ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);

添加鼠标控制

为了更好地控制小球,我们可以用 mousemove事件让它跟随鼠标活动。下面例子中,click 事件会释放小球然后让它重新跳起。

canvas.addEventListener('click',function(e){
  if (!running) {
    raf = window.requestAnimationFrame(draw);
    running = true;
  }
});

canvas.addEventListener('mouseout', function(e){
  window.cancelAnimationFrame(raf);
  running = false;
});

最终效果

    var ctx = document.getElementById('canvas').getContext('2d');
    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    var raf;
    var running = false;
    let queue = {
        list: [],   
        length: function(){return this.list.length},
        set: function(item){
            if(this.list.length > 99) this.list.pop();    // 超过10条就去掉队尾数据
            this.list.unshift(item);    // 新数据插入头部
        },
        pop: function(){
            if(this.list.length>0) return this.list.pop();
        }
    }
    var ball = {
        x: 100,
        y: 100,
        vx: 2,
        vy: 2,
        radius: 25,
        shadow: queue,
        color: 'red',
        draw: function () {
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
            ctx.closePath();
            ctx.fillStyle = this.color;
            ctx.fill();
        }
    };
    function clear() {
        ctx.fillStyle = 'rgba(255,255,255,0.3)';
        ctx.fillRect(0,0,canvas.width,canvas.height);
    }

    function draw() {
        // ctx.clearRect(0, 0, canvas.width, canvas.height);    
        // 换成长尾效果
        clear();  
        ball.draw();
        ball.x += ball.vx;
        ball.y += ball.vy;
        ball.shadow.set({...ball})
        // ball.vy += .5   // 加速度
        // ball.vx *= .99
        if (ball.y + ball.vy + ball.radius > canvas.height || ball.y + ball.vy < ball.radius) {
            ball.vy = -ball.vy;
        }
        if (ball.x + ball.vx + ball.radius > canvas.width || ball.x + ball.vx < ball.radius) {
            ball.vx = -ball.vx;
        }
        raf = window.requestAnimationFrame(draw);
    }

    function run(){
        if (!running) {
            raf = window.requestAnimationFrame(draw);
            // running = true;
        }else{
            window.cancelAnimationFrame(raf);
            let shadow = ball.shadow;
            runderShadow(shadow); 
        }
        running = !running
    }
    function runderShadow(shadow){
        let {list} = shadow
        let frequence = 1;
        for (let i = 20; i > 0; i--) {
            const item = list[i];
            frequence++;
            setTimeout(() => {
                item.color = 'rgba(255,255,255,0.6)'
                item.radius += 1
                item.draw();
                ball.draw();
            }, 50 * frequence);
        }
        frequence = 1;
        for (let i = list.length-1; i > 20; i--) {
            const item = list[i];
            frequence++;
            setTimeout(() => {
                item.color = 'rgba(255,255,255,1)'
                item.radius += 1
                item.draw();
                ball.draw();
            }, 1 * frequence);
        }
        frequence = 1;
        for (let i = 20; i > 0; i--) {
            const item = list[i];
            frequence++;
            setTimeout(() => {
                item.radius += 1
                item.color = 'rgba(255,255,255,1)'
                item.draw();
                ball.draw();
            }, 25 * frequence);
        }
    }
    
    canvas.addEventListener('mousemove', function (e) {
        if (!running) {
            clear();
            ball.x = e.offsetX;
            ball.y = e.offsetY;
            ball.draw();
        }
    });
    
    canvas.addEventListener('click', run);
    
    window.addEventListener('keydown', (event)=>{
        switch(event.keyCode){
        case 32:    // 空格
            run();break;
        case 38: // up y轴加速
            if(ball.vy >= 0){
                ball.vy += 2   // 加速度
            }else{
                ball.vy -= 2   // 反向加速度
            }break;
        case 40: // down y轴减速
            if(ball.vy >= 0){
                ball.vy -= 2   // 减加速度
            }else{
                ball.vy += 2   // 反向减加速度
            }break;
        case 39: // right x轴加速
            if(ball.vx >= 0){
                ball.vx += 2   // 加速度
            }else if(ball.vx-2 == 0){
                ball.vx -= 2   // 反向加速度
            }break;
        case 37: // left x轴减速
            if(ball.vx >= 0){
                ball.vx -= 2   // 减加速度
            }else if(ball.vx+2 == 0){
                ball.vx += 2   // 反向减加速度
            }
        }
    })

    // canvas.addEventListener('mouseover', function(e){
    //     raf = window.requestAnimationFrame(draw);
    // });

    canvas.addEventListener('mouseout', function (e) {
        window.cancelAnimationFrame(raf);
        // 移出重置x、y轴速率
        ball.vy = 2;
        ball.vx = 5;
        running = false;
    });
    window.addEventListener("beforeunload", function( event ) {
        event.returnValue = "\o/";
        // event.returnValue = "\n/";
        // event.preventDefault();

        /*https://developer.mozilla.org/zh-CN/docs/Web/API/BeforeUnloadEvent*/
    });

    ball.draw();

像素操作

到目前为止,我们尚未深入了解 Canvas 画布真实像素的原理,事实上,你可以直接通过 ImageData 对象操纵像素数据,直接读取或将数据数组写入该对象中。稍后我们也将深入了解如何控制图像使其平滑(反锯齿)以及如何从 Canvas 画布中保存图像。

ImageData对象

ImageData对象中存储着canvas对象真实的像素数据,其包含以下几个只读属性:

width 图片宽度,单位是像素

height 图片高度,单位是像素

data Uint8ClampedArray 类型的一维数组,包含着 RGBA 格式的整型数据,范围在 0 至 255 之间(包括 255)。

data 属性返回一个 Uint8ClampedArray,它可以被使用作为查看初始像素数据。每个像素用 4 个 1bytes 值 (按照红,绿,蓝和透明值的顺序; 这就是"RGBA"格式) 来代表。每个颜色值部份用 0 至 255 来代表。每个部份被分配到一个在数组内连续的索引,左上角像素的红色部份在数组的索引 0 位置。像素从左到右被处理,然后往下,遍历整个数组。

Uint8ClampedArray 包含高度 × 宽度 × 4 bytes 数据,索引值从 0 到 (高度× 宽度 ×4)-1

如,要读取图片中位于第 50 行,第 200 列的像素的蓝色部份,你会写以下代码:

blueComponent = imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 2];

根据行、列读取某像素点的 R/G/B/A 值的公式:

imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 0/1/2/3];

你可能用会使用 Uint8ClampedArray.length 属性来读取像素数组的大小(以 bytes 为单位):

var numBytes = imageData.data.length;

创建一个ImageData对象

去创建一个新的,空白的ImageData对象,可以使用createImageData()方法。

var myImageData = ctx.createImageData(widht,height);

上面的代码创建了一个宽为width、高为height的空白ImageData对象,此时所有的rgba颜色值都是0。因此是一个透明黑的效果。

var myImageData = ctx.createImageData(anotherImageData);

上面的代码创建了一个被anotherImageData对象指定的相同像素的ImageData对象。这个新对象全部被预设为透明黑(rgba(0,0,0,0))。这并非复制了图片数据


得到场景像素数据

为获得一个包含画布场景像素数据的ImageData对象,可以使用getImageData()方法:

var myImageData = ctx.getImageData(left,top,width,height);

这个方法会返回一个ImageData对象,它代表了画布区域的对象数据,此画布的四个角落分别表示为(left、top),(left+width,top),(left,top+height)与(left+width,top+height)四点。这些坐标点被设定为画布坐标空间元素。

注意:任何在画布以外的元素都会被返回成一个透明黑的ImageData对象。

    let img = new Image();
    img.crossOrigin = 'anonymous';
    img.src = './bg5.jpg';
    let canvas = document.getElementById('canvas')
    let ctx = canvas.getContext('2d');
    img.onload = ()=>{
        ctx.drawImage(img,0,0);
        img.style.display = 'none'
    }

    let hoverColor = document.getElementsByClassName('hovered')[0]
    let selectColor = document.getElementsByClassName('selected')[0]
    
    function pick(event, destination, offsetX=0, offsetY=0){
        let x = event.layerX - offsetX;
        let y = event.layerY - offsetY;
        let pixel = ctx.getImageData(x,y,1,1);
        let data = pixel.data;

        const rgba = `rgba(${data[0]},${data[1]},${data[2]},${data[3] / 255})`;
        destination.style.background = rgba;
        destination.textContent = rgba;

        return rgba;
    }

    canvas.addEventListener('mousemove', function(event){
        pick(event,hoverColor,50,50);
    })
    canvas.addEventListener('click', function(event){
        pick(event,selectColor,50,50);
        console.log(event.layerX,event.layerY)
    })

    let body = document.getElementsByTagName('body')[0]
    body.addEventListener('click', function(event){
        console.log(event.layerX,event.layerY)
    });

在场景中写入像素数据

可以使用putImageData()方法去对场景进行像素数据的写入。

ctx.putImageData(myImageData, dx, dy);

dx 和 dy参数表示从场景左上角绘制的像素数据所得到的设备坐标。

eg:在场景内左上角绘制myImageData代表的图片,可以写如下代码:

ctx.putImageData(myImageData, 0, 0);
<body>
    图片偏移:
    <canvas id="canvas1" width="510" height="255"> </canvas>
    putImageData:
    <canvas id="canvas2" width="510" height="255"> </canvas>
</body>
<script type="module">
    let img = new Image();
    img.crossOrigin = 'anonymous';
    img.src = './bg5.jpg';
    let canvas1 = document.getElementById('canvas1');
    let ctx1 = canvas1.getContext('2d');
    let canvas2 = document.getElementById('canvas2');
    let ctx2 = canvas2.getContext('2d');
    img.onload = ()=>{
        // ctx1.drawImage(img,0,0);
        // let imageData = ctx1.getImageData(0,0,canvas1.clientWidth,canvas1.clientHeight);
        // ctx2.putImageData(imageData,0,0)
        
        ctx1.drawImage(img,-700,-350);
        let imageData = ctx1.getImageData(200,0,300,210);
        ctx2.putImageData(imageData,0,0);
    }
</script>

图片灰度和反相颜色

在这个例子里,我们遍历所有像素以改变他们的数值。然后我们将被修改的像素数组通过 putImageData() 放回到画布中去。invert 函数仅仅是去减掉颜色的最大色值 255。grayscale 函数仅仅是用红绿和蓝的平均值。你也可以用加权平均,例如 x = 0.299r + 0.587g + 0.114b 这个公式。更多资料请参考百度百科的灰度。

<body>
    <canvas id="canvas" width="510" height="255"> </canvas>
    <form>
        <span class="span">
            <label>原图</label><input name="color" value="original" type="radio"/>
        </span>
        <sapn class="span">
            <label>反相</label><input name="color" value="invert" type="radio"/>
        </sapn>
        <sapn class="span">
            <label>灰度</label><input name="color" value="grayscale" type="radio"/>
        </sapn>
        <sapn class="span">
            <label>深灰</label><input name="color" value="sepia" type="radio"/>
        </sapn>
    </form>
</body>
<script type="module">
    let img = new Image();
    img.crossOrigin = 'anonymous';
    img.src = './bg5.jpg';
    let canvas = document.getElementById('canvas');
    let ctx = canvas.getContext('2d');
    img.onload = ()=>{
        ctx.drawImage(img,-700,-350);    
    }

    // 原图
    var original = ()=>{
        ctx.drawImage(img,-700,-350); 
    }
    
    // 反相
    var invert = ()=>{
        ctx.drawImage(img,-700,-350); // 重置数据源,否则会在之前图像处理的结果上进行反相
        const imageDate = ctx.getImageData(0,0,canvas.width,canvas.height);
        const data = imageDate.data;
        for (let i = 0; i < data.length; i+=4) {
            data[i] = 255 - data[i];    // red
            data[i+1] = 255 - data[i+1];    // green
            data[i+2] = 255 - data[i+2];    // blue
        }
        ctx.putImageData(imageDate,0,0);
    }

    // 灰度
    var grayscale = ()=>{
        ctx.drawImage(img,-700,-350); 
        const imageDate = ctx.getImageData(0,0,canvas.width,canvas.height);
        const data = imageDate.data;
        for (let i = 0; i < data.length; i+=4) {
            var avg = (data[i] + data[i+1] + data[i+2]) / 3;
            data[i] = data[i+1] = data[i+2] = avg;  
        }
        ctx.putImageData(imageDate,0,0);
    }

    // 深灰色
    var sepia = ()=>{
        ctx.drawImage(img,-700,-350); 
        const imageDate = ctx.getImageData(0,0,canvas.width,canvas.height);
        const data = imageDate.data;
        for (let i = 0; i < data.length; i+=4) {
            let red = data[i], green = data[i + 1], blue = data[i + 2];
    		data[i] = Math.min(Math.round(0.393 * red + 0.769 * green + 0.189 * blue), 255);
	    	data[i + 1] = Math.min(Math.round(0.349 * red + 0.686 * green + 0.168 * blue), 255);
		    data[i + 2] = Math.min(Math.round(0.272 * red + 0.534 * green + 0.131 * blue), 255);
        }
        ctx.putImageData(imageDate,0,0);
    }

    const inputs = document.querySelectorAll('[name=color]');
    for (const input of inputs) {
        input.addEventListener("change", function(evt) {
            switch (evt.target.value) {
                case "invert":
                    return invert();
                case "grayscale":
                    return grayscale();
                case "sepia":
                    return sepia();
                default:
                    return original();
            }
        });
    }
</script>

缩放和反锯齿

在drawImage()方法,第二个画布和imageSmoothingEnabled属性的帮助下,我们可以放大显示我们的图片和看到的详细内容。

得到鼠标位置,并裁剪出5像素内的图片,将这幅图复制到另一个画布然后调整大小。在缩放画布中,将10x10像素的对原画布的剪裁调整为200x200.

zoomctx.drawImage(canvas,Math.abs(x-5),Math.abs(y-5),10,10,0,0,200,200);

因为反锯齿默认是启用的,可以关闭它以看到清除的像素,可以通过切换勾选框来看到imageSmoothingEnabled属性的效果(不同浏览器需要不同前缀)。

<canvas id="canvas" width="300" height="227"></canvas>
<canvas id="zoom" width="300" height="227"></canvas>
<div>
<label for="smoothbtn">
  <input type="checkbox" name="smoothbtn" checked="checked" id="smoothbtn">
  Enable image smoothing
</label>
</div>
var img = new Image();
img.src = './bg5.jpg';
img.onload = function() {
  draw(this);
};

function draw(img) {
  var canvas = document.getElementById('canvas');
  var ctx = canvas.getContext('2d');
  ctx.drawImage(img, -700, -350);
  img.style.display = 'none';
  var zoomctx = document.getElementById('zoom').getContext('2d');

  var smoothbtn = document.getElementById('smoothbtn');
  var toggleSmoothing = function(event) {
    zoomctx.imageSmoothingEnabled = this.checked;
    zoomctx.mozImageSmoothingEnabled = this.checked;
    zoomctx.webkitImageSmoothingEnabled = this.checked;
    zoomctx.msImageSmoothingEnabled = this.checked;
  };
  smoothbtn.addEventListener('change', toggleSmoothing);

  var zoom = function(event) {
    var x = event.layerX;
    var y = event.layerY;
    zoomctx.drawImage(canvas,
                      Math.abs(x - 5),
                      Math.abs(y - 5),
                      50, 50,
                      0, 0,
                      200, 200);
  };

  canvas.addEventListener('mousemove', zoom);
}

保存图片

HTMLCanvasElement 提供一个toDataURL() 方法,它返回一个包含被类型参数规定的图像表现格式的数据链接。返回的图片分辨率是96dpi。

canvas.toDataURL(‘image/png’) 默认设定。创建一个PNG图片

canvas.toDataURL(‘image/jpeg’,quality) 创建一个JPG图片。你可以有选择地提供从0到1的品质量,1表示最好的品质,0基本不被辨析但有比较小的文件大小。

当你从画布中生成了一个数据链接,可将其用于任何元素,或者将其放在一个有download属性的超链接中用于保存到本地。

也可以从画布中创建已Blob对象。

canvas.toBlob(callback,type,encoderOptions) 创建一个在画布中代表图片的Blob对象。


canvas优化

<canvas>元素是众多广泛使用的网络 2D 图像渲染标准之一。它被广泛用于游戏及复杂的图像可视化中。然而,随着网站和应用将 canvas 画布推至极限,性能开始成为问题。此文目标是给使用 canvas 画布元素的优化带来建议,去保证你的网站或者应用表现卓越。

1.在离屏canvas上预渲染相似的图形或重复的对象

如果发现自己在每个动画帧上重复了一些相同的绘制操作,请考虑将其分流到屏幕外的画布上。然后根据需要频繁地将屏幕外图像渲染到主画布上,而不必首先重复生成该图像的步骤。

myEntity.offscreenCanvas = document.createElement("canvas");
myEntity.offscreenCanvas.width = myEntity.width;
myEntity.offscreenCanvas.height = myEntity.height;
myEntity.offscreenContext = myEntity.offscreenCanvas.getContext("2d");

myEntity.render(myEntity.offscreenContext);

2.避免浮点数的坐标点,用整数取而代之

当画一个没有整数坐标点的对象时会发生子像素渲染。

ctx.drawImage(myImage, 0.3, 0.5);

浏览器为了达到抗锯齿的效果会做额外的运算。为了避免这种情况,请保证在调用drawImage()函数时,用Math.floor()函数对所有的坐标点取整。


3.不要在用drawImage时缩放图像

在离屏canvas中缓存图片的不同尺寸,而不要用drawImage()去缩放它们。


4.使用多层画布去画一个复杂的场景

应用程序中,某些对象可能需要经常移动或更改,而其他对象则保持相对的静态。在这种情况下,可以使用多个<canvas>元素对项目进行分层优化。

例如:一个游戏,其UI位于顶部,中间是游戏性动作,底部是静态背景。在这种情况下,可将游戏分为三层<canvas>。UI将仅在用户输入时发生变化,游戏层随每个新框架发生变化,并且背景通常保持不变。

<div id="stage">
  <canvas id="ui-layer" width="480" height="320"></canvas>
  <canvas id="game-layer" width="480" height="320"></canvas>
  <canvas id="background-layer" width="480" height="320"></canvas>
</div>

<style>
  #stage {
    width: 480px;
    height: 320px;
    position: relative;
    border: 2px solid black
  }
  canvas { position: absolute; }
  #ui-layer { z-index: 3 }
  #game-layer { z-index: 2 }
  #background-layer { z-index: 1 }
</style>
5.用css设置大的背景图

如果像大多数游戏那样,有一张静态的背景图,用一个静态的<div>元素,结合background特性,将其置于画布元素之后。这么做可以避免在每一帧画布上绘制大图。

6.用css transforms特性缩放画布

CSS transforms使用GPU,因此速度更快。最好的情况是不直接缩放画布,或者具有较小的画布并按比例放大,而不是较大的画布并按比例缩小。

var scaleX = window.innerWidth / canvas.width;
var scaleY = window.innerHeight / canvas.height;

var scaleToFit = Math.min(scaleX, scaleY);
var scaleToCover = Math.max(scaleX, scaleY);

stage.style.transformOrigin = '0 0'; //scale from top left
stage.style.transform = 'scale(' + scaleToFit + ')';
7.关闭透明度

如果游戏使用画布而且不需要透明,当使用HTMLCanvasElement.getContext()创建一个绘图上下文时把alpha选项设置为false。这个选项可以帮助浏览器进行内部优化。

var ctx = canvas.getContext('2d', { alpha: false });
8.更多贴士
  • 将画布的函数调用集合到一起(如:画一条折线,而不是画多条分开的直线)
  • 避免不必要的画布状态改变
  • 渲染画布中的不同点,而非整个状态
  • 尽可能避免shadowBlur特性
  • 尽可能避免text rendering
  • 尝试不同的方法来清除画布(clearRect() vs.fillRect() vs.调整canvas大小)
  • 有动画,请使用window.requestAnimationFrame() 而非 window.setInterval()
  • 请谨慎使用大型物理库


终曲

以上就是Canvas教程,下面将推荐一些网站可以去跟着学习、实践。

Codepen.io 前端开发人员操练场和在线编辑器

HTML5CanvasTutorials 包括大多数canvas API实例教程

31 days of Canvas tutorials JavaScript可视化编程入门教程

Game development MDN游戏开发教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/111146.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

oracle DML与DDL

一、数据库语言介绍 DML&#xff08;数据库操作语言&#xff09;&#xff1a;其中包括 insert/delete/update/select等操作。 DDL&#xff08;数据库定义语言&#xff09;&#xff1a;其中包括create/alter/drop等操作。 区别&#xff1a;1、DDL是针对数据库对象(例如&#…

【安全学习】apache通告中的漏洞描述

分析 漏洞描述 我们可以看看在apache通告中对漏洞的简单的描述 很明显&#xff0c;从这个漏洞描述中&#xff0c;我们能够明白这个CVE的造成主要是因为dubbo中内置的hessian项目&#xff0c;主要是因为在中的及以前版本中存在有这个漏洞hessian-lite3.2.12 来看看是因为哪里…

3. 请求

1. 请求映射路径 RequestMapping注解 名称&#xff1a;RequestMapping类型&#xff1a;方法注解 类注解位置&#xff1a;SpringMVC控制器方法定义上方作用&#xff1a;设置当前控制器方法请求访问路径&#xff0c;如果设置在类上统一设置当前控制器方法请求访问路径前缀范例 …

Matter理论介绍-通用-1-03:桥接器-数据结构

【源码、文档、软件、硬件、技术交流、技术支持&#xff0c;入口见文末】 【所有相关IDE、SDK和例程源码均可从群文件免费获取&#xff0c;免安装&#xff0c;解压即用】 持续更新中&#xff0c;欢迎关注&#xff01; 一、简介 Bridge&#xff08;桥接器&#xff09;存在的意…

springboot集成webstock实战:服务端数据推送数据到客户端实现实时刷新

背景 之前介绍过springboot集成webstock方式,具体参考: springboot集成websocket实战:站内消息实时推送 这里补充另外一个使用webstock的场景,方便其他同学理解和使用,废话不多说了,直接开始!简单介绍一下业务场景: 现在有一个投票活动,活动详情中会显示投票活动的参与人数…

【代码随想录】二刷-贪心算法

贪心算法 《代码随想录》 什么是贪心&#xff1f; 贪心的本质是选择每一阶段的局部最优&#xff0c;从而达到全局最优。 贪心没有规定的套路。 刷题或面试的时候&#xff0c;手动模拟一下感觉可以局部最优退出整体最优&#xff0c;而且想不到反例&#xff0c;那么就试一试贪心。…

Python游戏开发之Dungeon Crawler 游戏源码大全

源码一 功能: WASD 移动 SPACE 攻击 SHIFT 使用楼梯 介绍 每个级别都包含一把钥匙和一个上锁的舱口。收集钥匙以解锁通往下一层的通道(SHIFT解锁)。 史莱姆造成的伤害最小,但每一层都会产生更多。他们可以降低健康或 XP。更多 XP = 每次攻击造成更多伤害。硬币目前没有…

OpenCV 读取、显示和保存图像

目录 一、OpenCV 读取图像 OpenCV 读取函数 参数&#xff1a; 二、OpenCV 显示图像 imshow函数 imshow函数功能 imshow函数原型 三、OpenCV 保存图像 四、结果和代码 一、OpenCV 读取图像 OpenCV 允许我们对图像执行多种操作&#xff0c;但要做到这一点&#xff0c;需…

云原生之使用Docker部署Python应用

云原生之使用Docker部署Python应用一、检查系统版本1.检查系统 版本2.检查系统内核二、检查docker状态三、编辑python文件1.创建目录2.编辑test.py文件四、构建镜像1.编辑dockerfile文件2.使用dockerfile构建镜像五、运行镜像容器1.运行python_app容器2.查看容器状态六、访问Py…

java大学校园饭卡管理系统饭卡查询系统饭卡挂失网站源码

springboot开发的大学生饭卡系统&#xff0c;学生可以登录该系统充值饭卡&#xff0c;挂失饭卡&#xff0c;取消挂失&#xff0c;查看自己的所有饭卡&#xff0c;以及发卡的充值记录。超级管理员拥有最大的权限&#xff0c;饭卡管理员可以添加饭卡信息&#xff0c;充值&#xf…

如何将敏捷开发与V模型深度融合?这家企业给出答案

随着汽车智能化的快速发展&#xff0c;消费者对智能汽车的需求正在发生变化&#xff0c;车企开始思考如何能快速满足消费者的这些需求。这也对汽车供应商提出更大挑战&#xff0c;Tier 1不仅应该具备提供相应高质量产品的量产能力&#xff0c;更要快速满足主机厂和用户对汽车智…

【MySQL基础教程】图形化界面工具介绍、安装与使用

前言 本文为 【MySQL基础教程】图形化界面工具介绍、安装与使用 相关介绍&#xff0c;下边将对图形化界面工具介绍&#xff0c;DataGrip的安装与DataGrip的使用进行详尽介绍~ &#x1f4cc;博主主页&#xff1a;小新要变强 的主页 &#x1f449;Java全栈学习路线可参考&#x…

八、Express框架 安装

对于Express框架的就不进行过多介绍&#xff0c;Express官方对该框架的定位是&#xff1a;"基于Node.js平台&#xff0c;快速、开放、极简的 Web 开发框架"&#xff0c;本篇开始记录关于 Node.js 轻量级Web开发框架 &#xff1a;Express框架&#xff0c;下面先进行Ex…

高可用架构和系统设计经验

导语 | 本文从研发规范层面、应用服务层面、存储层面、产品层面、运维部署层面、异常应急层面这六大层面去剖析一个高可用架构和系统需要有哪些关键的设计和考虑。 一、高可用系统架构设计思想 1-1、可用性和高可用概念 可用性是一个可以量化的指标&#xff0c;计算的公式在维…

刷爆力扣之亲密字符串

刷爆力扣之亲密字符串 HELLO&#xff0c;各位看官大大好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 今天阿呆继续记录下力扣刷题过程&#xff0c;收录在专栏算法中 &#x1f61c;&#x1f61c;&#x1f61c; 该专栏按照不同类别标签进行刷题&#xff0c;…

基于VUE学生选课管理系统

开发工具(eclipse/idea/vscode等)&#xff1a;idea 数据库(sqlite/mysql/sqlserver等)&#xff1a;mysql 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 一、登录注册模块: 1.学生&#xff0c;教师&#xff0c;管理员三个角色&#xff08;同一时刻&#xff0c;账户…

Incorporating Dynamic Semantics into Pre-Trained Language Model for ABSA 论文阅读笔记

一、作者 Kai Zhang、Kun Zhang、Mengdi Zhang、Hongke Zhao、Qi Liu、Wei Wu、Enhong Chen School of Data Science, University of Science and Technology of China School of Computer Science and Information Engineering, Hefei University of Technology Meituan …

移动端Swiper的一些尝试(中间完整,两边有内容,循环展示)

【背景】最近公司的需求很多实现一个Swiper滑块&#xff0c;中间完整。两边展示一些内容&#xff08;可能是固定的提示方案&#xff0c;也可能是前后上下一页的部分内容&#xff09;&#xff0c;然后还需要循环滚动&#xff0c;其中遇到了一些问题特此记录 1、实现两边展示内容…

Canvas 从进阶到退学

本文简介 点赞 关注 收藏 学会了 接着 《Canvas 从入门到劝朋友放弃&#xff08;图解版&#xff09;》 &#xff0c;本文继续补充 canvas 基础知识点。 这次我不手绘了&#xff01; 本文会涉及到 canvas 的知识包括&#xff1a;变形、像素控制、渐变、阴影、路径 变形 这…

向量杂记(python)

以 O 点为圆心&#xff0c; 绘制如上图&#xff0c; 则有向量 vOA&#xff0c; vOB, vOC, vOD 再给定一个X轴方向的单位向量 vX [1, 0] 则 向量 vOA&#xff0c; vOB, vOC, vOD 分别与 vX 进行叉乘&#xff1a; # python 代码 # X Y 轴上的单位向量 vX [1, 0] vY [0, 1]vA …