canvas

208 阅读12分钟

canvas

概述

  • h5 增加的标签
  • 可以用来绘图

使用

  • canvas 只提供了一个画布,想要对画布进行操作的话,需要使用 js
  • 所以可以把 canvas 分为两部分,一部分就是 canvas 标签,另一部分就是 js 部分的绘图程序,他依赖于绘图上下文

canvas 提供的属性和方法

属性

  • width
  • height

方法

  • getContext
    • 一个参数,类型为 string
      • '2d' 返回 2 维上下文
      • 更多参数,查看 mdn
    • 返回绘图上下文上下文
  • toDataURL
    • 将 canvas 画布生成一个 url
    • 两个参数,分别是生成的图像类型和图像质量
      • 图像类型:'image/jpeg'或'image/png'
      • 图像质量:0-1.0 之间
      • 参数不填的话默认为('image/png',1)
    • 返回的 url 是 base64 的
    • 需要注意的是,如果是生成的是 jpeg,且透明背景,透明部分会是黑色的
const canvas = document.querySelector("canvas");
const img = document.querySelector("img");
const ctx = canvas.getContext("2d");
ctx.lineWidth = 10; //设置线宽
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(10, 100);
ctx.stroke();

img.src = canvas.toDataURL();
  • toBlob
    • 创建一个表示当前 canvas 图像的 Blob上传文件就靠它了
    • 三个参数
    • 回调
    • 图像类型:'image/jpeg'或'image/png'
    • 图像质量:0-1.0 之间

创建画布

  • 创建了一个空白的画布没有任何颜色(透明背景)的画布
  • 因为是 h5 新增的,所以可能会有一些浏览器或设备不支持,这时候就会现实标签中间的文字
  • 是一个行内块标签
<canvas id="canvas" width="300" height="300">不兼容</canvas>

设置画布大小

  • 画布默认大小是 300pxX150px
  • css 设置的宽高优先级高于行间设置的不推荐
  • 设置的宽高的时候,避免使用 css 来设置,会导致画布拉伸,尽量在行间设置
  • 画布拉伸问题可以这样理解:
    • canvas 大小可以划分为两个,一个为标签大小(容器),一个为绘图表面大小(真实画布)
    • 我们使用 css 来更改 canvas 的时候,修改的是容器大小,当浏览器发现画布大小和容器大小不一致的时候,会将画布拉伸到和容器大小一致举个例子:将一个1kb的jpg图片打印一个楼那么大
    • 这时候,会引起一些问题,常见的有:画质降低,图像拉伸
  • 推荐两种设置宽高方法,注意:设置时候不要加单位
    • 行间设置
    • js 设置
<!-- 错误做法1 -->
<style>
  canvas {
    width: 1000px;
    height: 1000px;
  }
</style>
<!-- 错误做法2 -->
<script>
  canvas.style.width = "1000px";
</script>

<!-- 正确做法1 -->
<canvas width="1000" height="1000"></canvas>
<!-- 正确做法2 -->
<script>
  canvas.width = 1000;
</script>

绘图上下文

  • 对 cavas 所有的操作都依赖于绘图上下文

2d 上下文

  • 2 维上下文,平面操作
获取 2d 上下文
  1. 拿到 canvas 的 dom
  2. 通过 dom 上的 getContext 方法拿到上下文
    • 方法接收一个参数,2d 代表获取 2 维上下文
let canvas = document.querySelector("#canvas");
let ctx = canvas.getContext("2d");
2d 上下文上的属性
  • 下面出现的 ctx 代表上下文对象
canvas
  • 指向 canvas 对象,可以拿到 canvas 行间属性的宽高,不带单位
  • 注意:拿到的是 cavas 画布的,而不是标签的
  • canvas.width
  • canvas.height
console.log(ctx.canvas.width); //canvas画布的宽
console.log(canvas.width); //canvas画布的宽
// 两种效果一致
  • 来道题区分一下
<style>
  canvas {
    width: 100px;
    height: 100px;
  }
</style>
<body>
  <canvas id="canvas" width="300" height="200"></canvas>
</body>
<script>
  let canvas = document.querySelector("#canvas");
  let ctx = canvas.getContext("2d");
  console.log(ctx.canvas.width); //300
  // 我们需要明确,虽然我们在页面上看到的canvas宽是100px,但是那是画板拉伸过后的,实际上他的width还是300
</script>
fillstyle
  • 指定绘制填充颜色
font
  • 指定 fillText 和 strokeText 的字体
globalAlpha
  • 全局透明度,值为 0-1.0 之间
lineCap
  • 设置绘制线段端点
    • butt 默认
    • round 圆角
    • square 多出一节
lineWidth
  • 线段的宽度,默认 1.0
  • 也为描边宽度
lineJoin
  • 设置线段交点两线相交的点
    • bevel 凸出一节,将连接处以直线磨平,呈现为一个平角
    • round 圆角,将连接处添加弧线,呈显形式为圆角
    • miter 默认值 将两条线段的边延伸,直至相交,常见为凸出一个尖
miterLimit
  • 设置 miter 形式的交点
    • bevel
    • round
    • miter 默认值
shadowBlur
  • 设置阴影模糊度延伸,值越大,范围越大
  • 默认为0
shadowColor
  • 设置阴影颜色
  • 默认为黑色
shadowOffsetX
  • 设置阴影水平偏移
  • 默认为0
shadowOffsetY
  • 设置阴影垂直偏移
  • 默认为0
strokeStyle
  • 设置描边颜色
textAlign
  • 设置 fillText 或 storkeText 设置文字的水平对齐方式
textBaseline
  • 设置 fillText 或 storkeText 设置文字的垂直对齐方式
globalCompositeOperation
  • 设置后续如何在画布上进行绘制
  • 属性:
    • source-over 默认值 在目标图像上显示源图像。
    • source-atop 在目标图像顶部显示源图像。源图像位于目标图像之外的部分是不可见的。
    • source-in 在目标图像中显示源图像。只有目标图像内的源图像部分会显示,目标图像是透明的。
    • source-out 在目标图像之外显示源图像。只会显示目标图像之外源图像部分,目标图像是透明的。
    • destination-over 在源图像上方显示目标图像。
    • destination-atop 在源图像顶部显示目标图像。源图像之外的目标图像部分不会被显示。
    • destination-in 在源图像中显示目标图像。只有源图像内的目标图像部分会被显示,源图像是透明的。
    • destination-out 在源图像外显示目标图像。只有源图像外的目标图像部分会被显示,源图像是透明的。
    • lighter 混合图像,相交位置会融合
    • copy 只展示最后一次绘制的图像,前面的所有绘制都会被清除
    • xor 使用异或操作对源图像与目标图像进行组合。
  • 刮刮卡demo
<!-- 
    1. 给父级设置背景图片
    2. 然后给canvas填充颜色
    3. 设置globalCompositeOperation属性为destination-out
    用户触发事件,执行绘制,暴露父级底图,实现效果
     -->
<head>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .box {
            height: 300px;
            width: 100vw;
            background: url(../精灵图.png);
        }
    </style>
</head>

<body>
    <div class="box">
        <canvas height="300"></canvas>
    </div>
</body>
<script>
    const div = document.querySelector('.box');
    const canvas = document.querySelector('canvas');
    const ctx = canvas.getContext('2d');
    const width = document.body.clientWidth;
    canvas.width = width;
    ctx.fillStyle = '#b0b0b0';
    ctx.fillRect(0, 0, width, 300);
    ctx.globalCompositeOperation = 'destination-out';

    let isDown = false;
    // 鼠标落下
    canvas.addEventListener('mousedown', () => {
        isDown = true;
    }, false);
    // 鼠标抬起
    canvas.addEventListener('mouseup', () => {
        isDown = false;
    }, false);
    // 鼠标移动
    canvas.addEventListener('mousemove', (e) => {
        if (!isDown) return;
        const {
            offsetX,
            offsetY
        } = e;
        ctx.fillStyle = 'none';
        ctx.beginPath();
        ctx.arc(offsetX, offsetY, 20, 0, 2 * Math.PI);
        ctx.fill();
    }, false);
</script>
2d 上下文方法
strokeRect 方法
  • 作用:描边矩形
  • strokeRect(x,y,w,h)
    • x 为起始 x 坐标,y 为起始 y 坐标,w 为宽度,h 为高度
  • 默认为颜色为黑色,线宽为 1
// 以(0,0)为左上角坐标点,绘制了一个宽20,高10的矩形描边
ctx.strokeRect(0, 0, 20, 10);
fillRect 方法
  • 填充矩形
  • fillRect(x,y,w,h)
    • x 为起始 x 坐标,y 为起始 y 坐标,w 为宽度,h 为高度
  • 默认颜色为黑色
// 以(50,50)为左上角坐标点,绘制了一个管40,高40的实体矩形
ctx.fillRect(50, 40, 40, 40);
clearRect 方法
  • 作用:清除矩形
  • clearRect(x,y,w,h)
    • x 为起始 x 坐标,y 为起始 y 坐标,w 为宽度,h 为高度
// 添加填充颜色
ctx.fillStyle = "red";
// 添加描边颜色
ctx.strokeStyle = "red";
// 描边
ctx.strokeRect(20, 20, 20, 20);
// 填充
ctx.fillRect(50, 40, 40, 40);
// 清除刚才的描边矩形
ctx.clearRect(19, 19, 22, 22);
beginPath 方法
  • 将当前路径之中的所有子路径清除掉,重新开始一条路径,新旧路径互不影响
//可以尝试去掉第二个beginPath看下效果
    ctx.strokeStyle = "blue";
    ctx.fillStyle = "red";
    ctx.lineWidth = '5';
    ctx.beginPath();
    ctx.rect(20, 30, 10, 100);
    ctx.stroke();
    ctx.beginPath();
    ctx.rect(100, 150, 150, 100);
    ctx.fill();
closePath 方法
  • 显式封闭某某段开放路径
  • 举例:半圆描边的时候,描边是没有链接到一块的,因为起始和结束点没连接到一起,使用这个方法可以将起点终点连接
arc 方法
  • 创建圆、半圆、弧线
  • sAngle和eAngle最大为2π,也就是2*Math.PI
  • arc(x,y,r,sAngle,eAngle,counterclockwise)
  • x为圆心起始x坐标,y为圆心起始y坐标,r为半径,sAngle为起始角度,eAngle为结束角度,counterclockwise为true就是逆时针绘制,false就是顺时针绘制,默认为false
  • 圆为2π,
  • 补充知识已知起始坐标(x1,y1),和结束坐标(x2,y2),求两点距离,方程式为 z²=(x1-x2)²+(y1-y2)²
    ctx.strokeStyle = "blue";
    ctx.fillStyle = "red";
    ctx.beginPath();
    ctx.arc(200, 100, 100, 0, Math.PI);
    ctx.closePath();
    ctx.stroke();
    ctx.fill();
    // 整圆
    // ctx.arc(200, 200, 100, 0, 2 * Math.PI);
  • demo拖拽画圆
    const canvas = document.querySelector('canvas');
    const ctx = canvas.getContext('2d');
    ctx.lineWidth = 10;
    const start = {};

    function addArc({
        x,
        y,
        r
    }) {
        ctx.beginPath();
        ctx.arc(x, y, r, 0, 2 * Math.PI);
        ctx.stroke();
    };
    canvas.addEventListener('mousedown', (e) => {
        start['x'] = e.clientX;
        start['y'] = e.clientY;
    });
    // 起始x和起始y我们都可以拿到,但是两点距离也就是圆的半径我们拿不到
    // 方程式,已知起始坐标(x1,y1),和结束坐标(x2,y2),求两点距离,方程式为 z²=(x1-x2)²+(y1-y2)²
    canvas.addEventListener('mouseup', (e) => {
        const {
            clientX,
            clientY
        } = e;
        start['r'] = Math.sqrt(Math.pow(start['x'] - clientX, 2) + Math.pow(start['y'] - clientY,
            2));
        addArc(start);
        // console.log(start['r'], '两点距离');
    });
  • canvas规定,当时也arc向当前路径中添加子路径的时候,该方法会将上一条子路径的终点与当前圆弧的起点相连
    //因为第二个arc没有使用beginPath ,所以属于第一个arc的子路径
    ctx.beginPath();
    ctx.arc(300, 190, 40, 0, Math.PI * 2);
    ctx.arc(500, 190, 40, 0, Math.PI * 2);
    ctx.stroke();
arcTo 方法
  • 绘制一个圆角矩形
  • 5个值 x,y,w,h,r
    • 起始x坐标
    • 起始y坐标
    • 圆角弧度,最小为0
const roundedRect = (x, y, w, h, r) => {
        if (w > 0) ctx.moveTo(x + r, y)
        else ctx.moveTo(x - r, y);
        ctx.arcTo(x + w, y, x + w, y + h, r);
        ctx.arcTo(x + w, y + h, x, y + h, r);
        ctx.arcTo(x, y + h, x, y, r);
        if (w > 0) {
            ctx.arcTo(x, y, x + r, y, r);
        } else {
            ctx.arcTo(x, y, x - r, y, r);
        };
    };

    const drawRounderRect = (strokeStyle, fillStyle, x, y, w, h, r) => {
        ctx.beginPath();
        roundedRect(x, y, w, h, r);
        ctx.strokeStyle = strokeStyle;
        ctx.fillStyle = fillStyle;
        ctx.stroke();
        ctx.fill();
    };
    drawRounderRect('blue', 'yellow', 50, 40, 100, 100, 50);
    drawRounderRect('purple', 'green', 275, 40, -100, -100, 20);
    drawRounderRect('blue', 'yellow', 300, 140, 100, -100, 30);
    drawRounderRect('blue', 'yellow', 525, 140, -100, -100, 40);
stroke 方法
  • 使用strokeStyle来描边当前路径
fill 方法
  • 使用filleStyle来填充当前路径
save 方法
  • 保存状态
rect 方法
  • 建立一个矩形
  • rect(x,y,w,h)
  • x是左上角x轴起始位置,y是左上角y轴起始位置,w是宽,h是高
    ctx.strokeStyle = "blue";
    ctx.lineWidth = '5';
    ctx.beginPath();
    ctx.rect(20, 30, 10, 100);
    ctx.stroke();
restore 方法
  • 恢复状态
drawImage 方法
  • 插入图片
  • 如果使用的是 img 标签,那么插入的步骤需要放在 image 的 onload 事件内
  • 用法 1:drawImage(image,x,y)
    • image 可为 canvas 画布也可为 img 图片,x 为起点 x 坐标,y 为起点 y 坐标
  • 用法 2:drawImage(image,x,y,w,h)
    • w 为图片宽,h 为图片高
  • 用法 3:drawImage(image,x1,y1,w1,h2,x,y,w,h)
    • x1 为裁剪 x 起点,y1 为裁剪 y 起点,w1 为裁剪宽度,h2 为裁剪高度
const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.src = "url";
img.onload = () => {
  ctx.drawImage(img, 0, 0);
};
getImageData 方法
  • 获取当前 canvas 的视图
putImageDate 方法
  • 覆盖当前 canvas 的视图
createLinearGradient 线性渐变色
  • 用法:createLinearGradient(x,y,x1,y1)
  • x 是开始 x 坐标,y 为开始 y 坐标,x1 为结束 x 坐标,y1 为结束 y 坐标
  • 将当前渐变当做颜色,赋值给要填充的形状
  • 配合实例上的 addColorStop 方法使用
    • addColorStop(num,color);
    • 作用:在指定范围内填充颜色
    • num 为阈值,区间在 0-1 之间,可为小数
  • 可以把它理解为一个色块,x 和 y 代表一个点,x1 和 y1 代表另一个点,水平或垂直就是直线,反之则是倾斜
  • 还有一个概念,绘制的 x,y,x1,y1 这些坐标点,是针对于当前画布的,填充图像的时候,可以理解为是 ps 里面的蒙版,它只取对应坐标的颜色,给图像
  • 超出 createLinearGradient 范围的颜色,为临近的实色
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.width); //创建实例
gradient.addColorStop(0, "red"); //填充颜色
gradient.addColorStop(1, "yellow"); //填充颜色
ctx.fillStyle = gradient; //赋值
ctx.fillRect(0, 0, canvas.width, canvas.height); //绘制
createRadialGradient 放射型渐变/圆型
  • 用法:createRadialGradient(x,y,r,x1,y1,r1)
    • x 为圆心起始 x 坐标,y 为圆心起始 y 坐标,r 为起始圆心半径,x1 为圆心结束 x 坐标,y1 为圆心结束 y 坐标,r1 为圆心结束半径
  • 可以用来做圆形渐变,也可以用来做放射型渐变(如手电筒放射光)
const gradient = ctx.createRadialGradient(
  canvas.width / 2,
  canvas.height,
  10,
  canvas.width / 2,
  0,
  100
);
gradient.addColorStop(0, "blue");
gradient.addColorStop(0.25, "white");
gradient.addColorStop(0.5, "purple");
gradient.addColorStop(0.75, "red");
gradient.addColorStop(1, "yellow");
ctx.fillStyle = gradient;
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fill();
createPattern 方法
  • 创建填充背景图片
  • 参数:createPattern(image,填充方式); image 参数可为视频、canvas、图片
    • 填充方式:
      • repeat 平铺
      • repeat-y 平铺 y 轴
      • repeat-x 平铺 x 轴
      • no-repeat 不平铺
  • 注意:如果后期想更新图片的话,需要再创建一个新的 createPattern 实例,只改变 src 的话不生效
image.src = "../企鹅.webp";
image.onload = () => {
  // 使用createPattern创建填充颜色
  var pattern = ctx.createPattern(image, "repeat");
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = pattern;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fill();
};
使用阴影
  • 浏览器绘制阴影需要先将阴影渲染到一个位图中,然后将位图与canvas进行图像合成,所以如果添加过多阴影可能会有性能问题
添加事件
  • 与正常 dom 一致,可以使用 on 方法,也可以使用 addEventListener
canvas.onmousedown = () => {};
canvas.addEventListener("mousedown", () => {});
坐标转换
  • 将事件对象上的坐标转换为 canvas 坐标
  • 好处:比 offsetX 和 offsetY 更精准(是浮点型)
function windowToCanvas(canvas, x, y) {
  // getBoundingClientRect方法返回该dom相对整个窗口的坐标和宽高信息
  // left 是距离左侧距离,top是距离上方距离
  var bbox = canvas.getBoundingClientRect();
  return {
    x: x - bbox.left * (canvas.width / bbox.width),
    y: y - bbox.top * (canvas.height / bbox.height),
  };
}
canvas.onmousedown = function (e) {
  var loc = windowToCanvas(canvas, e.clientX, e.clientY);
};
离屏 canvas
  • 指的是当前 canvas 没有在窗口中展示
  • 作用:常用在不展示 canvas 的情况下使用 toDataURL 来生成图片
// 场景1
canvas.style.display = "none";
//场景2
document.createElement("canvas");
非零环绕
  • canvas会隐式的对当前视图绘制辅助线,该辅助线穿过当先视图(也可理解Wie穿过我们绘制的线段)
  • 顺时针就+1,逆时针就-1,最终绘制不为0的地方
        ctx.beginPath();
        ctx.arc(300, 190, 150, 0, Math.PI * 2);
        ctx.arc(300, 190, 100, 0, Math.PI * 2, true);
        ctx.shadowColor = "rgba(0,0,0,0.8)";
        ctx.shadowOffsetX = 12;
        ctx.shadowOffsetY = 12;
        ctx.shadowBlur = 15;
        ctx.fill();
        ctx.shadowColor = 'red';
        ctx.shadowOffsetX = 10;
        ctx.shadowOffsetY = 10;
        ctx.stroke();
角度和弧度
  • 角度就是两边中间的度数,弧度就是整个圆的弧度
  • canvas 中有关角度的 api 都需要弧度来表示
  • Math.sin()、Math.cos()、Math.tan()也采用的是弧度值
  • 角度和弧度的计算公式:
    • 360° = 2π弧度
    • 1° = 180 / π * 1弧度
    • 1弧度 = π / 180 * 1°
    • π 约等于 3.14
    • 计算得知:1° = 57.324840764331206弧度1弧度 = 0.017444444444444446°
正弦、余弦、正切函数
  • 正弦( sine 简称 sin),余弦(cosine 简称 cos),正切(tangent 简称 tan)
  • cos(n) = 邻边 / 斜边
  • sin(n) = 对边 / 斜边
  • tan(n) = 对边 / 邻边
已知两点坐标,求距离
  • 已知(x1,y1)与(x2,y2)
  • 方程式为 z²=(x1-x2)²+(y1-y2)²`