vue canvas typescript 绘制时间标尺

2023-11-19

项目中有个需求,将对象一天内对应的不同的状态在时间轴上显示出来。调研的方案有5种,
1、时间轴用div画,时间轴上遮罩的状态改变则改变时间轴div本身的颜色。
2、时间轴用div画,时间轴上的遮罩用div画,状态改变则改变遮罩div的颜色,时间轴div只做展示不做样式更改。
3、时间轴用静态的图片展示,时间轴上的遮罩用div画,状态改变则改变遮罩div的颜色。
4、时间轴用canvas画,时间轴上的遮罩用canvas画,每次状态改变重绘canvas的时间轴和遮罩层。
5、时间轴用canvas画,时间轴用另外一个canvas画,每次状态改变都修改遮罩层的canvas。

调研的5种方案中,1 2 3都会面临频繁的回流和重绘,所以最后采用4 5,但是最后采用的时方案4, 5应该是最优方案,因为4是一个canvas。状态改变时重绘时间轴是浪费的,但是项目时间紧张,没有按照方案5处理。后续会继续优化,下面我将方案四的绘制思路整理出来。

1、画canvas标签,canvas标签悬浮到状态时要展示对应的时间,时间样式用div展示,内容都在下面

 <template>
<div class="time-scale-container">
    <div v-show="visible" class="time-scale-tooltip">
        <div class="scale-tooltip-label">
            <div>开始时间</div>
            <div class="scale-tooltip-hour">{{ startTime }}</div>
        </div>
        <div class="scale-tooltip-arrow"></div>
        <div class="scale-tooltip-label">
            <div>结束时间</div>
            <div class="scale-tooltip-hour">{{ endTime }}</div>
        </div>
    </div>
    <canvas id="vehicleTimeScale" class="time-scale-canvas"></canvas>
</div>
 </template>

2、封装canvas工具类

     /**
 * @file 绘制 Canvas 工具函数
 */

import {InitCanvas, DrawText2d, DrawLine2d, DrawFillRect2d, ClearRect} from './types';

const canvasScale = window.devicePixelRatio;

// 初始化canvas
export const initCanvas: InitCanvas = (canvasId, options) => {
    const canvas: HTMLCanvasElement | null = document.querySelector(`#${canvasId}`);
    if (!canvas) {
        throw new Error(`canvas容器不存在`);
    }

// 获取canvas画布的宽高,css中已经设置canvas的宽高,则以css为准,如果没设置默认值为 300 * 150
const clientWidth = canvas.clientWidth;
const clientHeight = canvas.clientHeight;

// canvas 宽高 TODO 设置canvas宽高后,canvas不能随浏览器缩放而缩放,暂时注释掉
// canvas.style.width = canvas.clientWidth + 'px';
// canvas.style.height = canvas.clientHeight + 'px';

// <canvas>可能在视网膜屏幕上显得太模糊。
// 使用window.devicePixelRatio确定应添加多少额外的像素密度以使图像更清晰
canvas.width = clientWidth * canvasScale;
canvas.height = clientHeight * canvasScale;

const ctx = canvas.getContext(options.contextType);

return {canvas, ctx};
 };

export const drawText2d: DrawText2d = (ctx, content, offsetX, offsetY, options) => {
    // 设置文本填充颜色
    if (options && options.fillStyle) {
        ctx.fillStyle = options.fillStyle;
    }

// 设置文本字号 注:时间和字体要一起设置才生效
if (options && options.fontSize) {
    ctx.font = `${options.fontSize}px PingFangSC-Regular, PingFang SC`;
}

// 设置文本字体
if (options && options.fontFamily) {
    ctx.font += options.fontFamily;
}

// 设置文本水平对齐方式
if (options && options.textAlign) {
    ctx.textAlign = options.textAlign;
}
// 设置文本基线对齐方式
if (options && options.textBaseline) {
    ctx.textBaseline = options.textBaseline;
}
// 设置文本显示方向
if (options && options.direction) {
    ctx.direction = options.direction;
}

// 文字填充
ctx.fillText(content, offsetX, offsetY);
};

export const drawLine2d: DrawLine2d = (ctx, moveToX, moveToY, lineToX, lineToY, options) => {
    // 画线
    ctx.beginPath();

// 设置线条描边颜色
if (options && options.strokeStyle) {
    ctx.strokeStyle = options.strokeStyle;
}

// 设置线条转角的样式
if (options && options.lineJoin) {
    ctx.lineJoin = options.lineJoin;
}

// 设置线条宽度
if (options && options.lineWidth) {
    ctx.lineWidth = options.lineWidth;
}

// 当lineJoin类型是miter时候,miter效果生效的限制值
if (options && options.miterLimit) {
    ctx.miterLimit = options.miterLimit;
}
ctx.moveTo(moveToX, moveToY);
ctx.lineTo(lineToX, lineToY);
ctx.stroke();
};

export const drawFillRect2d: DrawFillRect2d = (ctx, startX, startY, width, height, options) => {
    if (options && options.fillStyle) {
        ctx.fillStyle = options.fillStyle;
    }

    ctx.fillRect(startX, startY, width, height);
};

export const clearRect: ClearRect = (ctx, startX, startY, width, height) => {
    ctx.clearRect(startX, startY, width, height);
};

3、初始化canvas并定义鼠标移入移出事件

// 初始化canvas
    const option = {
        contextType: ContextType.D2
    };
    const {canvas, ctx} = initCanvas('vehicleTimeScale', option);
    this.canvas = canvas;
    this.ctx = ctx;

    // canvas时间轴加鼠标移动事件
    this.canvas!.addEventListener('mousemove', this.handler);
    // canvas鼠标移除事件
    this.canvas!.addEventListener('mouseout', () => {
        this.visible = false;
    });

4、计算24小时时间轴对应的位置及画线

 // 计算每个时间线对应X轴位置
getTimeLineXpoint(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
    // 计算每一个格子的宽度,canvas前后各空两个格,基础参数已经多加一个,所以在加三个
    const perStep = canvas.width / (TIME_INTERVA_NUMBER + 3);

    for (let i = 1; i <= TIME_INTERVA_NUMBER; i++) {
        this.scaleXpointArr.push((i + 1) * perStep);
    }
}

// 画时间轴线 和 时间标识
drawTimeLineAText(height: number) {
    for (let i = 0; i < this.scaleXpointArr.length; i++) {
        if (i % BASE_SCALE_NUM === 0) {
            //整数刻度
            drawLine2d(this.ctx!, this.scaleXpointArr[i], height * 0.1, this.scaleXpointArr[i], height * 0.35, {
                strokeStyle: LINE_OPTION.strokeStyle
            });

            // 时间标识
            let textContent = i / BASE_SCALE_NUM + '';
            // 文字测量。获得是字符占据的宽度
            const textWidth = this.ctx!.measureText(textContent).width;
            drawText2d(this.ctx!, textContent, this.scaleXpointArr[i] - textWidth, height * 0.8, {
                fontSize: TEXT_OPTION.fontSize,
                fillStyle: TEXT_OPTION.fillStyle
            });

            const textContent1 = ':00';
            drawText2d(this.ctx!, textContent1, this.scaleXpointArr[i], height * 0.8, {
                fontSize: TEXT_OPTION.fontSize,
                fillStyle: TEXT_OPTION.fillStyle
            });
        } else {
            //半数刻度
            drawLine2d(this.ctx!, this.scaleXpointArr[i], height * 0.1, this.scaleXpointArr[i], height * 0.2, {
                strokeStyle: LINE_OPTION.strokeStyle
            });
        }
    }
}

5、根据状态开始时间结束时间,画状态遮罩层

 / 计算时间轴 0:00对应的位置
getTimeAndPosition() {
    const timer = this.getCurrentZeroTime();
    this.scaleTimePosition = {time: timer, positionX: this.scaleXpointArr[0]};
}

// 计算状态时间与位置的对应关系
getVehiclePosition(vehicleOrderStatus: VehicleOrderStatus[]) {
    vehicleOrderStatus.forEach((item: VehicleOrderStatus) => {
        const {startX, endX} = this.getXposition(item);
        const startTime = formatTimeHelper(item.startTime, 'HH:mm:ss');
        const endTime = formatTimeHelper(item.endTime, 'HH:mm:ss');

        const height = this.canvas!.height;
        const newCanvas = {
            x: startX,
            y: 0,
            width: endX - startX,
            height: height,
            startTime,
            endTime,
            status: item.vehicleStatus
        };
            this.vehicleStatusCanvas.push(newCanvas);
    });
}

// 计算开始时间结束时间对应的x轴位置
getXposition(vehicleOrderStatus: VehicleOrderStatus) {
    if (!this.scaleTimePosition) return {startX: 0, endX: 0};
    const startTimeDiff = vehicleOrderStatus.startTime - this.scaleTimePosition.time;
    const endTimeDiff = vehicleOrderStatus.endTime - this.scaleTimePosition.time;

    // 开始时间和结束时间如果为0点,则开始位置为0点对应的位置
    const startX = startTimeDiff
        ? (startTimeDiff / 1000) * this.perSecondStep + this.scaleTimePosition.positionX
        : this.scaleTimePosition.positionX;
    const endX = endTimeDiff
        ? (endTimeDiff / 1000) * this.perSecondStep + this.scaleTimePosition.positionX
        : this.scaleTimePosition.positionX;

    return {startX, endX};
}

// 绘制状态图层
drawVehicleOrderStatus() {
    for (let i = 0; i < this.vehicleStatusCanvas.length; i++) {
        drawFillRect2d(
            this.ctx!,
            this.vehicleStatusCanvas[i].x,
            this.vehicleStatusCanvas[i].y,
            this.vehicleStatusCanvas[i].width,
            this.vehicleStatusCanvas[i].height,
            {fillStyle: ORDER_TYPE_HASH[this.vehicleStatusCanvas[i].status].color}
        );
    }
}

6、canvas鼠标悬浮显示对应的悬浮框

  handler(event: MouseEvent) {
    const selectX = event.clientX;
    const hoverCanvas = this.getIsExit(selectX);
    // canvas存在且车辆接单状态不为-1
    if (hoverCanvas && hoverCanvas.status !== -1) {
        this.startTime = hoverCanvas.startTime;
        this.endTime = hoverCanvas.endTime;
        this.visible = true;
        const ele: HTMLElement | null = document.querySelector('.time-scale-tooltip');
        ele!.style.left = selectX + 'px';
    } else {
        this.visible = false;
    }
}

// 计算鼠标位置是否在接单状态中
getIsExit(x: number): VehicleStatusCanvas | null {
    x = x * window.devicePixelRatio; // 注意:将css像素转为物理像素
    if (Array.isArray(this.vehicleStatusCanvas) && this.vehicleStatusCanvas.length > 0) {
        const hoverCanvas = this.vehicleStatusCanvas.filter((obj) => {
            return obj.x <= x && x <= obj.x + obj.width;
        });
        if (hoverCanvas && hoverCanvas.length > 0) {
            return hoverCanvas[0];
        }
    }
    return null;
}

上述为止 canvas时间轴及状态的展示就画完了,鼠标悬浮展示对应的tooltip时间标识。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vue canvas typescript 绘制时间标尺 的相关文章

随机推荐

  • Unity中的重载和重写

    Unity中的重载和重写 一 重载 二 重写 三 重载和重写的区别 一 重载 重载 两个必须一个可以 参数名必须相同 参数列表必须不同 返回值类型可以不同 代码示例 using System Collections using System
  • Linux 磁盘命令工具 比df更好用

    对于分析磁盘使用情况 有两个非常好用的命令 du 和 df 简单来说 这两个命令的作用是这样的 du 命令 它是英文单词 disk usage 的简写 主要用于查看文件与目录占用多少磁盘空间 df 命令 它是英文单词 disk free 的
  • python爬取证券之星网站

    周末无聊 找点乐子 coding utf 8 import requests from bs4 import BeautifulSoup import random import time 抓取所需内容 user agent Mozilla
  • 安卓逆向学习-Crack01 学习记录

    Crack01 学习记录 要感谢京峰教育 资料下载 https download csdn net download m0 47210241 85053839 利用jadx gui打开 分析代码 package com zhy editVi
  • nodejs封装api

    安装了nodeJs 执行 安装淘宝镜像 npm install g cnpm registry https registry npm taobao org 安装 yarn 我使用这个 淘宝镜像总是莫名其妙各种bug npm install
  • aix安装 php,CNESA

    aix安装samba服务器可以使用两种方式安装 一种是使用rpm包进行安装 一种是使用源码编译安装 一 使用samba的rpm包进行安装 1 下载samba的rpm包 下载地址为http www bullfreeware com searc
  • C++笔记--线程间共享数据

    当线程在访问共享数据的时候 必须制定一些规矩 用来限定线程可访问的数据位 还有 一个线程更新了共享数据 需要对其他线程进行通知 从易用性的角度 同一进程中的多个线程进行数据共享 错误的共享数据使用是产生并发bug的一个主要原因 当涉及到共享
  • 为什么训练集用fit_transform()而测试集用transform()及sklearn.feature_extraction.text.CountVectorizer API详解

    真正讲明白的 https blog csdn net yyhhlancelot article details 85097656 API https scikit learn org stable modules generated skl
  • mysql+mybatis 批量插入与批量更新

    首先批量更新需要增加 数据库的链接必须加上但是数据库连接必须加上 allowMultiQueries true 属性 不然会报错 You have an error in your SQL syntax check the manual t
  • 各种源码下载地址(目前只有ffmpeg和nginx,libcurl,RapidJSON 文档)

    各种源码下载地址 目前只有ffmpeg和nginx libcurl RapidJSON 文档 ffmpeg源码下载地址 http ffmpeg org download html releases nginx源码下载地址 http hg n
  • H5监听移动端物理/浏览器返回键

    JavaScript没有监听物理返回键的API 所以只能使用 popstate 事件监听 工具类如下 export function handleBrowserGoBack back console log back pushHistory
  • 论文阅读——基于深度学习智能垃圾分类

    B Fu S Li J Wei Q Li Q Wang and J Tu A Novel Intelligent Garbage Classification System Based on Deep Learning and an Emb
  • su命令切换用户输入密码后,提示:鉴定故障

    在终端通过su命令切换用户输入密码后 提示 鉴定故障 这是因为在安装linux系统时未设置root用户密码造成的 需要重新设置密码后再切换用户 具体操作命令如下 设置root用户密码 sudo passwd root 切换用户 su
  • 三步搞定ABAP DOI操作EXCEL

    ABAP可以使用OLE与DOI两种方式实现操作EXCEL 使用OLE时 每个单元格的值和样式都需要写代码实现 特别是对于不规则的格式 代码量巨大 而DOI是从服务器已经上传的EXCEL模板中下载模板然后打开修改实现数据保存 当然 也可以直接
  • springboot中的kafka的KafkaTemplate的配置类

    package com lf report utils import org apache kafka clients producer ProducerConfig import org apache kafka common seria
  • opencv 智能答卷识别系统(一)

    目标 这里是2 智能答卷之别系统二 识别答卷答案 识别准号证号码 识别姓名 识别试卷类别 试卷是有标记的 如试卷上方的黑框 排序结构 使用c 的标准排序算法 struct Ruley bool operator const Rect a1
  • 华为OD机试真题-座位调整-2023年OD统一考试(B卷)

    题目描述 疫情期间课堂的座位进行了特殊的调整 不能出现两个同学紧挨着 必须隔至少一个空位 给你一个整数数组 desk表示当前座位的占座情况 由若干 0 和 1 组成 其中 0 表示没有占位 1 表示占位 在不改变原有座位秩序情况下 还能安排
  • C#获取当前路径 -- 7种方法

    目录 一 代码 二 路径区别 三 参考文献 一 代码 推荐使用第3种 internal static class Program static void Main 获取模块的完整路径 string path1 System Diagnost
  • C++:入门学习C++,它在C的基础上做了哪些修改?

    文章目录 命名空间 缺省参数 函数重载 引用 引用作为函数返回值 引用的收益 引用和指针的区别 引用和指针的对比 内联函数 内联函数的特性 内联函数和宏的关系 指针空值问题 命名空间 首先看这样的代码 include
  • vue canvas typescript 绘制时间标尺

    项目中有个需求 将对象一天内对应的不同的状态在时间轴上显示出来 调研的方案有5种 1 时间轴用div画 时间轴上遮罩的状态改变则改变时间轴div本身的颜色 2 时间轴用div画 时间轴上的遮罩用div画 状态改变则改变遮罩div的颜色 时间