7.11 morning
This commit is contained in:
+3
-1
@@ -2,8 +2,9 @@
|
||||
import { ref } from 'vue';
|
||||
import Game from './pages/Game.vue';
|
||||
import Page1 from './pages/Page1.vue';
|
||||
import assets from './assets';
|
||||
|
||||
const stage = ref(1);
|
||||
const stage = ref(0);
|
||||
|
||||
const userData = ref({
|
||||
region: '奥莱',
|
||||
@@ -18,6 +19,7 @@ function startExploration(payload: { region: string; store: string; username: st
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<link v-for="ani in Object.values(assets.ani)" rel="preload" :href="ani" as="fetch">
|
||||
<main>
|
||||
<Page1 class="page" v-if="stage == 0" @start-exploration="startExploration"/>
|
||||
<Game class="page" v-if="stage == 1" :userdata="userData"/>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -23,6 +23,10 @@ export default {
|
||||
后端效果: new URL('./ani/后段效果.zip', import.meta.url).href,
|
||||
主标出现: new URL('./ani/主标出现.zip', import.meta.url).href,
|
||||
结尾: new URL('./ani/结尾.zip', import.meta.url).href,
|
||||
P1太阳总: new URL('./ani/P1太阳总.zip', import.meta.url).href,
|
||||
点击选择阵营: new URL('./ani/点击选择阵营.zip', import.meta.url).href,
|
||||
//x, y = (484, 782) -> (786, 908)
|
||||
开始探索: new URL('./ani/开始探索.zip', import.meta.url).href,
|
||||
},
|
||||
icons: [
|
||||
[new URL('./icons/icon1.png', import.meta.url).href, new URL('./icons/bigicon1.png', import.meta.url).href],
|
||||
|
||||
+87
-83
@@ -35,13 +35,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
autoPlay: true
|
||||
})
|
||||
|
||||
// 响应式状态
|
||||
// 响应式状态 (不变)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const progress = ref(0)
|
||||
const images = ref<string[]>([]) // 仍然用于临时存储Blob URL
|
||||
// 【优化】: 存储 ImageData 而不是 HTMLImageElement
|
||||
const loadedImages = ref<ImageData[]>([])
|
||||
const images = ref<string[]>([])
|
||||
const loadedImages = ref<HTMLImageElement[]>([])
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const canvasWidth = ref(props.width)
|
||||
const canvasHeight = ref(props.height)
|
||||
@@ -63,17 +62,20 @@ const loadImageSequence = async (zipUrl: string) => {
|
||||
progress.value = 0
|
||||
images.value = []
|
||||
loadedImages.value = []
|
||||
|
||||
console.log(`开始加载图片序列: ${zipUrl}`);
|
||||
|
||||
const response = await fetch(zipUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
111
|
||||
const zipBlob = await response.blob()
|
||||
progress.value = 30
|
||||
console.log(`加载压缩包: ${zipUrl}, 大小: ${(zipBlob.size / 1024).toFixed(2)} KB`);
|
||||
const zip = new JSZip()
|
||||
const zipData = await zip.loadAsync(zipBlob)
|
||||
progress.value = 50
|
||||
console.log(`压缩包加载完成,包含文件: ${Object.keys(zipData.files).length} 个文件`);
|
||||
|
||||
const webpFiles = Object.keys(zipData.files)
|
||||
.filter(name => !zipData.files[name].dir && (name.endsWith('.webp') || name.endsWith('.png') || name.endsWith('.jpg')))
|
||||
@@ -100,7 +102,7 @@ const loadImageSequence = async (zipUrl: string) => {
|
||||
images.value = imageUrls
|
||||
await loadAllImages()
|
||||
loading.value = false
|
||||
if (props.log) console.log(`成功加载并转换 ${loadedImages.value.length} 帧 ImageData`)
|
||||
if (props.log) console.log(`成功加载 ${imageUrls.length} 张图片`)
|
||||
|
||||
if (props.autoPlay) {
|
||||
startAnimation()
|
||||
@@ -113,87 +115,53 @@ const loadImageSequence = async (zipUrl: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 核心优化区域 1: loadAllImages 函数
|
||||
// 将图片转换为 ImageData
|
||||
// =================================================================
|
||||
// loadAllImages 函数 (不变)
|
||||
const loadAllImages = async () => {
|
||||
const imageDataArray: ImageData[] = [];
|
||||
// 创建一个离屏Canvas用于转换
|
||||
const offscreenCanvas = document.createElement('canvas');
|
||||
const offscreenCtx = offscreenCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
if (!offscreenCtx) {
|
||||
throw new Error("无法创建离屏Canvas上下文用于图片转换");
|
||||
}
|
||||
|
||||
const imageElements: HTMLImageElement[] = []
|
||||
for (let i = 0; i < images.value.length; i++) {
|
||||
const img = new Image();
|
||||
img.src = images.value[i];
|
||||
const img = new Image()
|
||||
img.src = images.value[i]
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
// 第一张图片决定了Canvas的尺寸
|
||||
if (i === 0) {
|
||||
canvasWidth.value = props.width || img.width;
|
||||
canvasHeight.value = props.height || img.height;
|
||||
// 设置离屏Canvas的尺寸
|
||||
offscreenCanvas.width = canvasWidth.value;
|
||||
offscreenCanvas.height = canvasHeight.value;
|
||||
canvasWidth.value = props.width || img.width
|
||||
canvasHeight.value = props.height || img.height
|
||||
}
|
||||
|
||||
// 将图片绘制到离屏Canvas上
|
||||
offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
|
||||
offscreenCtx.drawImage(img, 0, 0, offscreenCanvas.width, offscreenCanvas.height);
|
||||
|
||||
// 从离屏Canvas中提取ImageData
|
||||
const imageData = offscreenCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height);
|
||||
imageDataArray.push(imageData);
|
||||
resolve()
|
||||
}
|
||||
img.onerror = reject
|
||||
})
|
||||
|
||||
// 【内存优化】: 转换完成后,立即释放Blob URL,因为它不再需要
|
||||
URL.revokeObjectURL(img.src);
|
||||
|
||||
resolve();
|
||||
};
|
||||
img.onerror = reject;
|
||||
});
|
||||
|
||||
progress.value = 80 + (i + 1) / images.value.length * 20;
|
||||
imageElements.push(img)
|
||||
progress.value = 80 + (i + 1) / images.value.length * 20
|
||||
}
|
||||
loadedImages.value = imageElements
|
||||
}
|
||||
|
||||
loadedImages.value = imageDataArray;
|
||||
// 【内存优化】: 清空临时的URL数组
|
||||
images.value = [];
|
||||
};
|
||||
|
||||
|
||||
// =================================================================
|
||||
// 核心优化区域 2: drawFrame 函数
|
||||
// 使用 putImageData 进行绘制
|
||||
// =================================================================
|
||||
// drawFrame 函数 (不变)
|
||||
const drawFrame = (frameIndex: number) => {
|
||||
if (!canvasRef.value || !loadedImages.value.length) return;
|
||||
const safeIndex = Math.max(0, Math.min(frameIndex, loadedImages.value.length - 1));
|
||||
if (!canvasRef.value || !loadedImages.value.length) return
|
||||
const safeIndex = Math.max(0, Math.min(frameIndex, loadedImages.value.length - 1))
|
||||
|
||||
// if (props.log) {
|
||||
// console.log(`绘制帧: ${safeIndex}`);
|
||||
// }
|
||||
|
||||
const canvas = canvasRef.value;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// 清空画布
|
||||
// ctx.clearRect(0, 0, canvas.width, canvas.height); // putImageData会完全覆盖,所以clearRect可以省略
|
||||
|
||||
const imageData = loadedImages.value[safeIndex];
|
||||
if (imageData) {
|
||||
// 使用putImageData直接绘制像素数据,性能更高
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
if (props.log) {
|
||||
// console.log(`绘制帧: ${safeIndex}, 总帧数: ${loadedImages.value.length}`);
|
||||
}
|
||||
};
|
||||
|
||||
// jumpToRule 函数 (不变)
|
||||
const canvas = canvasRef.value
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
const img = loadedImages.value[safeIndex]
|
||||
if (img) {
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 核心修复区域 1: jumpToRule 函数
|
||||
// =================================================================
|
||||
const jumpToRule = (ruleName: string) => {
|
||||
const ruleIndex = findRuleIndex(ruleName)
|
||||
if (ruleIndex === -1) {
|
||||
@@ -217,6 +185,7 @@ const jumpToRule = (ruleName: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 【修复】: 如果规则是反向的,则从结束帧开始播放
|
||||
const endFrame = targetRule.endFrame ?? (startFrame + targetRule.frame - 1);
|
||||
currentFrame.value = targetRule.reverse ? endFrame : startFrame;
|
||||
|
||||
@@ -235,7 +204,9 @@ const jumpToRule = (ruleName: string) => {
|
||||
return true
|
||||
}
|
||||
|
||||
// startAnimation 函数 (不变)
|
||||
// =================================================================
|
||||
// 核心修复区域 2: startAnimation 函数
|
||||
// =================================================================
|
||||
const startAnimation = () => {
|
||||
if (!props.rules.length || !loadedImages.value.length || isPlaying.value) return
|
||||
|
||||
@@ -254,6 +225,7 @@ const startAnimation = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 【修复】: 如果规则是反向的,则从结束帧开始播放
|
||||
const endFrame = currentRule.endFrame ?? (startFrame + currentRule.frame - 1);
|
||||
currentFrame.value = currentRule.reverse ? endFrame : startFrame;
|
||||
|
||||
@@ -263,7 +235,9 @@ const startAnimation = () => {
|
||||
animate()
|
||||
}
|
||||
|
||||
// animate 函数 (不变)
|
||||
// =================================================================
|
||||
// 核心修复区域 3: animate 函数
|
||||
// =================================================================
|
||||
const animate = () => {
|
||||
if (!isPlaying.value || currentRuleIndex.value >= props.rules.length) {
|
||||
animationId.value = undefined;
|
||||
@@ -274,11 +248,12 @@ const animate = () => {
|
||||
|
||||
const now = performance.now();
|
||||
const currentRule = props.rules[currentRuleIndex.value];
|
||||
const frameDuration = currentRule.duration ?? 33;
|
||||
const frameDuration = currentRule.duration ?? 33; // 默认约30fps
|
||||
|
||||
if (now - lastFrameTime.value >= frameDuration) {
|
||||
lastFrameTime.value = now - (now - lastFrameTime.value) % frameDuration;
|
||||
|
||||
// 1. 计算当前规则的帧范围
|
||||
let startFrame = currentRule.startFrame ?? 0;
|
||||
if (currentRule.startFrame === undefined) {
|
||||
for (let i = 0; i < currentRuleIndex.value; i++) {
|
||||
@@ -286,53 +261,79 @@ const animate = () => {
|
||||
}
|
||||
}
|
||||
const endFrame = currentRule.endFrame ?? (startFrame + currentRule.frame - 1);
|
||||
|
||||
// 2. 计算下一帧
|
||||
let nextFrame = currentFrame.value + (currentRule.reverse ? -1 : 1);
|
||||
|
||||
// 3. 检查是否超出当前规则的边界
|
||||
const isEndOfRuleSegment = currentRule.reverse ? (nextFrame < startFrame) : (nextFrame > endFrame);
|
||||
|
||||
if (isEndOfRuleSegment) {
|
||||
// 到达边界,完成了一轮循环
|
||||
currentLoopCount.value++;
|
||||
|
||||
const isRuleFinished = currentRule.loop > 0 && currentLoopCount.value >= currentRule.loop;
|
||||
|
||||
if (isRuleFinished) {
|
||||
// 规则已完成,准备过渡到下一条
|
||||
if (pendingJumpTo.value) {
|
||||
const targetRule = pendingJumpTo.value;
|
||||
pendingJumpTo.value = undefined;
|
||||
jumpToRule(targetRule);
|
||||
return;
|
||||
return; // jumpToRule会启动新的animate, 故此处直接返回
|
||||
}
|
||||
|
||||
if (currentRule.pauseAfter) {
|
||||
isPaused.value = true;
|
||||
isPlaying.value = false;
|
||||
if(props.log) console.log(`规则 "${currentRule.name}" 完成,暂停等待跳转指令`);
|
||||
// 定格在当前规则的最后一帧
|
||||
drawFrame(currentRule.reverse ? startFrame : endFrame);
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动进入下一个规则
|
||||
currentRuleIndex.value++;
|
||||
currentLoopCount.value = 0;
|
||||
|
||||
if (currentRuleIndex.value >= props.rules.length) {
|
||||
// 所有规则播放完毕
|
||||
isPlaying.value = false;
|
||||
if(props.log) console.log('所有规则播放完毕');
|
||||
// 定格在最后一个规则的最后一帧
|
||||
drawFrame(currentRule.reverse ? startFrame : endFrame);
|
||||
return;
|
||||
}
|
||||
|
||||
// 【修复】: 为下一个规则设置正确的起始帧
|
||||
const nextRule = props.rules[currentRuleIndex.value];
|
||||
let nextRuleStartFrame = nextRule.startFrame ?? 0;
|
||||
if (nextRule.startFrame === undefined) {
|
||||
// 使用刚结束的规则的结束帧的下一帧作为起点
|
||||
nextRuleStartFrame = endFrame + 1;
|
||||
}
|
||||
const nextRuleEndFrame = nextRule.endFrame ?? (nextRuleStartFrame + nextRule.frame - 1);
|
||||
|
||||
// 根据新规则的 reverse 属性设置 currentFrame
|
||||
currentFrame.value = nextRule.reverse ? nextRuleEndFrame : nextRuleStartFrame;
|
||||
if(props.log) console.log(`进入下一规则 "${nextRule.name}", 实际开始帧: ${currentFrame.value}`);
|
||||
|
||||
} else {
|
||||
// 规则未完成(无限循环或循环次数未到),循环播放
|
||||
// 【修复】: 重置循环时,也需要根据 reverse 属性设置正确的帧
|
||||
currentFrame.value = currentRule.reverse ? endFrame : startFrame;
|
||||
}
|
||||
} else {
|
||||
// 仍在当前规则内,正常播放
|
||||
currentFrame.value = nextFrame;
|
||||
}
|
||||
|
||||
// 4. 绘制计算出的最终帧
|
||||
drawFrame(currentFrame.value);
|
||||
}
|
||||
};
|
||||
|
||||
// stopAnimation, resetAnimation, etc. (不变)
|
||||
// stopAnimation 函数 (不变)
|
||||
const stopAnimation = () => {
|
||||
isPlaying.value = false
|
||||
isPaused.value = false
|
||||
@@ -342,6 +343,7 @@ const stopAnimation = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// resetAnimation 函数 (不变)
|
||||
const resetAnimation = () => {
|
||||
stopAnimation()
|
||||
currentRuleIndex.value = 0
|
||||
@@ -353,6 +355,7 @@ const resetAnimation = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// togglePlayback, setJumpTarget, jumpToRuleImmediately, resumeFromPause (不变)
|
||||
const togglePlayback = () => {
|
||||
if (isPlaying.value) {
|
||||
stopAnimation()
|
||||
@@ -382,12 +385,13 @@ const resumeFromPause = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// watch 和 onMounted (清理逻辑现在更加健壮)
|
||||
|
||||
// watch 和 onMounted (微调清理逻辑)
|
||||
watch(() => props.url, (newUrl) => {
|
||||
if (newUrl) {
|
||||
stopAnimation()
|
||||
// 由于Blob URL在加载时已释放,这里主要是清空状态
|
||||
images.value.forEach(url => URL.revokeObjectURL(url)); // 以防加载中途切换URL
|
||||
// 清理旧的图片数据
|
||||
images.value.forEach(url => URL.revokeObjectURL(url));
|
||||
images.value = [];
|
||||
loadedImages.value = [];
|
||||
loadImageSequence(newUrl)
|
||||
@@ -405,7 +409,7 @@ watch(() => props.rules, () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAnimation();
|
||||
// 由于images数组在加载成功后会被清空,此处的forEach可能为空操作,但作为安全保障是好的。
|
||||
// 组件卸载时,确保释放所有Blob URL
|
||||
images.value.forEach(url => URL.revokeObjectURL(url));
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
|
||||
<!-- AniLoader.vue -->
|
||||
<template>
|
||||
<!-- This component is renderless; it just provides context -->
|
||||
<slot :is-all-loaded="isAllLoaded" :progress="overallProgress"></slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, provide, onMounted, computed, watch } from 'vue'
|
||||
import JSZip from 'jszip'
|
||||
import { animationStore, AnimationStoreKey, type AnimationData } from './animation-store'
|
||||
|
||||
interface Asset {
|
||||
id: string; // A unique identifier for the animation, e.g., 'character-jump'
|
||||
url: string; // The URL to the .zip file
|
||||
}
|
||||
|
||||
interface Props {
|
||||
assets: Asset[];
|
||||
autoLoad?: boolean; // Start loading immediately on mount
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autoLoad: true,
|
||||
})
|
||||
|
||||
const totalAssets = ref(props.assets.length);
|
||||
const loadedAssetsCount = ref(0);
|
||||
|
||||
// Provide the reactive store to all descendant components
|
||||
provide(AnimationStoreKey, animationStore)
|
||||
|
||||
const isAllLoaded = computed(() => {
|
||||
if (totalAssets.value === 0) return true;
|
||||
// Check if every asset in the store that should be loaded is actually loaded.
|
||||
return props.assets.every(asset => animationStore.get(asset.id)?.status === 'loaded');
|
||||
});
|
||||
|
||||
const overallProgress = computed(() => {
|
||||
if (totalAssets.value === 0) return 100;
|
||||
let totalProgress = 0;
|
||||
for (const asset of props.assets) {
|
||||
totalProgress += animationStore.get(asset.id)?.progress ?? 0;
|
||||
}
|
||||
return totalProgress / totalAssets.value;
|
||||
});
|
||||
|
||||
|
||||
const loadAndProcessAnimation = async (id: string, url: string) => {
|
||||
// Prevent re-loading if already loaded or loading
|
||||
if (animationStore.has(id) && animationStore.get(id)?.status !== 'error') {
|
||||
if(animationStore.get(id)?.status === 'loaded') {
|
||||
loadedAssetsCount.value++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Set initial state in the store
|
||||
animationStore.set(id, {
|
||||
status: 'loading',
|
||||
frames: [],
|
||||
width: 0,
|
||||
height: 0,
|
||||
totalFrames: 0,
|
||||
progress: 0,
|
||||
});
|
||||
|
||||
const updateProgress = (p: number) => {
|
||||
const data = animationStore.get(id);
|
||||
if (data) data.progress = p;
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
updateProgress(15);
|
||||
|
||||
const zipBlob = await response.blob()
|
||||
const zip = new JSZip()
|
||||
const zipData = await zip.loadAsync(zipBlob)
|
||||
updateProgress(30);
|
||||
|
||||
const imageFiles = Object.keys(zipData.files)
|
||||
.filter(name => !zipData.files[name].dir && /\.(webp|png|jpg)$/i.test(name))
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.match(/(\d+)\.\w+$/)?.[1] || '0')
|
||||
const numB = parseInt(b.match(/(\d+)\.\w+$/)?.[1] || '0')
|
||||
return numA - numB
|
||||
})
|
||||
|
||||
if (imageFiles.length === 0) throw new Error('No supported image files found in ZIP.')
|
||||
|
||||
const blobUrls = await Promise.all(
|
||||
imageFiles.map(name => zipData.files[name].async('blob').then(URL.createObjectURL))
|
||||
);
|
||||
updateProgress(50);
|
||||
|
||||
const imageDataArray: ImageData[] = [];
|
||||
const offscreenCanvas = document.createElement('canvas');
|
||||
const offscreenCtx = offscreenCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!offscreenCtx) throw new Error("Failed to create offscreen canvas context.");
|
||||
|
||||
let canvasWidth = 0, canvasHeight = 0;
|
||||
|
||||
for (let i = 0; i < blobUrls.length; i++) {
|
||||
const img = new Image();
|
||||
img.src = blobUrls[i];
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
if (i === 0) {
|
||||
canvasWidth = img.width;
|
||||
canvasHeight = img.height;
|
||||
offscreenCanvas.width = canvasWidth;
|
||||
offscreenCanvas.height = canvasHeight;
|
||||
}
|
||||
offscreenCtx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
offscreenCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
|
||||
imageDataArray.push(offscreenCtx.getImageData(0, 0, canvasWidth, canvasHeight));
|
||||
URL.revokeObjectURL(img.src);
|
||||
resolve();
|
||||
};
|
||||
img.onerror = reject;
|
||||
});
|
||||
updateProgress(50 + (i + 1) / blobUrls.length * 50);
|
||||
}
|
||||
|
||||
// Update store with final data
|
||||
animationStore.set(id, {
|
||||
status: 'loaded',
|
||||
frames: imageDataArray,
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
totalFrames: imageDataArray.length,
|
||||
progress: 100
|
||||
});
|
||||
loadedAssetsCount.value++;
|
||||
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown loading error';
|
||||
console.error(`Failed to load animation "${id}":`, err);
|
||||
animationStore.set(id, {
|
||||
status: 'error',
|
||||
error: message,
|
||||
frames: [], width: 0, height: 0, totalFrames: 0, progress: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const loadAll = () => {
|
||||
props.assets.forEach(asset => loadAndProcessAnimation(asset.id, asset.url));
|
||||
}
|
||||
|
||||
// Watch for changes in assets prop to dynamically load new ones
|
||||
watch(() => props.assets, (newAssets) => {
|
||||
totalAssets.value = newAssets.length;
|
||||
if (props.autoLoad) {
|
||||
loadAll();
|
||||
}
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
|
||||
// Expose the loader function for manual triggering
|
||||
defineExpose({
|
||||
loadAll,
|
||||
isAllLoaded,
|
||||
animationStore
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,302 @@
|
||||
<!-- AniPlayer.vue -->
|
||||
<template>
|
||||
<div class="ani-ele">
|
||||
<div v-if="loading">Loading Animation...</div>
|
||||
<div v-else-if="error">{{ error }}</div>
|
||||
<canvas v-else ref="canvasRef" :width="canvasWidth" :height="canvasHeight"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onBeforeUnmount, inject, computed, nextTick } from 'vue'
|
||||
import { AnimationStoreKey } from './animation-store'
|
||||
|
||||
// Rules interface remains the same
|
||||
interface AnimationRule {
|
||||
name: string; frame: number; duration?: number; loop: number;
|
||||
startFrame?: number; endFrame?: number; reverse?: boolean;
|
||||
pauseAfter?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
animationId: string; // The ID to look up in the store
|
||||
rules: AnimationRule[];
|
||||
width?: number; // Optional override for width
|
||||
height?: number; // Optional override for height
|
||||
autoPlay?: boolean;
|
||||
log?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autoPlay: true,
|
||||
});
|
||||
|
||||
// Inject the central store
|
||||
const animationStore = inject(AnimationStoreKey);
|
||||
if (!animationStore) {
|
||||
throw new Error("AniPlayer must be used within an AniLoader provider.");
|
||||
}
|
||||
|
||||
// --- Reactive state derived from the store ---
|
||||
const animationData = computed(() => animationStore.get(props.animationId));
|
||||
|
||||
const loading = computed(() => !animationData.value || animationData.value.status === 'loading');
|
||||
const error = computed(() => animationData.value?.status === 'error' ? animationData.value.error : '');
|
||||
const loadedImages = computed(() => animationData.value?.frames ?? []);
|
||||
|
||||
// Canvas dimensions are derived from loaded data but can be overridden by props
|
||||
const canvasWidth = computed(() => props.width ?? animationData.value?.width ?? 300);
|
||||
const canvasHeight = computed(() => props.height ?? animationData.value?.height ?? 150);
|
||||
|
||||
|
||||
// --- Playback State (mostly unchanged) ---
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const isPlaying = ref(false)
|
||||
const isPaused = ref(false)
|
||||
const currentRuleIndex = ref(0)
|
||||
const currentFrame = ref(0)
|
||||
const currentLoopCount = ref(0)
|
||||
const animationId = ref<number>()
|
||||
const lastFrameTime = ref(0)
|
||||
const pendingJumpTo = ref<string>()
|
||||
|
||||
// --- Core Logic (unchanged, but now operates on computed refs) ---
|
||||
|
||||
const drawFrame = (frameIndex: number) => {
|
||||
if (!canvasRef.value || loadedImages.value.length === 0) return;
|
||||
const safeIndex = Math.max(0, Math.min(frameIndex, loadedImages.value.length - 1));
|
||||
const ctx = canvasRef.value.getContext('2d');
|
||||
if (!ctx) return;
|
||||
const imageData = loadedImages.value[safeIndex];
|
||||
if (imageData) {
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const jumpToRule = (ruleName: string) => { /* ... (code is identical to previous version) ... */
|
||||
const ruleIndex = findRuleIndex(ruleName)
|
||||
if (ruleIndex === -1) {
|
||||
console.warn(`未找到名为 "${ruleName}" 的规则`)
|
||||
return false
|
||||
}
|
||||
if (props.log) console.log(`跳转到规则 "${ruleName}" (索引: ${ruleIndex})`);
|
||||
|
||||
if (isPlaying.value) {
|
||||
stopAnimation()
|
||||
}
|
||||
|
||||
currentRuleIndex.value = ruleIndex
|
||||
currentLoopCount.value = 0
|
||||
const targetRule = props.rules[ruleIndex]
|
||||
|
||||
let startFrame = targetRule.startFrame ?? 0
|
||||
if (targetRule.startFrame === undefined) {
|
||||
for (let i = 0; i < ruleIndex; i++) {
|
||||
startFrame += props.rules[i].frame
|
||||
}
|
||||
}
|
||||
|
||||
const endFrame = targetRule.endFrame ?? (startFrame + targetRule.frame - 1);
|
||||
currentFrame.value = targetRule.reverse ? endFrame : startFrame;
|
||||
|
||||
if (props.log) {
|
||||
console.log(`跳转到规则 "${ruleName}", 实际开始帧: ${currentFrame.value}, 规则帧范围: ${startFrame} - ${endFrame}`);
|
||||
}
|
||||
|
||||
drawFrame(currentFrame.value)
|
||||
isPaused.value = false
|
||||
pendingJumpTo.value = undefined
|
||||
|
||||
isPlaying.value = true
|
||||
lastFrameTime.value = performance.now()
|
||||
animate()
|
||||
|
||||
return true
|
||||
};
|
||||
|
||||
const startAnimation = () => { /* ... (code is identical to previous version) ... */
|
||||
if (!props.rules.length || loadedImages.value.length === 0 || isPlaying.value) return
|
||||
|
||||
isPlaying.value = true
|
||||
isPaused.value = false
|
||||
|
||||
if (currentRuleIndex.value >= props.rules.length) {
|
||||
resetAnimation()
|
||||
}
|
||||
|
||||
const currentRule = props.rules[currentRuleIndex.value]
|
||||
let startFrame = currentRule.startFrame ?? 0
|
||||
if (currentRule.startFrame === undefined) {
|
||||
for (let i = 0; i < currentRuleIndex.value; i++) {
|
||||
startFrame += props.rules[i].frame
|
||||
}
|
||||
}
|
||||
|
||||
const endFrame = currentRule.endFrame ?? (startFrame + currentRule.frame - 1);
|
||||
currentFrame.value = currentRule.reverse ? endFrame : startFrame;
|
||||
|
||||
if (props.log) console.log(`开始播放规则 "${currentRule.name}", 实际开始帧: ${currentFrame.value}`);
|
||||
|
||||
lastFrameTime.value = performance.now()
|
||||
animate()
|
||||
};
|
||||
|
||||
const animate = () => { /* ... (code is identical to previous version) ... */
|
||||
if (!isPlaying.value || currentRuleIndex.value >= props.rules.length) {
|
||||
animationId.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
animationId.value = requestAnimationFrame(animate);
|
||||
|
||||
const now = performance.now();
|
||||
const currentRule = props.rules[currentRuleIndex.value];
|
||||
const frameDuration = currentRule.duration ?? 33;
|
||||
|
||||
if (now - lastFrameTime.value >= frameDuration) {
|
||||
lastFrameTime.value = now - (now - lastFrameTime.value) % frameDuration;
|
||||
|
||||
let startFrame = currentRule.startFrame ?? 0;
|
||||
if (currentRule.startFrame === undefined) {
|
||||
for (let i = 0; i < currentRuleIndex.value; i++) {
|
||||
startFrame += props.rules[i].frame;
|
||||
}
|
||||
}
|
||||
const endFrame = currentRule.endFrame ?? (startFrame + currentRule.frame - 1);
|
||||
let nextFrame = currentFrame.value + (currentRule.reverse ? -1 : 1);
|
||||
const isEndOfRuleSegment = currentRule.reverse ? (nextFrame < startFrame) : (nextFrame > endFrame);
|
||||
|
||||
if (isEndOfRuleSegment) {
|
||||
currentLoopCount.value++;
|
||||
const isRuleFinished = currentRule.loop > 0 && currentLoopCount.value >= currentRule.loop;
|
||||
if (isRuleFinished) {
|
||||
if (pendingJumpTo.value) {
|
||||
const targetRule = pendingJumpTo.value;
|
||||
pendingJumpTo.value = undefined;
|
||||
jumpToRule(targetRule);
|
||||
return;
|
||||
}
|
||||
if (currentRule.pauseAfter) {
|
||||
isPaused.value = true;
|
||||
isPlaying.value = false;
|
||||
if (props.log) console.log(`规则 "${currentRule.name}" 完成,暂停等待跳转指令`);
|
||||
drawFrame(currentRule.reverse ? startFrame : endFrame);
|
||||
return;
|
||||
}
|
||||
currentRuleIndex.value++;
|
||||
currentLoopCount.value = 0;
|
||||
if (currentRuleIndex.value >= props.rules.length) {
|
||||
isPlaying.value = false;
|
||||
if (props.log) console.log('所有规则播放完毕');
|
||||
drawFrame(currentRule.reverse ? startFrame : endFrame);
|
||||
return;
|
||||
}
|
||||
const nextRule = props.rules[currentRuleIndex.value];
|
||||
let nextRuleStartFrame = nextRule.startFrame ?? 0;
|
||||
if (nextRule.startFrame === undefined) {
|
||||
nextRuleStartFrame = endFrame + 1;
|
||||
}
|
||||
const nextRuleEndFrame = nextRule.endFrame ?? (nextRuleStartFrame + nextRule.frame - 1);
|
||||
currentFrame.value = nextRule.reverse ? nextRuleEndFrame : nextRuleStartFrame;
|
||||
if (props.log) console.log(`进入下一规则 "${nextRule.name}", 实际开始帧: ${currentFrame.value}`);
|
||||
} else {
|
||||
currentFrame.value = currentRule.reverse ? endFrame : startFrame;
|
||||
}
|
||||
} else {
|
||||
currentFrame.value = nextFrame;
|
||||
}
|
||||
drawFrame(currentFrame.value);
|
||||
}
|
||||
};
|
||||
|
||||
const stopAnimation = () => { /* ... (code is identical) ... */
|
||||
isPlaying.value = false
|
||||
isPaused.value = false
|
||||
if (animationId.value) {
|
||||
cancelAnimationFrame(animationId.value)
|
||||
animationId.value = undefined
|
||||
}
|
||||
};
|
||||
const resetAnimation = () => { /* ... (code is identical) ... */
|
||||
stopAnimation()
|
||||
currentRuleIndex.value = 0
|
||||
currentFrame.value = 0
|
||||
currentLoopCount.value = 0
|
||||
pendingJumpTo.value = undefined
|
||||
if (loadedImages.value.length > 0) {
|
||||
drawFrame(0)
|
||||
}
|
||||
};
|
||||
const findRuleIndex = (ruleName: string): number => { /* ... (code is identical) ... */
|
||||
return props.rules.findIndex(rule => rule.name === ruleName)
|
||||
};
|
||||
// Other exposed methods...
|
||||
const togglePlayback = () => { if (isPlaying.value) { stopAnimation() } else { startAnimation() } };
|
||||
const setJumpTarget = (ruleName: string) => { if (isPaused.value) { jumpToRule(ruleName) } else { pendingJumpTo.value = ruleName } };
|
||||
const jumpToRuleImmediately = (ruleName: string) => { return jumpToRule(ruleName) };
|
||||
const resumeFromPause = () => { if (isPaused.value) { isPaused.value = false; isPlaying.value = true; lastFrameTime.value = performance.now(); animate() } };
|
||||
|
||||
|
||||
// Watch for the data to become available to start the animation
|
||||
watch(loadedImages, (newFrames, oldFrames) => {
|
||||
// Only trigger if we're transitioning from 'no frames' to 'having frames'
|
||||
if (newFrames.length > 0 && oldFrames.length === 0) {
|
||||
resetAnimation();
|
||||
// Use nextTick to ensure canvas is rendered before drawing
|
||||
nextTick(() => {
|
||||
drawFrame(0); // Draw the first frame immediately
|
||||
if (props.autoPlay) {
|
||||
startAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for rules changing
|
||||
watch(() => props.rules, () => {
|
||||
if (loadedImages.value.length > 0) {
|
||||
resetAnimation();
|
||||
if (props.autoPlay) {
|
||||
startAnimation();
|
||||
}
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAnimation();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
// Expose control methods
|
||||
play: startAnimation,
|
||||
pause: stopAnimation,
|
||||
reset: resetAnimation,
|
||||
toggle: togglePlayback,
|
||||
resume: resumeFromPause,
|
||||
jumpTo: jumpToRuleImmediately,
|
||||
setJumpTarget,
|
||||
drawFrame,
|
||||
// Expose state
|
||||
isPlaying,
|
||||
isPaused,
|
||||
currentFrame,
|
||||
getCurrentRule: () => props.rules[currentRuleIndex.value],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ani-ele {
|
||||
/* Add styling for loading/error states if desired */
|
||||
min-height: 100px;
|
||||
/* Example */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
// src/composables/animation-store.ts (or wherever you prefer)
|
||||
import { reactive } from 'vue'
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
// Defines the data structure for a single loaded animation
|
||||
export interface AnimationData {
|
||||
status: 'idle' | 'loading' | 'loaded' | 'error';
|
||||
frames: ImageData[];
|
||||
width: number;
|
||||
height: number;
|
||||
totalFrames: number;
|
||||
error?: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
// Defines the store, which is a map from a unique animation ID to its data
|
||||
export type AnimationStore = Map<string, AnimationData>
|
||||
|
||||
// Create a reactive store instance
|
||||
export const animationStore = reactive<AnimationStore>(new Map())
|
||||
|
||||
// Create a unique Symbol as the injection key. This is best practice.
|
||||
export const AnimationStoreKey: InjectionKey<AnimationStore> = Symbol('AnimationStore')
|
||||
+28
-5
@@ -17,6 +17,7 @@ const sunEndEle = useTemplateRef('sun-ani-end');
|
||||
|
||||
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
|
||||
async function shoot() {
|
||||
//sunEle.value?.jumpTo('天上飞')
|
||||
sunEle.value?.jumpTo('蓄力飞')
|
||||
@@ -126,9 +127,9 @@ function gameLoop(currentTime = 0) {
|
||||
giftPos.value = [null, null];
|
||||
giftShow.value = false;
|
||||
lastTime = currentTime;
|
||||
if (giftProgress.value >= giftList.length || 1) {
|
||||
if (giftProgress.value >= giftList.length ) {
|
||||
// 游戏结束逻辑
|
||||
console.log('游戏结束!');
|
||||
console.log('游戏结束!耗时:', timeSpent.value / 1000, '秒');
|
||||
gameEnd()
|
||||
return;
|
||||
}
|
||||
@@ -546,6 +547,7 @@ const leaderBoard = ref([
|
||||
])
|
||||
|
||||
const fmtTime = (time: number) => {
|
||||
time = Math.round(time);
|
||||
// 将时间格式化为 mm:ss
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = time % 60;
|
||||
@@ -609,7 +611,7 @@ async function finishCollect() {
|
||||
|
||||
<AniEle :url="assets.ani.下拉蓄力提示" ref="aniEle" :height="334" :width="337" :rules="[
|
||||
{ name: '蓄力', frame: 241, loop: 2 },
|
||||
]" class="abs arrow" style="bottom: 7%;width: 30vw;left: 35%;" @pointerdown="shoot" />
|
||||
]" class="abs arrow" style="bottom: 7%;width: 30vw;left: 35%;" @pointerdown.once="shoot" />
|
||||
|
||||
|
||||
<img class="cloud" src="../assets/game/云.png" v-for="c in [
|
||||
@@ -712,7 +714,7 @@ async function finishCollect() {
|
||||
]" style="width: 100%; height: auto;" />
|
||||
</div>
|
||||
|
||||
<img src="../assets/game/完成收集.png" alt="" v-if="showScore" class="finish-collect abs" @click="finishCollect"
|
||||
<img src="../assets/game/完成收集.png" alt="" v-if="showScore" class="finish-collect abs" @click.once="finishCollect"
|
||||
style="bottom: 8%;left: 33%;width: 34vw;animation: scale-in 0.3s ease-out;">
|
||||
|
||||
|
||||
@@ -736,6 +738,27 @@ async function finishCollect() {
|
||||
<img src="../assets/game/再玩一次.png" alt="" class="abs"
|
||||
style="width: 34vw;bottom: -19%;left: 51%;animation: last-btn-in 0.3s ease-out forwards; animation-delay: 200ms;">
|
||||
|
||||
<div class="time-spent abs" style="left: 10%; top:44%;width: 24%; display: flex;
|
||||
align-items: center; flex-direction: column;">
|
||||
<div class="line-1"
|
||||
style="display: flex; align-items: center; gap: 1vw; animation: line-in 0.5s ease-out 0.6s forwards;transform: translateX(-210%);;">
|
||||
<div class="username" style="font-size: 2.9vw;">{{ userdata.username }}</div>
|
||||
<div class="cost-info"
|
||||
style="font-size: 2.9vw; color: white; display: flex;align-items: center;justify-content: center;background-color: black;padding: 0 1.2vw; height: 4.5vw; border-radius: 4vw;">
|
||||
用时</div>
|
||||
</div>
|
||||
|
||||
<div class="time"
|
||||
style="font-size: 9vw;animation: line-in 0.5s ease-out 0.8s forwards;transform: translateX(-210%);">
|
||||
{{ fmtTime(timeSpent/1000) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="region-area abs" style="left: 10%; top:30%;width: 24%;height: 10%;gap:2vw; display: flex;animation: line-in 0.5s ease-out 0.4s forwards;transform: translateX(-210%);
|
||||
align-items: center; flex-direction: column;justify-content: center;">
|
||||
<div class="region" style="width: 20vw; background-color: white;font-size: 3vw;text-align: center;height: 4.5vw;line-height: 4.5vw; border-radius: 4.5vw;">{{ userdata.region }}</div>
|
||||
<div class="store" style="font-size: 2.8vw; color: gray;">{{ userdata.store }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -812,7 +835,7 @@ async function finishCollect() {
|
||||
|
||||
@keyframes line-in {
|
||||
from {
|
||||
transform: translateX(-150%);
|
||||
transform: translateX(-210%);
|
||||
}
|
||||
|
||||
to {
|
||||
|
||||
+53
-90
@@ -6,7 +6,7 @@ import { regions, type RegionData, type Store } from '../data';
|
||||
import AniEle from '../components/AniEle.vue';
|
||||
|
||||
const regionsPos = [
|
||||
{ w: 68.8, h: 68.8, l: -1, t: -4, src: assets.p1.大北区, tt: '大北区', trt: -13, ad: 6, key: '大北区' },
|
||||
{ w: 68.8, h: 68.8, l: -1, t: 0, src: assets.p1.大北区, tt: '大北区', trt: -13, ad: 6, key: '大北区' },
|
||||
{ w: 73.3, h: 73.3, l: 44, t: 20, src: assets.p1.大东区, tt: '大东区', ad: 3, key: '大东区' },
|
||||
{ w: 62.3, h: 63.7, l: -32, t: 72, src: assets.p1['大南区&免税'], tt: '大南区&免税', trt: 5, ad: 3, key: '大南区&免税' },
|
||||
{ w: 59.6, h: 59.6, l: -37, t: 15, src: assets.p1.大西区, tt: '大西区', ad: 5, key: '大西区' },
|
||||
@@ -112,6 +112,7 @@ function startExploration() {
|
||||
alert('请输入你的名字!');
|
||||
return;
|
||||
}
|
||||
startExplorationRules.value[0].reverse = true;
|
||||
emit('startExploration', {
|
||||
region: selectedRegion.value,
|
||||
store: selectedStores.value[selectedStoreIndex.value].店铺,
|
||||
@@ -122,7 +123,6 @@ function startExploration() {
|
||||
// 动画管理
|
||||
let fadeInAnimations: Animation[] = [];
|
||||
let swapInAnimations: Animation[] = [];
|
||||
let scaleInAnimations: Animation[] = [];
|
||||
let bubbleDropAnimations: Animation[] = [];
|
||||
|
||||
// 保存所有动画的数组
|
||||
@@ -160,38 +160,6 @@ function createSwapInAnimation(element: Element, delay: number = 0) {
|
||||
return animation;
|
||||
}
|
||||
|
||||
// Scale in 动画(容器)
|
||||
function createScaleInContainerAnimation(element: Element, delay: number = 0) {
|
||||
const animation = element.animate([
|
||||
{ transform: 'scale(0)' },
|
||||
{ transform: 'scale(1)' }
|
||||
], {
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.5, 0, 0.71, 1.85)',
|
||||
delay: (delay + 0.2) * 1000,
|
||||
fill: 'forwards'
|
||||
});
|
||||
scaleInAnimations.push(animation);
|
||||
allAnimations.push(animation);
|
||||
return animation;
|
||||
}
|
||||
|
||||
// Scale in 动画(文本)
|
||||
function createScaleInTextAnimation(element: Element, delay: number = 0) {
|
||||
const animation = element.animate([
|
||||
{ transform: 'scale(0)' },
|
||||
{ transform: 'scale(1)' }
|
||||
], {
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.5, 0, 0.71, 1.5)',
|
||||
delay: (delay + 0.2 + 0.1) * 1000,
|
||||
fill: 'forwards'
|
||||
});
|
||||
scaleInAnimations.push(animation);
|
||||
allAnimations.push(animation);
|
||||
return animation;
|
||||
}
|
||||
|
||||
// 气泡下降动画
|
||||
function createBubbleDropAnimation(element: Element, startTop: string, startLeft: string, endTop: string, endLeft: string, delay: number = 0, duration: number = 600) {
|
||||
const animation = element.animate([
|
||||
@@ -216,15 +184,18 @@ function createBubbleDropAnimation(element: Element, startTop: string, startLeft
|
||||
return animation;
|
||||
}
|
||||
|
||||
const mainLogo = useTemplateRef('main-logo');
|
||||
|
||||
const mainLogoRules = ref([
|
||||
{ name: 'main', frame: 41, loop: 1, pauseAfter: true, duration: 33, reverse: false }
|
||||
]);
|
||||
const selectInfoRules = ref([{ name: 'main', frame: 26, loop: 1, duration: 33, reverse: false }]);
|
||||
const startExplorationRules = ref([{ name: 'start', frame: 19, loop: 1, duration: 33, reverse: false }]);
|
||||
|
||||
|
||||
// 反向播放所有动画来淡出
|
||||
function reverseAllAnimations() {
|
||||
mainLogoRules.value[0].reverse = true;
|
||||
|
||||
selectInfoRules.value[0].reverse = true;
|
||||
|
||||
allAnimations.forEach(animation => {
|
||||
// 确保动画已经完成
|
||||
if (animation.playState === 'finished') {
|
||||
@@ -252,9 +223,13 @@ function fadeOutAllAnimations() {
|
||||
(window as any).reverseAllAnimations = reverseAllAnimations;
|
||||
(window as any).fadeOutAllAnimations = fadeOutAllAnimations;
|
||||
|
||||
|
||||
const showSelectInfo = ref(false);
|
||||
const showSun = ref(false);
|
||||
onMounted(() => {
|
||||
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
// 等待一帧确保所有元素都已渲染
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
// 初始化logo淡入动画
|
||||
const logo = document.querySelector('.logo');
|
||||
if (logo) {
|
||||
@@ -269,21 +244,6 @@ onMounted(() => {
|
||||
createSwapInAnimation(element, delay);
|
||||
});
|
||||
|
||||
// 初始化scale-in动画
|
||||
const scaleInContainers = document.querySelectorAll('.scale-in-ctn');
|
||||
scaleInContainers.forEach((element) => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
const delay = parseFloat(htmlElement.style.getPropertyValue('--base-delay')?.replace('s', '') || '0');
|
||||
createScaleInContainerAnimation(element, delay);
|
||||
});
|
||||
|
||||
const scaleInTexts = document.querySelectorAll('.scale-in-txt');
|
||||
scaleInTexts.forEach((element) => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
const delay = parseFloat(htmlElement.style.getPropertyValue('--base-delay')?.replace('s', '') || '0');
|
||||
createScaleInTextAnimation(element, delay);
|
||||
});
|
||||
|
||||
// 初始化气泡动画
|
||||
const regionBubbles = document.querySelectorAll('.region-bubble');
|
||||
regionBubbles.forEach((bubble, index) => {
|
||||
@@ -365,6 +325,11 @@ onMounted(() => {
|
||||
bubble.addEventListener('pointerdown', pDown);
|
||||
bubble.addEventListener('pointerup', pUp, { once: true });
|
||||
});
|
||||
|
||||
await wait(1300);
|
||||
showSelectInfo.value = true;
|
||||
await wait(300);
|
||||
showSun.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -381,7 +346,12 @@ onUnmounted(() => {
|
||||
<img :src="assets.p1.蓝天" alt="" class="bg">
|
||||
|
||||
<AniEle :url="assets.ani.主标出现" :width="1015" :height="336" :rules="mainLogoRules"
|
||||
style="top: 10%;width: 84%;left: 8%;" ref="main-logo" />
|
||||
style="top: 10%;width: 84%;left: 8%;" />
|
||||
|
||||
<div class="sun-warp" v-if="showSun">
|
||||
<AniEle :url="assets.ani.P1太阳总" :width="1000" :height="1000"
|
||||
:rules="[{ name: '下落', frame: 69, loop: 1, duration: 33 }, { name: '循环', frame: 75, loop: 0, duration: 33, reverse: false }]" />
|
||||
</div>
|
||||
|
||||
<!-- <img :src="assets.p1.太阳" alt="" style="top: 30%;left: 5%;width: 26%;"> -->
|
||||
<div style="width: 86vw;height: 86vw;top: 47%; left: 50%;transform: translate3d(-50%, -50%, 0); z-index: -1;">
|
||||
@@ -400,31 +370,10 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="color: white; bottom: 16%;left: 50%;transform: translateX(-50%); font-size: 4.5vw;">
|
||||
<span class="block tag scale-in-ctn"
|
||||
style="background-color: #000;top: -1vw;left: -12vw;color: transparent;transform: scale(0);--base-delay:1s;">
|
||||
<span>点击</span>
|
||||
</span>
|
||||
<span class="block tag scale-in-ctn"
|
||||
style="background-color: #304A7B;top: 4vw;left: -6vw;color: transparent;transform: scale(0);--base-delay:1.1s;">
|
||||
<span>选择</span>
|
||||
</span>
|
||||
<span class="block tag scale-in-ctn"
|
||||
style="background-color: #8FBE56;top: .8vw;left: 4.5vw;color: transparent;transform: scale(0);--base-delay:1.1s;">
|
||||
<span>阵营</span>
|
||||
</span>
|
||||
<span class="block tag scale-in-txt"
|
||||
style="background-color: transparent;top: -1vw;left: -12vw;transform: scale(0);--base-delay:1.2s;">
|
||||
<span>点击</span>
|
||||
</span>
|
||||
<span class="block tag scale-in-txt"
|
||||
style="background-color: transparent;top: 4vw;left: -6vw;transform: scale(0);--base-delay:1.2s;">
|
||||
<span>选择</span>
|
||||
</span>
|
||||
<span class="block tag scale-in-txt"
|
||||
style="background-color: transparent;top: .8vw;left: 4.5vw;transform: scale(0);--base-delay:1.3s;">
|
||||
<span>阵营</span>
|
||||
</span>
|
||||
<div style="inset: 0;width: 100%;top: 52%;pointer-events: none;" v-if="showSelectInfo">
|
||||
<AniEle :url="assets.ani.点击选择阵营" :width="1179" :height="2462" ref="click-select-ani"
|
||||
:rules="selectInfoRules" />
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 店铺选择界面 -->
|
||||
@@ -454,16 +403,34 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons relative">
|
||||
<div class="start-button relative" @click="startExploration">
|
||||
</div>
|
||||
</div>
|
||||
<AniEle :url="assets.ani.开始探索" :width="548" :height="143" :rules="startExplorationRules" class="start-button relative"
|
||||
@click="startExploration">
|
||||
</AniEle>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@keyframes sun-drop {
|
||||
0% {
|
||||
transform: translateY(-90vw);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sun-warp {
|
||||
top: 32%;
|
||||
width: 24%;
|
||||
left: 4.5%;
|
||||
z-index: -1;
|
||||
transform: translateY(-90vw);
|
||||
animation: sun-drop 1s cubic-bezier(0.54, 0.46, 0.44, 1.28) forwards;
|
||||
}
|
||||
|
||||
@mixin center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -535,7 +502,7 @@ img {
|
||||
|
||||
.bg {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
z-index: -11;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
@@ -675,13 +642,9 @@ img {
|
||||
|
||||
.start-button {
|
||||
width: 40vw;
|
||||
height: 15vw;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
left: 30%;
|
||||
cursor: pointer;
|
||||
background-image: url('../assets/p1/开始探索.png');
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.start-button:hover {
|
||||
|
||||
Reference in New Issue
Block a user