javaScript高级程序设计(18)——动画与Canvas图形

requestAnimationFrame

javascript早期定时动画类似这样

const boxEle = document.querySelector("#box");
boxEle.style.marginLeft = 0;
let left = Number(boxEle.style.marginLeft.split("px")[0]);
function doAnimation1() {
  if (left < window.innerWidth - boxEle.clientWidth) {
    boxEle.style.marginLeft = left + 3 + "px";
    left = Number(boxEle.style.marginLeft.split("px")[0]);
  } else {
    boxEle.style.marginLeft = 0 + "px";
    left = 0;
  }
}
(function () {
  function updateAnimations() {
    doAnimation1();
  }
  setInterval(updateAnimations, 17);
})();

上述定时动画存在的问题:

1.  定时时间需要足够短,才能让不同的动画类型都能平滑顺畅;但又要足够长,让浏览器来得及渲染出来。

2. 无法准确知晓循环之间的延时。setInterval和setTimeout都不能保证时间精度,第二个参数的时间只能保证何时会把代码添加到浏览器的任务队列,如果任务队列前面还有其他任务,需要等这些任务执行完在执行,浏览器自身计时器的精度问题更是雪上加霜。

一般计算机显示器的屏幕刷新率都是60Hz,基本上意味着每秒需要重绘60次。大多数浏览器会限制重绘频率,使其不超出屏幕的刷新率,这是因为超过刷新率,用户也感知不到。因此,实现平滑动画最佳的重绘间隔大约为17ms。

requestAnimationFrame()方法接收一个参数,此参数是一个要在重绘屏幕前调用的函数。这个函数就是修改DOM样式以反映下一次重绘有什么变化的地方。

用requestAnimationFrame的方式实现上述动画,因为这个方法只会调用一次传入的函数。因此在每次更新之后,需要重新传入一次,这样就实现了循环动画。这样就有效解决了上述最佳时间间隔的问题。

const boxEle = document.querySelector("#box");
boxEle.style.marginLeft = 0;
let left = Number(boxEle.style.marginLeft.split("px")[0]);
function doAnimation1() {
  if (left < window.innerWidth - boxEle.clientWidth) {
    boxEle.style.marginLeft = left + 3 + "px";
    left = Number(boxEle.style.marginLeft.split("px")[0]);
  } else {
    boxEle.style.marginLeft = 0 + "px";
    left = 0;
  }
}
(function () {
  function updateAnimations() {
    doAnimation1();
    requestAnimationFrame(updateAnimations);
  }
  requestAnimationFrame(updateAnimations);
})();

cancelAnimationFrame(requestId)取消重绘人任务

const requestId = requestAnimationFrame(updateAnimations);
cancelAnimationFrame(requestId);

requestAnimationFrame()方法还可以配置实现节流

let enabled = true;
function fn() {
  console.log(`LLqTest`);
}
window.addEventListener("scroll", () => {
  if (enabled) {
    enabled = false;
    window.requestAnimationFrame(fn);
    window.setTimeout(() => {
      enabled = true;
    }, 50);
  }
});

Canvas

2D绘图上下文

2D绘图上下文提供了绘制2D图形的方法,包括矩形、弧形和路径。2D上下文的坐标原点(0, 0)在<canvas>元素的左上角。所有坐标值都相对于该点计算,因此x坐标向右增长,y坐标向下增长。默认情况下,width和height表示两个方向上像素的最大值。

填充和描边

填充以指定样式(颜色、渐变或图像)自动填充形状,而描边只为图形边界着色。这两个属性可以是字符串、渐变对象或图案对象,默认值都为“#000000”

<canvas id="drawing" width="844" height="390"></canvas>
  <script>
    const drawing = document.querySelector("#drawing");
    if (drawing.getContext) {
      const context = drawing.getContext("2d");
      context.strokeStyle = "red";
      context.fillStyle = "#0000ff";
    }
  </script>

绘制矩形

矩形是唯一一个可以直接在2D绘图上下文中绘制的形状。与绘制矩形相关的方法有3个:fillRect()、strokeRect()和clearRect()。这些方法都接收4个参数:矩形x坐标、矩形y坐标、矩形宽度和矩形高度。这几个参数的单位都是像素。

<canvas id="drawing" width="844" height="390"></canvas>
<script>
  const drawing = document.querySelector("#drawing");
  if (drawing.getContext) {
    const context = drawing.getContext("2d");
    context.strokeStyle = "red";
    context.lineWidth = 2; // 描边宽度:任意数值
    context.lineCap = "butt"; // 线条端点的形状:butt平头、round出圆头、square出方头
    context.lineJoin = "miter"; // 线条交点的形状:round圆转、bevel取平、miter出尖
    context.strokeRect(10, 10, 50, 50);
    context.fillStyle = "#0000ff";
    context.fillRect(30, 30, 50, 50);
    // clearRect擦除画布某个区域
    context.clearRect(40, 40, 10, 10);
  }
</script>

绘制路径

❑ arc(x, y, radius, startAngle, endAngle, counterclockwise):以坐标(x, y)为圆心,以radius为半径绘制一条弧线,起始角度为startAngle,结束角度为endAngle(都是弧度)。最后一个参数counterclockwise表示是否逆时针计算起始角度和结束角度(默认为顺时针)。
❑ arcTo(x1, y1, x2, y2, radius):以给定半径radius,经由(x1, y1)绘制一条从上一点到(x2, y2)的弧线。
❑ bezierCurveTo(c1x, c1y, c2x, c2y, x, y):以(c1x, c1y)和(c2x, c2y)为控制点,绘制一条从上一点到(x, y)的弧线(三次贝塞尔曲线)。
❑ lineTo(x, y):绘制一条从上一点到(x, y)的直线。
❑ moveTo(x, y):不绘制线条,只把绘制光标移动到(x, y)。
❑ quadraticCurveTo(cx, cy, x, y):以(cx, cy)为控制点,绘制一条从上一点到(x, y)的弧线(二次贝塞尔曲线)。
❑ rect(x, y, width, height):以给定宽度和高度在坐标点(x, y)绘制一个矩形。这个方法与strokeRect()和fillRect()的区别在于,它创建的是一条路径,而不是独立的图形。

<script>
  const drawing = document.querySelector("#drawing");
  if (drawing.getContext) {
    const context = drawing.getContext("2d");
    // 创建路径
    context.beginPath();
    // 绘制外圆
    context.arc(150, 150, 99, 0, 2 * Math.PI, false);
    // 绘制内圆
    context.moveTo(244, 150);
    context.arc(150, 150, 94, 0, 2 * Math.PI, false);
    // 绘制时针分针
    context.moveTo(150, 150);
    context.lineTo(85, 150);
    context.moveTo(150, 150);
    context.lineTo(150, 65);
    context.stroke();
  }
</script>

绘制文本

2D绘图上下文还提供了绘制文本的方法,即fillText()和strokeText()。这两个方法都接收4个参数:要绘制的字符串、x坐标、y坐标和可选的最大像素宽度。

这两个方法最终绘制结果取决于以下3个属性

❑ font:以CSS语法指定的字体样式、大小、字体族等,比如"10px Arial"。
❑ textAlign:指定文本的对齐方式,可能的值包括"start"、"end"、"left"、"right"和"center"。推荐使用"start"和"end",不使用"left"和"right",因为前者无论在从左到右书写的语言还是从右到左书写的语言中含义都更明确。
❑ textBaseLine:指定文本的基线,可能的值包括"top"、"hanging"、"middle"、"alphabetic"、"ideographic"和"bottom"。

// 绘制文本
context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 150, 70);
context.textAlign = "start";
context.fillText("12", 150, 90);
context.textAlign = "end";
context.fillText("12", 150, 110);

如果想把文本绘制到特定的区域,可以使用measureText()。measureText()方法使用font、textAlign和textBaseline属性当前的值计算绘制指定文本后的大小。

fillText()和strokeText()方法还有第四个参数,即文本的最大宽度。这个参数是可选的(Firefox 4是第一个实现它的浏览器),如果调用fillText()和strokeText()时提供了此参数,但要绘制的字符串超出了最大宽度限制,则文本会以正确的字符高度绘制,这时字符会被水平压缩,以达到限定宽度。

变换

以下方法可用于改变绘制上下文的变换矩阵。
❑ rotate(angle):围绕原点把图像旋转angle弧度。
❑ scale(scaleX, scaleY):通过在x轴乘以scaleX、在y轴乘以scaleY来缩放图像。scaleX和scaleY的默认值都是1.0。
❑ translate(x, y):把原点移动到(x, y)。执行这个操作后,坐标(0, 0)就会变成(x, y)。
❑ transform(m1_1, m1_2, m2_1, m2_2, dx, dy):像下面这样通过矩阵乘法直接修改矩阵。
​m1_1 m1_2 dx
​​​​​​m2_1 m2_2 dy
​​​​​​0     0     1​​
❑ setTransform(m1_1, m1_2, m2_1, m2_2, dx, dy):把矩阵重置为默认值,再以传入的参数调用transform()。

<canvas id="drawing" width="844" height="390"></canvas>
<script>
  const drawing = document.querySelector("#drawing");
  if (drawing.getContext) {
    const context = drawing.getContext("2d");
    // 创建路径
    context.beginPath();
    // 绘制外圆
    context.arc(150, 150, 99, 0, 2 * Math.PI, false);
    // 绘制内圆
    context.moveTo(244, 150);
    context.arc(150, 150, 94, 0, 2 * Math.PI, false);
    context.translate(150, 150);
    // 绘制时针分针
    context.rotate(1);
    context.moveTo(0, 0);
    context.lineTo(0, -85);
    context.moveTo(0, 0);
    context.lineTo(-65, 0);
    context.stroke();
  }
</script>

对context设置的属性和变换状态,会一直保留在上下文中,直到再次修改他们。可以用save()方法保存这个状态到暂存栈中,用restore()方法取出这个状态。

     const context = drawing.getContext("2d");
        context.fillStyle = "#ff0000";
        context.save();
        context.fillStyle = "#00ff00";
        context.translate(100, 100);
        context.save();
        context.fillStyle = "#0000ff";
        context.fillRect(0, 0, 100, 200);
        // 在(100, 100)绘制蓝色矩形​​​​
        context.restore();
        context.fillRect(10, 10, 100, 200);
        // 在(100, 100)绘制绿色矩形​​​
        context.restore();
        context.fillRect(0, 0, 100, 200);
        // 在(0, 0)绘制红色矩形​​

绘制图像

如果想把现有图像绘制到画布上,可以使用drawImage()方法。这个方法可以接收3组不同的参数,并产生不同的结果。

第一组:图像,绘制目标的x和y坐标

第二组:图像,绘制目标的x和y坐标,目标宽度和高度

第三组:要绘制的图像、源图像x坐标、源图像y坐标、源图像宽度、源图像高度、目标区域x坐标、目标区域y坐标、目标区域宽度和目标区域高度

<script>
  const drawing = document.querySelector("#drawing");
  const image = document.images[0];
  console.log(`LLqTest`, image);
  if (drawing.getContext) {
    const context = drawing.getContext("2d");
    image.onload = () => {
      // context.drawImage(image, 10, 10);
      // context.drawImage(image, 10, 10, 100, 100);
      context.drawImage(image, 0, 0, 100, 100, 100, 100, 50, 100);
    };
  }
</script>

阴影

2D上下文可以根据以下属性的值自动为已有形状或路径生成阴影。
❑ shadowColor: CSS颜色值,表示要绘制的阴影颜色,默认为黑色。
❑ shadowOffsetX:阴影相对于形状或路径的x坐标的偏移量,默认为0。
❑ shadowOffsetY:阴影相对于形状或路径的y坐标的偏移量,默认为0。
❑ shadowBlur:像素,表示阴影的模糊量。默认值为0,表示不模糊。

<script>
  const drawing = document.querySelector("#drawing");
  if (drawing.getContext) {
    const context = drawing.getContext("2d");
    context.shadowOffsetX = 5;
    context.shadowOffsetY = 5;
    context.shadowBlur = 4;
    context.shadowColor = "rgba(0,0,0,0.5)";
    context.fillStyle = "#ff0000";
    context.fillRect(10, 10, 100, 100);
  }
</script>

渐变

线性渐变

<script>
  function createRectLinearGradient(context, x, y, width, height) {
    return context.createLinearGradient(x, y, x + width, y + height);
  }
  const drawing = document.querySelector("#drawing");
  if (drawing.getContext) {
    const context = drawing.getContext("2d");
    let gradient = createRectLinearGradient(context, 30, 30, 100, 100);
    gradient.addColorStop(0, "white");
    gradient.addColorStop(1, "black");
    // 绘制渐变矩形
    context.fillStyle = gradient;
    context.fillRect(30, 30, 100, 100);
  }
</script>

径向渐变

<script>
  function createRectLinearGradient(context, x, y, width, height) {
    return context.createLinearGradient(x, y, x + width, y + height);
  }
  const drawing = document.querySelector("#drawing");
  if (drawing.getContext) {
    const context = drawing.getContext("2d");
    let gradient = context.createRadialGradient(55, 55, 30, 55, 55, 60);
    gradient.addColorStop(0, "white");
    gradient.addColorStop(1, "black");
    // 绘制红色矩形​​​
    context.fillStyle = "#ff0000";
    context.fillRect(10, 10, 100, 100);
    // 绘制渐变矩形
    context.fillStyle = gradient;
    context.fillRect(30, 30, 100, 100);
  }
</script>

图案

<script>
  function createRectLinearGradient(context, x, y, width, height) {
    return context.createLinearGradient(x, y, x + width, y + height);
  }
  const drawing = document.querySelector("#drawing");
  if (drawing.getContext) {
    const context = drawing.getContext("2d");
    let image = document.images[0];
    image.onload = () => {
      pattern = context.createPattern(image, "repeat");
      // 绘制矩形​​​
      context.fillStyle = pattern;
      context.fillRect(10, 10, 844, 390);
    };
  }
</script>

图像数据

2D上下文中比较强大的一种能力是可以使用getImageData()方法获取原始图像数据。这个方法接收4个参数:要取得数据中第一个像素的左上角坐标和要取得的像素宽度及高度。

<script>
  let drawing = document.getElementById("drawing");
  if (drawing.getContext) {
    let context = drawing.getContext("2d"),
      image = document.images[0],
      imageData,
      data,
      i,
      len,
      average,
      red,
      green,
      blue,
      alpha;
    image.onload = () => {
      context.drawImage(image, 0, 0);
      // 取得图像数据
      imageData = context.getImageData(0, 0, image.width, image.height);
      data = imageData.data;
      for (i = 0, len = data.length; i < len; i += 4) {
        red = data[i];
        green = data[i + 1];
        blue = data[i + 2];
        alpha = data[i + 3];
        // 取得RGB平均值
        average = Math.floor((red + green + blue) / 3);
        // 设置颜色,不管透明度
        data[i] = average;
        data[i + 1] = average;
        data[i + 2] = average;
      }
      // 将修改后的数据写回ImageData并应用到画布上显示出来
      imageData.data = data;
      context.putImageData(imageData, 0, 0);
    };
  }
</script>

合成

2D上下文中绘制的所有内容都会应用两个属性:globalAlpha和globalComposition Operation,其中,globalAlpha属性是一个范围在0~1的值(包括0和1),用于指定所有绘制内容的透明度,默认值为0。

globalCompositionOperation属性表示新绘制的形状如何与上下文中已有的形状融合。这个属性是一个字符串,可以取下列值

❑ source-over:默认值,新图形绘制在原有图形上面。
❑ source-in:新图形只绘制出与原有图形重叠的部分,画布上其余部分全部透明。
❑ source-out:新图形只绘制出不与原有图形重叠的部分,画布上其余部分全部透明。
❑ source-atop:新图形只绘制出与原有图形重叠的部分,原有图形不受影响。
❑ destination-over:新图形绘制在原有图形下面,重叠部分只有原图形透明像素下的部分可见。
❑ destination-in:新图形绘制在原有图形下面,画布上只剩下二者重叠的部分,其余部分完全透明。
❑ destination-out:新图形与原有图形重叠的部分完全透明,原图形其余部分不受影响。
❑ destination-atop:新图形绘制在原有图形下面,原有图形与新图形不重叠的部分完全透明。
❑ lighter:新图形与原有图形重叠部分的像素值相加,使该部分变亮。
❑ copy:新图形将擦除并完全取代原有图形。
❑ xor:新图形与原有图形重叠部分的像素执行“异或”计算。

WebGL

WebGL是画布的3D上下文。与其他Web技术不同,WebGL不是W3C制定的标准,而是Khronos Group的标准。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值