vue3 + ts实现canvas绘制的waterfall

news2024/12/23 14:45:01

实际运行效果(仅包含waterfall图表部分)
在这里插入图片描述

component.vue

<template>
  <div ref="heatmap" :style="{ height: props.containerHeight + 'px' }" />
</template>

<script setup>
import ColorMap from "colormap";
import { round } from "lodash";
import { ref, reactive, watch, onMounted, watchEffect } from "vue";
const props = defineProps({
  height: {
    type: Number,
    default: 50, // 代表一个屏幕有多少帧
  },
  minDb: {
    type: Number, // 最小值
    default: 0,
  },
  maxDb: {
    type: Number, // 最大值
    default: 1000,
  },
  containerHeight: {
    type: Number,
    default: 210, // 容器高度
  },
  legendWidth: {
    // 左侧色条宽度
    type: Number,
    default: 50,
  },
  isOnline: {
    type: Boolean, // 判断是否在线
    default: false,
  },
  sdata: {
    type: Array,
    default: () => [], // 实际要显示的数据
  },
  startVal: {
    type: Number,
    default: 0, // 数据开始的位置
  },
});
// 图表容器 DOM 的引用
const heatmap = ref(null);
const state = reactive({
  canvasCtx: null,
  fallsCanvasCtx: null,
  legendCanvasCtx: null,
  canvasWidth: 0,
  colormap: [],
});
const firstRender = ref(true);
const renderNum = ref(0);
const plotData = ref([]);
let playControl = reactive({ cycleStart: props.startVal });
const requestChartsData = () => {
  // const data = Array.from({ length: 20000 }, () => -Math.floor(Math.random() * 100) + 1);
  plotData.value = props.sdata;
};

const initComponent = () => {
  if (!heatmap.value) {
    return;
  }
  // 获取容器宽高
  const { clientWidth, clientHeight } = heatmap.value;
  // 初始化颜色图
  const colormap = initColormap();
  // 创建画布
  const { fallsCanvasCtx, canvasCtx, legendCanvasCtx, canvas } = createCanvas(
    clientWidth,
    clientHeight
  );
  // 绘制左边颜色图图例
  drawLegend(canvasCtx, legendCanvasCtx, colormap);
  state.canvasCtx = canvasCtx;
  state.colormap = colormap;
  state.fallsCanvasCtx = fallsCanvasCtx;
  state.legendCanvasCtx = legendCanvasCtx;
  state.canvasDom = canvas;
};

const initColormap = () => {
  return ColorMap({
    colormap: "jet",
    nshades: 150,
    format: "rba",
    alpha: 1,
  });
};

const createCanvas = (width, height) => {
  // 创建用来绘制的画布
  const fallsCanvas = document.createElement("canvas");
  fallsCanvas.width = 0;
  fallsCanvas.height = height;
  const fallsCanvasCtx = fallsCanvas.getContext("2d");

  // 创建最终展示的画布
  const canvas = document.createElement("canvas");
  canvas.className = "main_canvas";
  canvas.height = height - 2;
  canvas.width = width;
  heatmap.value.appendChild(canvas); // 唯一显示的canvas
  const canvasCtx = canvas.getContext("2d");

  // 创建图例图层画布
  const legendCanvas = document.createElement("canvas");
  legendCanvas.width = 1;
  const legendCanvasCtx = legendCanvas.getContext("2d");
  return {
    fallsCanvasCtx,
    canvasCtx,
    legendCanvasCtx,
    canvas,
  };
};

// 更新瀑布图 传入要渲染的数据
const updateChart = (start) => {
  let data = plotData.value.slice(start, start + 1024);
  console.log("start", start, data);
  updateWaterFallPlot(data);
};
const updateWaterFallPlot = (data) => {
  const len = data.length;
  if (len !== state.canvasWidth) {
    state.canvasWidth = len;
    state.fallsCanvasCtx.canvas.width = len;
  }
  if (len === 0) {
    return;
  }
  renderNum.value++;
  // removePrevImage()
  // 先在用于绘制的画布上绘制图像
  addWaterfallRow(data);
  // 再将画好的图像显示再页面中
  drawFallsOnCanvas(len);
  if (renderNum.value > props.height) {
    // state.canvasDom.height = renderNum.value * props.containerHeight / props.height
  }
};

const removePrevImage = () => {
  const { canvas } = state.fallsCanvasCtx;
  state.fallsCanvasCtx.clearRect(0, 0, canvas.width, canvas.height);
};

// 在用于绘制的画布上绘制图像
const addWaterfallRow = (data) => {
  // 先将已生成的图像向下移动一个像素
  if (!firstRender.value) {
    state.fallsCanvasCtx.drawImage(
      state.fallsCanvasCtx.canvas, // 当前cavas
      0,
      0,
      data.length,
      props.height,
      0,
      1,
      data.length,
      props.height
    );
  } else {
    firstRender.value = false;
  }

  // 再画一行的数据
  const imageData = rowToImageData(data);
  state.fallsCanvasCtx.putImageData(imageData, 0, 0);
};

// 绘制单行图像
const rowToImageData = (data) => {
  const imageData = state.fallsCanvasCtx.createImageData(data.length, 1);
  for (let i = 0; i < imageData.data.length; i += 4) {
    const cIndex = getCurrentColorIndex(data[i / 4]);
    const color = state.colormap[cIndex];
    imageData.data[i + 0] = color[0];
    imageData.data[i + 1] = color[1];
    imageData.data[i + 2] = color[2];
    imageData.data[i + 3] = 255;
  }
  return imageData;
};

// 将绘制好的图像显示在主页面中
const drawFallsOnCanvas = (len) => {
  const canvasWidth = state.canvasCtx.canvas.width;
  const canvasHeight = state.canvasCtx.canvas.height;
  if (!state.fallsCanvasCtx.canvas.width) return;
  state.canvasCtx.drawImage(
    state.fallsCanvasCtx.canvas,
    -1,
    0,
    len + 1,
    props.height,
    props.legendWidth + 5,
    0,
    canvasWidth - props.legendWidth,
    canvasHeight
  );
};
// 获取数据对应的颜色图索引
const getCurrentColorIndex = (data) => {
  const outMin = 0;
  const outMax = state.colormap.length - 1;
  if (data <= props.minDb) {
    return outMin;
  } else if (data >= props.maxDb) {
    return outMax;
  } else {
    return round(((data - props.minDb) / (props.maxDb - props.minDb)) * outMax);
  }
};

// 绘制颜色图图例
const drawLegend = (canvasCtx, legendCanvasCtx, colormap) => {
  const imageData = legendCanvasCtx.createImageData(1, colormap.length);
  // 遍历颜色图集合
  for (let i = 0; i < colormap.length; i++) {
    const color = colormap[i];
    imageData.data[imageData.data.length - i * 4 + 0] = color[0];
    imageData.data[imageData.data.length - i * 4 + 1] = color[1];
    imageData.data[imageData.data.length - i * 4 + 2] = color[2];
    imageData.data[imageData.data.length - i * 4 + 3] = 255;
  }
  legendCanvasCtx.putImageData(imageData, 0, 0);
  canvasCtx.drawImage(
    legendCanvasCtx.canvas,
    0, // source x
    0, // source y
    1, // source width
    colormap.length, // souce height
    0, // d x 目标
    0, // d y 目标
    props.legendWidth / 4, // d width
    canvasCtx.canvas.height // d height
  );
  canvasCtx.font = "12px Arial";
  canvasCtx.textAlign = "end";
  canvasCtx.fillStyle = "#fff";
  const x = (props.legendWidth * 3) / 4 - 10;
  canvasCtx.fillText(props.maxDb, x, 12);
  canvasCtx.fillText(props.minDb, x, props.containerHeight - 6);
  const dur = (props.maxDb - props.minDb) / 10;
  for (let i = 1; i < 10; i++) {
    canvasCtx.fillText(
      props.minDb + dur * i,
      x,
      (props.containerHeight * (10 - i)) / 10 + i
    );
  }
};

watch(
  () => props.maxDb,
  () => {
    const x = (props.legendWidth * 3) / 4 - 10;
    state.canvasCtx.clearRect(0, 0, x, props.containerHeight);
    state.canvasCtx.fillText(props.maxDb, x, 12);
    state.canvasCtx.fillText(props.minDb, x, props.containerHeight - 6);
    const dur = (props.maxDb - props.minDb) / 10;
    for (let i = 1; i < 10; i++) {
      state.canvasCtx.fillText(
        props.minDb + dur * i,
        x,
        (props.containerHeight * (10 - i)) / 10 + i
      );
    }
  },
  { immediate: false, deep: true }
);
watch(
  () => props.sdata, // 监控数据变化
  (newval, oldval) => {
    requestChartsData();
    updateWaterFallPlot(props.sdata);
  },
  { immediate: false, deep: true }
);
onMounted(() => {
  initComponent();
  if (!props.isOnline) {
    requestChartsData();
    // watchEffect(() => {
    updateChart(playControl.cycleStart);
    // });
    setInterval(() => {
      updateChart(playControl.cycleStart);
    }, 1000);
  }
});
</script>

父组件引用

            <Waterfall
              v-if="showlargeline"
              :sdata="probes[selectProbeIndex].series[0].data"
              :startVal="0"
              :isOnline="false"
              :height="50"
              :minDb="0"
              :maxDb="100"
              :containerHeight="210"
            ></Waterfall>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1657577.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Boot 整合 socket 实现简单聊天

来看一下实现的界面效果 pom.xml的maven依赖 <!-- 引入 socket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- 引入 Fastjson &#x…

安卓动态加载view

目录 前言一、addview1.1 addView 的重载方法1.2 在 LinearLayout 中的使用1.2.1 addView(View child)方法的分析&#xff1a;1.2.2 addView(View child, int index)方法的分析&#xff1a;1.2.3 小结 1.3 在 RelativeLayout 中的使用 二、addContentview2.1 测试 12.2 测试 22…

如何用画图处理截图【攻略】

如何用画图处理截图【攻略】 前言版权推荐如何用画图处理截图用画图打开图片简单使用操作&#xff1a;重设图片大小操作&#xff1a;简单覆盖 最后 前言 2024-5-9 22:29:27 以下内容源自《【攻略】》 仅供学习交流使用 版权 禁止其他平台发布时删除以下此话 本文首次发布于…

安卓开发--新建工程,新建虚拟手机,按键事件响应

安卓开发--新建工程&#xff0c;新建虚拟手机&#xff0c;按键事件响应 1.前言2.运行一个工程2.1布局一个Button2.2 button一般点击事件2.2 button属性点击事件2.2 button推荐点击事件 本篇博客介绍安卓开发的入门工程&#xff0c;通过使用按钮Buton来了解一个工程的运作机制。…

Java 语法 (杂七杂八的知识)

面向对象三大特性 封装, 多态, 继承 基本数据类型 一字节 (Byte) 占八位 (bit) JDK, JRE, JVM JDK (Java Development Kit) : Java 开发工具包, 包括了 JRE, 编译器 javac, 和调试工具 Jconsole, jstack 等 JRE (Java Runtime Environment) : Java 运行时环境, 包括了 JVM , …

代码随想录第四十三天|最后一块石头的重量 II 、目标和

题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 代码如下&#xff1a; 题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 代码如下&#xff1a;

实战教程:个性化生鲜超市小程序制作与运营全解析

生鲜电商行业一直以来都备受关注&#xff0c;而如今&#xff0c;小程序商城成为了这个行业的新潮流。乔拓云平台提供了一个便捷的平台&#xff0c;让我们可以轻松地进入商城后台管理页面。 浏览器搜索【乔拓云】并登陆平台后&#xff0c;我们可以点击【小程序商城】模块&#x…

Marin说PCB之如何快速打印输出整板的丝印位号图?

当小编我辛辛苦苦加班加点的把手上的板子做到投板评审状态的时候&#xff0c;坐在我旁边的日本同事龟田小郎君说让我把板子上的丝印也要调一下&#xff0c;我当时就急了&#xff0c;这么大的板子&#xff0c;将近1W多PIN 了都&#xff0c;光调丝印都要老半天啊&#xff0c;而且…

GDAL:Warning 1: All options related to creation ignored in update mode

01 警告说明 首先贴出相关代码&#xff1a; out_file_name Rs_{:4.0f}{:02.0f}.tiff.format(year, month) out_path os.path.join(out_dir, out_file_name) mem_driver gdal.GetDriverByName(MEM) mem_ds mem_driver.Create(, len(lon), len(lat), 1, gdal.GDT_Float32) …

怎么把多个视频合成一个视频?6个软件教你轻松合成视频

怎么把多个视频合成一个视频&#xff1f;6个软件教你轻松合成视频 合成多个视频成为一个视频可以通过专业的视频编辑软件或在线工具来实现。以下是六个方便使用的软件&#xff0c;它们可以帮助你轻松合成视频&#xff1a; 迅捷视频剪辑软件&#xff1a;这是专业的视频编辑软…

PTP 对时协议 IEEE1588 网络对时 硬件基础

前言 在很多应用场景有精确对时的需求&#xff0c;例如车载网络&#xff0c;音视频流&#xff0c;工业网络。本文档将会阐述对时的硬件需求。 协议 流行的协议为 IEEE1588 标准指定的对时方法&#xff0c;名为 PTP 对时协议。 网卡硬件要求 找到某型网卡的特性描述&#x…

SQL STRING_SPLIT函数,将指定的分隔符将字符串拆分为子字符串行

文章目录 STRING_SPLIT (Transact-SQL)1、语法2、参数3、样例样例1样例2 STRING_SPLIT (Transact-SQL) STRING_SPLIT 是一个表值函数&#xff0c;它根据指定的分隔符将字符串拆分为子字符串行。 1、语法 STRING_SPLIT ( string , separator [ , enable_ordinal ] ) 2、参数…

需求为何如此多

同事的心态炸了 最近各种需求倒排给M同事的心态整炸了&#xff0c;直接撂挑子&#xff0c;从一个TL转为安静的开发人员了。我目睹了整个过程&#xff0c;大抵是理解他的心情的。早年从PMP培训&#xff0c;到哈啰火种计划培训&#xff0c;到后来也带项目&#xff0c;有一些看法…

Powerdesigner导入mysql8之后注释丢失

目录 一、问题描述及解决思路 二、导入的步骤 1.先按正常步骤建立一个物理数据模型 &#xff08;1&#xff09;点击“文件-新建模型” &#xff08;2&#xff09;选择物理模型和数据库 2.从sql文件导入表 &#xff08;1&#xff09;点击“数据库-Update Model from Data…

【ai早报-01 project】

今天和大家分享一款有趣的开源项目 01 Project。 The 01 Project is building an open-source ecosystem for AI devices. 其主旨是基于开源生态&#xff0c;构建以LLM为核心的产品&#xff0c;提供软硬件方案。 市面上类似产品: Rabbit R1, Humane Pin。 如上图所示的这款产…

kafka(七)——消息偏移(消费者)

概念 消费者消费完消息后&#xff0c;向_consumer_offset主题发送消息&#xff0c;用来保存每个分区的偏移量。 流程说明 consumer发送JoinGroup请求&#xff1b;coordinator选出一个consumer作为leader&#xff0c;并将topics发送给leader消费者&#xff1b;leader consumer…

如何使用Transformer-TTS语音合成模型

1、技术原理及架构图 ​ Transformer-TTS主要通过将Transformer模型与Tacotron2系统结合来实现文本到语音的转换。在这种结构中&#xff0c;原始的Transformer模型在输入阶段和输出阶段进行了适当的修改&#xff0c;以更好地处理语音数据。具体来说&#xff0c;Transformer-TT…

NSSCTF Web方向的例题和相关知识点(一)

[SWPUCTF 2021 新生赛]jicao 解题&#xff1a; 打开环境&#xff0c;是一段php代码 包含了flag.php文件&#xff0c;设定了一个POST请求的id和GET请求的json 语句会对GET请求的数据进行json解码 如果id和json变量的值都等于设定字符串&#xff0c;则得到 flag 我们可以使用…

如何让加快OpenHarmony编译速度?

OpenHarmony 有两种编译方式&#xff0c;一种是通过 hb 工具编译&#xff0c;一种是通过 build.sh 脚本编译。本文笔者将提升 build.sh 方式编译速度的方法整理如下&#xff1a; 因为笔者只用 build.sh 脚本编译&#xff0c;没用过 hb 工具&#xff0c;好像下面的选项也可以用于…

Python中使用tkinter模块和类结构的结合使用举例——编写制作一个简单的加数GUI界面

Python中使用tkinter模块和类结构的结合使用举例——编写制作一个简单的加数GUI界面 这里写目录标题 Python中使用tkinter模块和类结构的结合使用举例——编写制作一个简单的加数GUI界面一、tkinter模块和类的简述1.1 tkinter的简要介绍1.2 类结构的简要介绍 二、基于类机构和t…