vue3+gasp实现✨星之卡比输入框✨

前言

在vue3中使用gasp去实现一个星之卡比的输入框,实现对输入框中的内容进行正则判断,根据正则结果实现不同的动画效果。

本组件是根据gasp官网的案例进行修改copy的有兴趣的话可以自己去对应的官网看看(ps:官网的是tsx的版本的,我这改成了vue3.2的setup语法糖组件)codepen.io/collection/...

效果图

技术栈

技术栈 官网
vue3 cn.vuejs.org/
gasp gsap.com/

代码结构

一、 星之卡比外观svg的绘制

外观没啥好说的直接复制就行了

html 复制代码
//KirbyComponent.vue
 <template>
  <div class="kirby-wrap">
    <svg id="kirby" class="kirby" viewBox="0 0 200 200">
      <use ref="star" href="#star" class="kirby-star-spit" />
      <defs>
        <path
          ref="bodySwallow"
          d="M167.58,120.58c0,18-6,30.75-62.29,29.58C49,149,43,138.58,43,120.58S70.89,85,105.29,85 S167.58,102.59,167.58,120.58z"
        ></path>
        <path
          id="star"
          class="kirby-star"
          d="M119.43,124.74l4.63-5.32a4.08,4.08,0,0,0-2.39-6.7l-5.23-1a.9.9,0,0,1-.56-.39l-4.67-7.69a4.28,4.28,0,0,0-7.27,0l-4.68,7.69a.85.85,0,0,1-.56.39l-5.23,1a4.09,4.09,0,0,0-2.39,6.7l4.63,5.32a.77.77,0,0,1,.18.72l-1.51,6a4.19,4.19,0,0,0,5.82,4.73l7-3a.89.89,0,0,1,.68,0l7,3a4.19,4.19,0,0,0,5.82-4.73l-1.51-6A.82.82,0,0,1,119.43,124.74Z"
        ></path>
      </defs>
      <g id="legLeft" ref="legRight">
        <ellipse class="kirby-foot" cx="90" cy="141.5" rx="23" ry="26"></ellipse>
        <ellipse class="kirby-stroke" cx="85" cy="141.5" rx="23" ry="26"></ellipse>
      </g>
      <g
        id="legRight"
        data-svg-origin="102 97.5"
        transform="matrix(1,0,0,1,0,0)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      >
        <ellipse class="kirby-foot" cx="138" cy="134.5" rx="23" ry="26"></ellipse>
        <ellipse class="kirby-stroke" cx="138" cy="129.5" rx="23" ry="26"></ellipse>
      </g>
      <g
        id="armLeft"
        ref="armLeft"
        data-svg-origin="100 97.5"
        transform="matrix(1,0,0,1,0,0)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      >
        <circle id="armLeft-2" class="kirby-body" cx="51.5" cy="76" r="20.5"></circle>
        <circle class="kirby-stroke" cx="50.5" cy="82" r="20.5"></circle>
      </g>
      <g
        id="body"
        ref="body"
        data-svg-origin="43 153"
        transform="matrix(1,0,0,1,0,0)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      >
        <path
          class="kirby-body"
          id="bodyFill"
          ref="bodyFill"
          d="M159,95.5 C159,127.25637 133.25637,153 101.5,153 69.74363,153 44,127.25637 44,95.5 44,63.74363 69.74363,38 101.5,38 133.25637,38 159,63.74363 159,95.5 z"
        ></path>
        <path
          class="kirby-stroke"
          id="bodyStroke"
          ref="bodyStroke"
          d="M158,91.5 C158,123.25637 132.25637,149 100.5,149 68.74363,149 43,123.25637 43,91.5 43,59.74363 68.74363,34 100.5,34 132.25637,34 158,59.74363 158,91.5 z"
        ></path>
      </g>
      <g
        id="armRight"
        ref="armRight"
        data-svg-origin="101 97.49999618530273"
        transform="matrix(1,0,0,1,0,0)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      >
        <path class="kirby-body" d="M141,58.73A20.5,20.5,0,1,1,154.68,94.5"></path>
        <path class="kirby-stroke" d="M136,58.73A20.5,20.5,0,1,1,149.68,94.5"></path>
      </g>
      <g
        id="face"
        ref="face"
        data-svg-origin="72 125.12000274658203"
        transform="matrix(1,0,0,1,0,0)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      >
        <path
          id="eyeRight"
          ref="eyeRight"
          class="kirby-eye"
          d="M84,84.53c0,5.8-1.57,9.47-3.5,9.47S77,90.33,77,84.53,78.57,73,80.5,73,84,78.73,84,84.53Z"
          data-svg-origin="80.5 90"
          transform="matrix(1,0,0,1,0,0)"
          style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
        ></path>
        <path
          id="eyeLeft"
          ref="eyeLeft"
          class="kirby-eye"
          d="M68,84.53c0,5.8-1.57,9.47-3.5,9.47S61,90.33,61,84.53,62.57,73,64.5,73,68,78.73,68,84.53Z"
          data-svg-origin="64.5 90"
          transform="matrix(1,0,0,1,0,0)"
          style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
        ></path>
        <path
          id="eyeRightClosed"
          ref="eyeRightClosed"
          class="kirby-stroke"
          d="M80.69,125.12c.37-1.41,1.89-3.8,4-6.35a22.84,22.84,0,0,1,5.61-5.14"
          data-svg-origin="85.49500274658203 125.12000274658203"
          transform="matrix(1,0,0,0,0,125.12)"
          style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
        ></path>
        <path
          id="eyeLeftClosed"
          ref="eyeLeftClosed"
          class="kirby-stroke"
          d="M55.05,113.61c1.33.61,3.42,2.52,5.56,5.07a23.08,23.08,0,0,1,4.09,6.42"
          data-svg-origin="59.87499809265137 125.0999984741211"
          transform="matrix(1,0,0,0,0,125.1)"
          style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
        ></path>
        <g ref="cheeks">
          <path
            id="cheekRight"
            class="kirby-blush kirby-stroke"
            d="M93,92.94A22.41,22.41,0,0,1,101.66,89l1.09,5.41s2.74-2.94,8.66-3.93"
          ></path>
          <path
            id="cheekLeft"
            class="kirby-blush kirby-blush-small kirby-stroke"
            d="M47,92.94A11,11,0,0,1,51.19,89l.52,5.41a8,8,0,0,1,4.19-3.93"
          ></path>
        </g>
      </g>
      <path
        id="mouth"
        ref="mouth"
        class="kirby-stroke"
        d="M77,98c0,1.93-2.12,3.5-4.5,3.5S68,99.93,68,98"
      ></path>
      <g
        id="mouthOpen"
        ref="mouthOpen"
        class="kirby-mouth-open"
        data-svg-origin="71 103"
        transform="matrix(0,0,0,0,71,103)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      >
        <path
          class="kirby-stroke kirby-mouth-open-inner"
          d="M94,105.69c0,14.36-11,23.31-22,23.31s-18-9-18-23.31S61,77,72,77,94,91.33,94,105.69Z"
        ></path>
        <path
          class="kirby-stroke kirby-mouth-open-tongue"
          d="M72,129a22,22,0,0,0,18.91-11.18C90,109.51,82.8,101.07,74,101.07c-9.39,0-17,9.6-17,18.41,0,.11,0,.21,0,.32A16.16,16.16,0,0,0,72,129Z"
        ></path>
      </g>
      <g
        id="mouthFull"
        ref="mouthFull"
        class="kirby-mouth-full"
        data-svg-origin="73.31500244140625 101.76000213623047"
        transform="matrix(0,0,0,0,73.315,101.76)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      >
        <path
          class="kirby-stroke"
          d="M68.45,101.5a6.42,6.42,0,0,1,4-1,7.1,7.1,0,0,1,4,1"
        ></path>
        <path
          class="kirby-stroke kirby-mouth-cheek"
          d="M78.07,106.19a5.17,5.17,0,0,1-1.2-4.75,5.63,5.63,0,0,1,2.76-4.11"
        ></path>
        <path
          class="kirby-stroke kirby-mouth-cheek"
          d="M67,99a4,4,0,0,1,1.5,2.76,4.31,4.31,0,0,1-.46,3.15"
        ></path>
      </g>
      <path
        id="mouthFrown"
        ref="mouthFrown"
        class="kirby-stroke"
        d="M70,133.5a2.2,2.2,0,0,1,2-1,2.4,2.4,0,0,1,2,1"
        data-svg-origin="72 132.99735260009766"
        transform="matrix(1,0,0,0,0,132.99735)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      ></path>
      <g
        id="hat"
        ref="hat"
        data-svg-origin="0 0"
        transform="matrix(0,0,0,0,0,0)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      ></g>
      <g
        id="envelope"
        ref="envelope"
        data-svg-origin="164.17000579833984 54"
        transform="matrix(0,0,0,0,164.17001,64)"
        style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
      >
        <rect
          class="kirby-envelope"
          x="130.99"
          y="9"
          width="67.5"
          height="45"
          rx="10"
        ></rect>
        <rect
          class="kirby-stroke"
          x="129.85"
          y="3"
          width="67.5"
          height="45"
          rx="10"
        ></rect>
        <polyline class="kirby-stroke" points="131.85 10 163.6 31.5 195.35 10"></polyline>
        <path
          class="kirby-envelope-star kirby-stroke"
          ref="envelopeStar"
          d="M170,31l2.48-2.93a2.26,2.26,0,0,0-1.28-3.68l-2.8-.56a.46.46,0,0,1-.3-.22l-2.51-4.23a2.26,2.26,0,0,0-3.89,0l-2.51,4.23a.46.46,0,0,1-.3.22l-2.8.56a2.26,2.26,0,0,0-1.28,3.68L157.25,31a.45.45,0,0,1,.1.4l-.81,3.32a2.26,2.26,0,0,0,3.12,2.6l3.76-1.67a.48.48,0,0,1,.37,0l3.76,1.67a2.26,2.26,0,0,0,3.12-2.6l-.81-3.32A.45.45,0,0,1,170,31Z"
          data-svg-origin="163.64500427246094 27.894429206848145"
          transform="matrix(0,0,0,0,163.645,27.89443)"
          style="translate: none; rotate: none; scale: none; transform-origin: 0px 0px"
        ></path>
      </g>
    </svg>
  </div>
 </template>
 
 <script></script>
  
<style lang="scss">
    $black: #87213c;
    $white: #fff;

    $primary-color: #f9dde3;
    $primary-color-tint: #fcf2f5;
    $secondary-color: #ff2961;
    $accent-color: #fae798;

    $mail-color: #326dcc;
    $star-sparkly-color: #b8d7ff;

    $primary-font: 'Fredoka One', sans-serif;

    :root {
            --black: #{$black};
            --white: #{$white};
            --primary-color: #{$primary-color};
            --primary-color-tint: #{$primary-color-tint};
            --secondary-color: #{$secondary-color};
            --accent-color: #{$accent-color};
            --mail-color: #{$mail-color};
            --star-color: #{$accent-color};
            --star-sparkly-color: #{$star-sparkly-color};
    }

    @import url('https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap');

    * { box-sizing: border-box; }

    body {
            display: grid;
            place-items: center;
            min-height: 100vh;
            background-color: var(--primary-color-tint);
            font-size: 16px;
            line-height: 1;
            font-family: $primary-font;
    }

    .container {
            display: grid;
            grid-template-columns: 2fr 1fr;
            align-items: center;
            width: 400px;
            max-width: 400px;
    }

    .input-text {
            position: absolute;
            z-index: 5;
            width: 17rem;
            font-size: 1.25rem;
            line-height: 1.5;
            color: var(--black);
            transform: translate(1rem, 0.625rem);
            overflow: hidden;

            &-letter {
                    display: inline-block;
                    transform-origin: center bottom;
            }
    }

    .input-wrap {
            position: relative;
    }

    .cloudy {
            position: absolute;
            top: 50%;
            left: 50%;
            width: 150%;
            transform: translate(-50%, -50%);

            &-poofs {
                    fill: var(--white);
            }
    }

    .fade {
            &-enter-active,
            &-leave-active {
                    transition: 0.3s;
            }

            &-enter-from,
            &-leave-to {
                    opacity: 0;
            }
    }

    .dreamy-input {
            --offset-top: 0.1875rem;
            --offset-left: -0.125rem;
            --control-height: 3.125rem;

            position: relative;

            &::after {
                    content: '';
                    position: absolute;
                    top: var(--offset-top);
                    left: var(--offset-left);
                    height: 100%;
                    width: 100%;
                    border: 3px solid var(--black);
                    pointer-events: none;
                    transition: border 0.5s ease-in-out;
            }

            &::after,
            &-control {
                    border-radius: var(--control-height);
            }

            &-control {
                    width: 15.625rem;
                    height: var(--control-height);
                    padding: 0 calc(1rem + var(--control-height)) 0 1rem;
                    border: 0;
                    background-color: var(--primary-color);
                    font-family: $primary-font;
                    font-size: 1.25rem;
                    color: var(--black);
                    caret-color: var(--secondary-color);
                    transition: background-color 0.5s ease-in-out;

                    &:focus {
                            outline: none;
                    }

                    &:disabled {
                            background-color: var(--primary-color-tint);
                    }
            }

            &:focus-within {
                    &::after {
                            border-color: var(--secondary-color);
                    }
            }

            &-label,
            &-error {
                    position: absolute;
                    left: 1rem;
                    font-size: 0.75rem;
                    letter-spacing: 0.05em;
            }

            &-label {
                    top: -0.75rem;
                    color: var(--black);
            }

            &-error {
                    bottom: -1.5rem;
                    color: var(--secondary-color);
            }

            &-button {
                    display: grid;
                    place-items: center;
                    position: absolute;
                    top: calc(var(--offset-top) + 3px);
                    right: 0;
                    height: var(--control-height);
                    width: var(--control-height);
                    padding: 0;
                    border: 0;
                    background-color: transparent;
                    color: var(--white);
                    cursor: pointer;
                    transition: 0.4s ease-in-out;

                    &-star {
                            fill: currentColor;
                            stroke: var(--star-stroke, transparent);
                            stroke-width: 3px;
                    }

                    &-svg {
                            width: 40%;
                            overflow: visible;
                    }

                    &:focus {
                            outline: none;
                            color: var(--secondary-color);
                    }

                    &:hover {
                            --star-stroke: var(--black);
                            transform: scale(1.2);
                            color: var(--accent-color);
                    }
            }

            &.disabled {
                    &::after {
                            border-color: var(--primary-color);
                    }

                    .dreamy-input-button {
                            color: var(--primary-color);
                    }
            }
    }
    </style>

    <style lang="scss" scoped>
    .kirby {
      position: relative;
      z-index: 5;
      display: inline-block;
      height: 120px;
      width: 120px;
      overflow: visible;
    }
    .kirby-stroke {
      stroke: var(--black);
      stroke-width: 3px;
      stroke-miterlimit: 10;
      stroke-linecap: round;
      stroke-linejoin: round;
      fill: none;
    }
    .kirby-body {
      fill: var(--primary-color);
    }
    .kirby-mouth-open-tongue,
    .kirby-foot {
      fill: var(--secondary-color);
    }
    .kirby-hat-dark,
    .kirby-mouth-open-inner,
    .kirby-eye {
      fill: var(--black);
    }
    .kirby-blush {
      stroke: var(--secondary-color);
    }
    .kirby-blush-small {
      stroke-width: 2.5px;
    }
    .kirby-star {
      stroke-width: 3px;
      stroke: currentColor;
      fill: var(--star-color);
    }
    .kirby-star-splashy {
      color: transparent;
    }
    .kirby-star-spit {
      color: var(--black);
    }
    .kirby-hat-band {
      fill: var(--accent-color);
    }
    .kirby-hat {
      fill: var(--mail-color);
    }
    .kirby-envelope {
      fill: var(--white);
    }
    .kirby-envelope-star {
      fill: var(--primary-color);
    }
</style>
二、引入gasp插件以及配置获取节点的ref
js 复制代码
//KirbyComponent.vue
import { ref, watch, type Ref, onMounted } from 'vue'
import { gsap } from 'gsap'
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin'
//gasp的额外插件需要用此方法注册
gsap.registerPlugin(MorphSVGPlugin)
/**接收父级组件传递过来的参数用来执行对应的动画 */
const props = defineProps({
  swallow: Boolean,
  spit: Boolean
})
const emits = defineEmits(['animationdone'])
//用于获取svg节点
const bodyPathFill: Ref<morphSVGType> = ref(void 0);
const bodyPathStroke: Ref<morphSVGType> = ref(void 0);
const starIsVisible = ref(0);
const starPosition: Ref<positionType> = ref(null);
const bodySwallow = ref(null) as any
const mouth = ref(null)
const mouthOpen = ref(null)
const face = ref(null)
const armLeft = ref(null)
const armRight = ref(null)
const legRight = ref(null)
const mouthFull = ref(null)
const body = ref(null)
const envelope = ref(null)
const envelopeStar = ref(null)
const eyeLeft = ref(null)
const eyeRight = ref(null)
const eyeLeftClosed = ref(null)
const eyeRightClosed = ref(null)
const hat = ref(null)
const star = ref(null)
const mouthFrown = ref(null)
const cheeks = ref(null)
const bodyFill: any = ref(null)
const bodyStroke: any = ref(null)
三、动画方法

所有的关于星之卡比的动画都在这里了(不包含输入框文字的动画),gasp的具体属性以及方法可以去官网看看文档,这边不做详细解释了

js 复制代码
//KirbyComponent.vue
**吸取数据动画 */
function animateInhale() {
  const tlInhale = gsap.timeline()

  const delay = 0.1
  const tlMouth = gsap.timeline()
  tlMouth
    .to(mouth.value, {
      duration: delay,
      scale: 0
    })
    .to(mouthOpen.value, {
      duration: 0.5,
      scale: 1
    })

  const kirbyFace = gsap.to(face.value, {
    delay,
    duration: 0.5,
    rotation: 5,
    y: -20
  })

  const kirbyArmLeft = gsap.to(armLeft.value, {
    delay,
    duration: 0.7,
    rotation: 80
  })

  const kirbyArmRight = gsap.to(armRight.value, {
    delay,
    duration: 0.7,
    rotation: -15
  })

  const kirbyLegRight = gsap.to(legRight.value, {
    delay: delay + 1,
    duration: 1,
    rotation: -15
  })

  tlInhale.add(tlMouth, 0)
  tlInhale.add(kirbyFace, 0)
  tlInhale.add(kirbyArmLeft, 0)
  tlInhale.add(kirbyArmRight, 0)
  tlInhale.add(kirbyLegRight, 0)
  return tlInhale
}
/**身体吸取数据动画 */
function animatePuffed() {
  const tlPuffed = gsap.timeline()

  const tlMouth = gsap.timeline()
  tlMouth
    .to(mouthOpen.value, {
      duration: 0.2,
      scale: 0
    })
    .to(mouthFull.value, {
      duration: 0.1,
      scale: 1,
      y: {
        duration: 0,
        value: 0
      } as any
    })

  const kirbyFace = gsap.to(face.value, {
    duration: 0.5,
    rotate: 0,
    y: 0
  })

  const kirbyBody = gsap.to(body.value, {
    duration: 1.5,
    ease: 'elastic',
    scale: 1.1
  })

  const kirbyArmLeft = gsap.to(armLeft.value, {
    duration: 0.3,
    rotate: 0
  })

  const kirbyArmRight = gsap.to(armRight.value, {
    duration: 1.5,
    ease: 'elastic',
    rotation: 0,
    x: 10
  })

  const kirbyLegRight = gsap.to(legRight.value, {
    duration: 0.6,
    rotation: 0,
    y: 10
  })

  tlPuffed.add(tlMouth, 0)
  tlPuffed.add(kirbyFace, 0)
  tlPuffed.add(kirbyBody, 0)
  tlPuffed.add(kirbyArmLeft, 0)
  tlPuffed.add(kirbyArmRight, 0)
  tlPuffed.add(kirbyLegRight, 0)
  return tlPuffed
}
/**吞下数据动画 */
function animateSwallow() {
  const tlSwallow = gsap.timeline()
  const tlEyes = gsap.timeline()
  const tlMouth = gsap.timeline()
  const dur = {
    duration: 0.8,
    ease: 'elastic'
  }

  tlEyes
    .to([eyeLeft.value, eyeRight.value], {
      duration: 0.1,
      scaleY: 0,
      y: 20
    })
    .to([eyeLeftClosed.value, eyeRightClosed.value], {
      duration: 0.1,
      scaleY: 1,
      y: 0
    })

  tlMouth
    .to(mouthFull.value, {
      duration: 0.1,
      scaleY: 0,
      y: 35
    })
    .to(mouthFrown.value, {
      duration: 0.1,
      scaleY: 1,
      y: {
        duration: 0,
        value: 0
      } as any,
      onComplete: () => {
        starIsVisible.value = 2
      }
    })

  const kirbyCheeks = gsap.to(cheeks.value, {
    ...dur,
    y: 40
  })

  const kirbyBody = gsap.to(body.value, {
    ...dur,
    scale: 1
  })

  const bodyStroke = gsap.to(['#bodyStroke', '#bodyFill'], {
    ...dur,
    morphSVG: bodySwallow.value
  })

  const kirbyArmLeft = gsap.to(armLeft.value, {
    ...dur,
    rotation: 70,
    y: 50
  })

  const kirbyArmRight = gsap.to(armRight.value, {
    ...dur,
    rotation: -50,
    x: 0,
    y: 50
  })

  tlSwallow.add(tlEyes, 0)
  tlSwallow.add(tlMouth, 0)
  tlSwallow.add(kirbyCheeks, 0)
  tlSwallow.add(kirbyBody, 0)
  tlSwallow.add(bodyStroke, 0)
  tlSwallow.add(kirbyArmLeft, 0)
  tlSwallow.add(kirbyArmRight, 0)
  return tlSwallow
}
/**邮件出现上升动画 */
function animatePowerUp() {
  const tlPowerUp = gsap.timeline()

  const delay = 0.6
  const tlEnvelope = gsap.timeline()

  animateReset()

  const kirbyLegRight = gsap.to(legRight.value, {
    delay: delay + 1,
    duration: 1,
    rotation: -15
  })

  const kirbyHat = gsap.to(hat.value, {
    onStart: () => {
      starPosition.value = {
        x: -10,
        y: -85
      }
      starIsVisible.value = 6
    },
    startAt: {
      opacity: 1,
      scale: 0,
      y: 0
    },
    delay,
    duration: 0.6,
    ease: 'elastic',
    scale: 1
  })

  tlEnvelope
    .to(envelope.value, {
      startAt: {
        opacity: 1,
        scale: 0,
        y: 10
      },
      delay: delay + 0.3,
      duration: 0.6,
      ease: 'elastic',
      scale: 1
    })
    .to(envelopeStar.value, {
      startAt: {
        rotation: 500
      },
      duration: 1,
      rotation: 0,
      scale: 1
    })
    .to(envelope.value, {
      delay: 1,
      duration: 0.2,
      scaleY: 0.7
    })
    .to(envelope.value, {
      duration: 0.1,
      scaleY: 1,
      y: -5
    })
    .to(envelope.value, {
      duration: 1.5,
      ease: 'power2.inOut',
      opacity: 0,
      y: -110
    })
    .to(hat.value, {
      delay: 0.5,
      duration: 0.3,
      opacity: 0,
      scale: 0.7,
      y: -20
    })
    .to(legRight.value, {
      duration: 0.6,
      rotation: 0
    })

  tlPowerUp.add(kirbyLegRight, 0)
  tlPowerUp.add(kirbyHat, 0)
  tlPowerUp.add(tlEnvelope, 0)
  return tlPowerUp
}
/**重置动画 */
function animateReset() {
  const tlEyes = gsap.timeline()
  const tlMouth = gsap.timeline()
  const dur = {
    delay: 0.2,
    duration: 1,
    ease: 'elastic'
  }
  tlEyes
    .to([eyeLeftClosed.value, eyeRightClosed.value], {
      delay: 0.2,
      duration: 0.1,
      scaleY: 0,
      y: -20
    })
    .to([eyeLeft.value, eyeRight.value], {
      duration: 0.2,
      scaleY: 1,
      y: {
        duration: 0.1,
        value: 0
      } as any
    })

  tlMouth
    .to(mouthFrown.value, {
      delay: 0.2,
      duration: 0.1,
      scaleY: 0,
      y: -35
    })
    .to(mouth.value, {
      duration: 0.1,
      scale: 1
    })

  gsap.to(cheeks.value, {
    ...dur,
    y: 0
  })

  gsap.to('#bodyStroke', {
    ...dur,
    morphSVG: bodyPathStroke.value
  })

  gsap.to('#bodyFill', {
    ...dur,
    morphSVG: bodyPathFill.value
  })

  gsap.to(armLeft.value, {
    ...dur,
    rotation: 0,
    y: 0
  })

  gsap.to(armRight.value, {
    ...dur,
    rotation: 0,
    y: 0
  })

  gsap.to(legRight.value, {
    y: 0
  })
}

function animateStar(el: gsap.TweenTarget, done: () => void) {
  const tl = gsap.timeline({
    onComplete: () => {
      starIsVisible.value = 0
      done()
    }
  })

  gsap.set(el, {
    scale: 0,
    transformOrigin: 'center center'
  })

  if (starIsVisible.value > 2) {
    gsap.set(el, {
      ...starPosition.value
    })

    tl.to(el, {
      delay: 'random(0, 0.5)',
      duration: 'random(0.7, 1.1)',
      opacity: 0,
      rotation: 'random(360, 720)',
      scale: 'random(0.3, 0.6)',
      x: `random(${starPosition.value!.x - 60}, ${starPosition.value!.x + 60})`,
      y: `random(${starPosition.value!.y - 60}, ${starPosition.value!.y + 60})`
    })
  } else {
    tl.to(el, {
      delay: 'random(0, 0.5)',
      duration: 0.7,
      rotation: 500,
      scale: 1,
      x: 'random(-120, 120)',
      y: 'random(-120, -50)'
    }).to(el, {
      duration: 1,
      ease: 'elastic',
      opacity: 0,
      scale: 2
    })
  }
}
/**吐出星星动画 */
function animateSpit() {
  const tlSpit = gsap.timeline()

  const tlMouth = gsap.timeline()

  tlMouth
    .to(mouthFull.value, {
      duration: 0.05,
      scaleY: 0
    })
    .to(mouthOpen.value, {
      duration: 0.4,
      scale: 0.3
    })
    .to(star.value, {
      startAt: {
        opacity: 1,
        rotation: 0,
        scale: 0,
        transformOrigin: 'center center',
        x: -40,
        y: -20
      },
      delay: -0.2,
      duration: 1,
      ease: 'power2.inOut',
      rotation: 720,
      scale: 1,
      x: -350
    })
    .to(star.value, {
      duration: 0.2,
      scaleX: 0.9,
      transformOrigin: 'left center'
    })
    .to(star.value, {
      onStart: () => {
        starPosition.value = {
          x: -350,
          y: -20
        }
        starIsVisible.value = 6
      },
      duration: 0.2,
      opacity: 0,
      scaleX: 1,
      transformOrigin: 'left center'
    })
    .to(
      mouthOpen.value,
      {
        duration: 0.1,
        scale: 0
      },
      '-=1'
    )
    .to(
      mouth.value,
      {
        duration: 0.1,
        scale: 1
      },
      '-=0.9'
    )

  const kirbyBody = gsap.to(body.value, {
    duration: 0.2,
    scale: 1
  })

  const kirbyArmRight = gsap.to(armRight.value, {
    duration: 0.7,
    x: 0
  })

  const kirbyLegRight = gsap.to(legRight.value, {
    duration: 0.6,
    rotation: 0,
    y: 0
  })

  tlSpit.add(tlMouth, 0)
  tlSpit.add(kirbyBody, 0)
  tlSpit.add(kirbyArmRight, 0)
  tlSpit.add(kirbyLegRight, 0)
  return tlSpit
}
/**添加样式方法,style中配置了就可以不执行这个方法 */
function styleInject(css: string, ref?: { insertAt?: any } | undefined) {
  if (ref === void 0) ref = {}
  const insertAt = ref.insertAt

  if (!css || typeof document === 'undefined') {
    return
  }

  const head = document.head || document.getElementsByTagName('head')[0]
  const style = document.createElement('style') as any
  style.type = 'text/css'

  if (insertAt === 'top') {
    if (head.firstChild) {
      head.insertBefore(style, head.firstChild)
    } else {
      head.appendChild(style)
    }
  } else {
    head.appendChild(style)
  }

  if (style.styleSheet) {
    style.styleSheet.cssText = css
  } else {
    style.appendChild(document.createTextNode(css))
  }
}
四、组件初始化以及对是否符合正则的对应动画执行方法

在onMounted钩子里面初始化星之卡比的外观

js 复制代码
//KirbyComponent.vue
onMounted(() => {
  bodyPathFill.value = MorphSVGPlugin.convertToPath(bodyFill.value) as any
  bodyPathStroke.value = MorphSVGPlugin.convertToPath(bodyStroke.value) as any

  gsap.set(mouthOpen.value, {
    transformOrigin: '17px 26px',
    scale: 0
  })

  gsap.set(mouthFull.value, {
    transformOrigin: 'center center',
    scale: 0
  })

  gsap.set(mouthFrown.value, {
    transformOrigin: 'center center',
    scaleY: 0
  })

  gsap.set(eyeLeft.value, {
    transformOrigin: 'center 17px'
  })

  gsap.set(eyeRight.value, {
    transformOrigin: 'center 17px'
  })

  gsap.set(eyeLeftClosed.value, {
    transformOrigin: 'center bottom',
    scaleY: 0
  })

  gsap.set(eyeRightClosed.value, {
    transformOrigin: 'center bottom',
    scaleY: 0
  })

  gsap.set(face.value, {
    transformOrigin: '25px bottom'
  })

  gsap.set(armLeft.value, {
    transformOrigin: '70px 42px'
  })

  gsap.set(armRight.value, {
    transformOrigin: '-35px 44px'
  })

  gsap.set(legRight.value, {
    transformOrigin: '-13px -6px'
  })

  gsap.set(body.value, {
    transformOrigin: 'left bottom'
  })

  gsap.set(bodyStroke.value, {
    transformOrigin: 'center bottom'
  })

  gsap.set(bodyFill.value, {
    transformOrigin: 'center bottom'
  })

  gsap.set(hat.value, {
    scale: 0,
    transformOrigin: 'center bottom'
  })

  gsap.set(envelope.value, {
    scale: 0,
    transformOrigin: 'center bottom',
    y: 10
  })

  gsap.set(envelopeStar.value, {
    scale: 0,
    transformOrigin: 'center center'
  })

  gsap.set(star.value, {
    scale: 0,
    transformOrigin: 'center center'
  })
})

监听props中的值,根据值去动态执行相应的动画

js 复制代码
//设置接收props的值
const props = defineProps({
  swallow: Boolean,
  spit: Boolean
})

//动画结束向父组件传递结束信息
const emits = defineEmits(['animationdone'])

//监听props中的值,去执行对应方法
watch(() => props.swallow, (newValue) => {
  if (newValue) {
    aswallow()
  }
})

watch(() => props.spit, (newValue) => {
  if (newValue) {
    aspit()
  }
})


//正则匹配为true时的动画
function aswallow(): any {
  if (props.swallow) {
    animateInhale()
      .then(animatePuffed as any)
      .then(animateSwallow)
      .then(animatePowerUp)
      .then(() => {
        emits('animationdone')
      })
  }
}

//正则匹配为false时的动画
function aspit(): any {
  if (props.spit) {
    console.log(2)
    animateInhale()
      .then(animatePuffed)
      .then(animateSpit)
      .then(() => {
        emits('animationdone')
      })
  }
}
五、输入框外观
html 复制代码
//KirbyView.vue
<template>
	<main class="container">
		<div class="input-wrap">
			<svg 
				class="cloudy"
				viewBox="0 0 277 145"
			>
				<path 
					class="cloudy-poofs"
					d="M218,20c-0.74,0-1.47,0.03-2.2,0.06C204.99,7.77,189.16,0,171.5,0c-12.22,0-23.58,3.72-33,10.09 C129.08,3.72,117.72,0,105.5,0C87.84,0,72.01,7.77,61.2,20.06C60.47,20.03,59.74,20,59,20C26.42,20,0,46.42,0,79 c0,32.58,26.42,59,59,59c5.26,0,10.36-0.7,15.21-1.99C83.28,141.7,94,145,105.5,145c12.22,0,23.58-3.72,33-10.09 c9.42,6.37,20.78,10.09,33,10.09c11.5,0,22.22-3.3,31.29-8.99c4.85,1.29,9.95,1.99,15.21,1.99c32.58,0,59-26.42,59-59 C277,46.42,250.58,20,218,20z"
				/>
			</svg>
			<div 
				v-if="emailText"
				class="input-text"
			>
				<span 
					v-for="(letter, index) in emailText" 
					:key="index"
					class="input-text-letter"
				>{{ letter }}</span>
			</div>
			<div 
				:class="{ disabled: emailIsDisabled }"
				class="dreamy-input"
			>
				<label 
					class="dreamy-input-label"
					for="dreamyInput"
				>Email</label>
				<input 
					id="dreamyInput"
					v-model="emailValue"
					:aria-describedby="isNotValid ? 'dreamyInputErr' : undefined"
					:disabled="emailIsDisabled"
					autocomplete="off"
					class="dreamy-input-control"
					type="email"
					@keydown.enter="submitMail"
				>
				<transition name="fade">
					<span 
						v-if="isNotValid"
						id="dreamyInputErr"
						class="dreamy-input-error"
					>
						Please enter a valid email.
					</span>
				</transition>
				<button
					aria-label="Subscribe"
					class="dreamy-input-button"
					@click="submitMail"
				>
					<svg 
						class="dreamy-input-button-svg"
						viewBox="0 0 35 35"
					>
						<path 
							class="dreamy-input-button-star" 
							d="M29.36,23.14l4.63-5.32c2.08-2.39,0.77-6.08-2.39-6.7l-5.23-1.02c-0.23-0.05-0.44-0.19-0.56-0.39l-4.67-7.7 c-1.64-2.69-5.64-2.69-7.27,0l-4.67,7.7c-0.12,0.2-0.33,0.34-0.56,0.39L3.4,11.13c-3.16,0.62-4.47,4.31-2.39,6.7l4.63,5.32 c0.17,0.2,0.24,0.47,0.18,0.73L4.31,29.9c-0.83,3.32,2.61,6.12,5.82,4.74l7.03-3.03c0.22-0.09,0.47-0.09,0.68,0l7.03,3.03 c3.21,1.38,6.65-1.42,5.82-4.74l-1.51-6.04C29.12,23.61,29.18,23.34,29.36,23.14z"
						/>
					</svg>
				</button>
			</div>
		</div>
		<Kirby 
			:swallow="isValid"
			:spit="isNotValid"
			@animationdone="reset"
		/>
	</main>
</template>

<style lang="scss">
$black: #87213c;
$white: #fff;

$primary-color: #f9dde3;
$primary-color-tint: #fcf2f5;
$secondary-color: #ff2961;
$accent-color: #fae798;

$mail-color: #326dcc;
$star-sparkly-color: #b8d7ff;

$primary-font: 'Fredoka One', sans-serif;

:root {
	--black: #{$black};
	--white: #{$white};
	--primary-color: #{$primary-color};
	--primary-color-tint: #{$primary-color-tint};
	--secondary-color: #{$secondary-color};
	--accent-color: #{$accent-color};
	--mail-color: #{$mail-color};
	--star-color: #{$accent-color};
	--star-sparkly-color: #{$star-sparkly-color};
}
	
@import url('https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap');
	
* { box-sizing: border-box; }

body {
	display: grid;
	place-items: center;
	min-height: 100vh;
	background-color: var(--primary-color-tint);
	font-size: 16px;
	line-height: 1;
	font-family: $primary-font;
}

.container {
	display: grid;
	grid-template-columns: 2fr 1fr;
	align-items: center;
	width: 400px;
	max-width: 400px;
}

.input-text {
	position: absolute;
	z-index: 5;
	width: 17rem;
	font-size: 1.25rem;
	line-height: 1.5;
	color: var(--black);
	transform: translate(1rem, 0.625rem);
	overflow: hidden;

	&-letter {
		display: inline-block;
		transform-origin: center bottom;
	}
}

.input-wrap {
	position: relative;
}

.cloudy {
	position: absolute;
	top: 50%;
	left: 50%;
	width: 150%;
	transform: translate(-50%, -50%);

	&-poofs {
		fill: var(--white);
	}
}

.fade {
	&-enter-active,
	&-leave-active {
		transition: 0.3s;
	}

	&-enter-from,
	&-leave-to {
		opacity: 0;
	}
}
	
.dreamy-input {
	--offset-top: 0.1875rem;
	--offset-left: -0.125rem;
	--control-height: 3.125rem;

	position: relative;

	&::after {
		content: '';
		position: absolute;
		top: var(--offset-top);
		left: var(--offset-left);
		height: 100%;
		width: 100%;
		border: 3px solid var(--black);
		pointer-events: none;
		transition: border 0.5s ease-in-out;
	}

	&::after,
	&-control {
		border-radius: var(--control-height);
	}

	&-control {
		width: 15.625rem;
		height: var(--control-height);
		padding: 0 calc(1rem + var(--control-height)) 0 1rem;
		border: 0;
		background-color: var(--primary-color);
		font-family: $primary-font;
		font-size: 1.25rem;
		color: var(--black);
		caret-color: var(--secondary-color);
		transition: background-color 0.5s ease-in-out;

		&:focus {
			outline: none;
		}

		&:disabled {
			background-color: var(--primary-color-tint);
		}
	}

	&:focus-within {
		&::after {
			border-color: var(--secondary-color);
		}
	}

	&-label,
	&-error {
		position: absolute;
		left: 1rem;
		font-size: 0.75rem;
		letter-spacing: 0.05em;
	}

	&-label {
		top: -0.75rem;
		color: var(--black);
	}

	&-error {
		bottom: -1.5rem;
		color: var(--secondary-color);
	}

	&-button {
		display: grid;
		place-items: center;
		position: absolute;
		top: calc(var(--offset-top) + 3px);
		right: 0;
		height: var(--control-height);
		width: var(--control-height);
		padding: 0;
		border: 0;
		background-color: transparent;
		color: var(--white);
		cursor: pointer;
		transition: 0.4s ease-in-out;

		&-star {
			fill: currentColor;
			stroke: var(--star-stroke, transparent);
			stroke-width: 3px;
		}

		&-svg {
			width: 40%;
			overflow: visible;
		}

		&:focus {
			outline: none;
			color: var(--secondary-color);
		}

		&:hover {
			--star-stroke: var(--black);
			transform: scale(1.2);
			color: var(--accent-color);
		}
	}

	&.disabled {
		&::after {
			border-color: var(--primary-color);
		}

		.dreamy-input-button {
			color: var(--primary-color);
		}
	}
}
</style>

<style lang="scss" scoped>
.kirby {
  position: relative;
  z-index: 5;
  display: inline-block;
  height: 120px;
  width: 120px;
  overflow: visible;
}
.kirby-stroke {
  stroke: var(--black);
  stroke-width: 3px;
  stroke-miterlimit: 10;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}
.kirby-body {
  fill: var(--primary-color);
}
.kirby-mouth-open-tongue,
.kirby-foot {
  fill: var(--secondary-color);
}
.kirby-hat-dark,
.kirby-mouth-open-inner,
.kirby-eye {
  fill: var(--black);
}
.kirby-blush {
  stroke: var(--secondary-color);
}
.kirby-blush-small {
  stroke-width: 2.5px;
}
.kirby-star {
  stroke-width: 3px;
  stroke: currentColor;
  fill: var(--star-color);
}
.kirby-star-splashy {
  color: transparent;
}
.kirby-star-spit {
  color: var(--black);
}
.kirby-hat-band {
  fill: var(--accent-color);
}
.kirby-hat {
  fill: var(--mail-color);
}
.kirby-envelope {
  fill: var(--white);
}
.kirby-envelope-star {
  fill: var(--primary-color);
}
</style>
六、配置输入框节点以及输入框文字移动动画
js 复制代码
//KirbyView.vue
import { ref,nextTick  } from 'vue';
import Kirby from '@/components/kirby/KirbyComponent.vue';
// import Kirby from '../components/kirby/kirby';
//引入gasp插件
import { gsap } from 'gsap'
//配置输入框节点
const emailValue = ref('');
const emailIsDisabled = ref(false);
const emailText = ref('');
const isValid = ref(false);
const isNotValid = ref(false);
//输入框判断方法
function submitMail() {
    if (emailValue.value) {
        const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
        isValid.value = emailRegex.test(emailValue.value);
        isNotValid.value = !isValid.value;
        emailText.value = emailValue.value.length > 15 ?
        emailValue.value.slice(0, 15) + '...' :
        emailValue.value;
        emailValue.value = '';
        emailIsDisabled.value = true;
        nextTick(animateText);
        }
    }
 //输入框文字吸入效果
function animateText() {
    const tlJiggle = gsap.timeline({ 
        delay: 1, 
        defaults: {
            duration: 0.1
        }
});

tlJiggle
    .to('.input-text-letter', {
            y: 'random(-6, 4)'
    })
    .to('.input-text-letter', {
            y: 'random(-6, 4)'
    })
    .to('.input-text-letter', {
            y: 'random(-6, 4)'
    })
    .to('.input-text-letter', {
            y: 'random(-6, 4)'
    })
    .to('.input-text-letter', {
            y: 0,
            duration: 0.1
    })
    .to('.input-text-letter', {
        delay: 0,
        duration: 0.5,
        ease: 'expo.in',
        x: 300,
        stagger: {
            amount: 0.3,
            from: 'end'
        }
    });
}
//重置动画flag
function reset() {
    emailIsDisabled.value = false;
    isValid.value = false;
    isNotValid.value = false;
    emailText.value = '';
}

代码地址

gitee:gitee.com/liu_soon/vu...

npm install

npm run dev

运行成功之后打开:http://localhost:5173/Kirby

结语

这个主要是学习gasp动画的,了解这篇文章能够较快的入门gasp这个技术。最主要的是这个输入框好看,而且适合用在博客上。

相关推荐
m0_7482552620 分钟前
前端安全——敏感信息泄露
前端·安全
鑫~阳2 小时前
html + css 淘宝网实战
前端·css·html
Catherinemin2 小时前
CSS|14 z-index
前端·css
漫天转悠2 小时前
Vue3项目中引入TailwindCSS(图文详情)
vue.js
qq_589568103 小时前
Echarts+vue电商平台数据可视化——后台实现笔记
vue.js·信息可视化·echarts
2401_882727573 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder4 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂4 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand4 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL4 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js