enhance(frontend): プロフィールバナー画像のparallaxをscroll-driven animationに置き換え
This commit is contained in:
parent
c48acad04b
commit
fc684f7c2d
|
@ -18,16 +18,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInfo v-if="user.host == null && user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
||||
|
||||
<div :key="user.id" class="main _panel">
|
||||
<div class="banner-container" :style="style">
|
||||
<div ref="bannerEl" class="banner" :style="style"></div>
|
||||
<div ref="bannerEl" class="banner-container">
|
||||
<div class="banner" :style="style"></div>
|
||||
<div class="fade"></div>
|
||||
<div class="title">
|
||||
<MkUserName class="name" :user="user" :nowrap="true"/>
|
||||
<div class="bottom">
|
||||
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
||||
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
|
||||
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||
<span v-if="user.isAdmin" :title="i18n.ts.administrator" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
|
||||
<span v-if="user.isLocked" :title="i18n.ts.suspend"><i class="ti ti-lock"></i></span>
|
||||
<span v-if="user.isBot" :title="i18n.ts.flagAsBot"><i class="ti ti-robot"></i></span>
|
||||
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
|
||||
<i class="ti ti-edit"/> {{ i18n.ts.addMemo }}
|
||||
</button>
|
||||
|
@ -44,9 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkUserName :user="user" :nowrap="false" class="name"/>
|
||||
<div class="bottom">
|
||||
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
||||
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
|
||||
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||
<span v-if="user.isAdmin" :title="i18n.ts.administrator" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
|
||||
<span v-if="user.isLocked" :title="i18n.ts.suspend"><i class="ti ti-lock"></i></span>
|
||||
<span v-if="user.isBot" :title="i18n.ts.flagAsBot"><i class="ti ti-robot"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="user.followedMessage != null" class="followedMessage">
|
||||
|
@ -161,9 +161,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
|
||||
import { defineAsyncComponent, computed, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { getScrollPosition } from '@@/js/scroll.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import MkAccountMoved from '@/components/MkAccountMoved.vue';
|
||||
|
@ -223,7 +223,6 @@ const emit = defineEmits<{
|
|||
const router = useRouter();
|
||||
|
||||
const user = ref(props.user);
|
||||
const parallaxAnimationId = ref<null | number>(null);
|
||||
const narrow = ref<null | boolean>(null);
|
||||
const rootEl = ref<null | HTMLElement>(null);
|
||||
const bannerEl = ref<null | HTMLElement>(null);
|
||||
|
@ -259,24 +258,6 @@ function menu(ev: MouseEvent) {
|
|||
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
||||
}
|
||||
|
||||
function parallaxLoop() {
|
||||
parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
|
||||
parallax();
|
||||
}
|
||||
|
||||
function parallax() {
|
||||
const banner = bannerEl.value;
|
||||
if (banner == null) return;
|
||||
|
||||
const top = getScrollPosition(rootEl.value);
|
||||
|
||||
if (top < 0) return;
|
||||
|
||||
const z = 1.75; // 奥行き(小さいほど奥)
|
||||
const pos = -(top / z);
|
||||
banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
|
||||
}
|
||||
|
||||
function showMemoTextarea() {
|
||||
isEditingMemo.value = true;
|
||||
nextTick(() => {
|
||||
|
@ -306,8 +287,38 @@ async function reload() {
|
|||
// TODO
|
||||
}
|
||||
|
||||
let bannerParallaxResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
function calcBannerParallax() {
|
||||
if (!bannerEl.value || !CSS.supports('view-timeline-inset', 'auto 100px')) return;
|
||||
const elRect = bannerEl.value.getBoundingClientRect();
|
||||
const scrollEl = getScrollContainer(bannerEl.value);
|
||||
const scrollPosition = scrollEl?.scrollTop ?? window.scrollY;
|
||||
const scrollContainerHeight = scrollEl?.clientHeight ?? window.innerHeight;
|
||||
const scrollContainerTop = scrollEl?.getBoundingClientRect().top ?? 0;
|
||||
const top = scrollPosition + elRect.top - scrollContainerTop;
|
||||
const bottom = scrollContainerHeight - top;
|
||||
bannerEl.value.style.setProperty('--bannerParallaxInset', `auto ${bottom}px`);
|
||||
}
|
||||
|
||||
function initCalcBannerParallax() {
|
||||
const scrollEl = bannerEl.value ? getScrollContainer(bannerEl.value) : null;
|
||||
if (scrollEl != null && CSS.supports('view-timeline-inset', 'auto 100px')) {
|
||||
bannerParallaxResizeObserver = new ResizeObserver(() => {
|
||||
calcBannerParallax();
|
||||
});
|
||||
bannerParallaxResizeObserver.observe(scrollEl);
|
||||
}
|
||||
}
|
||||
|
||||
function disposeBannerParallaxResizeObserver() {
|
||||
if (bannerParallaxResizeObserver) {
|
||||
bannerParallaxResizeObserver.disconnect();
|
||||
bannerParallaxResizeObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.requestAnimationFrame(parallaxLoop);
|
||||
narrow.value = rootEl.value!.clientWidth < 1000;
|
||||
|
||||
if (props.user.birthday) {
|
||||
|
@ -321,16 +332,24 @@ onMounted(() => {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
calcBannerParallax();
|
||||
adjustMemoTextarea();
|
||||
});
|
||||
|
||||
initCalcBannerParallax();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (parallaxAnimationId.value) {
|
||||
window.cancelAnimationFrame(parallaxAnimationId.value);
|
||||
onActivated(() => {
|
||||
if (bannerEl.value) {
|
||||
calcBannerParallax();
|
||||
initCalcBannerParallax();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(disposeBannerParallaxResizeObserver);
|
||||
onDeactivated(disposeBannerParallaxResizeObserver);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -355,14 +374,24 @@ onUnmounted(() => {
|
|||
overflow: clip;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
view-timeline-name: --bannerParallax;
|
||||
view-timeline-inset: var(--bannerParallaxInset, auto);
|
||||
view-timeline-axis: block;
|
||||
|
||||
> .banner {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 300%;
|
||||
background-color: #4c5e6d;
|
||||
background-size: cover;
|
||||
background-repeat: repeat-y;
|
||||
background-position: center;
|
||||
box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
|
||||
will-change: background-position;
|
||||
will-change: transform;
|
||||
animation: bannerParallaxKeyframes linear both;
|
||||
animation-timeline: --bannerParallax;
|
||||
animation-range: cover;
|
||||
}
|
||||
|
||||
> .fade {
|
||||
|
@ -718,6 +747,15 @@ onUnmounted(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bannerParallaxKeyframes {
|
||||
from {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(-60%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
Loading…
Reference in New Issue