animate on play btn

This commit is contained in:
Georges-Antoine Assi
2025-10-25 11:41:08 -04:00
parent 90e579e2ce
commit 2dc4cacd98
4 changed files with 160 additions and 63 deletions

View File

@@ -9,7 +9,6 @@ import {
onBeforeUnmount,
inject,
useTemplateRef,
nextTick,
} from "vue";
import { useDisplay } from "vuetify";
import type { SearchRomSchema } from "@/__generated__";
@@ -28,7 +27,7 @@ import storePlatforms from "@/stores/platforms";
import storeRoms from "@/stores/roms";
import { type SimpleRom } from "@/stores/roms";
import type { Events } from "@/types/emitter";
import { FRONTEND_RESOURCES_PATH, CD_BASED_SYSTEMS } from "@/utils";
import { FRONTEND_RESOURCES_PATH, isCDBasedSystem } from "@/utils";
import {
getMissingCoverImage,
getUnmatchedCoverImage,
@@ -204,72 +203,111 @@ const showNoteDialog = (event: MouseEvent | KeyboardEvent) => {
};
// Spinning disk animation variables
const maxRotationSpeed = 5600; // deg/sec (adjust top speed)
const accelerationRate = 1500; // deg/sec^2 (how fast it accelerates)
const decelerationRate = -2000; // deg/sec^2 (how fast it slows)
const maxRotationSpeed = 5000; // deg/sec (adjust top speed)
const accelerationRate = 2500; // deg/sec^2 (how fast it accelerates)
const decelerationRate = -1500; // deg/sec^2 (how fast it decelerates)
// Stored animation state
let angle = 0; // current rotation in degrees
let velocity = 0; // degrees / second
let lastTimestamp: number | null = null;
let isHovering = false;
let animationId: number | null = null;
let cdAngle = 0; // current rotation in degrees
let cdVelocity = 0; // degrees / second
let cdLastTimestamp: number | null = null;
let cAnimationId: number | null = null;
let cdIsHovering = false;
let cdPlayTriggered = false;
const onEnter = () => {
// Only animate physical disks
if (boxartStyle.value !== "physical_path") return;
if (!boxartStyleCover.value) return;
if (!romsStore.isSimpleRom(props.rom)) return;
if (!CD_BASED_SYSTEMS.includes(props.rom.platform_slug)) return;
const stepCD = (timestamp: number) => {
if (!tiltCardRef.value) return;
const container = tiltCardRef.value.querySelector(
".v-img",
) as HTMLElement | null;
const imageElement = tiltCardRef.value.querySelector(
".v-img__img.v-img__img--contain",
) as HTMLImageElement | null;
if (!imageElement || !container) return;
isHovering = true;
startAnimation();
if (cdLastTimestamp === null) cdLastTimestamp = timestamp;
const deltaTime = (timestamp - cdLastTimestamp) / 1000; // in seconds
cdLastTimestamp = timestamp;
// Update velocity and angle with acceleration
cdVelocity +=
(cdIsHovering ? accelerationRate : decelerationRate) * deltaTime;
cdVelocity = Math.max(0, cdVelocity);
cdAngle = (cdAngle + cdVelocity * deltaTime) % 360;
// Animate the rotation of the CD
imageElement.style.transform = `rotate(${cdAngle}deg)`;
if (cdPlayTriggered) {
cdVelocity = maxRotationSpeed;
// Apply snap-down animation
container.style.transition =
"transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55)";
container.style.transform = `translateY(${container.offsetHeight}px) scale(0.9)`;
}
cAnimationId = requestAnimationFrame(stepCD);
};
const onLeave = () => {
isHovering = false;
const startCDAnimation = () => {
cdLastTimestamp = null;
cAnimationId = requestAnimationFrame(stepCD);
};
const step = (timestamp: number) => {
if (lastTimestamp === null) lastTimestamp = timestamp;
const deltaTime = (timestamp - lastTimestamp) / 1000; // in seconds
lastTimestamp = timestamp;
const stopCDAnimation = () => {
if (cAnimationId !== null) {
cancelAnimationFrame(cAnimationId);
}
};
// Update velocity with acceleration or deceleration
velocity += (isHovering ? accelerationRate : decelerationRate) * deltaTime;
if (velocity > maxRotationSpeed) velocity = maxRotationSpeed;
if (velocity < 0) velocity = 0;
const animateCD = computed(() => {
return (
boxartStyle.value === "physical_path" &&
Boolean(boxartStyleCover.value) &&
romsStore.isSimpleRom(props.rom) &&
isCDBasedSystem(props.rom.platform_slug)
);
});
// Integrate angle
angle = (angle + velocity * deltaTime) % 360;
const onMouseEnter = () => {
if (animateCD.value) {
cdIsHovering = true;
startCDAnimation();
}
};
const onMouseLeave = () => {
cdIsHovering = false;
};
const startCartridgeAnimation = () => {
if (tiltCardRef.value) {
const container = tiltCardRef.value.querySelector(
".v-img",
) as HTMLElement | null;
const imageElement = tiltCardRef.value.querySelector(
".v-img__img.v-img__img--contain",
);
if (imageElement) {
(imageElement as HTMLImageElement).style.transform =
`rotate(${angle}deg)`;
}
}
) as HTMLImageElement | null;
if (!container || !imageElement) return;
// Only continue animation if we're hovering or still decelerating
if (isHovering || velocity > 0) {
animationId = requestAnimationFrame(step);
// Apply snap-down animation
imageElement.style.transition =
"transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55)";
imageElement.style.transform = `translateY(${container.offsetHeight}px) scale(0.9)`;
}
};
const startAnimation = () => {
lastTimestamp = null;
animationId = requestAnimationFrame(step);
};
emitter?.on("playCD", (romId: number) => {
if (romId !== props.rom.id) return;
cdPlayTriggered = true;
startCDAnimation();
});
const stopAnimation = () => {
if (animationId !== null) {
cancelAnimationFrame(animationId);
animationId = null;
}
};
emitter?.on("playCartridge", (romId: number) => {
if (romId !== props.rom.id) return;
startCartridgeAnimation();
});
onMounted(() => {
if (tiltCardRef.value && !smAndDown.value && props.enable3DTilt) {
@@ -287,7 +325,7 @@ onBeforeUnmount(() => {
if (tiltCardRef.value?.vanillaTilt && props.enable3DTilt) {
tiltCardRef.value.vanillaTilt.destroy();
}
stopAnimation();
stopCDAnimation();
});
</script>
@@ -352,8 +390,8 @@ onBeforeUnmount(() => {
@click="handleClick"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
@mouseenter="onEnter"
@mouseleave="onLeave"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<template v-if="titleOnHover">
<v-expand-transition>

View File

@@ -1,20 +1,40 @@
<script setup lang="ts">
import { useLocalStorage } from "@vueuse/core";
import type { Emitter } from "mitt";
import { storeToRefs } from "pinia";
import { computed, useAttrs } from "vue";
import { computed, inject, useAttrs } from "vue";
import { useRouter } from "vue-router";
import type { BoxartStyleOption } from "@/components/Settings/UserInterface/Interface.vue";
import { ROUTES } from "@/plugins/router";
import storeConfig from "@/stores/config";
import storeHeartbeat from "@/stores/heartbeat";
import { type SimpleRom } from "@/stores/roms";
import { isEJSEmulationSupported, isRuffleEmulationSupported } from "@/utils";
import storeRoms, { type SimpleRom } from "@/stores/roms";
import type { Events } from "@/types/emitter";
import {
isEJSEmulationSupported,
isRuffleEmulationSupported,
isCDBasedSystem,
} from "@/utils";
const props = defineProps<{ rom: SimpleRom; iconEmbedded?: boolean }>();
const props = defineProps<{
rom: SimpleRom;
iconEmbedded?: boolean;
animateCD?: boolean;
animateCartridge?: boolean;
}>();
const attrs = useAttrs();
const configStore = storeConfig();
const heartbeatStore = storeHeartbeat();
const romsStore = storeRoms();
const router = useRouter();
const { config } = storeToRefs(configStore);
const { value: heartbeat } = storeToRefs(heartbeatStore);
const emitter = inject<Emitter<Events>>("emitter");
const boxartStyle = useLocalStorage<BoxartStyleOption>(
"settings.boxartStyle",
"cover",
);
const isEmulationSupported = computed(() => {
return (
@@ -31,17 +51,50 @@ const isEmulationSupported = computed(() => {
);
});
const boxartStyleCover = computed(() => {
if (!romsStore.isSimpleRom(props.rom) || boxartStyle.value === "cover")
return null;
const ssMedia = props.rom.ss_metadata?.[boxartStyle.value];
const gamelistMedia = props.rom.gamelist_metadata?.[boxartStyle.value];
return ssMedia || gamelistMedia;
});
const animateCD = computed(() => {
return (
boxartStyle.value === "physical_path" &&
Boolean(boxartStyleCover.value) &&
romsStore.isSimpleRom(props.rom) &&
isCDBasedSystem(props.rom.platform_slug)
);
});
const animateCartridge = computed(() => {
return (
boxartStyle.value === "physical_path" &&
Boolean(boxartStyleCover.value) &&
romsStore.isSimpleRom(props.rom) &&
!isCDBasedSystem(props.rom.platform_slug)
);
});
async function goToPlayer(rom: SimpleRom) {
if (
isEJSEmulationSupported(rom.platform_slug, heartbeat.value, config.value)
) {
await router.push({
name: ROUTES.EMULATORJS,
params: { rom: rom.id },
});
// Force full reload to retrieve COEP/COOP headers from nginx
// Required to enable multi-threading in EmulatorJS.
router.go(0);
if (animateCD.value) emitter?.emit("playCD", rom.id);
if (animateCartridge.value) emitter?.emit("playCartridge", rom.id);
setTimeout(
async () => {
await router.push({
name: ROUTES.EMULATORJS,
params: { rom: rom.id },
});
// Force full reload to retrieve COEP/COOP headers from nginx
// Required to enable multi-threading in EmulatorJS
router.go(0);
},
animateCD.value || animateCartridge.value ? 500 : 0,
);
} else if (
isRuffleEmulationSupported(rom.platform_slug, heartbeat.value, config.value)
) {

View File

@@ -84,4 +84,6 @@ export type Events = {
stateSelected: StateSchema;
showAboutDialog: null;
showNoteDialog: SimpleRom;
playCD: number;
playCartridge: number;
};

View File

@@ -779,3 +779,7 @@ export const CD_BASED_SYSTEMS = [
"xbox360", // Xbox 360
"xboxone", // Xbox One
];
export function isCDBasedSystem(platformSlug: string): boolean {
return CD_BASED_SYSTEMS.includes(platformSlug);
}