Implementing Genshin Impact Character Switch Effect with Vue: Horizontal Scrolling, Background Animation, and Mobile Touch Handling
This article walks through recreating the Genshin Impact character switch UI using Vue, detailing the horizontal avatar carousel, background image scaling animations, and touch‑based swipe handling for mobile, complete with code snippets and edge‑case logic.
Introduction: The author recreated the Genshin Impact character switch effect from the official website, providing a tutorial that works on both PC and mobile.
Effect links: Official demo (https://ys.mihoyo.com/main/character/mondstadt?char=0) and the author's live demo (https://chenyajun.fun/#/ysRoleSwitch).
Technical point 1 – Bottom horizontal scrolling of character avatars: The outer container serves as the visible window, the inner container holds avatar elements, and each avatar is a scrolling element. The outer container uses overflow:hidden and the inner container’s left value is calculated as characterWidth × index with boundary checks to keep the third avatar active.
<div class="role-outer" :style="{ width: isPC ? '830px' : '320px' }">
<template>
<div class="left-arrow" :style="{ backgroundImage: `url(${leftSwitchArrow})` }" @click="lastPage"></div>
<div class="right-arrow" :style="{ backgroundImage: `url(${rightSwitchArrow})` }" @click="nextPage"></div>
</template>
<div class="role-contener">
<!-- inner container -->
<div
class="role-inner"
:class="{ activeTranstion: isCloseTranstion }"
:style="{ left: isPC ? moveDistancePC : moveDistanceMobile }"
ref="element"
>
<template>
<div
v-for="(item, index) in yuRoleMes"
class="role-item-pc"
:style="{ backgroundPosition: $index === index ? '0 -132px' : '', backgroundImage: `url(${bottomRoleBac})` }"
:key="index"
@click="handleRoleSwitchPC(index)"
>
<img class="item-avater" :src="item.roleAvatar" />
<p class="item-name" :style="{ color: $index === index ? '#000' : '#fff' }">{{ item.roleName }}</p>
</div>
</template>
</div>
</div>
</div>Index control functions for navigation:
function lastPage() {
if ($index.value === 0) {
$index.value = yuRoleMes.value.length - 1;
return;
}
$index.value--;
}
function nextPage() {
if ($index.value === yuRoleMes.value.length - 1) {
$index.value = 0;
return;
}
$index.value++;
}
function handleRoleSwitchPC(index) {
isCloseTranstion.value = false;
$index.value = index;
}Move distance calculation using Vue’s computed to handle three cases (first three, middle, last three) and produce moveDistancePC with a px suffix.
const moveDistance = computed(() => {
const firstThree = $index.value < 3;
if (firstThree) {
return 0;
}
const lastThree = $index.value > yuRoleMes.value.length - 4;
if (!lastThree) {
return ($index.value - 2) * -144;
}
return (yuRoleMes.value.length - 6) * -144;
});
const moveDistancePC = computed(() => moveDistance.value + 'px');Technical point 2 – Background image scaling and switching: Two background divs are always present; the first expands continuously via the breath keyframe, while the second alternates visibility using the bg-change animation.
.role-bg1 {
animation: breath 80s infinite linear;
opacity: 1;
}
.role-bg2 {
animation: bg-change 15s infinite linear, breath 80s infinite linear;
opacity: 0;
}
@keyframes bg-change {
48% { opacity: 0; }
50% { opacity: 1; }
98% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes breath {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}Technical point 3 – Background role switching: Images are positioned off‑screen by default; the active image receives right: 0 and opacity: 1 with a transition, while others are hidden.
.show-background-pc {
right: 0;
opacity: 1;
transition: all 0.3s;
}
.show-background-mobile {
right: 445px;
opacity: 1;
transition: all 0.3s;
}Mobile implementation: Touch events are used to differentiate between clicks and swipes. The start position is recorded, movement distance is calculated, and the container’s left value is updated via moveDistanceMobile. Edge cases handle small image sets, left/right limits, and rounding to multiples of 64 px.
function handleTouchStart(e) {
isCloseTranstion.value = true;
startX.value = e.touches[0].pageX || e.changedTouches[0].pageX;
}
function handleTouchMove(e) {
e.preventDefault();
isCloseTranstion.value = true;
moveDistanceX.value = (e.changedTouches[0].pageX || e.touches[0].pageX) - startX.value;
moveDistanceM.value = recordLastMove.value + moveDistanceX.value;
}
function handleTouchEnd() {
if (yuRoleMes.value.length < 5) {
moveDistanceM.value = 0;
recordLastMove.value = 0;
return;
}
const rightMaxValue = (yuRoleMes.value.length - 5) * -64;
if (moveDistanceM.value < rightMaxValue) {
moveDistanceM.value = rightMaxValue;
recordLastMove.value = rightMaxValue;
return;
}
moveDistanceM.value = recordLastMove.value + Math.round(moveDistanceX.value / 64) * 64;
recordLastMove.value = Math.round(moveDistanceM.value / 64) * 64;
}Conclusion: All source code is uploaded to GitHub (chenyajun-create/ysRoleSwitch). Readers are encouraged to star the repository and provide feedback for any issues.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
