自定义动画
如果你不了解 CSS 动画或 CSS 过渡,建议先学习相关内容。
延时
延时
点击查看代码
html
<div class="text-center">
<timered-counter-number id="advanced-delay-counter"/>
</div>
<hr />
<div class="flex gap-4">
<input class="border border-solid p-1" v-model="number" type="number" />
<button class="border border-solid px-2 py-1" @click="switchNumber">🔄</button>
</div>
<div class="flex gap-4 mt-4 items-center">
<label class="inline-flex gap-1 border border-solid p-1">
延时(ms):
<input v-model="delay" type="range" min="0" max="3000" step="100" />
{{ delay }}
</label>
<label class="inline-flex gap-1 border border-solid p-1">
延迟递增:
<input type="checkbox" v-model="increase" />
</label>
</div>
js
import {onMounted, ref, watch} from "vue";
const number = ref(114514);
function switchNumber() {
number.value = Math.floor(Math.random() * 1000000);
}
const delay = ref(100);
const increase = ref(false);
onMounted(() => watch([number, delay, increase,], update, { immediate: true }));
function update() {
const _number = number.value;
const _delay = delay.value;
const _increase = increase.value;
const counter = document.getElementById('advanced-delay-counter');
counter.value = _number;
counter.animationOptions = ({ preprocessData }) => {
if (!_increase) return { delay: _delay };
let count = 0;
return preprocessData.map((part) =>
part.map(() => ({ delay: count++ * _delay }))
);
};
}
vue
<script setup>
// #region js
import {onMounted, ref, watch} from "vue";
const number = ref(114514);
function switchNumber() {
number.value = Math.floor(Math.random() * 1000000);
}
// #region increaseDelay
const delay = ref(100);
const increase = ref(false);
onMounted(() => watch([number, delay, increase,], update, { immediate: true }));
function update() {
const _number = number.value;
const _delay = delay.value;
const _increase = increase.value;
const counter = document.getElementById('advanced-delay-counter');
counter.value = _number;
counter.animationOptions = ({ preprocessData }) => {
if (!_increase) return { delay: _delay };
let count = 0;
return preprocessData.map((part) =>
part.map(() => ({ delay: count++ * _delay }))
);
};
}
// #endregion increaseDelay
// #endregion js
</script>
<template>
<!-- #region html -->
<div class="text-center">
<timered-counter-number id="advanced-delay-counter"/>
</div>
<hr />
<div class="flex gap-4">
<input class="border border-solid p-1" v-model="number" type="number" />
<button class="border border-solid px-2 py-1" @click="switchNumber">🔄</button>
</div>
<div class="flex gap-4 mt-4 items-center">
<label class="inline-flex gap-1 border border-solid p-1">
延时(ms):
<input v-model="delay" type="range" min="0" max="3000" step="100" />
{{ delay }}
</label>
<label class="inline-flex gap-1 border border-solid p-1">
延迟递增:
<input type="checkbox" v-model="increase" />
</label>
</div>
<!-- #endregion html -->
</template>
<style scoped></style>
缓动
为了更容易观察缓动效果,可调整动画时长并调大了字体。
缓动
Easing functions provided by MDN and easings.net .
点击查看代码
html
<div class="text-center">
<timered-counter-number class="font-bold" id="advanced-easing-counter" style="line-height: 1.2"/>
</div>
<hr />
<div class="flex gap-4">
<input class="border border-solid p-1" v-model="number" type="number" />
<button class="border border-solid px-2 py-1" @click="switchNumber">🔄</button>
</div>
<div class="flex gap-4 mt-4">
<label class="inline-flex gap-1 border border-solid p-1">
字号
<input v-model="fontSize" type="range" min="1" max="128" />
{{ fontSize }}px
</label>
<label class="inline-flex gap-1 border border-solid p-1">
持续时间(s):
<input
v-model="animationOptions.duration"
type="range"
min="0"
max="6000"
step="500"
/>
{{ animationOptions.duration }}
</label>
</div>
<div class="flex gap-4 mt-4">
<div class="flex-none w-64 flex flex-col">
<select
v-model="animationOptions.easing"
@update:model-value="switchNumber"
class="w-full border border-solid p-1 self-start appearance-auto"
>
<optgroup label="Build-in Easings">
<option value="linear">linear</option>
<option value="ease">ease</option>
<option value="ease-in">ease-in</option>
<option value="ease-out">ease-out</option>
<option value="ease-in-out">ease-in-out</option>
<option value="cubic-bezier(0.3, 0.2, 0.2, 1.4)">
cubic-bezier(0.3, 0.2, 0.2, 1.4)
</option>
<option value="steps(4, end)">steps(4, end)</option>
</optgroup>
<optgroup label="easings.net Easing">
<option value="easeInQuad">easeInQuad</option>
<option value="easeOutQuad">easeOutQuad</option>
<option value="easeInOutQuad">easeInOutQuad</option>
<option value="easeInCubic">easeInCubic</option>
<option value="easeOutCubic">easeOutCubic</option>
<option value="easeInOutCubic">easeInOutCubic</option>
<option value="easeInQuart">easeInQuart</option>
<option value="easeOutQuart">easeOutQuart</option>
<option value="easeInOutQuart">easeInOutQuart</option>
<option value="easeInQuint">easeInQuint</option>
<option value="easeOutQuint">easeOutQuint</option>
<option value="easeInOutQuint">easeInOutQuint</option>
<option value="easeInSine">easeInSine</option>
<option value="easeOutSine">easeOutSine</option>
<option value="easeInOutSine">easeInOutSine</option>
<option value="easeInExpo">easeInExpo</option>
<option value="easeOutExpo">easeOutExpo</option>
<option value="easeInOutExpo">easeInOutExpo</option>
<option value="easeInCirc">easeInCirc</option>
<option value="easeOutCirc">easeOutCirc</option>
<option value="easeInOutCirc">easeInOutCirc</option>
<option value="easeInBack">easeInBack</option>
<option value="easeOutBack">easeOutBack</option>
<option value="easeInOutBack">easeInOutBack</option>
<option value="easeInElastic">easeInElastic</option>
<option value="easeOutElastic">easeOutElastic</option>
<option value="easeInOutElastic">easeInOutElastic</option>
<option value="easeInBounce">easeInBounce</option>
<option value="easeOutBounce">easeOutBounce</option>
<option value="easeInOutBounce">easeInOutBounce</option>
</optgroup>
</select>
<span class="text-xs">
Easing functions provided by
<a
href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/easing-function"
target="_blank"
>
MDN
</a>
and
<a href="https://easings.net" target="_blank"> easings.net </a>
.
</span>
</div>
<easing-view :easing="animationOptions.easing" />
</div>
js
import {onMounted, ref, watch} from "vue";
import EasingView from "./EasingView.vue";
const number = ref(1);
const fontSize = ref(64);
function switchNumber() {
number.value = Math.floor(Math.random() * 100);
}
const animationOptions = ref({
easing: "ease",
duration: 2000,
});
onMounted(() => watch([number, fontSize, animationOptions], update, { immediate: true }));
function update() {
const _number = number.value;
const _fontSize = fontSize.value;
const _animationOptions = animationOptions.value;
const counter = document.getElementById('advanced-easing-counter');
counter.value = _number;
counter.style.fontSize = _fontSize + 'px';
counter.animationOptions = _animationOptions;
}
vue
<script setup>
// #region js
import {onMounted, ref, watch} from "vue";
import EasingView from "./EasingView.vue";
const number = ref(1);
const fontSize = ref(64);
function switchNumber() {
number.value = Math.floor(Math.random() * 100);
}
const animationOptions = ref({
easing: "ease",
duration: 2000,
});
onMounted(() => watch([number, fontSize, animationOptions], update, { immediate: true }));
function update() {
const _number = number.value;
const _fontSize = fontSize.value;
const _animationOptions = animationOptions.value;
const counter = document.getElementById('advanced-easing-counter');
counter.value = _number;
counter.style.fontSize = _fontSize + 'px';
counter.animationOptions = _animationOptions;
}
// #endregion js
</script>
<template>
<!-- #region html -->
<div class="text-center">
<timered-counter-number class="font-bold" id="advanced-easing-counter" style="line-height: 1.2"/>
</div>
<hr />
<div class="flex gap-4">
<input class="border border-solid p-1" v-model="number" type="number" />
<button class="border border-solid px-2 py-1" @click="switchNumber">🔄</button>
</div>
<div class="flex gap-4 mt-4">
<label class="inline-flex gap-1 border border-solid p-1">
字号
<input v-model="fontSize" type="range" min="1" max="128" />
{{ fontSize }}px
</label>
<label class="inline-flex gap-1 border border-solid p-1">
持续时间(s):
<input
v-model="animationOptions.duration"
type="range"
min="0"
max="6000"
step="500"
/>
{{ animationOptions.duration }}
</label>
</div>
<div class="flex gap-4 mt-4">
<div class="flex-none w-64 flex flex-col">
<select
v-model="animationOptions.easing"
@update:model-value="switchNumber"
class="w-full border border-solid p-1 self-start appearance-auto"
>
<optgroup label="Build-in Easings">
<option value="linear">linear</option>
<option value="ease">ease</option>
<option value="ease-in">ease-in</option>
<option value="ease-out">ease-out</option>
<option value="ease-in-out">ease-in-out</option>
<option value="cubic-bezier(0.3, 0.2, 0.2, 1.4)">
cubic-bezier(0.3, 0.2, 0.2, 1.4)
</option>
<option value="steps(4, end)">steps(4, end)</option>
</optgroup>
<optgroup label="easings.net Easing">
<option value="easeInQuad">easeInQuad</option>
<option value="easeOutQuad">easeOutQuad</option>
<option value="easeInOutQuad">easeInOutQuad</option>
<option value="easeInCubic">easeInCubic</option>
<option value="easeOutCubic">easeOutCubic</option>
<option value="easeInOutCubic">easeInOutCubic</option>
<option value="easeInQuart">easeInQuart</option>
<option value="easeOutQuart">easeOutQuart</option>
<option value="easeInOutQuart">easeInOutQuart</option>
<option value="easeInQuint">easeInQuint</option>
<option value="easeOutQuint">easeOutQuint</option>
<option value="easeInOutQuint">easeInOutQuint</option>
<option value="easeInSine">easeInSine</option>
<option value="easeOutSine">easeOutSine</option>
<option value="easeInOutSine">easeInOutSine</option>
<option value="easeInExpo">easeInExpo</option>
<option value="easeOutExpo">easeOutExpo</option>
<option value="easeInOutExpo">easeInOutExpo</option>
<option value="easeInCirc">easeInCirc</option>
<option value="easeOutCirc">easeOutCirc</option>
<option value="easeInOutCirc">easeInOutCirc</option>
<option value="easeInBack">easeInBack</option>
<option value="easeOutBack">easeOutBack</option>
<option value="easeInOutBack">easeInOutBack</option>
<option value="easeInElastic">easeInElastic</option>
<option value="easeOutElastic">easeOutElastic</option>
<option value="easeInOutElastic">easeInOutElastic</option>
<option value="easeInBounce">easeInBounce</option>
<option value="easeOutBounce">easeOutBounce</option>
<option value="easeInOutBounce">easeInOutBounce</option>
</optgroup>
</select>
<span class="text-xs">
Easing functions provided by
<a
href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/easing-function"
target="_blank"
>
MDN
</a>
and
<a href="https://easings.net" target="_blank"> easings.net </a>
.
</span>
</div>
<easing-view :easing="animationOptions.easing" />
</div>
<!-- #endregion html -->
</template>
<style scoped></style>
vue
<script setup>
import { computed, toRefs } from "vue";
import {
linear,
easeInQuad,
easeOutQuad,
easeInOutQuad,
easeInCubic,
easeOutCubic,
easeInOutCubic,
easeInQuart,
easeOutQuart,
easeInOutQuart,
easeInQuint,
easeOutQuint,
easeInOutQuint,
easeInSine,
easeOutSine,
easeInOutSine,
easeInExpo,
easeOutExpo,
easeInOutExpo,
easeInCirc,
easeOutCirc,
easeInOutCirc,
easeInBack,
easeOutBack,
easeInOutBack,
easeInElastic,
easeOutElastic,
easeInOutElastic,
easeInBounce,
easeOutBounce,
easeInOutBounce,
cubicBezier,
} from "timered-counter";
function steps(stepCount, stepPosition = "end") {
if (stepCount < 1 || (stepPosition === "jump-none" && stepCount < 2)) {
throw new Error("Invalid step count or step position");
}
return function (inputProgress, before = false) {
if (before) return 0;
let stepValue;
switch (stepPosition) {
case "jump-start":
case "start":
stepValue = Math.ceil(inputProgress * stepCount) / stepCount;
break;
case "jump-end":
case "end":
stepValue = Math.floor(inputProgress * stepCount) / stepCount;
break;
case "jump-none":
stepValue =
Math.floor(inputProgress * (stepCount - 1)) / (stepCount - 1);
break;
case "jump-both":
stepValue =
Math.ceil(inputProgress * (stepCount + 1)) / (stepCount + 1);
break;
default:
throw new Error("Invalid step position");
}
return Math.min(Math.max(stepValue, 0), 1);
};
}
const BuildInEasingFunction = {
linear,
ease: cubicBezier(0.25, 0.1, 0.25, 1),
["ease-in"]: cubicBezier(0.42, 0, 1, 1),
["ease-out"]: cubicBezier(0, 0, 0.58, 1),
["ease-in-out"]: cubicBezier(0.42, 0, 0.58, 1),
steps,
easeInQuad,
easeOutQuad,
easeInOutQuad,
easeInCubic,
easeOutCubic,
easeInOutCubic,
easeInQuart,
easeOutQuart,
easeInOutQuart,
easeInQuint,
easeOutQuint,
easeInOutQuint,
easeInSine,
easeOutSine,
easeInOutSine,
easeInExpo,
easeOutExpo,
easeInOutExpo,
easeInCirc,
easeOutCirc,
easeInOutCirc,
easeInBack,
easeOutBack,
easeInOutBack,
easeInElastic,
easeOutElastic,
easeInOutElastic,
easeInBounce,
easeOutBounce,
easeInOutBounce,
};
const props = defineProps({
easing: String,
});
const { easing } = toRefs(props);
const easingFunction = computed(() => {
const easingName = easing.value;
let result = BuildInEasingFunction[easingName];
if (easingName.startsWith("cubic-bezier")) {
const cubicBezierValues = easingName
.replace("cubic-bezier(", "")
.replace(")", "")
.split(",")
.map((value) => parseFloat(value));
result = cubicBezier(...cubicBezierValues);
} else if (easingName.startsWith("steps")) {
const stepsValues = easingName
.replace("steps(", "")
.replace(")", "")
.split(",")
.map((value) => value.trim());
result = steps(Number.parseInt(stepsValues[0], 10), stepsValues[1]);
}
return result;
});
const pathData = computed(() => {
const easingFunctionValue = easingFunction.value;
const points = Array.from({ length: 101 }, (_, i) => i / 100);
return points
.map((t, i) => {
const x = t * 160;
const y = 120 - easingFunctionValue(t) * 120;
return `${i === 0 ? "M" : "L"}${x},${y}`;
})
.join(" ");
});
</script>
<template>
<svg
class="overflow-visible mt-2 w-32 border p-1"
width="160"
height="120"
viewBox="0 0 160 120"
>
<defs>
<linearGradient id="out" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#ed556a"></stop>
<stop offset="30%" stop-color="#ed556a"></stop>
<stop offset="50%" stop-color="#7a7374"></stop>
<stop offset="100%" stop-color="#7a7374"></stop>
</linearGradient>
</defs>
<path :d="pathData" stroke="url(#out)" fill="none" stroke-width="3px" />
</svg>
</template>
<style scoped></style>