update:优化稳定训练
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user