update:对接个人训练改版

This commit is contained in:
2026-07-23 09:44:58 +08:00
parent a78ab1daeb
commit 3c5754b3fd
64 changed files with 1769 additions and 488 deletions
+143 -147
View File
@@ -21,34 +21,29 @@ const props = defineProps({
type: Boolean,
default: false,
},
// 是否显示象限文字。
showQuadrantLabels: {
type: Boolean,
default: false,
},
// 是否显示环数文字。
showRingLabels: {
type: Boolean,
default: true,
},
// 象限文字配置,key 为 1/2/3/4
quadrantLabels: {
type: Object,
default: () => ({
1: "1",
2: "2",
3: "3",
4: "4",
}),
// 从正上方开始顺时针等分的区域数量
sectorCount: {
type: Number,
default: 0,
},
// 高亮区域数组
// quadrant: 1/2/3/4,表示第几个象限。
// rings: "all" 或环数数组,例如 [7, 8, 9, 10]。
// scope: "box" 表示整象限矩形,"sector" 表示环形扇区。
// style: 可覆盖高亮填充色、描边色、线宽比例。
highlightAreas: {
type: Array,
default: () => [],
// 当前高亮区域,范围为 1 到 sectorCount
activeSector: {
type: Number,
default: 0,
},
// 指定环数,1 到 10;无效值表示高亮整个区域。
activeRing: {
type: Number,
default: 0,
},
showSectorLabels: {
type: Boolean,
default: false,
},
// 只绘制透明高亮层,不绘制完整靶纸;用于叠加在靶纸图片上。
highlightOnly: {
@@ -74,8 +69,18 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
// 象限文字样式覆盖配置。
quadrantLabelStyle: {
// 区域分割线样式覆盖配置。
sectorStyle: {
type: Object,
default: () => ({}),
},
// 区域数字样式覆盖配置。
sectorLabelStyle: {
type: Object,
default: () => ({}),
},
// 高亮样式覆盖配置。
highlightStyle: {
type: Object,
default: () => ({}),
},
@@ -120,16 +125,24 @@ const defaultCrosshairStyle = {
lineWidthRatio: 0.0025,
};
// 象限文字默认样式。
const defaultQuadrantLabelStyle = {
// 顺时针等分线默认样式。
const defaultSectorStyle = {
color: "rgba(255, 255, 255, 0.82)",
lineWidthRatio: 0.004,
};
// 区域数字默认样式。
const defaultSectorLabelStyle = {
color: "#ffffff",
fontSizeRatio: 0.045,
offsetRatio: 0.78,
backgroundColor: "rgba(0, 0, 0, 0.62)",
fontSizeRatio: 0.075,
radiusRatio: 0.76,
badgeRadiusRatio: 0.07,
};
// 高亮区域默认样式。
const defaultHighlightStyle = {
color: "rgba(254, 216, 71, 0.34)",
color: "rgba(255, 228, 0, 0.6)",
strokeColor: "rgba(254, 216, 71, 0.82)",
lineWidthRatio: 0.003,
};
@@ -155,40 +168,24 @@ const getRingColor = (ring, config) => {
return config.ringColors?.[ring] || config.ringColors?.[String(ring)] || "#ffffff";
};
// 规范化高亮环数配置;"all" 表示全部环,数组或单值会过滤非法环数。
const normalizeRings = (rings, ringCount) => {
if (rings === "all") {
return "all";
}
const rawRings = Array.isArray(rings) ? rings : [rings];
return rawRings
.map((ring) => Number(ring))
.filter((ring) => Number.isInteger(ring) && ring >= 1 && ring <= ringCount);
const getPositiveInteger = (value) => {
const numberValue = Number(value);
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
};
// 获取象限对应的扇形弧度范围
const getQuadrantAngles = (quadrant) => {
const angleMap = {
1: [Math.PI, Math.PI * 1.5],
2: [Math.PI * 1.5, Math.PI * 2],
3: [Math.PI * 0.5, Math.PI],
4: [0, Math.PI * 0.5],
// 正上方作为第一区起始边界,Canvas 角度递增方向即为顺时针
const getSectorAngles = (sector, sectorCount) => {
const count = getPositiveInteger(sectorCount);
const index = getPositiveInteger(sector);
if (!count || !index || index > count) return null;
const step = (Math.PI * 2) / count;
const startAngle = -Math.PI / 2 + (index - 1) * step;
return {
startAngle,
endAngle: startAngle + step,
middleAngle: startAngle + step / 2,
};
return angleMap[Number(quadrant)] || null;
};
// 获取象限对应的矩形区域,用于整象限高亮。
const getQuadrantBox = (quadrant, centerX, centerY, radius) => {
const boxMap = {
1: [centerX - radius, centerY - radius, radius, radius],
2: [centerX, centerY - radius, radius, radius],
3: [centerX - radius, centerY, radius, radius],
4: [centerX, centerY, radius, radius],
};
return boxMap[Number(quadrant)] || null;
};
// 绘制实心圆,靶纸环区和中心点都会用到。
@@ -240,58 +237,61 @@ const drawTargetRings = (ctx, centerX, centerY, targetRadius, config) => {
}
};
// 绘制所有高亮区域,支持整象限矩形高亮和指定环数扇区高亮
const drawHighlights = (ctx, centerX, centerY, targetRadius, config) => {
props.highlightAreas.forEach((area = {}) => {
const angles = getQuadrantAngles(area.quadrant);
// 高亮后端指定区域;activeRing 有效时只高亮该区域内的单个环
const drawSectorHighlight = (ctx, centerX, centerY, targetRadius, config) => {
const angles = getSectorAngles(props.activeSector, props.sectorCount);
if (!angles) return;
if (!angles) {
return;
}
const ring = getPositiveInteger(props.activeRing);
const hasActiveRing = ring >= 1 && ring <= config.ringCount;
const innerRadius = hasActiveRing
? targetRadius * ((config.ringCount - ring) / config.ringCount)
: 0;
const outerRadius = hasActiveRing
? targetRadius * ((config.ringCount + 1 - ring) / config.ringCount)
: targetRadius;
const style = {
...defaultHighlightStyle,
...props.highlightStyle,
};
const highlightStyle = {
...defaultHighlightStyle,
...(area.style || {}),
};
const highlightLineWidth = Math.max(1, targetRadius * highlightStyle.lineWidthRatio);
const rings = normalizeRings(area.rings || "all", config.ringCount);
const scope = area.scope || (rings === "all" ? "box" : "sector");
drawAnnularSector(
ctx,
centerX,
centerY,
innerRadius,
outerRadius,
angles.startAngle,
angles.endAngle,
style.color,
style.strokeColor,
Math.max(1, targetRadius * style.lineWidthRatio)
);
};
// 整象限默认画成矩形高亮,便于对应 1/2/3/4 号框训练提示
if (rings === "all" && scope === "box") {
const box = getQuadrantBox(area.quadrant, centerX, centerY, targetRadius);
if (!box) return;
ctx.beginPath();
ctx.rect(...box);
ctx.setFillStyle(highlightStyle.color);
ctx.fill();
ctx.setStrokeStyle(highlightStyle.strokeColor);
ctx.setLineWidth(highlightLineWidth);
ctx.stroke();
return;
}
// 从正上方开始顺时针绘制所有区域边界
const drawSectorLines = (ctx, centerX, centerY, targetRadius) => {
const count = getPositiveInteger(props.sectorCount);
if (!count) return;
const targetRings = rings === "all"
? Array.from({ length: config.ringCount }, (_, index) => index + 1)
: rings;
const style = {
...defaultSectorStyle,
...props.sectorStyle,
};
const step = (Math.PI * 2) / count;
targetRings.forEach((ring) => {
const innerRadius = targetRadius * ((config.ringCount - ring) / config.ringCount);
const outerRadius = targetRadius * ((config.ringCount + 1 - ring) / config.ringCount);
drawAnnularSector(
ctx,
centerX,
centerY,
innerRadius,
outerRadius,
angles[0],
angles[1],
highlightStyle.color,
highlightStyle.strokeColor,
highlightLineWidth
);
});
});
ctx.beginPath();
for (let index = 0; index < count; index += 1) {
const angle = -Math.PI / 2 + index * step;
ctx.moveTo(centerX, centerY);
ctx.lineTo(
centerX + Math.cos(angle) * targetRadius,
centerY + Math.sin(angle) * targetRadius
);
}
ctx.setStrokeStyle(style.color);
ctx.setLineWidth(Math.max(1, targetRadius * style.lineWidthRatio));
ctx.stroke();
};
// 绘制各环之间的分割线。
@@ -351,44 +351,30 @@ const drawRingLabels = (ctx, centerX, centerY, targetRadius, config) => {
}
};
// 绘制象限文字
const drawQuadrantLabels = (ctx, centerX, centerY, targetRadius) => {
if (!props.showQuadrantLabels) {
return;
}
// 在每个区域中线位置绘制编号,编号层始终位于高亮和分割线之上
const drawSectorLabels = (ctx, centerX, centerY, targetRadius) => {
const count = getPositiveInteger(props.sectorCount);
if (!props.showSectorLabels || !count) return;
const style = {
...defaultQuadrantLabelStyle,
...props.quadrantLabelStyle,
};
const offset = targetRadius * style.offsetRatio;
const positions = {
1: [centerX - offset, centerY - offset],
2: [centerX + offset, centerY - offset],
3: [centerX - offset, centerY + offset],
4: [centerX + offset, centerY + offset],
...defaultSectorLabelStyle,
...props.sectorLabelStyle,
};
const labelRadius = targetRadius * style.radiusRatio;
const badgeRadius = Math.max(10, targetRadius * style.badgeRadiusRatio);
ctx.setFontSize(Math.max(12, targetRadius * style.fontSizeRatio));
ctx.setFontSize(Math.max(11, targetRadius * style.fontSizeRatio));
ctx.setTextAlign("center");
ctx.setTextBaseline("middle");
ctx.setFillStyle(style.color);
Object.entries(positions).forEach(([key, position]) => {
const label = props.quadrantLabels?.[key] || props.quadrantLabels?.[Number(key)];
if (label === undefined || label === null || label === "") return;
ctx.fillText(String(label), position[0], position[1]);
});
};
// 生成高亮区域的绘制 key,只保留真正影响画面的字段。
const getHighlightDrawKeyAreas = () => {
return props.highlightAreas.map((area = {}) => ({
quadrant: area.quadrant,
rings: area.rings,
scope: area.scope,
style: area.style,
}));
for (let sector = 1; sector <= count; sector += 1) {
const angles = getSectorAngles(sector, count);
const x = centerX + Math.cos(angles.middleAngle) * labelRadius;
const y = centerY + Math.sin(angles.middleAngle) * labelRadius;
drawCircle(ctx, x, y, badgeRadius, style.backgroundColor);
ctx.setFillStyle(style.color);
ctx.fillText(String(sector), x, y);
}
};
// 生成本次绘制状态的唯一 key,用于避免相同内容重复 draw。
@@ -398,12 +384,16 @@ const getDrawKey = (width, height) => {
height,
coordinateRadius: props.coordinateRadius,
showCrosshair: props.showCrosshair,
showQuadrantLabels: props.showQuadrantLabels,
showRingLabels: props.showRingLabels,
highlightAreas: getHighlightDrawKeyAreas(),
sectorCount: props.sectorCount,
activeSector: props.activeSector,
activeRing: props.activeRing,
showSectorLabels: props.showSectorLabels,
targetStyleConfig: props.targetStyleConfig,
crosshairStyle: props.crosshairStyle,
quadrantLabelStyle: props.quadrantLabelStyle,
sectorStyle: props.sectorStyle,
sectorLabelStyle: props.sectorLabelStyle,
highlightStyle: props.highlightStyle,
highlightOnly: props.highlightOnly,
});
};
@@ -431,7 +421,7 @@ const drawTarget = () => {
drawTargetRings(ctx, centerX, centerY, targetRadius, config);
}
drawHighlights(ctx, centerX, centerY, targetRadius, config);
drawSectorHighlight(ctx, centerX, centerY, targetRadius, config);
if (!props.highlightOnly) {
drawRingLines(ctx, centerX, centerY, targetRadius, config);
@@ -444,9 +434,12 @@ const drawTarget = () => {
);
drawCrosshair(ctx, centerX, centerY, targetRadius);
drawRingLabels(ctx, centerX, centerY, targetRadius, config);
drawQuadrantLabels(ctx, centerX, centerY, targetRadius);
}
// 高亮先画,等分线和编号后画,避免高亮覆盖区域边界。
drawSectorLines(ctx, centerX, centerY, targetRadius);
drawSectorLabels(ctx, centerX, centerY, targetRadius);
ctx.draw();
lastDrawKey.value = drawKey;
};
@@ -494,16 +487,19 @@ watch(
() => [
props.coordinateRadius,
props.showCrosshair,
props.showQuadrantLabels,
props.showRingLabels,
props.highlightAreas,
props.sectorCount,
props.activeSector,
props.activeRing,
props.showSectorLabels,
props.highlightOnly,
props.canvasWidth,
props.canvasHeight,
props.quadrantLabels,
props.targetStyleConfig,
props.crosshairStyle,
props.quadrantLabelStyle,
props.sectorStyle,
props.sectorLabelStyle,
props.highlightStyle,
],
scheduleDraw,
{