根据S-T教学分析法绘制图形-前端实现

根据S-T教学分析法绘制图形-前端实现

最近有一个需求,就是根据S-T教学分析法得到的相关数据,比如:[[0,0],[0,1],[1.1],[1,2]],最后一个元素中的x,y之和就是整堂课的总时长,最终会得到一个图,由这个图可以看出是这一堂课是学生主导还是教师主导,大致就是这个意思,可以参考S-T教学分析法这篇文章。

此篇文章主要就是记录一下实现这种类似得图形如何绘制,之前想过直接使用ECharts中得折线图,然后设置series中的step属性,发现最终的效果始终不满足上面文章概念中的那种效果,于是就使用canvas手绘了一个类似的。

效果

直接上代码

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Animated Line Graph</title>
  <style>
    canvas {
      border: 1px solid #ccc;
      margin: 20px;
    }
  </style>
</head>

<body>
  <canvas id="myCanvas" width="600" height="400"></canvas>
  <script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');

    const dpr = window.devicePixelRatio
    ctx.imageSmoothingEnabled = true // 启用抗锯齿
    ctx.scale(dpr, dpr)
    canvas.style.width = canvas.clientWidth + 'px'
    canvas.style.width = canvas.clientHeight + 'px'
    canvas.width = Math.round(canvas.clientWidth * dpr)
    canvas.height = Math.round(canvas.clientHeight * dpr)

    const margin = { top: 20, right: 20, bottom: 40, left: 30 }
    const width = canvas.width - margin.left - margin.right
    const height = canvas.height - margin.top - margin.bottom
    const textFont = '12px Philosopher-Regular'
    const strokeStyle = '#333'
    const gridLineColor = `rgba(118, 118, 118, 0.5)`

    const tooltipInfo = null // 存储tooltip信息
    const activeGridLine = null // 存储当前激活的网格线索引

    const data = [{ "x": 0, "y": 0 }, { "x": 0, "y": 1 }, { "x": 0, "y": 2 }, { "x": 0, "y": 3 }, { "x": 0, "y": 4 }, { "x": 1, "y": 4 }, { "x": 2, "y": 4 }, { "x": 2, "y": 5 }, { "x": 2, "y": 6 }, { "x": 3, "y": 6 }, { "x": 3, "y": 7 }, { "x": 3, "y": 8 }, { "x": 4, "y": 8 }, { "x": 4, "y": 9 }, { "x": 4, "y": 10 }, { "x": 4, "y": 11 }, { "x": 4, "y": 12 }, { "x": 5, "y": 12 }, { "x": 6, "y": 12 }, { "x": 7, "y": 12 }, { "x": 7, "y": 13 }, { "x": 7, "y": 14 }, { "x": 7, "y": 15 }, { "x": 7, "y": 16 }, { "x": 8, "y": 16 }, { "x": 8, "y": 17 }, { "x": 8, "y": 18 }, { "x": 8, "y": 19 }, { "x": 9, "y": 19 }, { "x": 9, "y": 20 }, { "x": 9, "y": 21 }, { "x": 10, "y": 21 }, { "x": 10, "y": 22 }, { "x": 10, "y": 23 }, { "x": 10, "y": 24 }, { "x": 10, "y": 25 }, { "x": 11, "y": 25 }, { "x": 12, "y": 25 }, { "x": 13, "y": 25 }, { "x": 13, "y": 26 }, { "x": 13, "y": 27 }, { "x": 13, "y": 28 }, { "x": 13, "y": 29 }]
    const max = data.length;

    const gridCountX = Math.ceil(max);
    const gridCountY = Math.ceil(max);

    let animationProgress = 0;
    let animationDuration = 2000;
    let startTime = null;

    // 绘制函数
    function drawGraph(timestamp) {
      if (!startTime) startTime = timestamp;
      const elapsed = timestamp - startTime;
      animationProgress = Math.min(elapsed / animationDuration, 1);

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

      // 绘制坐标轴
      ctx.beginPath();
      ctx.moveTo(margin.left, margin.top);
      ctx.lineTo(margin.left, margin.top + height);
      ctx.lineTo(margin.left + width, margin.top + height);
      ctx.strokeStyle = strokeStyle;
      ctx.lineWidth = 2;
      ctx.stroke();

      // 绘制网格线(带淡入动画)
      for (let i = 0; i <= gridCountX; i++) {
        // 这里可以控制垂直网格线的数量
        if (i % 5 === 0) {
          const x = margin.left + (i / gridCountX) * width;
          let alpha = animationProgress * 0.3
          if (i === 0) {
            alpha = animationProgress * 0.6
          }

          // 垂直网格线
          ctx.beginPath();
          ctx.moveTo(x, margin.top);
          ctx.lineTo(x, margin.top + height);
          ctx.strokeStyle = `rgba(118, 118, 118, ${alpha})`;
          ctx.lineWidth = 1;
          ctx.stroke();

          // X轴刻度
          ctx.beginPath();
          ctx.moveTo(x, margin.top + height);
          ctx.lineTo(x, margin.top + height + 5);
          ctx.strokeStyle = strokeStyle;
          ctx.stroke();

          ctx.fillStyle = strokeStyle;
          ctx.font = textFont;
          ctx.textAlign = 'center';
          ctx.fillText(i, x, margin.top + height + 20);
        }
      }

      for (let i = 0; i <= gridCountY; i++) {
        // 这里可以控制水平网格线的数量
        if (i % 5 === 0) {
          const y = margin.top + height - (i / gridCountY) * height;
          const alpha = animationProgress;

          // 水平网格线
          ctx.beginPath();
          ctx.moveTo(margin.left, y);
          ctx.lineTo(margin.left + width, y);
          ctx.strokeStyle = `rgba(118, 118, 118, ${alpha})`;
          ctx.lineWidth = 1;
          ctx.stroke();

          // Y轴刻度
          ctx.beginPath();
          ctx.moveTo(margin.left, y);
          ctx.lineTo(margin.left - 5, y);
          ctx.strokeStyle = strokeStyle;
          ctx.stroke();

          ctx.fillStyle = strokeStyle;
          ctx.font = textFont;
          ctx.textAlign = 'right';
          ctx.textBaseline = 'middle';
          ctx.fillText(i, margin.left - 10, y);
        }
      }

      // 绘制单位
      ctx.fillStyle = strokeStyle
      ctx.font = '12px Arial'
      ctx.textAlign = 'left'
      ctx.fillText('T', margin.left + width + 10, margin.top + height + 12)
      ctx.fillText('S', margin.left - 20, margin.top - 10)

      // 绘制连接线(带绘制动画)
      if (animationProgress > 0) {
        ctx.beginPath()

        // 计算需要绘制的线段数
        const totalSegments = data.length - 1
        const segmentsToDraw = Math.ceil(totalSegments * animationProgress)

        // 如果有线段需要绘制
        if (segmentsToDraw > 0) {
          // 绘制完整线段
          for (let i = 0; i < segmentsToDraw - 1; i++) {
            const p1 = data[i]
            const p2 = data[i + 1]
            const x1 = margin.left + (p1.x / max) * width
            const y1 = margin.top + height - (p1.y / max) * height
            const x2 = margin.left + (p2.x / max) * width
            const y2 = margin.top + height - (p2.y / max) * height

            if (i === 0) {
              ctx.moveTo(x1, y1)
            }
            ctx.lineTo(x2, y2)
          }

          // 绘制部分线段(动画进行中)
          if (segmentsToDraw < data.length) {
            const p1 = data[segmentsToDraw - 1]
            const p2 = data[segmentsToDraw]
            const x1 = margin.left + (p1.x / max) * width
            const y1 = margin.top + height - (p1.y / max) * height
            const x2 = margin.left + (p2.x / max) * width
            const y2 = margin.top + height - (p2.y / max) * height

            // 计算部分线段的终点
            const partialProgress = animationProgress * totalSegments - (segmentsToDraw - 1)
            const partialX = x1 + (x2 - x1) * partialProgress
            const partialY = y1 + (y2 - y1) * partialProgress

            if (segmentsToDraw - 1 === 0) {
              ctx.moveTo(x1, y1)
            }
            ctx.lineTo(partialX, partialY)
          }

          ctx.strokeStyle = '#38C6FD'
          ctx.lineWidth = 2
          ctx.stroke()
        }
      }
      // 继续动画循环
      if (animationProgress < 1) {
        requestAnimationFrame(drawGraph)
      }
    }

    // 初始化绘制
    requestAnimationFrame(drawGraph);
  </script>
</body>

</html>

总结

上面的代码可能需要一定的canvas绘画的基础,逻辑其实不算复杂。反正鄙人是全网没有找到相关内容的文章,所以只能手绘,如果大家有更好的办法,请分享!

狗头保命(又水一篇)

相关推荐
葬送的代码人生6 分钟前
React组件化哲学:如何优雅地"变秃也变强"
前端·javascript·react.js
用户52709648744907 分钟前
🚀 前端项目代码质量配置Prettier + Commitlint + Husky + Lint-staged
前端
xiaok8 分钟前
await返回之后的赋值给一个变量可以打印出数值,但是直接return回去之后,在另一个函数打印出来却是一个promise
前端
Bl_a_ck11 分钟前
【JS进阶】ES6 实现继承的方式
开发语言·前端·javascript
小马虎本人12 分钟前
如果接口返回的数据特别慢?要怎么办?难道就要在当前页面一直等吗
前端·react.js·aigc
蓝胖子的多啦A梦15 分钟前
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚
前端·npm·node.js
LinCC717 分钟前
在Vite中构建项目出错-Top-level await is not available in the configured target environme
前端
用户8820932166718 分钟前
如何优雅拆分一个充斥十几种逻辑的 SDK 回调函数?
前端
Momoly0820 分钟前
vue3+el-table 利用插槽自定义数据样式
前端·javascript·vue.js
唯有选择20 分钟前
让你的应用界面好看的基石:Flutter主题Theme使用和扩展自定义字段
前端·flutter