update:优化稳定训练

This commit is contained in:
2026-09-23 10:35:11 +08:00
parent 9de435a369
commit 9ad2c5e12e
6 changed files with 162 additions and 49 deletions
@@ -14,6 +14,10 @@ const props = defineProps({
type: Number,
default: 0,
},
introPending: {
type: Boolean,
default: false,
},
});
const WIDTH = 750;
@@ -33,30 +37,28 @@ const ENTRY_ANIMATION_DURATION = 1000;
const UPDATE_DURATION = 240;
const SWEEP_DELAY = 100;
const SWEEP_DURATION = 1000;
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 ASSET_ROOT =
"/pages/training/static/stability-energy";
const getAssetSource = (filename) => `${ASSET_ROOT}/${filename}`;
const instance = getCurrentInstance();
const canvasId = `stability-energy-${Math.random().toString(36).slice(2, 10)}`;
const canvasReady = ref(false);
const fallbackVisible = ref(true);
const normalizedPercent = computed(() =>
Math.max(0, Math.min(100, Number(props.percent) || 0))
);
const shouldPlayEntryAnimation = ref(normalizedPercent.value <= 0);
const fallbackImageSource = computed(
() =>
getAssetSource(
shouldPlayEntryAnimation.value ? "progress-full.png" : "progress-empty.png"
)
);
let engine = null;
let disposed = false;
let animationHandle = null;
let renderedPercent = normalizedPercent.value;
let renderedPercent = props.introPending ? 100 : normalizedPercent.value;
let entryAnimationPlaying = false;
let entryAnimationRequested = false;
const showCanvas = () => {
canvasReady.value = true;
};
const requestFrame = (callback) => {
if (typeof requestAnimationFrame === "function") {
@@ -81,6 +83,27 @@ const cancelFrame = () => {
animationHandle = null;
};
const waitForPaint = () =>
new Promise((resolve) => {
const schedule = (callback) => {
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(callback);
} else {
setTimeout(callback, 16);
}
};
schedule(() => schedule(resolve));
});
const presentCanvas = async () => {
showCanvas();
await nextTick();
await waitForPaint();
if (disposed) return false;
fallbackVisible.value = false;
return true;
};
const smoothStep = (value) => {
const amount = Math.max(0, Math.min(1, value));
return amount * amount * (3 - 2 * amount);
@@ -185,14 +208,11 @@ const loadCanvasImage = (canvas, source) =>
image.src = source;
});
const createEnergyEngine = async (canvas) => {
const createEnergyEngine = async (canvas, preloadedFullImage = null) => {
const context = canvas.getContext("2d");
if (!context) throw new Error("Canvas unavailable");
enableImageSmoothing(context);
canvas.width = WIDTH;
canvas.height = HEIGHT;
const maskLayer = createOffscreenCanvas(canvas, MASK_WIDTH, MASK_HEIGHT);
const emptyLayer = createOffscreenCanvas(canvas);
const fullLayer = createOffscreenCanvas(canvas);
@@ -208,7 +228,8 @@ const createEnergyEngine = async (canvas) => {
const images = await Promise.all([
loadCanvasImage(canvas, getAssetSource("progress-empty.png")),
loadCanvasImage(canvas, getAssetSource("progress-full.png")),
preloadedFullImage ||
loadCanvasImage(canvas, getAssetSource("progress-full.png")),
loadCanvasImage(canvas, getAssetSource("progress-glow.png")),
]);
@@ -514,10 +535,11 @@ const drawPercent = (targetPercent, animate = true) => {
const runEntryAnimation = () => {
if (!engine || disposed) return;
cancelFrame();
entryAnimationRequested = false;
entryAnimationPlaying = true;
renderedPercent = 100;
engine.draw(100);
canvasReady.value = true;
showCanvas();
let startedAt = null;
const step = (timestamp) => {
@@ -537,7 +559,6 @@ const runEntryAnimation = () => {
animationHandle = null;
entryAnimationPlaying = false;
shouldPlayEntryAnimation.value = false;
renderedPercent = 0;
engine.draw(0);
@@ -565,36 +586,82 @@ const initialize = async () => {
if (disposed) return;
if (!info?.node?.getContext) {
shouldPlayEntryAnimation.value = false;
return;
}
try {
engine = await createEnergyEngine(info.node);
const canvas = info.node;
canvas.width = WIDTH;
canvas.height = HEIGHT;
const context = canvas.getContext("2d");
if (!context) throw new Error("Canvas unavailable");
enableImageSmoothing(context);
let preloadedFullImage = null;
if (props.introPending) {
preloadedFullImage = await loadCanvasImage(
canvas,
getAssetSource("progress-full.png")
);
if (disposed) return;
context.clearRect(0, 0, WIDTH, HEIGHT);
context.drawImage(
preloadedFullImage,
0,
FULL_TRACK_OFFSET_Y,
WIDTH,
HEIGHT
);
renderedPercent = 100;
if (!(await presentCanvas())) return;
// 先让满能量首帧完成合成,再开始较重的遮罩和轨道计算。
await new Promise((resolve) => setTimeout(resolve, 0));
if (disposed) return;
}
engine = await createEnergyEngine(canvas, preloadedFullImage);
if (disposed) return;
if (shouldPlayEntryAnimation.value) {
runEntryAnimation();
if (props.introPending || entryAnimationRequested) {
renderedPercent = 100;
engine.draw(100);
showCanvas();
if (!props.introPending && entryAnimationRequested) {
runEntryAnimation();
}
return;
}
renderedPercent = normalizedPercent.value;
engine.draw(renderedPercent);
canvasReady.value = true;
await presentCanvas();
} catch (error) {
// Canvas 能力不可用时保留单张兜底图,不影响训练业务。
shouldPlayEntryAnimation.value = false;
console.log("stability energy canvas unavailable", error);
}
};
watch(normalizedPercent, (value) => {
if (!engine) {
// Canvas 初始化前已经恢复出真实能量时,视为重连并跳过入场归零动画。
if (value > 0) shouldPlayEntryAnimation.value = false;
return;
}
if (entryAnimationPlaying) return;
if (!engine || props.introPending || entryAnimationPlaying) return;
drawPercent(value);
});
watch(
() => props.introPending,
(pending, wasPending) => {
if (pending) {
entryAnimationRequested = false;
if (!engine) return;
cancelFrame();
entryAnimationPlaying = false;
renderedPercent = 100;
engine.draw(100);
showCanvas();
return;
}
if (!wasPending) return;
entryAnimationRequested = true;
if (engine) runEntryAnimation();
}
);
onMounted(initialize);
onBeforeUnmount(() => {
@@ -607,20 +674,18 @@ onBeforeUnmount(() => {
<template>
<view class="stability-energy-track" aria-label="稳定训练能量进度">
<image
v-if="!canvasReady"
:class="[
'stability-energy-track__image',
shouldPlayEntryAnimation
? 'stability-energy-track__image--full'
: '',
]"
:src="fallbackImageSource"
mode="aspectFit"
v-if="fallbackVisible"
class="stability-energy-track__image"
:src="getAssetSource('progress-full.png')"
mode="scaleToFill"
/>
<canvas
:id="canvasId"
:canvas-id="canvasId"
class="stability-energy-track__canvas"
:class="[
'stability-energy-track__canvas',
canvasReady ? 'stability-energy-track__canvas--ready' : '',
]"
type="2d"
:width="WIDTH"
:height="HEIGHT"
@@ -636,6 +701,7 @@ onBeforeUnmount(() => {
height: 228rpx;
margin: -164rpx 0 -80rpx;
margin-left: -10rpx;
overflow: hidden;
pointer-events: none;
}
@@ -648,7 +714,17 @@ onBeforeUnmount(() => {
height: 100%;
}
.stability-energy-track__image--full {
transform: translateY(-2.8rpx);
.stability-energy-track__image {
z-index: 0;
transform: translateY(-3rpx);
}
.stability-energy-track__canvas {
z-index: 1;
opacity: 0;
}
.stability-energy-track__canvas--ready {
opacity: 1;
}
</style>