「掘金·启航计划」花了一天完成的简单又有趣的马赛克小工具

1,273 阅读5分钟

mosaic

我正在参加「掘金·启航计划」

作者的话

最近刷掘金和抖音刷到实现马赛克功能的相关文章和视频,觉得挺有趣的,想自己也实现一个类似的,但是如果差不多效果,那么又显得没啥意思,所以自己想了一点新创意,在原来基础上进行了一点点的简单升级,然后就可以实现一些简单漂亮的效果(可以在下方【小案例】中浏览效果),包括了:

  • 基本的马赛克功能
  • 马赛克加载动画
  • 马赛克画笔
  • 散点图加载动画

实现思路

基本的马赛克功能

  1. 首先获取到整个canvas的像素信息,使用getImageData函数
  2. 然后定义马赛克小方格的宽度,也就是一个马赛克占用几个像素的宽度
  3. 确定好后那么将每个马赛克小方格内的像素设定为统一的颜色,为了简单,将颜色设置为第一个像素的颜色
  4. 设置好每个canvas的像素信息后,使用putImageData函数将canvas重新画一遍,这样就能得到马赛克图片了

💡 其中的难点可能就是计算得到每个马赛克小方格的像素在imageData中的下标了

效果如下:

马赛克加载动画

  1. 如果已经实现了图片马赛克,要实现马赛克加载动画就非常简单了
  2. 因为只要动画的每一帧更改每个马赛克占用的像素宽度就可以了,也就是从max到1
  3. 那么用js实现动画,一般就是使用requestAnimationFrame函数了,将每一帧的操作封装好然后执行该函数就能实现马赛克动画了

效果如下:

马赛克画笔

  1. 马赛克画笔首先要监听用户的点击事件,计算得到用户鼠标在这个canvas中的坐标
  2. 然后当用户移动时,也要监听移动事件,根据移动的坐标画出每个马赛克
  3. 当用户鼠标弹起时,取消移动事件即可

效果如下:

散点图加载动画

  1. 首先默认情况,最开始画布为透明的
  2. 根据用户屏幕的刷新率,一般为60,然后根据canvas所有的像素点个数,计算出每次刷新时要展示的点数frameNum
  3. 随机生成frameNum个点的下标,并将透明的imageData相应下标点颜色值进行替换为图像该点的颜色值
  4. 然后用set记录已经画出的点的下标,每次刷新的时候将点画到画布上

效果如下:

基本使用

⭕️ 作者已经发布了npm包,附上地址,包里面有图片和用例代码,大家感兴趣的可以看看

npm i @karl_fang/mosaic

import DrawTool from '@karl_fang/mosaic';

const side = 50;
const draw = new DrawTool({
	el: canvas,
  imageData: res,
  immediate: false,
  mosaic: {
  	side
  }
});

构造函数

  • 使用方法:new DrawTool(config)
参数功能类型默认值是否必须
el渲染canvas的节点DOM
imageDatacanvas的像素信息ImageData
immediate是否立即显示马赛克Booleantrue
mosaic.side每个马赛克边长的像素值Number10
mosaic.color每个马赛克边长的颜色"first"|"last"|"random"|valid color|(i,j)=>{}'first'

valid color: 也就是合法的颜色字符串(十六进制),如#f00或#123456

(i,j)=>{}: 传入的参数代表是第i行第j列的马赛克

mosaic

  • 使用方法:DrawTool.prototype.mosaic(config)
参数功能类型默认值是否必须
width马赛克的宽度Numberel.width
height马赛克的高度Numberel.height
dx马赛克的起点的横坐标Number0
dy马赛克的起点的纵坐标Number0
newSide每个马赛克边长的像素值Numbermosaic.side

scatter

  • 使用方法:DrawTool.prototype.scatter(duration, fn)
参数功能类型默认值是否必须
duration动画时长(秒)Number1
fn动画开始前每个像素的颜色值Function(i) => [0, 0, 0, 0]

(i) => [0, 0, 0, 0]: 默认每个像素的颜色为透明色,传入的参数i代表第i个像素值(将canvas展平成一行后)

setSide

  • 使用方法:DrawTool.prototype.setSide(newVal)
参数功能类型默认值是否必须
newVal设置每个马赛克边长的像素值Number

setColor

  • 使用方法:DrawTool.prototype.setColor(fn)
参数功能类型默认值是否必须
fn每个马赛克边长的颜色(i,j)=>{}

(i,j)=>{}: 传入的参数代表是第i行第j列的马赛克,同mosaic.color

小案例

基本代码

<body>
  <canvas id="myCanvas"></canvas>

  <script type="module">
    import DrawTool from './index.js';
    // 获取<canvas>元素和绘图上下文
    const canvas = document.querySelector("#myCanvas");
    const ctx = canvas.getContext('2d');
    function request(url) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.src = url;
        img.onload = function () {
          var scale = 0.5;

          // 计算缩小后的宽度和高度
          const width = img.width * scale;
          const height = img.height * scale;

          canvas.width = width;
          canvas.height = height;

          ctx.drawImage(img, 0, 0, width, height);
          resolve(ctx.getImageData(0, 0, width, height));
        }

        img.onerror = function () {
          reject('img load fail!');
        }
      })
    }
  </script>
</body>

显示马赛克

request('./demo.jpeg').then(res => {
  const side = 50;
  const draw = new DrawTool({
    el: canvas,
    imageData: res,
    immediate: true,
    mosaic: {
      side
    }
  });
}).catch(console.error);

马赛克加载动画

request('./demo.jpeg').then(res => {
  const side = 50;
  const draw = new DrawTool({
    el: canvas,
    imageData: res,
    immediate: false,
    mosaic: {
      side
    }
  });
  
  let cnt = 50, step = 1, timer = null;
  const loading = () => {
    cnt -= step;
    draw.setSide(cnt);
    draw.mosaic();
    if(cnt > 0) {
      timer = requestAnimationFrame(loading);
    } else {
      cancelAnimationFrame(timer);
    }
  }
  requestAnimationFrame(loading);
  
}).catch(console.error)

马赛克画笔

request('./demo.jpeg').then(res => {
  const side = 50;
  const draw = new DrawTool({
    el: canvas,
    imageData: res,
    immediate: false,
    mosaic: {
      side
    }
  });
  
  
  canvas.addEventListener('mousedown', (e) => {

  	const { offsetTop, offsetLeft } = canvas;

      const move = e => {
        const { pageX, pageY } = e;
        draw.mosaic({
          dx: pageX - offsetLeft,
          dy: pageY - offsetTop,
          newSide: 5
        });
      }
      
    	document.addEventListener('mousemove', move);

    	document.addEventListener('mouseup', () => {

        document.removeEventListener('mousemove', move);
      })
  	})
  
}).catch(console.error)

散点加载动画

request('./demo.jpeg').then(res => {
  const side = 50;
  const draw = new DrawTool({
    el: canvas,
    imageData: res,
    immediate: false,
    mosaic: {
      side
    }
  });
  draw.scatter(2, (i) => {
    if(i % 3 === 0) {
      return [255, 0, 0, 255]
    } else if(i % 3 === 1) {
      return [0, 255, 0, 255]
    } else {
      return [0, 0, 255, 255];
    }
  });
  
}).catch(console.error)

写在最后

作者花了一天时间完成了这个简单的小Demo,写不完心里难受,哈哈,大家有什么问题或者建议可以评论区讨论一下,如果喜欢的话也可以点赞➕收藏 🌟,感谢大家的支持。