canvas实现雪花效果加文字碎屑特效

380 阅读1分钟

雪花文字特效.gif 使用canvas实现下雪花的效果和文字变成碎屑再慢慢收集起来组合成原有的文字特效

雪花效果的思路:生成一定长度的数组,内容通过Math.random()canvas画布的长宽范围内随机获取x,y的坐标值,通过这些坐标使用ctx.rect()绘制矩形,然后通过requestAnimationFrame来实现动画,在每次调用requestAnimationFrame前遍历坐标的集合给每个坐标的Y坐标加上指定值,

文字特效的思路:绘制部分和上面的雪花类似,文字的处理上通过ctx.getImageData()获取当前画布的内容数据,根据文字的颜色、稀释的倍数作为条件,拿到构成文字的每一个像素点,让这些像素点再文字尺寸的范围内随机排布,使用requestAnimationFrame不断更新画布修改坐标为原始的位置,即可实现文字碎屑到完整的效果

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body style="margin: 0;">
  <canvas id="canvas"></canvas>
</body>

<script>
/** @type {HTMLCanvasElement} */
const canvas = document.querySelector('#canvas');

const ctx = canvas.getContext("2d");

const {clientWidth: width, clientHeight: height} = document.documentElement;

canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
canvas.style.background = '#000000'

const snows = Array.from({length: 400}, () => {
  return {
    x: Math.random() * width,
    y: Math.random() * height,
    speed: Math.random() * 2.5
  }
});

const colors = [];

function drawText(text, fontSize = 100, stepV = 40) {
  ctx.fillStyle = '#000000';
  ctx.fillRect(0, 0, width, height);
  ctx.font = `bold ${fontSize}px 微软雅黑`;
  ctx.fillStyle = '#ffffff';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(text, width / 2, height / 2);

  const data = ctx.getImageData(0, 0, width, height).data;

  let index = 0;
  let bl = 4;
  let useIndex = 0;

  for (let i = 0; i < data.length; i += 4) {
    const x = index % width;
    const y = Math.ceil(index / width);

    if (x % bl === 0 && y % bl === 0 && data[i] === 255 && data[i+1] === 255 && data[i+2] === 255) {
      const rx = Math.floor(Math.random() * fontSize + width / 2 - fontSize / 2);
      const ry = Math.floor(Math.random() * fontSize + height / 2 - fontSize / 2);

      const item = colors[useIndex];

      if (item) {
        colors[useIndex] = {
          x,
          y,
          rx: item.x,
          ry: item.y,
          stepX: Math.abs(item.x - x) / stepV,
          stepY: Math.abs(item.y - y) / stepV,
        }
      } else {
        colors[useIndex] = {
          x,
          y,
          rx,
          ry,
          stepX: Math.abs(rx - x) / stepV,
          stepY: Math.abs(ry - y) / stepV,
        }
      }
      useIndex++
    }
    index++
  }

  if (useIndex < colors.length) {
    colors.splice(useIndex, colors.length - useIndex)
  }
};

ctx.fillStyle = '#ffffff';


function render() {

  ctx.clearRect(0, 0, width, height);
  ctx.beginPath();

  colors.forEach((v) => {
    if (v.rx > v.x) {
      v.rx -= v.stepX;
      if (v.rx < v.x) {
        v.rx = v.x
      }
    } else if (v.rx < v.x) {
      v.rx += v.stepX;
      if (v.rx > v.x) {
        v.rx = v.x
      }
    }

    if (v.ry > v.y) {
      v.ry -= v.stepY;
      if (v.ry < v.y) {
        v.ry = v.y
      }
    } else if (v.ry < v.y) {
      v.ry += v.stepY;
      if (v.ry > v.y) {
        v.ry = v.y
      }
    }

    ctx.rect(v.rx, v.ry, 3, 3)
  })
  
  snows.forEach(item => {
    item.y = item.y > height ? 0 : (item.y + item.speed);
    ctx.rect(item.x, item.y, 3, 3)
  });

  ctx.fill()

  requestAnimationFrame(render)
};

render();

const texts = 'wow~ canvas!,我来了'.split(',')

function awaitDrawText (text, fontSize, stepV, index) {
  return new Promise((resolve) => {
    colors.sort(() => Math.random() - 0.5)
    drawText(text, fontSize, stepV)
    setTimeout(() => {
      resolve();
      if (index + 1 === texts.length) {
        run()
      }
    }, 2000 + (stepV > 40 ? 1000 : 0))
  })
};

async function run() {
  for (let index = 0; index < texts.length; index++) {
    await awaitDrawText(texts[index], 150, index === 0 ? 100 : 40, index)
  }
}

run()
</script>
</html>