Skip to content

自定义动画

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

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

延时

CodeSandbox Logo

011011044055011044


点击查看代码
vue
<script setup>
import { ref } from "vue";

const number = ref(114514);

function switchNumber() {
  number.value = Math.floor(Math.random() * 1000000);
}

// #region increaseDelay
const delay = ref(0.1);
const increase = ref(false);
const animationOptions = ({ testResults }) => {
  if (!increase.value) return { delay: delay.value };

  let count = 0;
  return testResults.map((part) =>
    part.map(() => ({ delay: count++ * delay.value }))
  );
};
// #endregion increaseDelay
</script>

<template>
  <div class="text-center">
    <vue-to-counter-number
      :value="number"
      :animation-options="animationOptions"
    />
  </div>
  <hr />
  <div class="flex gap-4">
    <input class="border border-solid p-1" v-model="number" type="number" />
    <button class="border border-solid p-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">
      延时(s):0
      <input v-model="delay" type="range" min="0" max="3" step="0.1" />
      {{ delay }}
    </label>
    <label class="inline-flex gap-1 border border-solid p-1">
      延迟递增:
      <input type="checkbox" v-model="increase" />
    </label>
  </div>
</template>

<style scoped></style>
vue
<script setup>
import StackblitzLogo from "../assets/stackblitz-logo.svg";
import { ref, toRefs } from "vue";
import { getCodeStackblitzParams } from "./generate-stackblitz-params";
import sdk from "@stackblitz/sdk";
import packageInfo from "../../vue-to-counter/package.json";

const props = defineProps({
  title: {
    type: String,
    required: true,
  },
});
const { title } = toRefs(props);

const containerRef = ref();

function handleStackblitz() {
  if (!containerRef.value) return;

  const tabs = containerRef.value.querySelectorAll(".tabs > label");
  const blocks = containerRef.value.querySelectorAll(".blocks > div code");
  if (tabs.length !== blocks.length) {
    window.alert("The number of tabs and code blocks should be the same.");
    return;
  }

  const files = Array.from(tabs).map((tab, index) => ({
    filename: tab.textContent.trim(),
    content: blocks[index].textContent,
  }));

  const params = getCodeStackblitzParams(files, {
    title: `${title.value} - vue-to-counter@${packageInfo.version}`,
  });
  sdk.openProject(params, {
    openFile: "src/demo.vue",
  });
}
</script>

<template>
  <div ref="containerRef" class="demo-container">
    <div class="flex relative">
      <span class="flex-auto" />
      <span
        title="Open In Stackblitz"
        class="inline-block p-1 cursor-pointer hover:outline hover:outline-[#1389FD] outline-1"
        @click="handleStackblitz"
      >
        <img
          class="h-4 w-4 pointer-events-none"
          :src="StackblitzLogo"
          alt="CodeSandbox Logo"
        />
      </span>
    </div>
    <hr />
    <slot />
  </div>
</template>

<style lang="scss">
.demo-container {
  @apply flex flex-col justify-center border p-4 rounded-lg mt-4 text-sm;

  .vue-to-counter {
    @apply font-mono text-4xl;
  }

  .custom-block {
    @apply m-0;
  }
}
</style>

缓动

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

CodeSandbox Logo

011

Easing functions provided by Motion build-in easing functions and easings.net .

点击查看代码
vue
<script setup>
import { ref } from "vue";
import EasingView from "./EasingView.vue";
import { steps } from "vue-to-counter";

const number = ref(1);

function switchNumber() {
  number.value = Math.floor(Math.random() * 100);
}

const fontSize = ref(64);

const animationOptions = ref({
  ease: "easeIn",
  duration: 2,
});
</script>

<template>
  <div class="text-center">
    <vue-to-counter-number
      class="font-bold"
      :style="{
        fontSize: fontSize + 'px',
        lineHeight: 1.2,
      }"
      :value="number"
      :animation-options="{
        ...animationOptions,
        ease:
          animationOptions.ease === 'steps' ? steps(4) : animationOptions.ease,
      }"
    />
  </div>
  <hr />
  <div class="flex gap-4">
    <input class="border border-solid p-1" v-model="number" type="number" />
    <button class="border border-solid p-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):0
      <input
        v-model="animationOptions.duration"
        type="range"
        min="0"
        max="6"
        step="0.5"
      />
      {{ animationOptions.duration }}
    </label>
  </div>
  <div class="flex gap-4 mt-4">
    <div class="flex-none w-64 flex flex-col">
      <select
        v-model="animationOptions.ease"
        @update:model-value="switchNumber"
        class="w-full border border-solid p-1 self-start appearance-auto"
      >
        <optgroup label="Motion Build-in Easings">
          <option value="linear">linear</option>
          <option value="easeIn">easeIn</option>
          <option value="easeOut">easeOut</option>
          <option value="easeInOut">easeInOut</option>
          <option value="anticipate">anticipate</option>
          <option value="steps">steps(4) 模拟卡顿感</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://motion.dev/docs/easing-functions#functions"
          target="_blank"
        >
          Motion build-in easing functions
        </a>
        and
        <a href="https://easings.net" target="_blank"> easings.net </a>
        .
      </span>
    </div>
    <easing-view :easing="animationOptions.ease" />
  </div>
</template>

<style scoped></style>
vue
<script setup>
import { computed, toRefs } from "vue";
import {
  linear,
  easeIn,
  easeOut,
  easeInOut,
  anticipate,
  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,
} from "vue-to-counter";

const BuildInEasingFunction = {
  linear,
  easeIn,
  easeOut,
  easeInOut,
  anticipate,
  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];

  {
    switch (easingName) {
      case "steps":
        result = result(4);
    }
  }

  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>
vue
<script setup>
import StackblitzLogo from "../assets/stackblitz-logo.svg";
import { ref, toRefs } from "vue";
import { getCodeStackblitzParams } from "./generate-stackblitz-params";
import sdk from "@stackblitz/sdk";
import packageInfo from "../../vue-to-counter/package.json";

const props = defineProps({
  title: {
    type: String,
    required: true,
  },
});
const { title } = toRefs(props);

const containerRef = ref();

function handleStackblitz() {
  if (!containerRef.value) return;

  const tabs = containerRef.value.querySelectorAll(".tabs > label");
  const blocks = containerRef.value.querySelectorAll(".blocks > div code");
  if (tabs.length !== blocks.length) {
    window.alert("The number of tabs and code blocks should be the same.");
    return;
  }

  const files = Array.from(tabs).map((tab, index) => ({
    filename: tab.textContent.trim(),
    content: blocks[index].textContent,
  }));

  const params = getCodeStackblitzParams(files, {
    title: `${title.value} - vue-to-counter@${packageInfo.version}`,
  });
  sdk.openProject(params, {
    openFile: "src/demo.vue",
  });
}
</script>

<template>
  <div ref="containerRef" class="demo-container">
    <div class="flex relative">
      <span class="flex-auto" />
      <span
        title="Open In Stackblitz"
        class="inline-block p-1 cursor-pointer hover:outline hover:outline-[#1389FD] outline-1"
        @click="handleStackblitz"
      >
        <img
          class="h-4 w-4 pointer-events-none"
          :src="StackblitzLogo"
          alt="CodeSandbox Logo"
        />
      </span>
    </div>
    <hr />
    <slot />
  </div>
</template>

<style lang="scss">
.demo-container {
  @apply flex flex-col justify-center border p-4 rounded-lg mt-4 text-sm;

  .vue-to-counter {
    @apply font-mono text-4xl;
  }

  .custom-block {
    @apply m-0;
  }
}
</style>