Skip to content

自定义动画

此章节假设你对 CSS 动画CSS 过渡有一定了解。

如果你不了解 CSS 动画或 CSS 过渡,建议先学习相关内容。

延时

延时CodeSandbox Logo



点击查看代码
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>

缓动

为了更容易观察缓动效果,可调整动画时长并调大了字体。

缓动CodeSandbox Logo


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>