update:个人训练优化

This commit is contained in:
2026-08-14 17:13:23 +08:00
parent d662db9465
commit e7f073ed7c
25 changed files with 481 additions and 344 deletions
+137 -16
View File
@@ -1,8 +1,18 @@
<script setup>
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from "vue";
import {
computed,
getCurrentInstance,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch,
} from "vue";
const defaultCanvasSize = 300;
const defaultRingCount = 10;
const highlightRevealProgressFrames = [0.08, 0.2, 0.38, 0.6, 0.82, 1];
const highlightRevealFrameInterval = 60;
const props = defineProps({
// canvas 唯一标识;不传时组件内部自动生成,避免多个靶面 canvas-id 冲突。
@@ -41,6 +51,11 @@ const props = defineProps({
type: Number,
default: 0,
},
// 每次变化时以固定低帧数重新展开当前高亮扇区;默认关闭。
highlightRefreshToken: {
type: Number,
default: 0,
},
showSectorLabels: {
type: Boolean,
default: false,
@@ -94,6 +109,10 @@ const canvasSize = ref({
width: defaultCanvasSize,
height: defaultCanvasSize,
});
let drawRequestGeneration = 0;
let highlightAnimationGeneration = 0;
let highlightAnimationTimer = null;
let mountDrawTimer = null;
// 完整靶纸默认样式,调用方可以通过 targetStyleConfig 局部覆盖。
const defaultTargetStyleConfig = {
@@ -238,10 +257,23 @@ const drawTargetRings = (ctx, centerX, centerY, targetRadius, config) => {
};
// 高亮后端指定区域;activeRing 有效时只高亮该区域内的单个环。
const drawSectorHighlight = (ctx, centerX, centerY, targetRadius, config) => {
const drawSectorHighlight = (
ctx,
centerX,
centerY,
targetRadius,
config,
revealProgress = 1
) => {
const angles = getSectorAngles(props.activeSector, props.sectorCount);
if (!angles) return;
const safeRevealProgress = Math.min(
Math.max(getNumber(revealProgress, 1), 0),
1
);
if (safeRevealProgress <= 0) return;
const ring = getPositiveInteger(props.activeRing);
const hasActiveRing = ring >= 1 && ring <= config.ringCount;
const innerRadius = hasActiveRing
@@ -262,7 +294,8 @@ const drawSectorHighlight = (ctx, centerX, centerY, targetRadius, config) => {
innerRadius,
outerRadius,
angles.startAngle,
angles.endAngle,
angles.startAngle +
(angles.endAngle - angles.startAngle) * safeRevealProgress,
style.color,
style.strokeColor,
Math.max(1, targetRadius * style.lineWidthRatio)
@@ -399,12 +432,12 @@ const getDrawKey = (width, height) => {
};
// 主绘制入口:根据 highlightOnly 决定画完整靶纸,还是只画透明高亮层。
const drawTarget = () => {
const drawTarget = ({ force = false, highlightProgress = 1 } = {}) => {
const width = Math.max(getNumber(canvasSize.value.width, defaultCanvasSize), 1);
const height = Math.max(getNumber(canvasSize.value.height, defaultCanvasSize), 1);
const drawKey = getDrawKey(width, height);
if (drawKey === lastDrawKey.value) {
if (!force && drawKey === lastDrawKey.value) {
return;
}
@@ -421,7 +454,14 @@ const drawTarget = () => {
drawTargetRings(ctx, centerX, centerY, targetRadius, config);
}
drawSectorHighlight(ctx, centerX, centerY, targetRadius, config);
drawSectorHighlight(
ctx,
centerX,
centerY,
targetRadius,
config,
highlightProgress
);
if (!props.highlightOnly) {
drawRingLines(ctx, centerX, centerY, targetRadius, config);
@@ -441,26 +481,82 @@ const drawTarget = () => {
drawSectorLabels(ctx, centerX, centerY, targetRadius);
ctx.draw();
lastDrawKey.value = drawKey;
lastDrawKey.value = highlightProgress >= 1 ? drawKey : "";
};
const setCanvasSizeAndDraw = async (width, height) => {
const cancelHighlightAnimation = () => {
highlightAnimationGeneration += 1;
if (highlightAnimationTimer) {
clearTimeout(highlightAnimationTimer);
highlightAnimationTimer = null;
}
};
// 固定 6 帧展开黄色扇区,避免对原生 canvas 节点做缩放导致低端机错位。
const runHighlightRevealAnimation = () => {
cancelHighlightAnimation();
const generation = highlightAnimationGeneration;
let frameIndex = 0;
const drawNextFrame = () => {
if (generation !== highlightAnimationGeneration) return;
drawTarget({
force: true,
highlightProgress: highlightRevealProgressFrames[frameIndex],
});
frameIndex += 1;
if (frameIndex < highlightRevealProgressFrames.length) {
highlightAnimationTimer = setTimeout(
drawNextFrame,
highlightRevealFrameInterval
);
} else {
highlightAnimationTimer = null;
}
};
drawNextFrame();
};
const setCanvasSizeAndDraw = async (
width,
height,
{ animateHighlight = false, requestGeneration } = {}
) => {
canvasSize.value = {
width: width > 0 ? width : defaultCanvasSize,
height: height > 0 ? height : width || defaultCanvasSize,
};
await nextTick();
drawTarget();
if (requestGeneration !== drawRequestGeneration) return;
const canAnimateHighlight =
animateHighlight &&
props.highlightOnly &&
!!getSectorAngles(props.activeSector, props.sectorCount);
if (canAnimateHighlight) {
runHighlightRevealAnimation();
} else {
cancelHighlightAnimation();
drawTarget();
}
};
// 读取 canvas 实际渲染尺寸后再绘制,保证小程序真机尺寸和坐标一致。
const measureAndDraw = () => {
const measureAndDraw = ({ animateHighlight = false } = {}) => {
const requestGeneration = ++drawRequestGeneration;
const propWidth = Math.round(getNumber(props.canvasWidth, 0));
const propHeight = Math.round(getNumber(props.canvasHeight, 0));
if (propWidth > 0 && propHeight > 0) {
setCanvasSizeAndDraw(propWidth, propHeight);
setCanvasSizeAndDraw(propWidth, propHeight, {
animateHighlight,
requestGeneration,
});
return;
}
@@ -469,18 +565,23 @@ const measureAndDraw = () => {
query
.select(`#${currentCanvasId.value}`)
.boundingClientRect(async (rect) => {
if (requestGeneration !== drawRequestGeneration) return;
const width = Math.round(getNumber(rect?.width, defaultCanvasSize));
const height = Math.round(getNumber(rect?.height, width || defaultCanvasSize));
await setCanvasSizeAndDraw(width, height);
await setCanvasSizeAndDraw(width, height, {
animateHighlight,
requestGeneration,
});
})
.exec();
};
// 等待 Vue 完成 DOM 更新后重新测量和绘制。
const scheduleDraw = async () => {
const scheduleDraw = async ({ animateHighlight = false } = {}) => {
await nextTick();
measureAndDraw();
measureAndDraw({ animateHighlight });
};
watch(
@@ -491,6 +592,7 @@ watch(
props.sectorCount,
props.activeSector,
props.activeRing,
props.highlightRefreshToken,
props.showSectorLabels,
props.highlightOnly,
props.canvasWidth,
@@ -501,14 +603,33 @@ watch(
props.sectorLabelStyle,
props.highlightStyle,
],
scheduleDraw,
(currentValues, previousValues = []) => {
const refreshTokenIndex = 6;
const refreshToken = Number(currentValues[refreshTokenIndex]);
const previousRefreshToken = Number(previousValues[refreshTokenIndex]);
const animateHighlight =
Number.isFinite(refreshToken) &&
refreshToken > 0 &&
refreshToken !== previousRefreshToken;
return scheduleDraw({ animateHighlight });
},
{
deep: true,
}
);
onMounted(() => {
setTimeout(measureAndDraw, 30);
mountDrawTimer = setTimeout(measureAndDraw, 30);
});
onBeforeUnmount(() => {
drawRequestGeneration += 1;
cancelHighlightAnimation();
if (mountDrawTimer) {
clearTimeout(mountDrawTimer);
mountDrawTimer = null;
}
});
</script>