查看原文
其他

canvas 中普通动效与粒子动效的实现

前端大全 2022-06-29

(给前端大全加星标,提升前端技能


作者:薄荷前端

https://github.com/BooheeFE/weekly/issues/26


canvas 用于在网页上绘制图像、动画,可以将其理解为画布,在这个画布上构建想要的效果。


canvas 可以绘制动态效果,除了常用的规则动画之外,还可以采用粒子的概念来实现较复杂的动效,本文分别采用普通动效与粒子特效实现了一个简单的时钟。


普通时钟


普通动效即利用 canvas 的 api,实现有规则的图案、动画。


效果



该效果实现比较简单,主要分析一下刻度与指针角度偏移的实现。


绘制刻度


此例为小时刻度的绘制:表盘上共有 12 个小时,Math.PI 为 180°,每小时占据 30°。

.save()表示保存 canvas 当前环境的状态,在此基础上进行绘制。绘制完成之后,返回之前保存过的路径状态和属性。


分钟刻度同理,改变角度与样式即可。


// 小时时间刻度
offscreenCanvasCtx.save();
for (var i = 0; i < 12; i++) {
  offscreenCanvasCtx.beginPath();
  // 刻度颜色
  offscreenCanvasCtx.strokeStyle = "#fff";
  // 刻度宽度
  offscreenCanvasCtx.lineWidth = 3;
  // 每小时占据30°
  offscreenCanvasCtx.rotate(Math.PI / 6);
  // 开始绘制的位置
  offscreenCanvasCtx.lineTo(1400);
  // 结束绘制的位置;
  offscreenCanvasCtx.lineTo(1200);
  // 绘制路径
  offscreenCanvasCtx.stroke();
}
offscreenCanvasCtx.restore();


指针指向


以秒针为例:获取当前时间的秒数,并计算对应的偏移角度


  var now = new Date(),
    sec = now.getSeconds(),
    min = now.getMinutes(),
    hr = now.getHours();
  hr = hr 12 ? hr - 12 : hr;

  //秒针
  offscreenCanvasCtx.save();
  offscreenCanvasCtx.rotate(sec * (Math.PI / 30));
  ......
  offscreenCanvasCtx.stroke();


粒子动效


canvas 可以用来绘制复杂,不规则的动画。粒子特效可以用来实现复杂、随机的动态效果。


粒子,指图像数据imageData中的每一个像素点,获取到每个像素点之后,添加属性或事件对区域内的粒子进行交互,达到动态效果。


效果



粒子获取


以下图的图片转化为例,该效果是先在 canvas 上渲染图片,然后获取文字所在区域的每个像素点。


let image = new Image();
image.src = "../image/logo.png";
let pixels = []; //存储像素数据
let imageData;
image.width = 300;
image.height = 300;
// 渲染图片,并获取该区域内像素信息
image.onload = function() {
  ctx.drawImage(
    image,
    (canvas.width - image.width) / 2,
    (canvas.height - image.height) / 2,
    image.width,
    image.height
  );
  imageData = ctx.getImageData(
    (canvas.width - image.width) / 2,
    (canvas.height - image.height) / 2,
    image.width,
    image.height
  ); //获取图表像素信息
  //绘制图像
};


像素信息


图片的大小为 300*300,共有 90000 个像素,每个像素占 4 位,存放 rgba 数据。




粒子绘制


function getPixels() {
  var pos = 0;
  var data = imageData.data; //RGBA的一维数组数据
  //源图像的高度和宽度为300px
  for (var i = 1; i <= image.width; i++) {
    for (var j = 1; j <= image.height; j++) {
      pos = [(i - 1) * image.width + (j - 1)] * 4//取得像素位置
      if (data[pos] >= 0) {
        var pixel = {
          x: (canvas.width - image.width) / 2 + j + Math.random() * 20//重新设置每个像素的位置信息
          y: (canvas.height - image.height) / 2 + i + Math.random() * 20//重新设置每个像素的位置信息
          fillStyle:
            "rgba(" +
            data[pos] +
            "," +
            data[pos + 1] +
            "," +
            data[pos + 2] +
            "," +
            data[pos + 3] +
            ")"
        };
        pixels.push(pixel);
      }
    }
  }
}
function drawPixels() {
  var canvas = document.getElementById("myCanvas");
  var ctx = canvas.getContext("2d");
  ctx.clearRect(00, canvas.width, canvas.height);
  var len = pixels.length,
    curr_pixel = null;
  for (var i = 0; i < len; i++) {
    curr_pixel = pixels[i];
    ctx.fillStyle = curr_pixel.fillStyle;
    ctx.fillRect(curr_pixel.x, curr_pixel.y, 11);
  }
}


粒子时钟


渲染文字时钟


function time() {
  ctx.clearRect(00, canvas.width, canvas.height);
  ctx.font = "150px 黑体";
  ctx.textBaseline = "top";
  ctx.fillStyle = "rgba(245,245,245,0.2)";
  ctx.fillText(
    new Date().format("hh:mm:ss"),
    (canvas.width - textWidth) / 2,
    (canvas.height - textHeight) / 2,
    textWidth,
    textHeight
  );
}


效果



获取粒子


文字转换粒子概念同上,获取选定区域的像素,根据筛选条件进行选择并存入数组。经过遍历后重新绘制。


function getPixels() {
  let imgData = ctx.getImageData(
    (canvas.width - textWidth) / 2,
    (canvas.height - textHeight) / 2,
    textWidth,
    textHeight
  );
  let data = imgData.data;
  pixelsArr = [];
  for (let i = 1; i <= textHeight; i++) {
    for (let j = 1; j <= textWidth; j++) {
      pos = [(i - 1) * textWidth + (j - 1)] * 4//取得像素位置
      if (data[pos] >= 0) {
        var pixel = {
          x: j + Math.random() * 20//重新设置每个像素的位置信息
          y: i + Math.random() * 20//重新设置每个像素的位置信息
          fillStyle:
            "rgba(" +
            data[pos] +
            "," +
            data[pos + 1] +
            "," +
            data[pos + 2] +
            "," +
            data[pos + 3] +
            ")"
        };
        pixelsArr.push(pixel);
      }
    }
  }
}


imgData保存了所选区域内的像素信息,每个像素点占据 4 位,保存了 RGBA 四位信息。筛选每个像素的第四位,这段代码中将所有透明度不为 0 的像素都保存到了数组pixelsArr中。


x、y记载了该粒子的位置信息,为了产生效果图中的运动效果,给每个粒子添加了 0-20 个像素的偏移位置,每次重绘时,偏移位置随机生成,产生运动效果。


粒子重绘


获取粒子之后,需要清除画布中原有的文字,将获取到的粒子重新绘制到画布上去。


function drawPixels() {
  // 清除画布内容,进行重绘
  ctx.clearRect(00, canvas.width, canvas.height);
  for (let i in pixelsArr) {
    ctx.fillStyle = pixelsArr[i].fillStyle;
    let r = Math.random() * 4;
    ctx.fillRect(pixelsArr[i].x, pixelsArr[i].y, r, r);
  }
}


粒子重绘时的样式为筛选像素时原本的颜色与透明度,并且每个在画布上绘制每个粒子时,定义大小参数 r,r 取值为 0-4 中随机的数字。最终生成的粒子大小随机。


实时刷新



获取粒子并成功重绘之后,需要页面实时刷新时间。这里采用window.requestAnimationFrame(callback)方法。


  function time() {
    ......
    getpixels(); //获取粒子
    drawPixels(); // 重绘粒子
    requestAnimationFrame(time);
  }


window.requestAnimationFrame(callback) 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。


该方法不需要设置时间间隔,调用频率采用系统时间间隔(1s)。


文档解释戳这里


效果



总结


本文主要通过两种不同的方式实现了时钟的动态效果,其中粒子时钟具有更多的可操作性。在以后的 canvas 系列中会针对粒子系统实现更多的动态效果。


推荐阅读

(点击标题可跳转阅读)

canvas 入门实战--邀请卡生成与下载

基于 HTML5 Canvas 的交互式地铁线路图

Canvas 动画之支付宝价格拖动选择



觉得本文对你有帮助?请分享给更多人

关注「前端大全」加星标,提升前端技能

喜欢就点一下「好看」呗~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存