150 lines
3.1 KiB
Vue
150 lines
3.1 KiB
Vue
<template>
|
|
<div class="blocks-list flex" ref="root" :class="{ 'in-view': inView }">
|
|
<div
|
|
class="block-item flex flex-col flex-center"
|
|
v-for="(item, idx) in blocksList"
|
|
:key="item.number"
|
|
:style="{ '--delay': `${idx * 0.18}s` }"
|
|
>
|
|
<div class="number">{{ item.number }}</div>
|
|
<div class="label">{{ item.label }}</div>
|
|
<div class="line"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
const blocksList = ref([
|
|
{
|
|
number: 'NETWORKING\n OPPORTUNITIES',
|
|
label: 'with international\nmedia and designers'
|
|
},
|
|
{
|
|
number: 'INTERNATIONAL\nMEDIA EXPOSE',
|
|
label: 'through\nleading outlets'
|
|
},
|
|
{
|
|
number: 'UP TO\nUS$9000',
|
|
label: 'in total prize\npool awards'
|
|
},
|
|
{
|
|
number: 'TRAVEL\NALLOWANCE',
|
|
label: 'for finalists to attend\naward ceremony'
|
|
}
|
|
])
|
|
const root = ref<HTMLElement | null>(null)
|
|
const inView = ref(false)
|
|
let io: IntersectionObserver | null = null
|
|
|
|
onMounted(() => {
|
|
io = new IntersectionObserver(
|
|
(entries) => {
|
|
for (const entry of entries) {
|
|
if (entry.isIntersecting) {
|
|
// 延迟 0.5s 后触发动画并断开观察
|
|
setTimeout(() => {
|
|
inView.value = true
|
|
}, 500)
|
|
if (io) {
|
|
io.disconnect()
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{ threshold: 0.05 }
|
|
)
|
|
if (root.value) {
|
|
io.observe(root.value)
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
io?.disconnect()
|
|
})
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.blocks-list {
|
|
height: 31.4rem;
|
|
background: linear-gradient(98.55deg, #232323 18.22%, #898989 101.1%);
|
|
|
|
.block-item {
|
|
flex: 1;
|
|
height: 100%;
|
|
color: #fff;
|
|
position: relative;
|
|
text-align: center;
|
|
white-space: pre-line;
|
|
row-gap: 3rem;
|
|
/* text scale-in animations */
|
|
.number {
|
|
font-size: 3.6rem;
|
|
font-family: 'PoppinsBold';
|
|
font-weight: 600;
|
|
transform: scale(0);
|
|
opacity: 0;
|
|
will-change: transform, opacity;
|
|
}
|
|
.label {
|
|
font-size: 2.4rem;
|
|
font-family: 'Arial';
|
|
font-weight: 400;
|
|
letter-spacing: 0.05em;
|
|
transform: scale(0);
|
|
opacity: 0;
|
|
will-change: transform, opacity;
|
|
}
|
|
/* vertical line grows top -> bottom */
|
|
.line {
|
|
position: absolute;
|
|
right: 0;
|
|
/* 固定 top 为最终高度的一半位置,这样 height 从 0 -> 27.4rem 时会从上向下增长 */
|
|
top: calc(50% - 13.7rem);
|
|
width: 0.1rem;
|
|
height: 0;
|
|
background-color: #8d8d8d;
|
|
will-change: height;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 当组件进入视口并且等待 0.5s 后,.in-view 会加入根节点,下面规则触发动画 */
|
|
.in-view .block-item .number {
|
|
animation: scaleIn 0.48s cubic-bezier(.2,.9,.2,1) forwards;
|
|
animation-delay: var(--delay);
|
|
}
|
|
|
|
.in-view .block-item .label {
|
|
animation: scaleIn 0.48s cubic-bezier(.2,.9,.2,1) forwards;
|
|
animation-delay: calc(var(--delay) + 0.12s);
|
|
}
|
|
|
|
.in-view .block-item .line {
|
|
animation: growLine 0.7s cubic-bezier(.2,.9,.2,1) forwards;
|
|
animation-delay: calc(var(--delay) + 0.18s);
|
|
}
|
|
|
|
/* keyframes */
|
|
@keyframes scaleIn {
|
|
from {
|
|
transform: scale(0);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes growLine {
|
|
from {
|
|
height: 0;
|
|
}
|
|
to {
|
|
height: 27.4rem;
|
|
}
|
|
}
|
|
</style>
|