likes
comments
collection
share

创意代码:基于 Canvas 的字符画生成,让龙动起来!

作者站长头像
站长
· 阅读数 4

在本篇文章中,我将分享如何使用 Canvas 和 JavaScript 创建一个独特的字符画生成器。通过此生成器,我们可以将图片转换为由字符构成的作品,并通过一些JavaScript属性让这些字符动起来。结合现代的 Web 技术,我们在网页上动态地生成字符画,并且通过添加一些动画效果,使其更富有趣味性。

创意代码:基于 Canvas 的字符画生成,让龙动起来!

准备工作

首先,我们需要一些基础的 HTML 结构和 JavaScript 代码,用于加载图片并创建 Canvas。

<body>
  <div class="glass">
    <img id="myImg" src="./xxx.png" style="display: none;"></img>
    <div id="canvasDiv">
      <canvas id="canvas" width="600" height="600" style="display: none;"></canvas>
    </div>
    <div id="textDiv" class="textDiv" style="line-height: 0.9;"></div>
  </div>
  <script>
    // ...(后续代码将在下文详细介绍)
  </script>
</body>

图片和canvas都不需要展示,设置它们为display: none;

图片加载与转换

window.onload 事件中,我们加载图片并将其绘制到 Canvas 上。为了保持生成的字符画不失真,我们需要进行一些调整。

window.onload = () => {
     canvas = document.getElementById("canvas");
     ctx = canvas.getContext("2d");
     const img = document.querySelector('#myImg');
     changeImg(img);
     let newimgText = imgText.split('</br>').map((e) => "<div class='amiDiv' style='display:flex'>" + e + "</div>") .join('');
      document.getElementById("textDiv").innerHTML = newimgText;
};

首先获取canvas标签,并且创建一个2D渲染上下文ctx,以便后续使用。接下来,该代码获取图片,并且将其作为参数传递给changeImg函数,该函数将会对这个图片进行处理,将其转换为字符画并将结果存储在全局变量imgText中。然后,定义新的字符串变量newimgText,将imgText字符串每一行分隔开,方便一会儿分别给每一行添加动态效果。

Canvas 操作与字符生成

changeImg 函数中,我们使用 Canvas 获取图片像素信息,并逐个读取每个像素的RGB值,计算出对应的灰度值并映射到字符上。同时,我们也可以调整字符大小控制显示图像的精度。

function changeImg(img) {
      // 清空字符画
      imgText = "";
      // 先记录比率,如果宽,那么先缩放宽,再用比率算出长,反之同理,这里是保证不超过不超过设定规模
      let rate = img.width / img.height;
      if (rate > 1) {
        img.style.width = IMAGE_SIZE + 'px';
        img.style.height = IMAGE_SIZE / rate + 'px';
        img.width = IMAGE_SIZE;
        img.height = IMAGE_SIZE / rate;
      } else {
        img.style.height = IMAGE_SIZE + 'px';
        img.style.width = IMAGE_SIZE * rate + 'px';
        img.height = IMAGE_SIZE;
        img.width = IMAGE_SIZE * rate;
      }

      ctx.clearRect(0, 0, canvas.width, canvas.height)
      // 将图片绘制在canvas上
      ctx.drawImage(img, 0, 0, img.width, img.height);

      // 获取像素信息
      let pixelInfo;
      try {
        pixelInfo = ctx.getImageData(0, 0, img.width, img.height);
      } catch {
        console("图片有问题!");
      }
      // 数值越小,精度越高
      let size = 2;
      // 设置zoom缩放
      document.querySelector("#textDiv").style.zoom = size / 6;
      // 生成主体,逐个读取字符
      for (let i = 0; i < img.height; i = i + size) {
        for (let j = 0; j < img.width; j = j + size) {
          const curPoint = (i * img.width + j) * 4; // ×4是因为,1为r,2为g,3为b,4为a,四个是一组rgba值
          const [r, g, b] = pixelInfo.data.slice(curPoint, curPoint + 3);
          const gray = r * 0.3 + g * 0.6 + b * 0.1; // 计算灰度值
          const color = `rgba(${r},${g},${b})`; // 保存像素点rgb值
          toText(gray, color)
        }
        imgText += "</br>";
      }
    }

拼接字符画

将字符拼接成一行一行的文本。

    function toText(g, color) {
      if (color) imgText += `<span style='color:${color}'>`;
      if (g == 255) imgText += "&nbsp;&nbsp;&nbsp;";
      else imgText += "@";
      if (color) imgText += "</span>";
    }

添加动态效果

最后,使用JavaScript的正弦函数来控制每个文字元素的位置和偏移量,从而实现字符画龙在页面上的动态效果。

  const container = document.querySelectorAll(".amiDiv");

  // 创建曲线运动
  function createCurve(func, range) {
    container.forEach(e => {
      const points = getCurvePoints(func, range, e.children.length, e.clientWidth);
      for (let i = 0; i < points.length; i++) {
        e.children[i].style.transform = `translateY(${points[i]}px)`;
      }
    });
  }

  let offset = 0;
  createCurve((x) => Math.sin(x), [offset, offset + 2 * Math.PI]);

  setInterval(() => {
    offset += 0.1;
    createCurve((x) => Math.sin(x), [offset, offset + 2 * Math.PI]);
  }, 30);

  // 辅助函数:获取曲线上的点

  function getCurvePoints(curveFunc, range, number, xLength) {
    if (number < 1) {
      return [];
    }
    if (number === 1) {
      return [0];
    }
    const piece = (range[1] - range[0]) / (number - 1);
    const result = [];
    const scale = xLength / (range[1] - range[0]);
    for (let i = 0; i < number; i++) {
      result.push(-curveFunc(i * piece + range[0]) * scale);
    }
    return result;
  }

源码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>字符龙动起来</title>
</head>
<style>
  body {
    background-color: #f21235;
    background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox='0 0 42 44' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Page-1' fill='none' fill-rule='evenodd'%3E%3Cg id='brick-wall' fill='%23b21111' fill-opacity='0.57'%3E%3Cpath d='M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  }

  .glass {
    height: 97vh;
    box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    border-radius: 10px;
    border: 1px solid rgba(255, 255, 255, 0.18);
  }

  #textDiv {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    width: 350px;
    margin-top: 400px;
  }
</style>

<body>
  <div class="glass">
    <img id="myImg" src="./123.jpg" style="display: none;"></img>
    <div id="canvasDiv">
      <canvas id="canvas" width="600" height="600" style="display: none;"></canvas>
    </div>
    <div id="textDiv" class="textDiv"></div>

  </div>
  <script>
    let canvas; // 定义canvas实例
    let ctx;
    let imgText; // 生成的字符画
    const IMAGE_SIZE = 600; // 画规模
    // 初始化
    window.onload = () => {
      canvas = document.getElementById("canvas");
      ctx = canvas.getContext("2d");
      const img = document.querySelector('#myImg');
      changeImg(img);
      let newimgText = imgText.split('</br>').map((e) => "<div class='amiDiv' style='display:flex'>" + e + "</div>")
        .join('');
      document.getElementById("textDiv").innerHTML = newimgText;

      // 给需要动的点添加动效
      setTimeout(() => {
        const container = document.querySelectorAll(".amiDiv");

        function createCurve(func, range) {
          container.forEach(e => {
            const points = getCurvePoints(func, range, e.children.length, e.clientWidth);
            // 循环每一个元素
            for (let i = 0; i < points.length; i++) {
              // 找到对应的子元素,改变子元素的 y 偏移量
              e.children[i].style.transform = `translateY(${points[i]}px)`;
            }
          })
        }
        // 定一个偏移量
        let offset = 0;
        createCurve((x) => Math.sin(x), [offset, offset + 2 * Math.PI]);
        setInterval(() => {
          // 每间隔一段时间改变偏移量
          offset += 0.1;
          createCurve((x) => Math.sin(x), [offset, offset + 2 * Math.PI]);
        }, 30);

        function getCurvePoints(curveFunc, range, number, xLength) {
          if (number < 1) {
            return [];
          }
          if (number === 1) {
            return [0];
          }
          const piece = (range[1] - range[0]) / (number - 1);
          const result = [];
          const scale = xLength / (range[1] - range[0]);
          for (let i = 0; i < number; i++) {
            result.push(-curveFunc(i * piece + range[0]) * scale);
          }
          return result;
        }
      }, 1000);
    };

    // 转换
    function changeImg(img) {
      // 清空字符画
      imgText = "";
      // 先记录比率,如果宽,那么先缩放宽,再用比率算出长,反之同理,这里是保证不超过不超过设定规模
      let rate = img.width / img.height;
      if (rate > 1) {
        img.style.width = IMAGE_SIZE + 'px';
        img.style.height = IMAGE_SIZE / rate + 'px';
        img.width = IMAGE_SIZE;
        img.height = IMAGE_SIZE / rate;
      } else {
        img.style.height = IMAGE_SIZE + 'px';
        img.style.width = IMAGE_SIZE * rate + 'px';
        img.height = IMAGE_SIZE;
        img.width = IMAGE_SIZE * rate;
      }

      ctx.clearRect(0, 0, canvas.width, canvas.height)
      // 将图片绘制在canvas上
      ctx.drawImage(img, 0, 0, img.width, img.height);

      // 获取像素信息
      let pixelInfo;
      try {
        pixelInfo = ctx.getImageData(0, 0, img.width, img.height);
      } catch {
        console("图片有问题!");
      }
      // 数值越小,精度越高
      let size = 2;
      // 设置zoom缩放
      document.querySelector("#textDiv").style.zoom = size / 6;
      // 生成主体,逐个读取字符
      for (let i = 0; i < img.height; i = i + size) {
        for (let j = 0; j < img.width; j = j + size) {
          const curPoint = (i * img.width + j) * 4; // ×4是因为,1为r,2为g,3为b,4为a,四个是一组rgba值
          const [r, g, b] = pixelInfo.data.slice(curPoint, curPoint + 3);
          const gray = r * 0.3 + g * 0.6 + b * 0.1; // 计算灰度值
          const color = `rgba(${r},${g},${b})`; // 保存像素点rgb值
          toText(gray, color)
        }
        imgText += "</br>";
      }
    }
    // 根据灰度转化字符,添加颜色
    function toText(g, color) {
      if (color) imgText += `<span style='color:${color}'>`;
      if (g == 255) imgText += "&nbsp;&nbsp;&nbsp;";
      else imgText += "@";
      if (color) imgText += "</span>";
    }
  </script>
</body>

</html>

结语

在这个龙年,愿我们的代码如同神龙一般威猛,技术如同飞龙一般翱翔,最后祝大家新的一年:龙行龘龘,前程朤朤!🐉✨