✨ 平滑趋势图缩放与刻度动画
This commit is contained in:
@@ -69,6 +69,11 @@ type ChartPoint = {
|
|||||||
row: AnalyticsDailyRow
|
row: AnalyticsDailyRow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScaleVisual = {
|
||||||
|
max: number
|
||||||
|
ticks: number[]
|
||||||
|
}
|
||||||
|
|
||||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
const offset = ref(0)
|
const offset = ref(0)
|
||||||
const windowSize = ref(30)
|
const windowSize = ref(30)
|
||||||
@@ -81,6 +86,8 @@ const inertiaRafId = ref<number | null>(null)
|
|||||||
const lastInertiaTime = ref(0)
|
const lastInertiaTime = ref(0)
|
||||||
const lastHoverTime = ref(0)
|
const lastHoverTime = ref(0)
|
||||||
const tooltip = ref<TooltipState | null>(null)
|
const tooltip = ref<TooltipState | null>(null)
|
||||||
|
const scaleVisual = ref<ScaleVisual | null>(null)
|
||||||
|
const lastScaleTime = ref(0)
|
||||||
|
|
||||||
const INERTIA_REFERENCE_FRAME_MS = 1000 / 120
|
const INERTIA_REFERENCE_FRAME_MS = 1000 / 120
|
||||||
const INERTIA_MAX_FRAME_MS = 1000 / 30
|
const INERTIA_MAX_FRAME_MS = 1000 / 30
|
||||||
@@ -88,6 +95,8 @@ const INERTIA_DECAY_PER_120HZ_FRAME = 0.96
|
|||||||
const INERTIA_STOP_VELOCITY = 0.01
|
const INERTIA_STOP_VELOCITY = 0.01
|
||||||
const HOVER_EASE_PER_120HZ_FRAME = 0.34
|
const HOVER_EASE_PER_120HZ_FRAME = 0.34
|
||||||
const HOVER_SNAP_DISTANCE = 0.2
|
const HOVER_SNAP_DISTANCE = 0.2
|
||||||
|
const SCALE_EASE_PER_120HZ_FRAME = 0.2
|
||||||
|
const SCALE_SNAP_DISTANCE = 0.01
|
||||||
const getYAxisTicks = (rawMax: number) => {
|
const getYAxisTicks = (rawMax: number) => {
|
||||||
const roundedMax = Math.max(1, Math.ceil(rawMax))
|
const roundedMax = Math.max(1, Math.ceil(rawMax))
|
||||||
if (roundedMax <= 4) {
|
if (roundedMax <= 4) {
|
||||||
@@ -99,6 +108,11 @@ const getYAxisTicks = (rawMax: number) => {
|
|||||||
return Array.from({ length: scaleMax / step + 1 }, (_, index) => scaleMax - index * step)
|
return Array.from({ length: scaleMax / step + 1 }, (_, index) => scaleMax - index * step)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatAxisTick = (value: number) => {
|
||||||
|
const rounded = Math.round(value)
|
||||||
|
return Math.abs(value - rounded) < 0.01 ? String(rounded) : value.toFixed(1)
|
||||||
|
}
|
||||||
|
|
||||||
const panelClass = computed(() =>
|
const panelClass = computed(() =>
|
||||||
props.isDarkMode
|
props.isDarkMode
|
||||||
? 'border-[#40545c] bg-[linear-gradient(135deg,rgba(34,49,56,0.96),rgba(24,35,42,0.92))] shadow-md shadow-black/25'
|
? 'border-[#40545c] bg-[linear-gradient(135deg,rgba(34,49,56,0.96),rgba(24,35,42,0.92))] shadow-md shadow-black/25'
|
||||||
@@ -177,8 +191,25 @@ const draw = (timestamp = performance.now()) => {
|
|||||||
0,
|
0,
|
||||||
...visible.flatMap((row) => [Number(row.downloads || 0), Number(row.uploads || 0)])
|
...visible.flatMap((row) => [Number(row.downloads || 0), Number(row.uploads || 0)])
|
||||||
)
|
)
|
||||||
const yTicks = getYAxisTicks(rawMax)
|
const targetTicks = getYAxisTicks(rawMax)
|
||||||
const max = yTicks[0]
|
const targetMax = targetTicks[0]
|
||||||
|
const elapsedScale = lastScaleTime.value
|
||||||
|
? Math.min(timestamp - lastScaleTime.value, INERTIA_MAX_FRAME_MS)
|
||||||
|
: INERTIA_REFERENCE_FRAME_MS
|
||||||
|
lastScaleTime.value = timestamp
|
||||||
|
if (!scaleVisual.value) {
|
||||||
|
scaleVisual.value = { max: targetMax, ticks: targetTicks }
|
||||||
|
} else {
|
||||||
|
const ease = 1 - Math.pow(1 - SCALE_EASE_PER_120HZ_FRAME, elapsedScale / INERTIA_REFERENCE_FRAME_MS)
|
||||||
|
const nextMax = scaleVisual.value.max + (targetMax - scaleVisual.value.max) * ease
|
||||||
|
scaleVisual.value = {
|
||||||
|
max: Math.abs(nextMax - targetMax) < SCALE_SNAP_DISTANCE ? targetMax : nextMax,
|
||||||
|
ticks: targetTicks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const max = scaleVisual.value.max
|
||||||
|
const yTicks = targetTicks.map((tick) => (tick * max) / targetMax)
|
||||||
|
const isScaleSettling = Math.abs(max - targetMax) >= SCALE_SNAP_DISTANCE
|
||||||
const muted = props.isDarkMode ? '#9ca3af' : '#6b7280'
|
const muted = props.isDarkMode ? '#9ca3af' : '#6b7280'
|
||||||
const border = props.isDarkMode ? '#374151' : '#e5e7eb'
|
const border = props.isDarkMode ? '#374151' : '#e5e7eb'
|
||||||
const downloadColor = props.isDarkMode ? '#8fb6c5' : '#5f8fa3'
|
const downloadColor = props.isDarkMode ? '#8fb6c5' : '#5f8fa3'
|
||||||
@@ -195,8 +226,9 @@ const draw = (timestamp = performance.now()) => {
|
|||||||
ctx.moveTo(padLeft, y)
|
ctx.moveTo(padLeft, y)
|
||||||
ctx.lineTo(padLeft + width, y)
|
ctx.lineTo(padLeft + width, y)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
ctx.fillText(String(tick), 4, y + 4)
|
ctx.fillText(formatAxisTick(tick), 4, y + 4)
|
||||||
})
|
})
|
||||||
|
if (isScaleSettling) scheduleDraw()
|
||||||
|
|
||||||
const denominator = Math.max(1, windowSize.value - 1)
|
const denominator = Math.max(1, windowSize.value - 1)
|
||||||
const plotLeft = padLeft
|
const plotLeft = padLeft
|
||||||
|
|||||||
Reference in New Issue
Block a user