✨ 优化趋势图悬浮平移动画
This commit is contained in:
@@ -70,16 +70,20 @@ const offset = ref(0)
|
||||
const windowSize = ref(30)
|
||||
const drag = ref<DragState | null>(null)
|
||||
const hoverX = ref<number | null>(null)
|
||||
const hoverVisual = ref<{ x: number; downloadY: number; uploadY: number } | null>(null)
|
||||
const velocity = ref(0)
|
||||
const rafId = ref<number | null>(null)
|
||||
const inertiaRafId = ref<number | null>(null)
|
||||
const lastInertiaTime = ref(0)
|
||||
const lastHoverTime = ref(0)
|
||||
const tooltip = ref<TooltipState | null>(null)
|
||||
|
||||
const INERTIA_REFERENCE_FRAME_MS = 1000 / 120
|
||||
const INERTIA_MAX_FRAME_MS = 1000 / 30
|
||||
const INERTIA_DECAY_PER_120HZ_FRAME = 0.96
|
||||
const INERTIA_STOP_VELOCITY = 0.01
|
||||
const HOVER_EASE_PER_120HZ_FRAME = 0.34
|
||||
const HOVER_SNAP_DISTANCE = 0.2
|
||||
|
||||
const panelClass = computed(() =>
|
||||
props.isDarkMode
|
||||
@@ -118,13 +122,13 @@ const clampState = () => {
|
||||
|
||||
const scheduleDraw = () => {
|
||||
if (rafId.value !== null) return
|
||||
rafId.value = window.requestAnimationFrame(() => {
|
||||
rafId.value = window.requestAnimationFrame((timestamp) => {
|
||||
rafId.value = null
|
||||
draw()
|
||||
draw(timestamp)
|
||||
})
|
||||
}
|
||||
|
||||
const draw = () => {
|
||||
const draw = (timestamp = performance.now()) => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
@@ -266,19 +270,40 @@ const draw = () => {
|
||||
const uploadTraffic = Number(row.uploadTraffic || 0)
|
||||
const downloadY = padTop + height - (downloads / max) * height
|
||||
const uploadY = padTop + height - (uploads / max) * height
|
||||
const anchorY = Math.min(downloadY, uploadY)
|
||||
const elapsed = lastHoverTime.value
|
||||
? Math.min(timestamp - lastHoverTime.value, INERTIA_MAX_FRAME_MS)
|
||||
: INERTIA_REFERENCE_FRAME_MS
|
||||
lastHoverTime.value = timestamp
|
||||
|
||||
if (!hoverVisual.value) {
|
||||
hoverVisual.value = { x: snapX, downloadY, uploadY }
|
||||
} else {
|
||||
const ease = 1 - Math.pow(1 - HOVER_EASE_PER_120HZ_FRAME, elapsed / INERTIA_REFERENCE_FRAME_MS)
|
||||
const nextX = hoverVisual.value.x + (snapX - hoverVisual.value.x) * ease
|
||||
const nextDownloadY = hoverVisual.value.downloadY + (downloadY - hoverVisual.value.downloadY) * ease
|
||||
const nextUploadY = hoverVisual.value.uploadY + (uploadY - hoverVisual.value.uploadY) * ease
|
||||
hoverVisual.value = {
|
||||
x: Math.abs(nextX - snapX) < HOVER_SNAP_DISTANCE ? snapX : nextX,
|
||||
downloadY:
|
||||
Math.abs(nextDownloadY - downloadY) < HOVER_SNAP_DISTANCE ? downloadY : nextDownloadY,
|
||||
uploadY: Math.abs(nextUploadY - uploadY) < HOVER_SNAP_DISTANCE ? uploadY : nextUploadY
|
||||
}
|
||||
}
|
||||
|
||||
const visual = hoverVisual.value
|
||||
const anchorY = Math.min(visual.downloadY, visual.uploadY)
|
||||
const placeBelow = anchorY < 104
|
||||
const tooltipInset = Math.min(mobile ? 116 : 132, Math.max(48, rect.width / 2 - 8))
|
||||
|
||||
ctx.strokeStyle = props.isDarkMode ? 'rgba(148, 163, 184, 0.35)' : 'rgba(91, 121, 132, 0.28)'
|
||||
ctx.lineWidth = 1
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(snapX, padTop)
|
||||
ctx.lineTo(snapX, padTop + height)
|
||||
ctx.moveTo(visual.x, padTop)
|
||||
ctx.lineTo(visual.x, padTop + height)
|
||||
ctx.stroke()
|
||||
|
||||
tooltip.value = {
|
||||
left: clamp(snapX, tooltipInset, rect.width - tooltipInset),
|
||||
left: clamp(visual.x, tooltipInset, rect.width - tooltipInset),
|
||||
top: placeBelow ? anchorY + 16 : anchorY - 12,
|
||||
date: row.date,
|
||||
downloads,
|
||||
@@ -287,9 +312,19 @@ const draw = () => {
|
||||
uploadTraffic: formatBytes(uploadTraffic),
|
||||
transform: placeBelow ? 'translate(-50%, 0)' : 'translate(-50%, -100%)'
|
||||
}
|
||||
drawHoverPoint({ x: snapX, y: downloadY }, downloadColor)
|
||||
drawHoverPoint({ x: snapX, y: uploadY }, uploadColor)
|
||||
drawHoverPoint({ x: visual.x, y: visual.downloadY }, downloadColor)
|
||||
drawHoverPoint({ x: visual.x, y: visual.uploadY }, uploadColor)
|
||||
|
||||
if (
|
||||
Math.abs(visual.x - snapX) >= HOVER_SNAP_DISTANCE ||
|
||||
Math.abs(visual.downloadY - downloadY) >= HOVER_SNAP_DISTANCE ||
|
||||
Math.abs(visual.uploadY - uploadY) >= HOVER_SNAP_DISTANCE
|
||||
) {
|
||||
scheduleDraw()
|
||||
}
|
||||
} else {
|
||||
hoverVisual.value = null
|
||||
lastHoverTime.value = 0
|
||||
tooltip.value = null
|
||||
}
|
||||
}
|
||||
@@ -333,7 +368,7 @@ const applyInertia = (timestamp: number) => {
|
||||
lastInertiaTime.value = timestamp
|
||||
|
||||
pan(velocity.value * elapsed, false)
|
||||
draw()
|
||||
draw(timestamp)
|
||||
velocity.value *= Math.pow(
|
||||
INERTIA_DECAY_PER_120HZ_FRAME,
|
||||
elapsed / INERTIA_REFERENCE_FRAME_MS
|
||||
@@ -385,6 +420,8 @@ const handlePointerCancel = () => {
|
||||
const handlePointerLeave = () => {
|
||||
drag.value = null
|
||||
hoverX.value = null
|
||||
hoverVisual.value = null
|
||||
lastHoverTime.value = 0
|
||||
scheduleDraw()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user