This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import type { ExtractPropTypes } from 'vue'
import { CLICK_EVENT } from '../_constants'
import { commonProps, makeNumericProp, makeStringProp } from '../_utils'
import type { AnimateAction, AnimateType } from './type'
export const animateProps = {
...commonProps,
/**
* @description 控制动画,当值从 false 变为 true 时会触发一次动画
*/
show: Boolean,
/**
* @description 动画类型
* @values 'fade', 'slide', 'zoom', ...
*/
type: makeStringProp<AnimateType | ''>(''),
/**
* @description 是否循环执行。`true`-循环执行; `false`-执行一次
*/
loop: Boolean,
/**
* @description 动画时长,单位 ms
*/
duration: makeNumericProp(500),
/**
* @description (不能与 show 同时使用)触发方式,`initial`-初始化执行; `click`-点击执行
* @values 'initial', 'click'
* @default initial
*/
action: makeStringProp<AnimateAction>('initial'),
}
export type AnimateProps = ExtractPropTypes<typeof animateProps>
export const animateEmits = {
[CLICK_EVENT]: (evt: MouseEvent) => evt instanceof Object,
animate: () => true,
}
export type AnimateEmits = typeof animateEmits

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import { computed, defineComponent, ref, watch } from 'vue'
import { CLICK_EVENT, PREFIX } from '../_constants'
import { getMainClass, getMainStyle } from '../_utils'
import requestAniFrame from '../_utils/raf'
import { animateEmits, animateProps } from './animate'
const props = defineProps(animateProps)
const emit = defineEmits(animateEmits)
const animated = ref(props.action === 'initial' || props.show === true || props.loop)
const classes = computed(() => {
const obj = {
[`${componentName}__container`]: true,
[`${componentName}-${props.type}`]: animated.value,
loop: props.loop,
}
return getMainClass(props, componentName, obj)
})
const getStyle = computed(() => {
return getMainStyle(props, {
animationDuration: props.duration ? `${props.duration}ms` : undefined,
})
})
function animate() {
animated.value = false
// #ifdef H5
requestAniFrame(() => {
requestAniFrame(() => {
animated.value = true
})
})
}
function handleClick(event: unknown) {
if (props.action === 'click') {
animate()
emit(CLICK_EVENT, event as MouseEvent)
emit('animate')
}
}
watch(
() => props.show,
(val) => {
if (val) {
animate()
emit('animate')
}
},
)
</script>
<script lang="ts">
const componentName = `${PREFIX}-animate`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view class="nut-animate">
<view
:class="classes"
:style="getStyle"
@click="handleClick"
>
<slot />
</view>
</view>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,316 @@
.nut-animate {
.nut-animate__container {
display: inline-block;
}
/* Animation css */
[class*="nut-animate-"] {
animation-duration: 0.5s;
animation-timing-function: ease-out;
animation-fill-mode: both;
}
// 抖动
.nut-animate-shake {
animation-name: shake;
}
// 心跳
.nut-animate-ripple {
animation-name: ripple;
}
// 漂浮
.nut-animate-float {
position: relative;
animation-name: float-pop;
}
// 呼吸灯
.nut-animate-breath {
animation-name: breath;
animation-duration: 2700ms;
animation-timing-function: ease-in-out;
animation-direction: alternate;
}
// 右侧向左侧划入
.nut-animate-slide-right {
animation-name: slide-right;
}
// 右侧向左侧划入
.nut-animate-slide-left {
animation-name: slide-left;
}
// 上面向下面划入
.nut-animate-slide-top {
animation-name: slide-top;
}
// 下面向上面划入
.nut-animate-slide-bottom {
animation-name: slide-bottom;
}
.nut-animate-jump {
transform-origin: center center;
animation: jump 0.7s linear;
}
// 循环
.loop {
animation-iteration-count: infinite;
}
// 抖动动画
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
10% {
transform: translateX(-9px);
}
20% {
transform: translateX(8px);
}
30% {
transform: translateX(-7px);
}
40% {
transform: translateX(6px);
}
50% {
transform: translateX(-5px);
}
60% {
transform: translateX(4px);
}
70% {
transform: translateX(-3px);
}
80% {
transform: translateX(2px);
}
90% {
transform: translateX(-1px);
}
}
// 心跳
@keyframes ripple {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
// 呼吸
@keyframes breath {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
// 右侧向左侧划入
// stylelint-disable-next-line keyframes-name-pattern
@keyframes slide-right {
0% {
opacity: 0;
transform: translateX(100%);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
// 左侧向右侧划入
// stylelint-disable-next-line keyframes-name-pattern
@keyframes slide-left {
0% {
opacity: 0;
transform: translateX(-100%);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
// 上面向下面划入
// stylelint-disable-next-line keyframes-name-pattern
@keyframes slide-top {
0% {
opacity: 0;
transform: translateY(-100%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
// 下面向上面划入
// stylelint-disable-next-line keyframes-name-pattern
@keyframes slide-bottom {
0% {
opacity: 0;
transform: translateY(100%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
// 漂浮 float
// stylelint-disable-next-line keyframes-name-pattern
@keyframes float-pop {
0% {
top: 0;
}
25% {
top: 1px;
}
50% {
top: 4px;
}
75% {
top: 1px;
}
100% {
top: 0;
}
}
// 跳跃
@keyframes jump {
0% {
transform: rotate(0deg) translateY(0);
animation-timing-function: ease-in;
}
25% {
transform: rotate(10deg) translateY(20 * 1px);
animation-timing-function: ease-out;
}
50% {
transform: rotate(0deg) translateY(-10 * 1px);
animation-timing-function: ease-in;
}
75% {
transform: rotate(-10deg) translateY(20 * 1px);
animation-timing-function: ease-out;
}
100% {
transform: rotate(0deg) translateY(0);
animation-timing-function: ease-in;
}
}
.nut-animate-twinkle {
position: relative;
&::after,
&::before {
position: absolute;
right: 50%;
z-index: 1;
box-sizing: border-box;
width: 60 * 1px;
height: 60 * 1px;
margin-top: calc(-30 / 2) * 1px;
margin-right: calc(-60 / 2) * 1px;
content: "";
border: 4 * 1px solid rgb(255 255 255 / 60%);
border-radius: calc(60 / 2) * 1px;
transform: scale(0);
animation: twinkle 2s ease-out infinite;
}
&::after {
animation-delay: 0.4s;
}
}
// 水波
@keyframes twinkle {
0% {
transform: scale(0);
}
20% {
opacity: 1;
}
50%,
100% {
opacity: 0;
transform: scale(1.4);
}
}
.nut-animate-flicker {
position: relative;
overflow: hidden;
&::after {
position: absolute;
top: 0;
left: 0;
width: 100 * 1px;
height: 60 * 1px;
content: "";
background-image: linear-gradient(106deg, rgb(232 224 255 / 0%) 24%, #e8e0ff 91%);
filter: blur(3 * 1px);
opacity: 0.73;
transform: skewX(-20deg);
animation: flicker 1.5s linear infinite;
}
}
@keyframes flicker {
0% {
transform: translateX(-100 * 1px) skewX(-20deg);
}
40%,
100% {
transform: translateX(150 * 1px) skewX(-20deg);
}
}
}

View File

@@ -0,0 +1 @@
export * from './animate'

View File

@@ -0,0 +1,4 @@
export const animateType = ['shake', 'ripple', 'breath', 'float', 'slide-right', 'slide-left', 'slide-top', 'slide-bottom', 'jump', 'twinkle', 'flicker'] as const
export type AnimateType = (typeof animateType)[number]
export const animateAction = ['initial', 'click', ''] as const
export type AnimateAction = (typeof animateAction)[number]