update:对接个人训练改版
This commit is contained in:
+143
-147
@@ -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,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user