卡片返转计时 + 粒子时钟
翻转卡片展示
- 00
- 11
- 22
- 33
- 44
- 55
- 66
- 77
- 88
- 88
- 77
- 66
- 55
- 44
- 33
- 22
- 11
- 00
翻转卡片组件 代码 点击展开
模板 和 逻辑
vue
<template>
<ul :class="['flip-card', { dark: isDark }]">
<li
class="item"
v-for="(item, key) in totalList"
:class="{ active: active === item, before: item === before }"
:key="item"
>
<div class="up">
<div class="shadow"></div>
<div class="number">{{ item }}</div>
</div>
<div class="down">
<div class="shadow"></div>
<div class="number">{{ item }}</div>
</div>
</li>
</ul>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useData } from "vitepress";
const { isDark } = useData();
const props = withDefaults(
defineProps<{
total?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
current?: number;
countdown?: boolean;
}>(),
{
total: 9,
current: 0,
countdown: false,
}
);
const active = computed(() => {
if (props.countdown) {
return props.current < 0 ? 0 : props.current;
} else {
return props.current > props.total ? props.total : props.current;
}
});
const before = computed(() => {
if (props.countdown) {
return active.value < props.total ? active.value + 1 : 0;
} else {
return active.value ? active.value - 1 : props.total;
}
});
const totalList = computed(() => {
const list = Array.from({ length: props.total + 1 }, (_, i) => i);
return props.countdown ? list.reverse() : list;
});
</script>
<style lang="scss" scoped>
.vp-doc li + li {
margin: 0;
}
@import "./flipCard.scss";
</style>
样式 和 动画
scss
@keyframes turn-down {
0% {
transform: rotateX(90deg);
}
100% {
transform: rotateX(0deg);
}
}
@keyframes turn-up {
0% {
transform: rotateX(0deg);
}
100% {
transform: rotateX(-90deg);
}
}
@keyframes zIndex {
0% {
z-index: 2;
}
5% {
z-index: 4;
}
100% {
z-index: 4;
}
}
@keyframes show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes hide {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.flip-card {
--flip-card-width: 60px;
--flip-card-height: 90px;
--flip-card-radius: 6px;
--flip-card-line-height: 1px;
--flip-card-animation-time: 0.5s;
--flip-card-number-color: #333;
--flip-card-number-background-color: #ddd;
--flip-card-box-shadow-color: rgba(0, 0, 0, 0.6);
--flip-card-line-color: rgba(200, 200, 200, 0.4);
--flip-card-flip-shadow-color: rgba(50, 50, 50, 0.1);
--flip-card-flip-shadow-color2: rgba(50, 50, 50, 1);
&.dark {
--flip-card-number-color: #ccc;
--flip-card-number-background-color: #333;
--flip-card-box-shadow-color: rgba(0, 0, 0, 0.7);
--flip-card-line-color: rgba(0, 0, 0, 0.4);
--flip-card-flip-shadow-color: rgba(0, 0, 0, 0.1);
--flip-card-flip-shadow-color2: rgba(0, 0, 0, 1);
}
position: relative;
margin: 5px;
width: var(--flip-card-width);
height: var(--flip-card-height);
font-size: 80px;
font-weight: bold;
line-height: calc(var(--flip-card-height) - var(--flip-card-line-height));
border-radius: var(--flip-card-radius);
box-shadow: 0 1px 10px var(--flip-card-box-shadow-color);
&:after {
content: "";
position: absolute;
width: 100%;
height: var(--flip-card-line-height);
top: 50%;
left: 0;
background-color: var(--flip-card-line-color);
z-index: 5;
}
.item {
list-style: none;
z-index: 1;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
perspective: 200px;
&.active,
&:first-child {
z-index: 2;
}
.up,
.down {
z-index: 1;
position: absolute;
left: 0;
width: 100%;
height: 50%;
overflow: hidden;
}
.up {
transform-origin: 50% 100%;
top: 0;
}
.down {
transform-origin: 50% 0%;
bottom: 0;
transition: opacity 0.3s;
border-radius: 0 0 var(--flip-card-radius) var(--flip-card-radius);
}
.number {
position: absolute;
left: 0;
z-index: 1;
width: 100%;
height: 200%;
color: var(--flip-card-number-color);
text-shadow: 0 1px 2px var(--flip-card-number-color);
text-align: center;
background-color: var(--flip-card-number-background-color);
border-radius: var(--flip-card-radius);
}
.up .number {
top: 0;
}
.down .number {
bottom: 0;
}
&.before {
z-index: 3;
}
&.active {
animation: zIndex calc(1s - var(--flip-card-animation-time))
var(--flip-card-animation-time) linear both;
z-index: 2;
}
&.before .up {
z-index: 2;
animation: turn-up var(--flip-card-animation-time) linear both;
}
&.active .down {
z-index: 2;
animation: turn-down calc(1s - var(--flip-card-animation-time))
var(--flip-card-animation-time) linear both;
}
}
.shadow {
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
}
.before .up .shadow {
background: linear-gradient(
var(--flip-card-flip-shadow-color) 0%,
var(--flip-card-flip-shadow-color2) 100%
);
animation: show var(--flip-card-animation-time) linear both;
}
.active .up .shadow {
background: linear-gradient(
var(--flip-card-flip-shadow-color) 0%,
var(--flip-card-flip-shadow-color2) 100%
);
animation: hide var(--flip-card-animation-time) 0.3s linear both;
}
.before .down .shadow {
background: linear-gradient(
var(--flip-card-flip-shadow-color2) 0%,
var(--flip-card-flip-shadow-color) 100%
);
animation: show var(--flip-card-animation-time) linear both;
}
.active .down .shadow {
background: linear-gradient(
var(--flip-card-flip-shadow-color2) 0%,
var(--flip-card-flip-shadow-color) 100%
);
animation: hide var(--flip-card-animation-time) 0.3s linear both;
}
}
翻转卡片调用 代码 点击展开
vue
<template>
<div class="cardTest">
<el-button @click="countChange">加一下</el-button>
<flipCard :total="8" :current="refCount"></flipCard>
<flipCard :total="8" :current="refCount2" countdown></flipCard>
<el-button @click="countChange2">减一下</el-button>
</div>
</template>
<script setup lang="ts">
import flipCard from "./flipCard.vue";
import { ref } from "vue";
const refCount = ref(0);
const refCount2 = ref(8);
const countChange = () => {
if (refCount.value === 8) {
refCount.value = 0;
} else {
refCount.value++;
}
};
const countChange2 = () => {
if (refCount2.value === 0) {
refCount2.value = 8;
} else {
refCount2.value--;
}
};
</script>
<style lang="scss" scoped>
.cardTest {
display: flex;
align-items: center;
}
</style>
小长假倒计时
距离下一个小长假 还有
- 99
- 88
- 77
- 66
- 55
- 44
- 33
- 22
- 11
- 00
- 99
- 88
- 77
- 66
- 55
- 44
- 33
- 22
- 11
- 00
- 99
- 88
- 77
- 66
- 55
- 44
- 33
- 22
- 11
- 00
天
- 22
- 11
- 00
- 99
- 88
- 77
- 66
- 55
- 44
- 33
- 22
- 11
- 00
:
- 55
- 44
- 33
- 22
- 11
- 00
- 99
- 88
- 77
- 66
- 55
- 44
- 33
- 22
- 11
- 00
:
- 55
- 44
- 33
- 22
- 11
- 00
- 99
- 88
- 77
- 66
- 55
- 44
- 33
- 22
- 11
- 00
小长假倒计时 代码 点击展开
vue
<template>
<div class="festivalCountdown">
<div class="title cor-tip" v-if="nowFestivalText">
现在正在 <TText type="danger">{{ nowFestivalText }}</TText> 小长假中
</div>
<div class="title cor-tip">
距离下一个小长假 <TText type="danger">{{ festivalText }}</TText> 还有
</div>
<div class="show-clock" v-loading="loading">
<div class="day">
<flipCard :total="9" :current="diffDayList[0]" countdown></flipCard>
<flipCard :total="9" :current="diffDayList[1]" countdown></flipCard>
<flipCard :total="9" :current="diffDayList[2]" countdown></flipCard>
</div>
<div class="time-text">天</div>
<div class="clock">
<flipCard :total="2" :current="diffTimeList[0]" countdown></flipCard>
<flipCard :total="9" :current="diffTimeList[1]" countdown></flipCard>
<div class="time-text">:</div>
<flipCard :total="5" :current="diffTimeList[2]" countdown></flipCard>
<flipCard :total="9" :current="diffTimeList[3]" countdown></flipCard>
<div class="time-text">:</div>
<flipCard :total="5" :current="diffTimeList[4]" countdown></flipCard>
<flipCard :total="9" :current="diffTimeList[5]" countdown></flipCard>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import flipCard from "./flipCard.vue";
import { getFestival } from "docs/api/mxnzp.ts";
import { diffDate } from "docs/utils/index.ts";
import { ref, onMounted, onUnmounted } from "vue";
const festivalText = ref("");
const nowFestivalText = ref("");
const loading = ref(false);
const diffDayList = ref([0, 0, 0]);
const diffTimeList = ref([0, 0, 0, 0, 0, 0]);
let onLineTime = 0;
const getRecentlyFestival = () => {
loading.value = true;
const nowDate = new Date();
getFestival(nowDate.getFullYear())
.then((data: any) => {
const festivalList = data
.reduce((prev: any, month: any) => {
return [...prev, ...month.days];
}, [])
.filter((item: any) => {
return diffDate(nowDate, new Date(item.date)).days <= 0;
});
let endDate;
if (festivalList.length === 0) {
endDate = new Date(`${nowDate.getFullYear() + 1}-01-01`);
festivalText.value = "元旦";
} else {
const firstFestival = festivalList[0];
if (diffDate(nowDate, new Date(firstFestival.date)).days === 0) {
const nowDes = firstFestival.typeDes;
nowFestivalText.value = nowDes;
for (let index = 1; index < festivalList.length; index++) {
const element = festivalList[index];
if (element.typeDes !== nowDes) {
endDate = new Date(element.date);
festivalText.value = element.typeDes;
break;
}
}
if (!endDate) {
endDate = new Date(`${nowDate.getFullYear() + 1}-01-01`);
festivalText.value = "元旦";
}
} else {
endDate = new Date(firstFestival.date);
festivalText.value = firstFestival.typeDes;
}
}
const diffDay = Math.abs(diffDate(nowDate, endDate).days) - 1;
const diffDayStr = diffDay.toString();
let diffDayStrLength = diffDayStr.length;
for (let len = 3; len--;) {
diffDayList.value[len] = +diffDayStr[--diffDayStrLength] || 0;
}
})
.catch(() => {
onLineTime++;
if (onLineTime < 4) {
setTimeout(getRecentlyFestival, 1000);
}
})
.finally(() => {
loading.value = false;
});
};
const formatTime = (time: number) => {
return time
.toString()
.padStart(2, "0")
.split("")
.map((x) => +x);
};
const getTimeToEndDay = () => {
const now = new Date();
const endDay =
new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
23,
59,
59
).getTime() + 1000;
const diffDay = endDay - now.getTime();
const S = formatTime(Math.floor((diffDay / 1000) % 60));
const M = formatTime(Math.floor((diffDay / 1000 / 60) % 60));
const H = formatTime(Math.floor(diffDay / 1000 / 60 / 60));
diffTimeList.value = [...H, ...M, ...S];
};
let clock: NodeJS.Timeout;
onMounted(() => {
getRecentlyFestival();
clearInterval(clock);
clock = setInterval(() => {
getTimeToEndDay();
}, 1000);
});
onUnmounted(() => {
clearInterval(clock);
});
</script>
<style lang="scss" scoped>
.festivalCountdown {
.show-clock,
.day,
.clock {
display: flex;
align-items: center;
font-size: 32px;
}
}
// 媒体查询
@media (max-width: 768px) {
.festivalCountdown {
.show-clock,
.day,
.clock {
flex-wrap: wrap;
font-size: 24px;
}
}
}
</style>
粒子时钟
粒子时钟 代码 点击展开
vue
<template>
<div class="content">
<canvas class="canvas" ref="canvasRef"></canvas>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { getRandomNum } from "docs/utils/index.ts";
const canvasRef = ref<HTMLCanvasElement>();
// 设置设备像素比为1 兼容高清屏
const devicePixelRatio = 1;
// const devicePixelRatio = window.devicePixelRatio || 1;
onMounted(() => {
if (!canvasRef.value) {
return;
}
// 创建字体
const getText = (): string => {
return new Date().toTimeString().substring(0, 8);
};
const canvas: HTMLCanvasElement = canvasRef.value;
const ctx = canvas.getContext("2d", {
// 频繁访问像素点优化配置
willReadFrequently: true,
});
// 初始化画布大小
const initCanvasSize = () => {
canvas.width = 650 * devicePixelRatio;
canvas.height = 650 * devicePixelRatio;
};
// 粒子
class Particle {
x: number;
y: number;
size: number;
duration: number;
constructor() {
// 半径
const r = Math.min(canvas.width, canvas.height) / 2 - 10;
// 圆心坐标
const cx = canvas.width / 2;
const cy = canvas.height / 2;
// 随机角度
const rad = (getRandomNum(0, 360) * Math.PI) / 180;
// 粒子位置
this.x = cx + r * Math.cos(rad);
this.y = cy + r * Math.sin(rad);
// 粒子大小
this.size = getRandomNum(2 * devicePixelRatio, 7 * devicePixelRatio);
// 位移时间
this.duration = 500;
}
// 画粒子
draw() {
if (ctx) {
ctx.beginPath();
ctx.fillStyle = "rgba(200, 200, 200, .3)";
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
}
}
// 移动粒子
moveTo(tx: number, ty: number) {
const sx = this.x;
const sy = this.y;
const xSpeed = (tx - sx) / this.duration;
const ySpeed = (ty - sy) / this.duration;
const startTime = Date.now();
// 缓动效果
const _move = () => {
const t = Date.now() - startTime;
this.x = sx + xSpeed * t;
this.y = sy + ySpeed * t;
if (t > this.duration) {
this.x = tx;
this.y = ty;
return;
}
requestAnimationFrame(_move);
};
_move();
}
}
// 创建粒子数组
const particles: Particle[] = [];
let text: string = "";
// 清空画布
const clear = () => {
ctx?.clearRect(0, 0, canvas.width, canvas.height);
};
// 获取粒子坐标数组
const getPoints = (): number[][] | undefined => {
if (ctx) {
const { width, height, data } = ctx.getImageData(
0,
0,
canvas.width,
canvas.height
);
const points = [];
const gap = 4;
for (let i = 0; i < width; i += gap) {
for (let j = 0; j < height; j += gap) {
const index = (i + j * width) * 4;
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
const a = data[index + 3];
if (r === 0 && g === 0 && b === 0 && a === 255) {
points.push([i, j]);
}
}
}
return points;
}
};
// 更新
const update = () => {
const newText = getText();
if (text !== newText) {
clear();
text = newText;
// 画文本
if (ctx) {
ctx.fillStyle = "#000";
ctx.textBaseline = "middle";
ctx.font = `${120 * devicePixelRatio}px DingTalkJinBuTi,sans-serif`;
ctx.fillText(
text,
(canvas.width - ctx.measureText(text).width) / 2,
canvas.height / 2
);
const points = getPoints();
if (!points?.length) return;
clear();
for (let i = 0; i < points?.length; i++) {
let p = particles[i];
if (!p) {
p = new Particle();
particles.push(p);
}
const [x, y] = points[i];
p.moveTo(x, y);
}
if (points?.length < particles.length) {
particles.splice(points.length);
}
}
}
};
// 绘制
const draw = () => {
clear();
update();
particles.forEach((p) => {
p.draw();
});
requestAnimationFrame(draw);
};
if (!particles.length) {
initCanvasSize();
draw();
console.log("启动");
}
});
// 创建一个类
</script>
<style lang="scss" scoped>
.content {
background: url("/assets/demo/bg-1.jpg") no-repeat;
background-size: cover;
.canvas {
width: 100%;
}
}
</style>