项目中有个需求,将对象一天内对应的不同的状态在时间轴上显示出来。调研的方案有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展示,内容都在下面
开始时间
{{ startTime }}
结束时间
{{ endTime }}
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';
//
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时间标识。