123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673 |
- import { CreateAnimationOptions } from './interface'
- // #ifdef APP-IOS
- import { Ref } from 'vue'
- // #endif
- // import { addUnit } from '@/uni_modules/lime-shared/addUnit'
- const defaultOption : CreateAnimationOptions = {
- duration: 400,
- timingFunction: 'linear',
- delay: 0,
- transformOrigin: '50% 50%'
- }
- function toCamelCase(str : string) : string {
- return str
- .replace(/(?:^\w|[A-Z]|\b\w)/g, (word : string, index : number, _ : string) : string => {
- return index == 0 ? word.toLowerCase() : word.toUpperCase();
- })
- .replace(/[-_\s]+/g, '');
- }
- function camelToKebabCase(str : string) : string {
- return str
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
- .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
- .toLowerCase();
- }
- function addUnit(value : any) : any {
- const regex = new RegExp("^(-)?\\d+(\\.\\d+)?$")
- const numberType = ['number', 'Int', 'UInt', 'Long', 'Float', 'Double']
- if (typeof value == 'string' && regex.test(value as string)) {
- return `${value}px`
- } else if (numberType.includes(typeof value)) {
- return `${value}px`
- }
- return value
- }
- // #ifdef APP-IOS
- // #endif
- const transitionProperty = [
- 'width', 'height', 'left', 'top', 'bottom', 'right', 'opacity', 'background-color', 'transform',
- 'border-color', 'border-left-color', 'border-top-color', 'border-right-color', 'border-bottom-color',
- 'margin', 'margin-left', 'margin-top', 'margin-right', 'margin-bottom',
- 'padding', 'padding-left', 'padding-top', 'padding-right', 'padding-bottom',
- // #ifdef WEB
- 'skew', 'skewX', 'skewY', 'matrix', 'matrix3d' //'perspective',
- // #endif
- ]
- const transitionSettings = ['transition-delay', 'transition-duration', 'transition-timing-function', 'transform-origin']
- /**
- * Step 类表示一个动画步骤
- */
- class Step {
- styles : Map<string, any>
- transform : Map<string, any>
- options : CreateAnimationOptions = {} as CreateAnimationOptions
- root : Step | null = null
- prev : Step | null = null
- next : Step | null = null
- stepIndex : number = 0
- currentStepIndex : number = 0
- stepCount : number = 1
- loop : number = 1
- loopCount : number = 1
- isPlaying : boolean = false
- timer : number = 0
- onFinish : () => void = () => { }
- onUpdate : () => void = () => { }
- /**
- * 构造函数
- * @param options 动画选项
- * @param stepIndex 步骤索引
- */
- constructor(options : CreateAnimationOptions, stepIndex : number = 0) {
- this.styles = new Map<string, any>()
- this.transform = new Map<string, any>()
- this.stepIndex = stepIndex;
- this.updateOption(options)
- }
- /**
- * 更新动画选项
- * @param options 动画选项
- */
- updateOption(options : CreateAnimationOptions) {
- for (let key in options) {
- const item = options[key]
- this.options[key] = item ?? defaultOption[key]
- }
- }
- /**
- * 添加一个新的动画步骤
- * @param options 动画选项
- * @returns 新的动画步骤
- */
- addStep(options : CreateAnimationOptions) : Step {
- if (this.next == null) {
- const root = this.root ?? this;
- this.next = new Step(options, this.stepIndex + 1);
- this.next!.prev = this;
- this.next!.root = root;
- return this.next!
- } else {
- return this.next!.addStep(options);
- }
- }
- /**
- * 停止动画
- */
- stop() {
- const root = this.root ?? this
- root.isPlaying = false
- clearTimeout(root.timer)
- }
- /**
- * 等待指定时间
- * @param time 等待时间
- * @returns Promise
- */
- private sleep(time : number = 0) : Promise<boolean> {
- return new Promise((resolve) => {
- setTimeout(function () {
- resolve(true)
- }, time);
- })
- }
- /**
- * 播放动画
- * @param element 动画元素
- */
- async play(element : UniElement, stepIndex : number = -1) {
- if (stepIndex > -1) {
- this.getStepByIndex(stepIndex)?.play(element)
- return
- }
- const duration = this.options.duration ?? 1
- const delay = this.options.delay ?? 0;
- const root = this.root ?? this
- root.isPlaying = true
- await this.sleep(this.stepIndex == 0 ? 100 : 0)
- // element.offsetHeight;
- for (let key in this.options) {
- const name = key === 'transformOrigin' ? key : toCamelCase(`transition-${key}`)
- let value = this.options[key]
- if (['delay', 'duration'].includes(key) && ['number', 'Int'].includes(typeof value)) {
- value = `${value}ms`
- }
- element.style.setProperty(camelToKebabCase(name), value)
- }
- // element.style.setProperty('transition-property', 'all')
- // await this.sleep()
- // element.offsetHeight
- let transformStr = ((element.style.getPropertyValue('transform') ?? '') as string).replace('none','')
- this.transform.forEach((value, key) => {
- const regex = new RegExp(`${key}\\([^)]*\\)`);
- if (regex.test(transformStr)) {
- // 如果存在,则修改 key 的 value
- transformStr = transformStr.replace(regex, `${key}(${value})`);
- } else {
- transformStr += ` ${key}(${value})`
- }
- })
- this.styles.forEach((value, key) => {
- element.style.setProperty(key, value)
- })
- element.style.setProperty('transform', transformStr)
- if (this.next != null && this.next!.styles.size != 0) {
- root.currentStepIndex = this.next!.stepIndex
- // await this.sleep(duration + delay)
- root.timer = setTimeout(() => {
- this.next?.play(element)
- }, duration + delay)
- } else if (root.loop == -1 || root.loopCount < root.loop) {
- // await this.sleep(duration + delay)
- root.currentStepIndex = 0
- root.timer = setTimeout(() => {
- root.onUpdate()
- root.loopCount++
- root.play(element)
- }, duration + delay)
- } else {
- let callbackWrapper : UniCallbackWrapper | null = null
- const transitionend = (_ : UniEvent) => {
- root.isPlaying = false
- root.loopCount = 1
- root.currentStepIndex = 0
- root.onFinish();
- // #ifdef WEB
- element.removeEventListener('transitionend', transitionend)
- // #endif
- // #ifdef APP
- if (callbackWrapper == null) return
- element.removeEventListener('transitionend', callbackWrapper!)
- // #endif
- }
- callbackWrapper = element.addEventListener('transitionend', transitionend)
- }
- }
- /**
- * 获取第一个步骤
- * @returns 第一个步骤
- */
- getFirstStep() : Step | null {
- if (this.prev == null) {
- return this;
- }
- return this.prev!.getFirstStep();
- }
- /**
- * 获取最后一个步骤
- * @returns 最后一个步骤
- */
- getLastStep() : Step | null {
- if (this.next == null) {
- return this;
- }
- return this.next!.getLastStep();
- }
- /**
- * 根据索引获取步骤
- * @param index 步骤索引
- * @returns 对应索引的步骤
- */
- getStepByIndex(index : number) : Step | null {
- if (this.stepIndex === index) {
- return this;
- }
- if (this.next != null && this.stepIndex < index) {
- return this.next!.getStepByIndex(index);
- }
- if (this.prev != null && this.stepIndex > index) {
- return this.prev!.getStepByIndex(index);
- }
- return null;
- }
- }
- /**
- * Animation 类
- */
- class Animation {
- private options : CreateAnimationOptions = {} as CreateAnimationOptions
- private steps : Step | null
- private _steps : Step | null = null
- private currentStep : Step | null
- private element : UniElement | null = null
- private cacheStyles : Map<string, any> = new Map<string, any>()
- onFinish : () => void = () => { }
- constructor(options : CreateAnimationOptions) {
- this.updateOption(options)
- this.steps = new Step(this.options)
- this.steps!.onFinish = () => {
- this.onFinish()
- }
- this.currentStep = this.steps
- // #ifdef WEB
- try {
- const directions = ['top', 'left', 'right', 'bottom'];
- directions.forEach((key) => {
- const name = `--lime-ani-${key}`;
- window.CSS.registerProperty({
- name,
- syntax: '<length-percentage>',
- inherits: false,
- initialValue: '0'
- })
- })
- } catch (error) {
- if (error instanceof DOMException) {
- // console.error(`Failed to register property`);
- } else {
- throw error;
- }
- }
- // #endif
- }
- /**
- * 更新选项
- * @param options 创建动画选项
- */
- private updateOption(options : CreateAnimationOptions) {
- for (let key in options) {
- const item = options[key]
- this.options[key] = item ?? defaultOption[key]
- }
- }
- /**
- * 更新 transform 属性
- * @param property transform 属性名
- * @param value transform 属性值
- */
- private updateTransform(property : string, value : string) {
- // 设置更新后的 transform 属性值
- this.currentStep?.styles!.set('transform', '');
- this.currentStep?.transform!.set(property, value)
- }
- /**
- * 设置透明度
- * @param value 透明度值
- * @returns Animation 实例
- */
- opacity(value : number) : Animation {
- this.currentStep?.styles!.set('opacity', value)
- return this
- }
- /**
- * 设置背景颜色
- * @param color 颜色值
- * @returns Animation 实例
- */
- backgroundColor(color : string) : Animation {
- this.currentStep?.styles!.set('backgroundColor', color)
- return this
- }
- width(length : any) : Animation {
- return this.add('width', length)
- }
- height(length : any) : Animation {
- return this.add('height', length)
- }
- top(length : any) : Animation {
- return this.add('top', length)
- }
- left(length : any) : Animation {
- return this.add('left', length)
- }
- right(length : any) : Animation {
- return this.add('right', length)
- }
- bottom(length : any) : Animation {
- return this.add('bottom', length)
- }
- rotateX(deg : number) : Animation {
- this.updateTransform('rotateX', `${deg}deg`);
- return this
- }
- rotateY(deg : number) : Animation {
- this.updateTransform('rotateY', `${deg}deg`);
- return this
- }
- rotateZ(deg : number) : Animation {
- this.updateTransform('rotateZ', `${deg}deg`);
- return this
- }
- rotate(deg : number) : Animation {
- this.updateTransform('rotate', `${deg}deg`);
- return this
- }
- scaleX(number : number) : Animation {
- this.updateTransform('scaleX', `${number.toString()}`)
- return this
- }
- scaleY(number : number) : Animation {
- this.updateTransform('scaleY', `${number.toString()}`)
- return this
- }
- scaleZ(number : number) : Animation {
- this.updateTransform('scaleZ', `${number.toString()}`)
- return this
- }
- scale(scaleX : number) : Animation
- scale(scaleX : number, scaleY : number = 0) : Animation {
- const _scaleY = scaleY == 0 ? scaleX : scaleY;
- this.updateTransform('scale', `${scaleX},${_scaleY}`);
- return this
- }
- // #ifdef WEB
- skewX(number : number) : Animation {
- this.updateTransform('skewX', `${number.toString()}deg`)
- return this
- }
- skewY(number : number) : Animation {
- this.updateTransform('skewY', `${number.toString()}deg`)
- return this
- }
- skew(skewX : number) : Animation
- skew(skewX : number, skewY : number = 0) : Animation {
- const _skewY = skewY == 0 ? skewX : skewY;
- this.updateTransform('skew', `${skewX}deg,${_skewY}deg`);
- return this
- }
- //matrix3d
- matrix(a : number, b : number, c : number, d : number, tx : number, ty : number) : Animation {
- this.updateTransform('skew', `${a},${b},${c},${d},${tx},${ty}`);
- return this
- }
- // #endif
- translateX(length : any) : Animation {
- const value = addUnit(length)
- this.updateTransform('translateX', `${value}`)
- return this
- }
- translateY(length : any) : Animation {
- const value = addUnit(length)
- this.updateTransform('translateY', `${value}`)
- return this
- }
- translate(x : any, y : any) : Animation {
- const _x = addUnit(x)
- const _y = addUnit(y)
- this.updateTransform('translate', `${_x},${_y}`);
- return this
- }
- margin(value : any) : Animation {
- if (typeof value == 'string') {
- return this.add('margin', value)
- } else if (Array.isArray(value)) {
- return this.add('margin', (value as any[]).join(' '))
- }
- return this
- }
- marginLeft(value : any) : Animation {
- return this.add('marginLeft', value)
- }
- marginRight(value : any) : Animation {
- return this.add('marginRight', value)
- }
- marginTop(value : any) : Animation {
- return this.add('marginTop', value)
- }
- marginBottom(value : any) : Animation {
- return this.add('marginBottom', value)
- }
- padding(value : any) : Animation {
- if (typeof value == 'string') {
- return this.add('padding', value)
- } else if (Array.isArray(value)) {
- return this.add('padding', (value as any[]).join(' '))
- }
- return this
- }
- paddingLeft(value : any) : Animation {
- return this.add('paddingLeft', value)
- }
- paddingRight(value : any) : Animation {
- return this.add('paddingRight', value)
- }
- paddingTop(value : any) : Animation {
- return this.add('paddingTop', value)
- }
- paddingBottom(value : any) : Animation {
- return this.add('paddingBottom', value)
- }
- borderColor(color : string) : Animation {
- return this.add('borderColor', color)
- }
- borderTopColor(color : string) : Animation {
- return this.add('borderTopColor', color)
- }
- borderBottomColor(color : string) : Animation {
- return this.add('borderBottomColor', color)
- }
- borderLeftColor(color : string) : Animation {
- return this.add('borderLeftColor', color)
- }
- borderRightColor(color : string) : Animation {
- return this.add('borderRightColor', color)
- }
- /**
- * 添加动画属性
- * @param type 动画属性类型
- * @param value 动画属性值
- * @returns Animation 实例
- */
- add(type : string, value : any) : Animation {
- const _v = addUnit(value)
- if (['opacity', 'transform', 'background-color', 'backgroundColor'].includes(type)) {
- this.currentStep?.styles!.set(type, value)
- } else {
- this.currentStep?.styles!.set(type, _v)
- }
- return this
- }
- step(options : UTSJSONObject | null = null) : Animation {
- if (options != null) {
- const _options = {} as CreateAnimationOptions
- for (let key in this.options) {
- const value = options[key]
- if (value != null) {
- _options[key] = value
- }
- }
- this.currentStep?.updateOption(_options)
- }
- this.currentStep = this.currentStep?.addStep(this.options)
- return this
- }
- /**
- * 设置初始样式
- * @param element 元素
- */
- private setCacheStyles(element : UniElement) {
- // @ts-ignore
- // #ifndef APP-IOS
- // element.style.cssText.split(';').forEach(style => {
- // if (style.includes(':')) {
- // const [property, value] = style.split(':');
- // if(!transitionProperty.includes(property)){
- // if(property == 'backgroundColor'){
- // this.cacheStyles.set(property, value)
- // }
- // }
- // }
- // })
- // #endif
- transitionProperty.forEach(property => {
- const value = this.getCSSValue(element, property) //element.style.getPropertyValue(property)
- if (value != null) {
- this.cacheStyles.set(property, value)
- }
- })
- transitionSettings.forEach(property => {
- const value = this.getCSSValue(element, property) //element.style.getPropertyValue(property)
- if (value != null) {
- this.cacheStyles.set(property, value)
- }
- })
- // this.cacheStyles.set('transform', '')
- }
- getCSSValue(el : UniElement, prop : string):any|null {
- const uppercasePropName = prop.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
- // #ifdef WEB
- // @ts-ignore
- const style = getComputedStyle(el) ?? el.style
- const value = style.getPropertyValue(uppercasePropName) || style[uppercasePropName]
- // #endif
- // #ifndef WEB
- const value = el.style.getPropertyValue(uppercasePropName)
- // #endif
- return value
- }
- /**
- * 重置样式
- * @param element 元素
- */
- private resetStyles(element : UniElement) {
- // @ts-ignore
- // #ifndef APP-IOS
- // element.style.cssText.split(';').forEach(style => {
- // if (style.includes(':')) {
- // const property = style.split(':')[0].trim();
- // if (property == 'transform-origin') {
- // element.style.setProperty(property, '50% 50%')
- // } else if (property == 'transition-duration') {
- // element.style.setProperty(property, '1ms')
- // } else {
- // element.style.setProperty(property, this.cacheStyles.get(property) ?? '')
- // }
- // }
- // })
- // #endif
- const directions = ['top', 'left', 'right', 'bottom'];
- transitionSettings.forEach(property => {
- const value = this.cacheStyles.get(property)
- if (property == 'transition-duration') {
- element.style.setProperty(property, value ?? '0ms')
- } else if (value != null) {
- element.style.setProperty(property, value)
- }
- })
- transitionProperty.forEach((property) => {
- const value = this.cacheStyles.get(property)
- if (value != null || directions.includes(property) || property == 'transform') {
- element.style.setProperty(property, value ?? '')
- }
- })
- element.style.setProperty('transition-property', transitionProperty.join(','))
- // #ifdef WEB
- directions.forEach(key => {
- element.style.setProperty(`${key}`, `var(--lime-ani-${key})`)
- })
- // #endif
- element.getBoundingClientRect()
- }
- export(element : UniElement, loopCount : number) : UTSJSONObject
- export(element : Ref<UniElement | null>, loopCount : number) : UTSJSONObject
- export(element : any | null = null, loopCount : number = 1) : UTSJSONObject {
- this._steps = this.steps
- this.steps = new Step(this.options)
- this.currentStep = this.steps
- this.steps!.onFinish = () => {
- this.onFinish()
- }
- // @ts-ignore
- this.play(element, loopCount)
- return {}
- }
- /**
- * 播放动画
- * @param element 元素,可选
- */
- play() : void
- play(element : UniElement, loopCount : number) : void
- play(element : Ref<UniElement | null>, loopCount : number) : void
- play(element : any | null = null, loopCount : number | null = null) : void {
- if (this._steps != null && this._steps!.isPlaying) return
- const _loopCount = loopCount ?? this._steps?.loop ?? 1
- if (element == null) {
- this._play(null, _loopCount)
- } else if (element instanceof UniElement) {
- this._play(element, _loopCount)
- } else {
- // #ifdef APP-ANDROID
- if (!(element instanceof Ref<UniElement | null>)) return
- // #endif
- // @ts-ignore
- watch(element, (el : UniElement) => {
- this._play(el, _loopCount)
- })
- }
- }
- private _play(element : UniElement | null, loopCount : number = 1) {
- if (this.element == null && element != null) {
- this.element = element
- this.setCacheStyles(this.element!)
- }
- if (this.element == null || this._steps == null || this._steps!.isPlaying) return
- if (this._steps!.currentStepIndex == 0) {
- this.resetStyles(this.element!)
- }
- this._steps!.loop = loopCount
- this._steps!.onUpdate = () => {
- this.resetStyles(this.element!)
- }
- this._steps!.play(this.element!, this._steps!.currentStepIndex)
- }
- /**
- * 停止动画
- */
- stop() {
- this._steps?.stop()
- }
- /**
- * 销毁动画
- */
- destroy() {
- this.stop();
- this.element = null;
- this.steps = null;
- this._steps = null;
- this.currentStep = null;
- this.cacheStyles.clear();
- }
- }
- export function createAnimation(options : CreateAnimationOptions = {} as CreateAnimationOptions) : Animation {
- // #ifdef APP||WEB
- return new Animation(options)
- // #endif
- // #ifndef APP||WEB
- // @ts-ignore
- return uni.createAnimation({ ...(options ?? {}) })
- // #endif
- }
|