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
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>
|