update:优化稳定训练

This commit is contained in:
2026-09-22 17:36:06 +08:00
parent 5357f131de
commit 9de435a369
4 changed files with 42 additions and 14 deletions
@@ -18,13 +18,15 @@ const props = defineProps({
const WIDTH = 750; const WIDTH = 750;
const HEIGHT = 228; const HEIGHT = 228;
const MASK_SCALE = 3; const MASK_SCALE = 2;
const MASK_WIDTH = WIDTH / MASK_SCALE; const MASK_WIDTH = WIDTH / MASK_SCALE;
const MASK_HEIGHT = HEIGHT / MASK_SCALE; const MASK_HEIGHT = HEIGHT / MASK_SCALE;
const ENTRY_PADDING = 24; const ENTRY_PADDING = 24;
const EXIT_PADDING = 32; const EXIT_PADDING = 32;
const MAX_FEATHER = 60; const MAX_FEATHER = 60;
const FULL_TRACK_OFFSET_Y = -3; const FULL_TRACK_OFFSET_Y = -3;
const ENTRY_EDGE_BLEND_PERCENT = 5;
const EXIT_EDGE_BLEND_START_PERCENT = 90;
const SWEEP_TRACK_RADIUS = 28; const SWEEP_TRACK_RADIUS = 28;
const SWEEP_TRACK_FEATHER = 8; const SWEEP_TRACK_FEATHER = 8;
const ENTRY_ANIMATION_DURATION = 1000; const ENTRY_ANIMATION_DURATION = 1000;
@@ -32,6 +34,9 @@ const UPDATE_DURATION = 240;
const SWEEP_DELAY = 100; const SWEEP_DELAY = 100;
const SWEEP_DURATION = 1000; const SWEEP_DURATION = 1000;
const ASSET_ROOT = "https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/stability-energy"; const ASSET_ROOT = "https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/stability-energy";
const ASSET_VERSION = "20260922";
const getAssetSource = (filename) =>
`${ASSET_ROOT}/${filename}?v=${ASSET_VERSION}`;
const instance = getCurrentInstance(); const instance = getCurrentInstance();
const canvasId = `stability-energy-${Math.random().toString(36).slice(2, 10)}`; const canvasId = `stability-energy-${Math.random().toString(36).slice(2, 10)}`;
@@ -42,9 +47,9 @@ const normalizedPercent = computed(() =>
const shouldPlayEntryAnimation = ref(normalizedPercent.value <= 0); const shouldPlayEntryAnimation = ref(normalizedPercent.value <= 0);
const fallbackImageSource = computed( const fallbackImageSource = computed(
() => () =>
`${ASSET_ROOT}/${ getAssetSource(
shouldPlayEntryAnimation.value ? "progress-full.png" : "progress-empty.png" shouldPlayEntryAnimation.value ? "progress-full.png" : "progress-empty.png"
}` )
); );
let engine = null; let engine = null;
@@ -81,15 +86,25 @@ const smoothStep = (value) => {
return amount * amount * (3 - 2 * amount); return amount * amount * (3 - 2 * amount);
}; };
const enableImageSmoothing = (context) => {
if ("imageSmoothingEnabled" in context) {
context.imageSmoothingEnabled = true;
}
if ("imageSmoothingQuality" in context) {
context.imageSmoothingQuality = "high";
}
};
// 复用参考 HTML 的贝塞尔路径,保证能量光带沿靶纸下方的弧线推进。 // 复用参考 HTML 的贝塞尔路径,保证能量光带沿靶纸下方的弧线推进。
const sampleTrack = () => { const sampleTrack = () => {
const curves = [ const curves = [
[280, 26, 226, 31, 121, 49, 125, 82], // 新素材移除了左上方延伸段,进度从箭头端点开始进入弯道。
[171, 53, 145, 56, 123, 68, 125, 82],
[125, 82, 128, 128, 272, 163, 412, 163], [125, 82, 128, 128, 272, 163, 412, 163],
[412, 163, 530, 164, 650, 137, 657, 98], [412, 163, 530, 164, 650, 137, 657, 98],
[657, 98, 659, 86, 650, 76, 643, 72], [657, 98, 659, 86, 650, 76, 643, 72],
]; ];
const points = [{ x: 280, y: 26, distance: 0 }]; const points = [{ x: 171, y: 53, distance: 0 }];
curves.forEach((curve) => { curves.forEach((curve) => {
for (let step = 1; step <= 32; step += 1) { for (let step = 1; step <= 32; step += 1) {
@@ -149,6 +164,7 @@ const createOffscreenCanvas = (canvas, width = WIDTH, height = HEIGHT) => {
layerCanvas.height = height; layerCanvas.height = height;
const context = layerCanvas.getContext("2d"); const context = layerCanvas.getContext("2d");
if (!context) throw new Error("Offscreen canvas context unavailable"); if (!context) throw new Error("Offscreen canvas context unavailable");
enableImageSmoothing(context);
return { canvas: layerCanvas, context }; return { canvas: layerCanvas, context };
}; };
@@ -172,6 +188,7 @@ const loadCanvasImage = (canvas, source) =>
const createEnergyEngine = async (canvas) => { const createEnergyEngine = async (canvas) => {
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
if (!context) throw new Error("Canvas unavailable"); if (!context) throw new Error("Canvas unavailable");
enableImageSmoothing(context);
canvas.width = WIDTH; canvas.width = WIDTH;
canvas.height = HEIGHT; canvas.height = HEIGHT;
@@ -190,9 +207,9 @@ const createEnergyEngine = async (canvas) => {
maskPixels.data.fill(255); maskPixels.data.fill(255);
const images = await Promise.all([ const images = await Promise.all([
loadCanvasImage(canvas, `${ASSET_ROOT}/progress-empty.png`), loadCanvasImage(canvas, getAssetSource("progress-empty.png")),
loadCanvasImage(canvas, `${ASSET_ROOT}/progress-full.png`), loadCanvasImage(canvas, getAssetSource("progress-full.png")),
loadCanvasImage(canvas, `${ASSET_ROOT}/progress-glow.png`), loadCanvasImage(canvas, getAssetSource("progress-glow.png")),
]); ]);
const segments = track.slice(1).map((end, index) => { const segments = track.slice(1).map((end, index) => {
@@ -338,11 +355,20 @@ const createEnergyEngine = async (canvas) => {
return; return;
} }
// 真实百分比映射轨道本体,避免 90%~100% 被出口和羽化缓冲吞掉 // 主体百分比映射轨道本体,头尾仅补偿端点羽化,避免整图与蒙版切换时跳变
const distance = (progress / 100) * pathLength; const distance = (progress / 100) * pathLength;
const entryBlend = smoothStep(progress / ENTRY_EDGE_BLEND_PERCENT);
const exitBlend = smoothStep(
(progress - EXIT_EDGE_BLEND_START_PERCENT) /
(100 - EXIT_EDGE_BLEND_START_PERCENT)
);
for (let pixel = 0; pixel < revealStarts.length; pixel += 1) { for (let pixel = 0; pixel < revealStarts.length; pixel += 1) {
const trackAlpha =
smoothStep(
(distance - revealStarts[pixel]) * revealRates[pixel]
) * entryBlend;
maskPixels.data[pixel * 4 + 3] = Math.round( maskPixels.data[pixel * 4 + 3] = Math.round(
255 * smoothStep((distance - revealStarts[pixel]) * revealRates[pixel]) 255 * (trackAlpha + (1 - trackAlpha) * exitBlend)
); );
} }
maskLayer.context.putImageData(maskPixels, 0, 0); maskLayer.context.putImageData(maskPixels, 0, 0);
@@ -361,12 +387,14 @@ const createEnergyEngine = async (canvas) => {
const headBlend = const headBlend =
smoothStep(progress / 10) * (1 - smoothStep((progress - 85) / 15)); smoothStep(progress / 10) * (1 - smoothStep((progress - 85) / 15));
const position = trackPosition(distance - 16); const position = trackPosition(distance - 16);
const headSize = 72 * (0.55 + 0.45 * headBlend); const headSize = Math.round(72 * (0.55 + 0.45 * headBlend));
const headX = Math.round(position.x - headSize / 2);
const headY = Math.round(position.y - headSize / 2);
glowLayer.context.clearRect(0, 0, WIDTH, HEIGHT); glowLayer.context.clearRect(0, 0, WIDTH, HEIGHT);
glowLayer.context.drawImage( glowLayer.context.drawImage(
images[2], images[2],
position.x - headSize / 2, headX,
position.y - headSize / 2, headY,
headSize, headSize,
headSize headSize
); );
@@ -621,6 +649,6 @@ onBeforeUnmount(() => {
} }
.stability-energy-track__image--full { .stability-energy-track__image--full {
transform: translateY(-3rpx); transform: translateY(-2.8rpx);
} }
</style> </style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB