根据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绘画的基础,逻辑其实不算复杂。反正鄙人是全网没有找到相关内容的文章,所以只能手绘,如果大家有更好的办法,请分享!

狗头保命(又水一篇)

相关推荐
vvilkim2 分钟前
Electron 应用中的内容安全策略 (CSP) 全面指南
前端·javascript·electron
aha-凯心14 分钟前
vben 之 axios 封装
前端·javascript·学习
遗憾随她而去.28 分钟前
uniapp 中使用路由导航守卫,进行登录鉴权
前端·uni-app
xjt_090144 分钟前
浅析Web存储系统
前端
foxhuli2291 小时前
禁止ifrmare标签上的文件,实现自动下载功能,并且隐藏工具栏
前端
青皮桔2 小时前
CSS实现百分比水柱图
前端·css
影子信息2 小时前
vue 前端动态导入文件 import.meta.glob
前端·javascript·vue.js
青阳流月2 小时前
1.vue权衡的艺术
前端·vue.js·开源
样子20182 小时前
Vue3 之dialog弹框简单制作
前端·javascript·vue.js·前端框架·ecmascript
kevin_水滴石穿2 小时前
Vue 中报错 TypeError: crypto$2.getRandomValues is not a function
前端·javascript·vue.js