mosaic
我正在参加「掘金·启航计划」
作者的话
最近刷掘金和抖音刷到实现马赛克功能的相关文章和视频,觉得挺有趣的,想自己也实现一个类似的,但是如果差不多效果,那么又显得没啥意思,所以自己想了一点新创意,在原来基础上进行了一点点的简单升级,然后就可以实现一些简单漂亮的效果(可以在下方【小案例】中浏览效果),包括了:
- 基本的马赛克功能
- 马赛克加载动画
- 马赛克画笔
- 散点图加载动画
实现思路
基本的马赛克功能
- 首先获取到整个canvas的像素信息,使用
getImageData
函数 - 然后定义马赛克小方格的宽度,也就是一个马赛克占用几个像素的宽度
- 确定好后那么将每个马赛克小方格内的像素设定为统一的颜色,为了简单,将颜色设置为第一个像素的颜色
- 设置好每个canvas的像素信息后,使用
putImageData
函数将canvas重新画一遍,这样就能得到马赛克图片了
💡 其中的难点可能就是计算得到每个马赛克小方格的像素在imageData中的下标了
效果如下:
马赛克加载动画
- 如果已经实现了图片马赛克,要实现马赛克加载动画就非常简单了
- 因为只要动画的每一帧更改每个马赛克占用的像素宽度就可以了,也就是从max到1
- 那么用js实现动画,一般就是使用
requestAnimationFrame
函数了,将每一帧的操作封装好然后执行该函数就能实现马赛克动画了
效果如下:
马赛克画笔
- 马赛克画笔首先要监听用户的点击事件,计算得到用户鼠标在这个canvas中的坐标
- 然后当用户移动时,也要监听移动事件,根据移动的坐标画出每个马赛克
- 当用户鼠标弹起时,取消移动事件即可
效果如下:
散点图加载动画
- 首先默认情况,最开始画布为透明的
- 根据用户屏幕的刷新率,一般为60,然后根据canvas所有的像素点个数,计算出每次刷新时要展示的点数
frameNum
- 随机生成
frameNum
个点的下标,并将透明的imageData相应下标点颜色值进行替换为图像该点的颜色值 - 然后用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 | ✅ | |
imageData | canvas的像素信息 | ImageData | ✅ | |
immediate | 是否立即显示马赛克 | Boolean | true | ❎ |
mosaic.side | 每个马赛克边长的像素值 | Number | 10 | ❎ |
mosaic.color | 每个马赛克边长的颜色 | "first"|"last"|"random"|valid color|(i,j)=>{} | 'first' | ❎ |
valid color: 也就是合法的颜色字符串(十六进制),如#f00或#123456
(i,j)=>{}: 传入的参数代表是第i行第j列的马赛克
mosaic
- 使用方法:
DrawTool.prototype.mosaic(config)
参数 | 功能 | 类型 | 默认值 | 是否必须 |
---|---|---|---|---|
width | 马赛克的宽度 | Number | el.width | ❎ |
height | 马赛克的高度 | Number | el.height | ❎ |
dx | 马赛克的起点的横坐标 | Number | 0 | ❎ |
dy | 马赛克的起点的纵坐标 | Number | 0 | ❎ |
newSide | 每个马赛克边长的像素值 | Number | mosaic.side | ❎ |
scatter
- 使用方法:
DrawTool.prototype.scatter(duration, fn)
参数 | 功能 | 类型 | 默认值 | 是否必须 |
---|---|---|---|---|
duration | 动画时长(秒) | Number | 1 | ❎ |
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,写不完心里难受,哈哈,大家有什么问题或者建议可以评论区讨论一下,如果喜欢的话也可以点赞➕收藏 🌟,感谢大家的支持。