10分钟教你如何拥有属于自己的马赛克萌妹

896 阅读4分钟

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

前言

这么可爱的萌妹子不想拥有一个吗。学起来,自定义一个抱回家

一、前期准备

初始化HTML,利用通配符 * 去除默认样式

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="divport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    *,*::before,*::after{
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }
  </style>
</head>
<body>
  <div id="app"></div>
</body>
<script ></script>
</html>

二、正文开始

先讲一下实现思路:

  1. 通过图片上传获取到图片文件,并生成图片地址
  2. 通过监听图片加载完成事件,将图片画在 canvas1
  3. 获取 canvas1 上的像素点数据,通过压缩,计算灰度,定位等操作,在 canvas2 上画出对应的点

2.1 图片上传

  1. 我们使用 input 来上传图片,通过 onchange 获取到图片的 file 文件。
  2. 使用 createObjectURL 生成图片地址
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="divport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    *,*::before,*::after{
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }
  </style>
</head>
<body>
  <div id="app">
      <div><input type="file"  accept="image/*" onChange="uploadImage(this)" /></div>
      <img src="" id="image" style="display: none;">
  </div>
</body>
<script >
  const image = document.getElementById('image')


  // 图片上传事件
  function uploadImage(obj){
    // 转换格式
    const newSrc = getObjectURL(obj.files[0]);
    testImage.src = newSrc;
  }

  // 生成file文件的临时路径
  function getObjectURL(file) {
    let url = null ;
    // 下面函数执行的效果是一样的,只是需要针对不同的浏览器执行不同的 js 函数而已
    if (window.createObjectURL!=undefined) { // basic
        url = window.createObjectURL(file) ;
    } else if (window.URL!=undefined) { // mozilla(firefox)
        url = window.URL.createObjectURL(file) ;
    } else if (window.webkitURL!=undefined) { // webkit or chrome
        url = window.webkitURL.createObjectURL(file) ;
    }
    return url ;
  }

</script>
</html>

2.2 画 canvas1

  1. 监听图片的 onload 事件
  2. 使用 canvasdrawImage方法将图片画在 canvas1
  3. 通过 canvasgetImageData方法获取 canvas1 的像素点数据
 <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="divport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    *,*::before,*::after{
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }
  </style>
</head>
<body>
  <div id="app">
    <div><input type="file"  accept="image/*" onChange="uploadImage(this)" /></div>

    <img src="" id="image" style="display: none;">
    <canvas id="canvas1"></canvas>
  </div>
</body>
<script >
  const image = document.getElementById('image')
  const canvas1 = document.getElementById('canvas1')
  
  // ...  上一步操作的代码与当前步骤无关,先隐藏

  // 监听图片加载完成事件
  image.onload = ()=>{
    
    const ctx1 = canvas1.getContext('2d')

    const { width, height } = image

    canvas1.width = width
    canvas1.height = height

    // 1. 将图片画在画布 1 中
    ctx1.drawImage(image,0, 0, width,height )
    
    // 2. 获取canvas1的图片像素点数据
    const imageData = ctx1.getImageData( 0, 0, width, height).data
     
  }

</script>
</html>

2.3 画 canvas2

在获取到 canvas1 的像素点数据后,通过压缩,计算灰度,定位,再画在 canvas2 上。先上代码

 // html
 <div id="app">
   <div><input type="file"  accept="image/*" onChange="uploadImage(this)" /></div>

   <img src="" id="image" style="display: none;">
   <canvas id="canvas1"></canvas>
   <canvas id="canvas2"></canvas>
 </div>

// js
 const image = document.getElementById('image')
 const canvas1 = document.getElementById('canvas1')
 const canvas2 = document.getElementById('canvas2')


  // 监听图片加载完成事件
  image.onload = ()=>{
    
    const ctx1 = canvas1.getContext('2d')
    const ctx2 = canvas2.getContext('2d')

    // 计算尺寸
    const { width, height } = image

    canvas1.width = width
    canvas1.height = height

    canvas2.width = width
    canvas2.height = height

    // 1. 将图片画在画布 1 中
    ctx1.drawImage(image, 0, 0, width, height)

    // 2. 获取canvas1的图片像素点数据
    const imageData = ctx1.getImageData( 0, 0, width, height).data

    // 3. 根据 canvas1 的像素数据  在 canvas2 上画点
    for(let i = 0; i < imageData.length; i += 4){

      // 计算当前像素点的 x,y(第 x 列 ,第 y 行 )
      const x = parseInt(i % (width * 4) / 4)
      const y = parseInt(i / (width * 4))

      // 像素点压缩倍数
      const bl = 6 

      //  压缩像素点  => 判断当前 x, y 与 压缩倍数的余数是否都为 0 
      if(x % bl === 0 && y % bl === 0 ){
      
        // 计算灰度
        const [r, g, b, a] = [imageData[i], imageData[i + 1], imageData[i + 2], imageData[i + 3]]
        const gray = (r + g + b) / 3

        // 填充文字
        ctx2.font = '12px'
        ctx2.fillStyle = `rgba(${gray},${gray},${gray},${a})`
        ctx2.fillText('a', x, y)
      }
    }
  }

2.3.1 压缩

  1. 为啥要压缩? Snipaste_2022-10-09_11-48-12.png 如图所示,右边是我们要现实的最终效果,是可以清晰看出图片是由深浅不一的字母 a 组成的(如果觉得不够直观,图片右击,在新标签页中打开图片)。不进行压缩的话,即每个像素点都使用 a 来展示的话,字母 a 会因为太小而无法直观看出,如图左边部分效果。
  2. 如何压缩?
    Snipaste_2022-10-09_13-30-17.png
    如图所示,假设红色框为一整张图片,每一个小灰格子为一个像素点,蓝色格子为四个像素点组成的。我们将蓝色格子的左上角的像素点的颜色作为这个蓝色格子颜色,这样我们就把四个像素点压缩成一个。

2.3.2 计算灰度

  1. 计算灰度前,我们先认识一下通过 canvasgetImageData 获取到的像素点数据。

Snipaste_2022-10-09_13-48-21.png imageData 为像素点颜色的RGBA色值数组构成,即[R,G,B,A,R,G,B,A,R,G,B,A]。数组每四个值代表一个像素点的颜色。
2. 计算灰度
一个像素点的灰度值即为 gray=(R+G+B)/3。该像素点的灰度色值即为rgba(gray,gray,gray,A)

2.3.3 定位

在认识像素点数据的构成后,我们得知所有像素点的色值都放在同一个数据里,这样无法判断像素点在图片上的位置,因此需要通过计算来定位。

  1. 计算每个像素点的位置 (x , y)
    width 为一整行的像素点的个数。因为一个像素点由四个色值组成,所以一整行所占的数组长度为width*4
  // 其中 width, height 为 canvas1 的宽高
  const imageData = ctx1.getImageData( 0, 0, width, height).data
  
  // 计算当前像素点的 x,y(第 x 列 ,第 y 行 )
  for(let i = 0; i < imageData.length; i += 4){
    const x = parseInt(i % (width * 4) / 4)
    const y = parseInt(i / (width * 4))
  }
  1. 根据前面压缩的算法,以左上角第一个像素点的颜色计算压缩后的颜色和位置
  // 其中 width, height 为 canvas1 的宽高
  const imageData = ctx1.getImageData( 0, 0, width, height).data
  
  // 像素点压缩倍数
  const bl = 6 
  
  // 计算当前像素点的 x,y(第 x 列 ,第 y 行 )
  for(let i = 0; i < imageData.length; i += 4){
    const x = parseInt(i % (width * 4) / 4)
    const y = parseInt(i / (width * 4))
    
       if(x % bl === 0 && y % bl === 0 ){
         // 此时,当前像素点即为压缩后左上角的像素点
      }
  }

2.3.4 在canvas2上画点

 // 填充文字
  ctx2.font = '12px'
  ctx2.fillStyle = `rgba(${gray},${gray},${gray},${a})` // gray为计算出来的灰度值,a为像素点的透明度
  ctx2.fillText('a', x, y) // x,y为计算出来的定位。‘a’为填充的字母,可为任意值

三、码上掘金

四、结束语

此篇文章介绍的是通过图片生成,用视频的话会更有意思。这里讲一下大概思路:将img标签换成video标签,播放视频时,利用 requestAnimationFrame 循环将视频的每一帧通过 ctx1.drawImage(video,0, 0, width,height)画在 canvas1 上,其他步骤差不多。有兴趣的同学可以自己动手实现下。