这是一个精美的3D卡片翻转动画效果,具有翻转动画和全息效果。这个效果综合运用了现代CSS和JavaScript技术,创造出一个视觉上非常吸引人的3D卡片,适合用于展示特殊活动或VIP通行证等场景。
以下是核心实现原理和主要效果:
核心实现原理:
-
3D变换:
- 使用CSS的
transform-style: preserve-3d
和 perspective
属性创建3D空间。
- 使用
rotateY
实现卡片的翻转效果。
-
CSS变量:
- 大量使用CSS变量(如
--r
, --o
, --h
, --p
)来控制动画和样式。
- GSAP动画库:
- 使用GSAP (GreenSock Animation Platform) 创建复杂的动画时间线。
- 控制卡片的旋转、透明度和全息效果的变化。
- 蒙版效果:
- 使用
mask-image
属性创建票据形状和全息logo效果。
- 渐变和混合模式:
- 使用复杂的渐变和
mix-blend-mode
创建全息效果。
主要效果:
- 卡片翻转:卡片在正面和背面之间平滑翻转,展示不同的信息。
- 全息效果:卡片上有一个彩虹色的全息效果,随着卡片旋转而变化。
- 光泽效果:卡片表面有一个模拟光泽的动画效果,增强3D感。
- 响应式设计:使用
vmin
和计算的像素单位 --px
使卡片在不同屏幕尺寸上保持合适的大小。
- 鼠标交互:当鼠标悬停在卡片上时,动画暂停;移开时,动画继续。
- 背景动画:背景有一个缓慢移动的渐变效果,增加深度感。
创作来源:Gambhir Sharma / Supabase Ticket
使用方式
复制源代码到空白的html格式文件,在浏览中打开运行即可。
源代码
可上下滑动查看完整源代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html {
--zoom: 120;
--green: #37996b;
--neon: #3ecf8e;
}
:root {
--unit: 1vmin;
--available-screen-min: 665;
--px: calc(var(--zoom) * (var(--unit) / var(--available-screen-min)));
--bg: #060809;
--logopng: url(https://res.cloudinary.com/dpphcu4gm/image/upload/v1712993492/supabase-outline-logo_u83xos.png);
--ticket: url(https://assets.codepen.io/13471/ticket-shape.svg);
--ar: 10/30;
--gutter: 8%;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
}
*::after,
*::before {
content: "";
display: block;
position: relative;
box-sizing: border-box;
}
head::before,
head::after,
body::before,
body::after,
html::before,
html::after {
content: "";
position: absolute;
background-repeat: no-repeat;
box-sizing: border-box;
filter: blur(0);
}
body {
all: unset;
background-color: var(--bg);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
a {
all: unset;
}
#app {
perspective: 1200px;
--o: 0;
--p: 100%;
--h: 50%;
--r: 0;
transform: translate3d(0, 0, 0.1px);
}
.ticket {
--scale: 1;
transform: translate3d(0, 0, 0.1px) scale(var(--scale)) rotateY(var(--r));
transform-style: preserve-3d;
pointer-events: auto;
}
.front,
.back {
grid-area: 1/1;
background-color: #6e6176;
background-image: radial-gradient(
circle at var(--p) 50%,
#111 10%,
transparent 100%
);
background-size: 100% 220vh;
background-position: center;
background-repeat: no-repeat;
border-radius: 15px;
display: grid;
backface-visibility: visible;
transform: translateZ(1px);
transform-style: preserve-3d;
mask-image: var(--ticket);
mask-size: cover;
mask-repeat: no-repeat;
height: calc(420 * var(--px));
}
.cutout {
position: absolute;
}
.front::after,
.back::after {
content: "";
position: absolute;
inset: 0;
background-image: linear-gradient(
-70deg,
transparent 40%,
rgba(255, 255, 255, 0.5) 40.5%,
transparent
);
background-size: 200% 200%;
background-position: var(--p) var(--p);
z-index: 5;
opacity: calc(var(--o) + 0.5);
pointer-events: none;
}
.front {
transform: rotateY(180deg) translateZ(1px);
}
.back {
padding: calc(20 * var(--px));
}
.holo {
display: block;
position: absolute;
inset: 0;
border-radius: 15px;
}
.holo {
--space: 5%;
--red: hsl(0, 100%, 50%);
--orange: hsl(30, 100%, 50%);
--yellow: hsl(60, 100%, 50%);
--green: hsl(120, 100%, 50%);
--cyan: hsl(180, 100%, 50%);
--blue: hsl(222, 100%, 50%);
--purple: hsl(258, 100%, 50%);
--magenta: hsl(300, 100%, 50%);
background-image: repeating-linear-gradient(
-45deg,
var(--red) 0%,
var(--orange) calc(var(--space) * 1),
var(--yellow) calc(var(--space) * 2),
var(--green) calc(var(--space) * 3),
var(--cyan) calc(var(--space) * 4),
var(--blue) calc(var(--space) * 5),
var(--purple) calc(var(--space) * 6),
var(--magenta) calc(var(--space) * 7),
var(--red) calc(var(--space) * 8)
);
background-size: 150vw 150vh;
background-position: calc(var(--h)) calc(var(--h));
background-repeat: no-repeat;
mask-image: var(--logopng);
mask-size: 80% 80%;
mask-repeat: no-repeat;
mask-position: 150% 180%;
mix-blend-mode: plus-lighter;
filter: brightness(0.9) contrast(0.7) saturate(2);
opacity: var(--o);
}
.logo {
width: 50%;
place-self: center;
transform: translateY(-14%);
}
.divider {
position: absolute;
display: flex;
align-items: center;
justify-content: start;
bottom: 2%;
left: 0;
right: 0;
height: 18%;
padding: 0 var(--gutter);
text-transform: uppercase;
background-image: repeating-linear-gradient(
90deg,
#fff0 0px,
#fff0 8px,
var(--green) 8px,
var(--green) 16px
),
radial-gradient(ellipse at center center, #fff0 10%, transparent 50%);
background-size: 100% 1.5px, 250% 1.5px;
background-repeat: no-repeat;
background-position: -4px top, var(--h) top;
background-blend-mode: overlay;
font-size: 16px;
font-weight: 400;
z-index: 2;
}
.divider div {
display: flex;
align-items: center;
justify-content: left;
}
.divider > div > img {
margin-right: 10px;
height: 40px;
}
#app {
color: #fff;
/* font-family: "Roboto Mono", monospace; */
display: grid;
grid: 1fr/1fr;
place-content: center;
overflow: hidden;
padding: 50px;
z-index: 999;
height: 90%;
}
.ticket {
display: grid;
grid-area: 1/1;
width: calc(300 * var(--px));
height: calc(400 * var(--px));
aspect-ratio: var(--ar);
}
@media screen and (max-width: 400px) {
.ticket {
--scale: 0.75;
}
}
#id_number {
position: absolute;
margin: calc(40 * var(--px)) calc(20 * var(--px));
}
.data {
position: absolute;
top: calc(70 * var(--px));
margin: calc(20 * var(--px));
}
.name {
font-size: calc(30 * var(--px));
}
.githubid {
font-size: calc(20 * var(--px));
}
h3 {
font-size: calc(15 * var(--px));
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.4/gsap.min.js"></script>
<main id="app">
<section class="ticket">
<header class="front">
<div class="holo"></div>
<img class="logo" src="https://res.cloudinary.com/dpphcu4gm/image/upload/v1712992262/full-logo_bdbltu.png" alt="" />
<aside class="divider"></aside>
</header>
<section class="back">
<div class="holo"></div>
<p id="id_number">NO 00004521</p>
<div class="data">
<p class="name">gambhir⚡</p>
<p></p>
</div>
<aside class="divider">
<div>
<img src="https://res.cloudinary.com/dpphcu4gm/image/upload/v1712992245/logo_ycu0zf.svg" alt="" />
<h3>April 15-19 / 7AM PT</h2>
</div>
</aside>
</section>
</section>
</main>
</body>
<script>
const speed = 7;
const r = gsap.timeline({ repeat: -1 });
const o = gsap.timeline({ repeat: -1 });
const h = gsap.timeline({ repeat: -1 });
const $ticket = document.querySelector(".ticket");
$ticket.addEventListener("mouseenter", () => {
r.pause();
o.pause();
h.pause();
});
$ticket.addEventListener("mouseleave", () => {
r.play();
o.play();
h.play();
});
r.to("#app", {
"--r": "180deg",
"--p": "0%",
duration: speed,
ease: "sine.in"
});
r.to("#app", {
"--r": "360deg",
"--p": "100%",
duration: speed,
ease: "sine.out"
});
o.to("#app", {
"--o": 1,
duration: speed / 2,
ease: "power1.in"
});
o.to("#app", {
"--o": 0,
duration: speed / 2,
ease: "power1.out"
});
h.to("#app", {
"--h": "100%",
duration: speed / 2,
ease: "sine.in"
});
h.to("#app", {
"--h": "50%",
duration: speed / 2,
ease: "sine.out"
});
h.to("#app", {
"--h": "0%",
duration: speed / 2,
ease: "sine.in"
});
h.to("#app", {
"--h": "50%",
duration: speed / 2,
ease: "sine.out"
});
</script>
</html>