You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

159 lines
4.2 KiB
Vue

2 months ago
<template>
<div class="flow-ruler-container">
<div class="ruler-x">
<canvas ref="rulerXCanvas" class="ruler-canvas"></canvas>
</div>
<div class="main-content">
<div class="ruler-y">
<canvas ref="rulerYCanvas" class="ruler-canvas"></canvas>
</div>
<slot />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useVueFlow } from '@vue-flow/core';
const rulerXCanvas = ref(null);
const rulerYCanvas = ref(null);
const { viewport } = useVueFlow();
let animationFrameId = null;
let lastRenderTime = 0;
const FRAME_INTERVAL = 1000 / 60;
const getNiceStep = (target) => {
const steps = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1000];
return steps.find(s => s * viewport.value.zoom >= target) || 1000;
};
const drawRulers = () => {
const scale = viewport.value.zoom;
const offsetX = viewport.value.x;
const offsetY = viewport.value.y;
const step = getNiceStep(10);
const bigStep = step * 10;
const leftOffset = 30;
const rulerSize = 30;
const xCanvas = rulerXCanvas.value;
const xCtx = xCanvas.getContext('2d');
xCanvas.width = xCanvas.offsetWidth * window.devicePixelRatio;
xCanvas.height = xCanvas.offsetHeight * window.devicePixelRatio;
xCtx.scale(window.devicePixelRatio, window.devicePixelRatio);
xCtx.clearRect(0, 0, xCanvas.offsetWidth, xCanvas.offsetHeight);
xCtx.fillStyle = '#999';
xCtx.font = '10px sans-serif';
const canvasWidth = xCanvas.offsetWidth;
const startWorldX = Math.floor((-offsetX - leftOffset) / scale / step) * step;
const endWorldX = (-offsetX + canvasWidth) / scale;
for (let worldX = startWorldX; worldX < endWorldX; worldX += step) {
const screenX = worldX * scale + offsetX + leftOffset;
xCtx.beginPath();
if (Math.round(worldX) % bigStep === 0) {
xCtx.strokeStyle = '#fff';
xCtx.moveTo(screenX, rulerSize - 20);
xCtx.lineTo(screenX, rulerSize);
xCtx.stroke();
xCtx.fillStyle = '#ccc';
xCtx.fillText(Math.round(worldX).toString(), screenX + 2, 10);
} else {
xCtx.strokeStyle = '#666';
xCtx.moveTo(screenX, rulerSize - 10);
xCtx.lineTo(screenX, rulerSize);
xCtx.stroke();
}
}
const yCanvas = rulerYCanvas.value;
const yCtx = yCanvas.getContext('2d');
yCanvas.width = yCanvas.offsetWidth * window.devicePixelRatio;
yCanvas.height = yCanvas.offsetHeight * window.devicePixelRatio;
yCtx.scale(window.devicePixelRatio, window.devicePixelRatio);
yCtx.clearRect(0, 0, yCanvas.offsetWidth, yCanvas.offsetHeight);
yCtx.fillStyle = '#999';
yCtx.font = '10px sans-serif';
const canvasHeight = yCanvas.offsetHeight;
const startWorldY = Math.floor((-offsetY) / scale / step) * step;
const endWorldY = (-offsetY + canvasHeight) / scale;
for (let worldY = startWorldY; worldY < endWorldY; worldY += step) {
const screenY = worldY * scale + offsetY;
yCtx.beginPath();
if (Math.round(worldY) % bigStep === 0) {
yCtx.strokeStyle = '#fff';
yCtx.save();
yCtx.translate(rulerSize - 2, screenY + 5);
yCtx.rotate(-Math.PI / 2);
yCtx.fillStyle = '#ccc';
yCtx.fillText(Math.round(worldY).toString(), 0, 0);
yCtx.restore();
yCtx.moveTo(rulerSize - 20, screenY);
yCtx.lineTo(rulerSize, screenY);
yCtx.stroke();
} else {
yCtx.strokeStyle = '#666';
yCtx.moveTo(rulerSize - 10, screenY);
yCtx.lineTo(rulerSize, screenY);
yCtx.stroke();
}
}
};
const renderLoop = (time) => {
if (time - lastRenderTime >= FRAME_INTERVAL) {
drawRulers();
lastRenderTime = time;
}
animationFrameId = requestAnimationFrame(renderLoop);
};
onMounted(() => {
animationFrameId = requestAnimationFrame(renderLoop);
});
</script>
<style scoped>
.flow-ruler-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.ruler-x {
height: 30px;
background: #1F2122;
border-bottom: 1px solid #444;
overflow: hidden;
}
.ruler-y {
width: 30px;
background: #1F2122;
border-right: 1px solid #444;
height: 100%;
overflow: hidden;
}
.main-content {
display: flex;
height: calc(100% - 30px);
}
.ruler-canvas {
width: 100%;
height: 100%;
display: block;
}
</style>