前言
在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这个技术。最主要的是这个输入框好看,而且适合用在博客上。