vue组件 canvas实现可滑动时间刻度轴

--canvas组件页面
 
<template>
  <div className="time_line_container" ref="containerRef">
    <canvas ref="canvasRef" height="40" width="100" />
  </div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, watchEffect } from "vue";
import { formatTimeLine } from "@/utils";
import "./index.scss";
import { throttle } from "lodash";
import moment, { now } from "moment";
const props = defineProps({
  timeParts: {
    type: Array,
    default: [],
  },
  isMove: {
    type: Boolean,
    default: false,
  },
  currentTime: {
    type: Number,
    default: new Date().getTime(),
  },
});

const emits = defineEmits(["changeCallback"]);

const getPixelRatio = function (context) {
  const backingStore =
    context.backingStorePixelRatio ||
    context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio ||
    1;
  return (window.devicePixelRatio || 1) / backingStore;
};
const ratio = getPixelRatio(window);

const canvasRef = ref(null);
const containerRef = ref(null);
const ctxRef = ref(null);
// 可选的每个间隔代表多少分钟
const minutePerStep = ref([
  1, 2, 5, 10, 15, 20, 30, 60, 120, 180, 240, 360, 720, 1440,
]);
// 最小刻度间距
const minScaleSpacing = ref(20);

// 整个时间轴表示的时间长度
const totalRulerHours = ref(20);
// 允许的最小大格长度px值 如果调小 大格会变密集
const minLargeScaleSpacing = ref(100);
// 缩放层级
const zoom = ref(24);
const currentTime = ref(props.currentTime);
const timeParts = ref(props.timeParts);
const isMove = ref(false);
// 游标颜色
const drawCursorColor = "#3B5EFF";
// 刻度处背景颜色
const fillScaleBgColor = "#DCDFE6";
// 刻度颜色
const drawScaleColor = "#606266";
// 鼠标悬浮移动 游标颜色
const hoverMoveColor = "#5674fd";
// 录像时间块颜色
const fillTimePartsColor = "rgba(140, 158, 238 , .5)";
const moveTimer = ref(null);
const startTimestamp = ref(0);
// 鼠标是否被按下 用来确认时hover事件还是拖拽事件
const isMouseDownFlag = ref(false);
// 是否拖拽 用来确认mouseup时是点击事件还是拖拽事件
const isDragFlag = ref(false);
// 鼠标按下时鼠标x位置 在处理拖拽事件中用来比对
const mousedownX = ref(0);

const refreshStartTimestamp = () => {
  // 当currentTime改变或者整条时间轴代表的totalHours改变的时候 就刷新左边开始时间
  startTimestamp.value =
    currentTime.value - (totalRulerHours.value * 60 * 60 * 1000) / 2;
};

const clearcanvas = () => {
  if (!ctxRef.value || !canvasRef.value) return;
  ctxRef.value.clearRect(
    0,
    0,
    canvasRef.value.width || 0,
    canvasRef.value.height || 0
  );
};

const fillScaleBg = () => {
  if (ctxRef.value) {
    ctxRef.value.fillStyle = fillScaleBgColor;
    ctxRef.value.fillRect(0, 0, canvasRef.value?.width || 0, 12);
  }
};

const createScaleText = (time) => {
  if (
    time.getHours() === 0 &&
    time.getMinutes() === 0 &&
    time.getMilliseconds() === 0
  ) {
    return moment(time).format("YYYY-MM-DD");
  }
  return moment(time).format("HH:mm");
};

const drawScale = () => {
  if (!canvasRef.value || !ctxRef.value) return;
  // 一分钟多少像素
  let oneMinutePx = canvasRef.value.width / (totalRulerHours.value * 60);
  // 一毫秒多少像素
  let oneMSPx = oneMinutePx / (60 * 1000);
  // 刻度间隔 默认20px
  let scaleSpacing = minScaleSpacing.value;
  // 每格代表多少分钟
  let scaleUnit = scaleSpacing / oneMinutePx;

  let len = minutePerStep.value.length;
  for (let i = 0; i < len; i += 1) {
    if (scaleUnit < minutePerStep.value[i]) {
      // 选择正确的刻度单位分钟
      scaleUnit = minutePerStep.value[i];
      // 每刻度之间的距离 = 一分钟多少像素 * 刻度单位
      // 即 scaleUnit = scaleSpacing / oneMinutePx 的变形
      // 主要是 totalRulerHours 会变化 需要根据这个的变化来计算...
      scaleSpacing = oneMinutePx * scaleUnit;
      break;
    }
  }

  // 有刻度文字的大格相当于多少分钟 相当于直尺上的1cm
  let mediumStep = 30;
  for (let i = 0; i < len; i++) {
    if (minLargeScaleSpacing.value / oneMinutePx <= minutePerStep.value[i]) {
      mediumStep = minutePerStep.value[i];
      break;
    }
  }

  let totalScales = canvasRef.value.width / scaleSpacing;
  // 某个刻度距离最左端得距离
  let graduationLeft;
  // 某个刻度得时间
  let graduationTime;
  let lineHeight;
  // 开始时间 = 中间时间 - 一半得整条时间
  startTimestamp.value =
    currentTime.value - (totalRulerHours.value * 60 * 60 * 1000) / 2;
  // 因为中间点是currentTime.value是固定的 最右边不一定在某个刻度上 会有一定的偏移量
  let leftOffsetMs =
    scaleUnit * 60 * 1000 - (startTimestamp.value % (scaleUnit * 60 * 1000));
  // 开始时间偏移距离(px)
  let leftOffsetPx = leftOffsetMs * oneMSPx;
  // 一刻度多少毫秒
  let oneScalesMS = scaleSpacing / oneMSPx;
  // 文字颜色
  ctxRef.value.fillStyle = drawScaleColor;
  ctxRef.value.font = "17px serif";

  // 刻度线颜色
  ctxRef.value.strokeStyle = drawScaleColor;
  ctxRef.value.beginPath();
  // 画刻度线
  function drawScaleLine(left, height) {
    ctxRef.value.moveTo(left, 0);
    ctxRef.value.lineTo(left, height);
    ctxRef.value.lineWidth = 1;
  }
  for (let i = 0; i < totalScales; i++) {
    // 距离 = 开始得偏移距离 + 格数 * 每格得px;
    graduationLeft = leftOffsetPx + i * scaleSpacing;
    // 时间 = 左侧开始时间 + 偏移时间 + 格数 * 一格多少毫秒
    graduationTime = startTimestamp.value + leftOffsetMs + i * oneScalesMS;
    let date = new Date(graduationTime);
    if ((graduationTime / (60 * 1000)) % mediumStep == 0) {
      // 大格刻度
      lineHeight = 12;
      let scaleText = createScaleText(date);
      ctxRef.value.fillText(scaleText, graduationLeft - 20, 42);
    } else {
      // 小格刻度
      lineHeight = 8;
    }
    drawScaleLine(graduationLeft, lineHeight);
  }
  ctxRef.value.stroke();
};

const fillTimeParts = (part) => {
  let onePxsMS =
    canvasRef.value.width / (totalRulerHours.value * 60 * 60 * 1000);
  let beginX = (part.start - startTimestamp.value) * onePxsMS;
  let partWidth = (part.end - part.start) * onePxsMS;
  if (part.style && part.style.background) {
    ctxRef.value.fillStyle = part.style.background;
  } else {
    ctxRef.value.fillStyle = fillTimePartsColor;
  }
  ctxRef.value.fillRect(beginX, 0, partWidth, 20);
};

const drawCursor = () => {
  if (!canvasRef.value || !ctxRef.value) return;
  ctxRef.value.beginPath();
  ctxRef.value.moveTo(canvasRef.value.width / 2, 0);
  ctxRef.value.lineTo(canvasRef.value.width / 2, 110);
  ctxRef.value.strokeStyle = drawCursorColor;
  ctxRef.value.lineWidth = 2;
  ctxRef.value.stroke();
  ctxRef.value.fillStyle = drawCursorColor;
  ctxRef.value.font = "18px serif";
  ctxRef.value.fillText(
    formatTimeLine(currentTime.value),
    canvasRef.value.width / 2 - 100,
    canvasRef.value.height - 17
  );
};

const init = () => {
  refreshStartTimestamp();
  // 清空画布
  clearcanvas();
  // 画刻度处背景
  fillScaleBg();
  // 画刻度
  drawScale();

  if (timeParts.value.length) {
    timeParts.value.forEach((element) => {
      fillTimeParts(element);
    });
  }
  // 画游标
  drawCursor();
};

const addTimeParts = (timeParts) => {
  setTimeParts(timeParts.value.concat(timeParts));
};
const setIsMove = (Move) => {
  if (isMove.value === Move) return;
  isMove.value = Move;
  const clearTimer = () => {
    if (moveTimer.value) {
      clearInterval(moveTimer.value);
      moveTimer.value = null;
    }
  };
  if (isMove.value) {
    // 先清除之前得timer 否则会有两个timer通知进行...
    if (moveTimer.value) {
      clearTimer();
    }
    moveTimer.value = setInterval(() => {
      currentTime.value += 1000;
      init();
    }, 1000);
  } else {
    clearTimer();
  }
};

const dragMove = (event) => {
  let posX = getMouseXRelativePos(event);
  let diffX = posX - mousedownX.value;
  let onePxsMS =
    canvasRef.value.width / (totalRulerHours.value * 60 * 60 * 1000);

  currentTime.value = currentTime.value - Math.round(diffX / onePxsMS);
  init();
  // 👇因为重新设置了currentTime 所以要重新设置鼠标按下位置
  // 否则偏移时间会进行累加 越拖越快越拖越快...
  mousedownX.value = posX;
};

const hoverMove = (event) => {
  const posX = getMouseXRelativePos(event);
  const t = getMousePosTime(event);
  init();
  ctxRef.value.beginPath();
  ctxRef.value.moveTo(posX * ratio + 1, 0);
  ctxRef.value.lineTo(posX * ratio + 1, canvasRef.value.height);
  ctxRef.value.strokeStyle = hoverMoveColor;
  ctxRef.value.lineWidth = 1;
  ctxRef.value.stroke();
  ctxRef.value.fillStyle = hoverMoveColor;
  ctxRef.value.fillText(
    formatTimeLine(t),
    posX * ratio - 125,
    canvasRef.value.height - 5
  );
};

const eventListener = {
  wheel(event) {
    wheelEvent(event);
    hoverMove(event);
  },
  mousedown(event) {
    isMouseDownFlag.value = true;
    mousedownX.value = getMouseXRelativePos(event);
  },
  mousemove(event) {
    if (isMouseDownFlag.value) {
      isDragFlag.value = true;
      dragMove(event);
    } else {
      hoverMove(event);
    }
  },
  mouseup(event) {
    if (!isDragFlag.value) {
      clickEvent(event);
      hoverMove(event);
    }
    emits("changeCallback", new Date(currentTime.value));
    // 初始化这俩值以免影响下次事件判断
    isMouseDownFlag.value = false;
    isDragFlag.value = false;
  },
  mouseleave(event) {
    init();
    // 初始化这俩值以免影响下次事件判断
    isMouseDownFlag.value = false;
    isDragFlag.value = false;
  },
};

const getMousePosTime = (event) => {
  let posX = getMouseXRelativePos(event);
  // 每像素多少毫秒
  let onePxsMS =
    canvasRef.value.width / (totalRulerHours.value * 60 * 60 * 1000);
  let time = new Date(startTimestamp.value + posX / onePxsMS);
  return time;
};
const clickEvent = (event) => {
  let time = getMousePosTime(event).getTime();
};
const wheelEvent = (event) => {
  event.preventDefault();
  // 是放大一倍还是缩小一倍
  let delta = Math.max(-1, Math.min(1, event.wheelDelta));
  if (delta < 0) {
    zoom.value = zoom.value + 4;
    if (zoom.value >= 24) {
      //放大最大24小时
      zoom.value = 24;
    }
    totalRulerHours.value = zoom.value;
  } else if (delta > 0) {
    // 放大
    zoom.value = zoom.value - 4;
    if (zoom.value <= 1) {
      //缩小最小1小时
      zoom.value = 1;
    }
    totalRulerHours.value = zoom.value;
  }
  init();
};
const getMouseXRelativePos = (event) => {
  let scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
  let x = event.pageX || event.clientX + scrollX;
  // canvasRef.value元素距离窗口左侧距离
  let baseLeft = canvasRef.value.getBoundingClientRect().x;
  return x - baseLeft;
};

const setCanvasWH = throttle(() => {
  if (canvasRef.value && containerRef.value) {
    canvasRef.value.width = containerRef.value.clientWidth * ratio;
    canvasRef.value.height = containerRef.value.clientHeight * ratio;
  }
  init();
}, 1000);

watchEffect(() => {
  if (props.currentTime) {
    //currentTime.value = props.currentTime;
  }
  if (props.timeParts) {
    timeParts.value = props.timeParts;
  }
  setIsMove(props.isMove);
});

onMounted(() => {
  ctxRef.value = canvasRef.value.getContext("2d");
  init();
  setCanvasWH();
  window.addEventListener("resize", setCanvasWH);
  canvasRef.value.addEventListener("wheel", eventListener.wheel);
  canvasRef.value.addEventListener("mousedown", eventListener.mousedown);
  canvasRef.value.addEventListener("mousemove", eventListener.mousemove);
  canvasRef.value.addEventListener("mouseup", eventListener.mouseup);
  canvasRef.value.addEventListener("mouseleave", eventListener.mouseleave);
});

onUnmounted(() => {
  destroy();
});

const destroy = () => {
  window.removeEventListener("resize", setCanvasWH);
  if (canvasRef.value) {
    canvasRef.value.removeEventListener("wheel", eventListener.wheel);
    canvasRef.value.removeEventListener("mousedown", eventListener.mousedown);
    canvasRef.value.removeEventListener("mousemove", eventListener.mousemove);
    canvasRef.value.removeEventListener("mouseup", eventListener.mouseup);
    canvasRef.value.removeEventListener("mouseleave", eventListener.mouseleave);
    clearcanvas();
  }

  if (moveTimer.value) {
    clearInterval(moveTimer.value);
    moveTimer.value = null;
  }
};
 
defineExpose({
  currentTime
})
</script>
 
//--css样式页面:
.time_line_container {
  height: 100%;
  overflow: hidden;
  // height: 50px;
  canvas {
    width: 100%;
    height: 100%;
    background: #ebeef5;
  }
}
//---
 
//--调用组件页面 通过循环加载显示 clickVehicleItem.videoCount数量循环显示
 
    <div
      class="channel_timeline_wrap1"
      v-for="(item, index) in [...Array(+clickVehicleItem.videoCount || 0)]"
      :key="index"
    >
      <!-- @click="changeVIdeoTimeLine(timeFileData[form.channelNo[index]])" -->
   <!--  :timeParts="timeFileData[form.channelNo[index]]&&timeFileData[form.channelNo[index]].fileData||[] "  -->
      <div class="label">通道{{ index + 1 }}</div>
      <VideoTimeLine
   :timeParts="[{
                    start: new Date().getTime() - 3 * 3600 * 1000,
                    end: new Date().getTime() - 1 * 3600 * 1000,
                  },
                  {
                    start: new Date().getTime() - 6 * 3600 * 1000,
                    end: new Date().getTime() - 4 * 3600 * 1000,
                  }]"
        :isMove="true"
        @changeCallback="(val) => changeCallback(val, index)"
   :ref="(el) => (videoTimeRefObj[index] = el)"
      />
    </div>
 
const timelinechangeTimes = ref([]);
///--changeCallback 拖动事件_获取时间
const changeCallback = (time, index) => {
  console.log(11111, formatTimeLine(time));
  timelinechangeTimes.value[index] = formatTimeLine(time);
};
 
 //存储刻度尺上实时时间,通过下标获取
const videoTimeRefObj = ref([])
//参考示例 index下标
const changeChannel = (index)=>{
  console.log(formatTimeLine(videoTimeRefObj.value[index].currentTime) );
};
 
const timeFormat = "YYYY-MM-DD HH:mm:ss";
// 转时间格式
const formatTimeLine = (time, format) => {
  return moment(time || new Date()).format(format || timeFormat);
};
 
css样式:
  .channel_timeline_wrap1 {
    position: relative;
    margin-top: 10px;
    height: 45px;
    background: #ebeef5;
    border-radius: 0px 0px 0px 0px;
    opacity: 1;
    .label {
      position: absolute;
      left: 2px;
      top: 18px;
      font-size: 12px;
    }
  }
//---
 
////-- timeFileData字段说明_查询录像时间
    timeFileData: {
      // 1: {
      //   deviceId: "100100100018", //终端id
      //   fileCount: 50, //fileData中文件数量
      //   fileData: [
      //     {
      //       channelNo: "1", //通道号,从1 开始
      //       beginTime: "2020-11-16 00:00:00", //开始时间
      //       endTime: "2020-11-17 00:00:00", //结束时间
      //       fileType: 0, //查询文件类型,0:音视频,1;音频,2:视频,3:音频或视频
      //       videoType: 1, //  1;主码流,2:子码流
      //       storageType: 1, // 1:主存储器,2:备份存储器
      //       fileSize: 1024000, //文件大小 单位字节(Byte)
   //      start:1672989813770, //刻度尺对应的开始时间显示背景颜色
   //      end:1672997100403, //刻度尺对应的结束时间显示背景颜色
      //     },
      //     {
      //       channelNo: "2", //通道号,从2 开始
      //       beginTime: "2020-11-16 00:00:00", //开始时间
      //       endTime: "2020-11-17 00:00:00", //结束时间
      //       fileType: 0, //查询文件类型,0:音视频,1;音频,2:视频,3:音频或视频
      //       videoType: 1, //  1;主码流,2:子码流
      //       storageType: 1, // 1:主存储器,2:备份存储器
      //       fileSize: 1024000, //文件大小 单位字节(Byte)
   //       start:1672979121562, //刻度尺对应的开始时间显示背景颜色
   //       end:1672986333548, //刻度尺对应的结束时间显示背景颜色
      //     },
      //   ],
      // },
    },
////----
----效果图

 

 

 
posted @   Kiss丿残阳  阅读(5287)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示