Skip to content

Custom Animation

This section assumes you have some understanding of CSS animations or CSS transitions.

If you are not familiar with CSS animations or CSS transitions, it is recommended to learn about them first.

Delay

CodeSandbox Logo

011011044055011044


Click to view code
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>

Easing

To make it easier to observe the easing effect, the animation duration has been adjusted and the font size increased.

CodeSandbox Logo

011

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

Click to view code
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>