idlize

Форк
0
488 строк · 18.8 Кб
1
/*
2
 * Copyright (c) 2022-2024 Huawei Device Co., Ltd.
3
 * Licensed under the Apache License, Version 2.0 (the "License");
4
 * you may not use this file except in compliance with the License.
5
 * You may obtain a copy of the License at
6
 *
7
 * http://www.apache.org/licenses/LICENSE-2.0
8
 *
9
 * Unless required by applicable law or agreed to in writing, software
10
 * distributed under the License is distributed on an "AS IS" BASIS,
11
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
 * See the License for the specific language governing permissions and
13
 * limitations under the License.
14
 */
15

16
import { float64, int32, int64, isFiniteNumber, uint32, uint64 } from "@koalaui/common"
17
import { AnimationRange, NumberAnimationRange } from "./AnimationRange"
18
import { Easing, EasingCurve } from "./Easing"
19
import { scheduleCallback } from "../states/GlobalStateManager"
20

21
export const DEFAULT_ANIMATION_DURATION: uint32 = 300
22

23
/**
24
 * This interface specifies animations in AnimatedState (or MutableAnimatedState).
25
 * Its methods should not be used directly.
26
 */
27
export interface TimeAnimation<Value> {
28
    readonly running: boolean
29
    getValue(time: uint64): Value
30
    onStart(time: uint64): void
31
    onPause(time: uint64): void
32
}
33

34
/**
35
 * This interface allows to control various settings of a duration-based animation.
36
 */
37
export interface AnimationSpec {
38
    readonly duration: uint32
39
    readonly delay: uint32
40
    readonly easing: EasingCurve
41
    readonly onEdge: OnEdge
42
    readonly onPause: OnPause
43
    readonly iterations: int32 | undefined // an amount of iterations of the specified function
44
    readonly onStart: (() => void) | undefined // called when the animation has started
45
    readonly onEnd: (() => void) | undefined // called when the animation has reached the target value
46
    readonly onReset: (() => void) | undefined // called when the animation is reset to the initial value
47
}
48

49
/**
50
 * This enumeration specifies the animation behavior when duration is expired.
51
 */
52
export enum OnEdge {
53
    Nothing, // the value stops changing
54
    Reverse, // the value starts changing to the initial state
55
    Restart, // the value starts changing from the initial state again
56
}
57

58
/**
59
 * This enumeration specifies the animation behavior on pause requested.
60
 */
61
export enum OnPause {
62
    Nothing, // the value stops changing immediately
63
    Reset, // the value resets to the initial state and stops changing
64
    Fade, // the value starts changing to the initial state and then stops changing
65
}
66

67

68
/**
69
 * @param value - value constantly returned by the animation
70
 * @returns animation that always produces the same value
71
 */
72
export function constAnimation<Value>(value: Value): TimeAnimation<Value> {
73
    return new ConstAnimationImpl<Value>(value)
74
}
75

76
/**
77
 * @param compute - value supplier to be computed for every frame
78
 * @param initialTime - initial time value of the animation
79
 * @returns simple time-based animation that starts with 0
80
 */
81
export function timeAnimation<Value>(compute: (time: int64) => Value, initialTime: int64 = 0): TimeAnimation<Value> {
82
    if (!isFiniteNumber(initialTime)) throw new Error("illegal initial time: " + initialTime)
83
    return new TimeAnimationImpl<Value>(compute, initialTime)
84
}
85

86
/**
87
 * Computes smoothly changing values from `from` to `to` with given period in milliseconds.
88
 * Current law of change is sinus.
89
 *
90
 * @param period during which value changes from `from` back to `from`
91
 * @param from which value animation starts, default is 0.0
92
 * @param to which value animation reaches, default is 1.0
93
 * @returns animated value in 0.0 to 1.0 range
94
 */
95
export function smoothAnimation(period: uint32, from: float64 = 0.0, to: float64 = 1.0): TimeAnimation<float64> {
96
    if (!isFiniteNumber(period) || (period < 1)) throw new Error("illegal period: " + period)
97
    if (from >= to) throw new Error("`from` must be smaller than `to`")
98
    return new TimeAnimationImpl<float64>((time: int64): float64 => (1 - Math.cos(time / period * Math.PI)) / 2 * (to - from) + from)
99
}
100

101
/**
102
 * @param period - number of milliseconds after which the animated state toggles boolean value
103
 * @param initialValue - initial blinking state of the animation
104
 * @returns animation of boolean value that is toggled periodically
105
 */
106
export function blinkAnimation(period: uint32, initialValue: boolean = false): TimeAnimation<boolean> {
107
    return periodicAnimation<boolean>(period, (count: int64): boolean => (count % 2) != 0, initialValue ? 1 : 0)
108
}
109

110
/**
111
 * @param period - number of milliseconds after which the animated state increments number value
112
 * @param initialCount - initial counter value of the animation
113
 * @returns animation of integer value that is incremented periodically
114
 */
115
export function countAnimation(period: uint32, initialCount: int64 = 0): TimeAnimation<int64> {
116
    return periodicAnimation<int64>(period, (count: int64): int64 => count, initialCount)
117
}
118

119
/**
120
 * @param period - number of milliseconds after which the animated state computes its value from period counter
121
 * @param compute - value supplier to be computed every `period` milliseconds
122
 * @param initialCount - initial counter value of the animation
123
 * @returns animation of integer value that is computed periodically from counter
124
 */
125
export function periodicAnimation<Value>(period: uint32, compute: (count: int64) => Value, initialCount: int64 = 0): TimeAnimation<Value> {
126
    if (!isFiniteNumber(period) || (period < 1)) throw new Error("illegal time period: " + period)
127
    if (!isFiniteNumber(initialCount)) throw new Error("illegal initial count: " + initialCount)
128
    return new PeriodicAnimationImpl<Value>(0, period, compute, initialCount)
129
}
130

131
/**
132
 * @param delay - number of milliseconds after which the animated state computes its first value
133
 * @param period - number of milliseconds after which the animated state computes its value from period counter
134
 * @param compute - value supplier to be computed every `period` milliseconds
135
 * @param initialCount - initial counter value of the animation
136
 * @returns animation of integer value that is computed periodically from counter
137
 */
138
export function periodicAnimationWithDelay<Value>(delay: uint32, period: uint32, compute: (count: int64) => Value, initialCount: int64 = 0): TimeAnimation<Value> {
139
    if (!isFiniteNumber(period) || (period < 1)) throw new Error("illegal time period: " + period)
140
    if (!isFiniteNumber(delay) || (delay < 1)) throw new Error("illegal time delay: " + delay)
141
    if (!isFiniteNumber(initialCount)) throw new Error("illegal initial count: " + initialCount)
142
    return new PeriodicAnimationImpl<Value>(delay - period, period, compute, initialCount)
143
}
144

145

146
/**
147
 * @param frameTime - array of frame durations in milliseconds
148
 * @param compute - value supplier to be computed when frame index is changed
149
 * @returns frame-based animation
150
 */
151
export function frameAnimation<Value>(frameTime: ReadonlyArray<uint32>, compute: (index: int64) => Value): TimeAnimation<Value> {
152
    const count = frameTime.length
153
    if (count == 1) return constAnimation<Value>(compute(0))
154
    if (count < 2) throw new Error("illegal frames count: " + count)
155
    const time = new Array<uint32>(count)
156
    for (let index = 0; index < count; index++) {
157
        const value = frameTime[index]
158
        if (!isFiniteNumber(value) || (value < 1)) throw new Error("illegal time of frame " + index + ": " + value)
159
        time[index] = index > 0 ? time[index - 1] + value : value
160
    }
161
    return new FrameAnimationImpl<Value>(time, compute)
162
}
163

164

165
/**
166
 * @param spec - the animation specification
167
 * @param from - a first base array that corresponds to the `0` state
168
 * @param to - a second base array that corresponds to the `1` state
169
 * @returns duration-based animation of number value
170
 */
171
export function numberAnimation(spec: Partial<AnimationSpec>, to: float64 = 1.0, from: float64 = 0.0): TimeAnimation<float64> {
172
    return animation<float64>(spec, NumberAnimationRange(from, to))
173
}
174

175
/**
176
 * @param spec - the animation specification
177
 * @param compute - value supplier to be computed when state animated from 0 to 1
178
 * @param initialState - initial inner state of the animation (-1..1]
179
 * @returns duration-based animation
180
 */
181
export function animation<Value>(spec: Partial<AnimationSpec>, compute: AnimationRange<Value>, initialState: float64 = 0): TimeAnimation<Value> {
182
    if (!isFiniteNumber(initialState) || (initialState <= -1) || (initialState > 1)) throw new Error("illegal initial state: " + initialState)
183
    let duration: uint32 = spec.duration ?? DEFAULT_ANIMATION_DURATION
184
    if (!isFiniteNumber(duration) || (duration < 0)) throw new Error("duration must not be negative, but is " + spec.duration)
185
    let delay: uint32 = spec.delay ?? 0
186
    if (!isFiniteNumber(delay) || (delay < 0)) throw new Error("delay must not be negative, but is " + spec.delay)
187
    let easing = spec.easing ?? Easing.Linear
188
    let onEdge = spec.onEdge ?? OnEdge.Nothing
189
    let iterations = spec.iterations
190
    if (iterations !== undefined) {
191
        if (!Number.isInteger(iterations) || (iterations < 1)) throw new Error("iterations must be positive integer, but is " + spec.iterations)
192
        if (onEdge == OnEdge.Reverse) {
193
            easing = Easing.thereAndBackAgain(easing)
194
            duration *= 2
195
        }
196
        if (iterations > 1) {
197
            easing = Easing.repeated(easing, iterations)
198
            duration *= iterations
199
        }
200
        if (onEdge == OnEdge.Restart) {
201
            easing = Easing.restarted(easing)
202
        }
203
        onEdge = OnEdge.Nothing
204
    }
205
    if (duration == 0 && onEdge != OnEdge.Nothing) {
206
        throw new Error("cyclic animation must have a positive duration, but has " + spec.duration)
207
    }
208
    return new AnimationImpl<Value>(
209
        duration, delay, easing, onEdge, spec.onPause ?? OnPause.Nothing,
210
        spec.onStart, spec.onEnd, spec.onReset, compute, initialState)
211
}
212

213

214
/**
215
 * @param duration - duration of state transition from 0 to 1
216
 * @param easing - a way in which a motion tween proceeds
217
 * @param compute - value supplier to be computed when state animated from 0 to 1
218
 * @param initialState - initial inner state of the animation (-1..1]
219
 * @returns duration-based value transition
220
 */
221
export function transition<Value>(duration: uint32, easing: EasingCurve, compute: AnimationRange<Value>, initialState: int64 = 0): TimeAnimation<Value> {
222
    if (!isFiniteNumber(initialState) || (initialState <= -1) || (initialState > 1)) throw new Error("illegal initial state: " + initialState)
223
    if (!isFiniteNumber(duration) || (duration <= 0)) throw new Error("duration must be positive, but is " + duration)
224
    return new AnimationImpl<Value>(duration, 0, easing, OnEdge.Nothing, OnPause.Fade, undefined, undefined, undefined, compute, initialState)
225
}
226

227
/**
228
 * @param duration - duration of state transition from 0 to 1
229
 * @param compute - value supplier to be computed when state animated from 0 to 1
230
 * @param initialState - initial inner state of the animation (-1..1]
231
 * @returns duration-based value transition
232
 */
233
export function linearTransition<Value>(duration: uint32, compute: AnimationRange<Value>, initialState: int64 = 0): TimeAnimation<Value> {
234
    return transition<Value>(duration, Easing.Linear, compute, initialState)
235
}
236

237

238
// IMPLEMENTATION DETAILS: DO NOT USE IT DIRECTLY
239

240

241
class TimeAnimationImpl<Value> implements TimeAnimation<Value> {
242
    private startTime: uint64 | undefined
243
    private lastState: int64
244
    private lastValue: Value
245
    private readonly compute: (time: int64) => Value
246

247
    running = false
248

249
    constructor(compute: (time: int64) => Value, initial: int64 = 0) {
250
        this.lastState = initial
251
        this.lastValue = compute(initial)
252
        this.compute = compute
253
    }
254

255
    get state(): int64 {
256
        return this.lastState
257
    }
258

259
    getState(startTime: uint64, currentTime: uint64): int64 {
260
        this.startTime = currentTime
261
        return this.lastState + (currentTime - startTime)
262
    }
263

264
    getValue(time: uint64): Value {
265
        if (this.startTime === undefined) return this.lastValue // paused
266
        if (this.startTime! >= time) return this.lastValue // delayed
267
        const state = this.getState(this.startTime!, time)
268
        if (this.lastState == state) return this.lastValue // not changed
269
        this.lastState = state
270
        this.lastValue = this.compute(state)
271
        return this.lastValue
272
    }
273

274
    onStart(time: uint64): void {
275
        this.startTime = time
276
        this.running = true
277
    }
278

279
    onPause(time: uint64): void {
280
        this.startTime = undefined
281
        this.running = false
282
    }
283
}
284

285

286
class PeriodicAnimationImpl<Value> extends TimeAnimationImpl<Value> {
287
    private readonly period: uint32
288
    private readonly delay: int32
289

290
    constructor(delay: int32, period: uint32, compute: (count: int64) => Value, initial: int64 = 0) {
291
        super(compute, initial)
292
        this.period = period
293
        this.delay = delay
294
    }
295

296
    getState(startTime: uint64, currentTime: uint64): int64 {
297
        let result = this.state
298
        let passedTime = currentTime - startTime
299
        if (passedTime > this.period) {
300
            result += Math.floor(passedTime / this.period)
301
            passedTime = passedTime % this.period
302
            // tune start time for long animations
303
            super.onStart(currentTime - passedTime)
304
        }
305
        return result
306
    }
307

308
    onStart(time: uint64) {
309
        super.onStart(time + this.delay)
310
    }
311
}
312

313

314
class ConstAnimationImpl<Value> implements TimeAnimation<Value> {
315
    private lastValue: Value
316

317
    constructor(value: Value) {
318
        this.lastValue = value
319
    }
320

321
    readonly running: boolean = false
322

323
    getValue(time: uint64): Value {
324
        return this.lastValue
325
    }
326

327
    onStart(time: uint64): void {
328
    }
329

330
    onPause(time: uint64): void {
331
    }
332
}
333

334

335
class FrameAnimationImpl<Value> extends TimeAnimationImpl<Value> {
336
    private readonly time: ReadonlyArray<uint32>
337

338
    constructor(time: ReadonlyArray<uint32>, compute: (time: int64) => Value) {
339
        super(compute)
340
        this.time = time
341
    }
342

343
    getState(startTime: uint64, currentTime: uint64): int64 {
344
        const cycleTime = this.time[this.time.length - 1]
345

346
        let passedTime = currentTime - startTime
347
        if (passedTime > cycleTime) {
348
            passedTime = passedTime % cycleTime
349
            // tune start time for long animations
350
            super.onStart(currentTime - passedTime)
351
        }
352
        for (let index = 0; index < this.time.length; index++) {
353
            if (passedTime < this.time[index]) return index
354
        }
355
        return 0
356
    }
357
}
358

359

360
class AnimationImpl<Value> implements TimeAnimation<Value> {
361
    private startTime: uint64 | undefined
362
    private lastState: float64
363
    private lastValue: Value
364
    private readonly duration: uint32
365
    private readonly delay: uint32
366
    private readonly onEdgePolicy: OnEdge
367
    private readonly onPausePolicy: OnPause
368
    private readonly onStartCallback: (() => void) | undefined
369
    private readonly onEndCallback: (() => void) | undefined
370
    private readonly onResetCallback: (() => void) | undefined
371
    private readonly compute: AnimationRange<Value>
372
    private isPauseRequested = false
373

374
    running = false
375

376
    constructor(
377
        duration: uint32,
378
        delay: uint32,
379
        easing: EasingCurve,
380
        onEdge: OnEdge,
381
        onPause: OnPause,
382
        onStart: (() => void) | undefined,
383
        onEnd: (() => void) | undefined,
384
        onReset: (() => void) | undefined,
385
        compute: AnimationRange<Value>,
386
        initial: float64 = 0
387
    ) {
388
        this.duration = duration
389
        this.delay = delay
390
        this.onEdgePolicy = onEdge
391
        this.onPausePolicy = onPause
392
        this.onStartCallback = onStart
393
        this.onEndCallback = onEnd
394
        this.onResetCallback = onReset
395
        this.compute = (state: float64): Value => compute(easing(Math.abs(state)))
396
        this.lastState = initial
397
        this.lastValue = this.compute(initial)
398
    }
399

400
    getState(startTime: uint64, currentTime: uint64): float64 {
401
        const onPause = this.isPauseRequested ? this.onPausePolicy : OnPause.Nothing
402
        if (onPause == OnPause.Reset) return this.onPauseReset() // pause
403
        if (this.duration == 0) return this.onEdgeReached() // stop immediately
404

405
        const cycleTime: uint64 = onPause == OnPause.Fade || this.onEdgePolicy == OnEdge.Reverse
406
            ? this.duration * 2
407
            : this.duration
408

409
        let passedTime = currentTime - startTime
410
        if (passedTime > cycleTime) {
411
            if (onPause == OnPause.Fade) return this.onPauseReset() // fade stopped
412
            if (this.onEdgePolicy == OnEdge.Nothing) return this.onEdgeReached() // stop on the edge
413
            passedTime = passedTime % cycleTime
414
            // tune start time for long animations
415
            this.running = true
416
            this.startTime = currentTime - passedTime
417
        }
418
        let state = passedTime / this.duration
419
        return state > 1 ? state - 2 : state
420
    }
421

422
    getValue(time: uint64): Value {
423
        if (this.startTime === undefined) return this.lastValue // paused
424
        if (this.startTime! > time) return this.lastValue // delayed
425
        const state = this.getState(this.startTime!, time)
426
        if (this.lastState == state) return this.lastValue // not changed
427
        this.lastState = state
428
        this.lastValue = this.compute(state)
429
        return this.lastValue
430
    }
431

432
    onStart(time: uint64): void {
433
        scheduleCallback(this.onStartCallback)
434
        if (this.isPauseRequested) {
435
            this.isPauseRequested = false
436
            if (this.lastState < 0) {
437
                // tune start time on direction change
438
                this.running = true
439
                this.startTime = time + this.lastState * this.duration
440
            }
441
        }
442
        else {
443
            // set start time to continue animation from the current state
444
            this.running = true
445
            this.startTime = time - (
446
                this.lastState < 0
447
                    ? (2 + this.lastState) * this.duration
448
                    : this.lastState > 0
449
                        ? this.lastState * this.duration
450
                        : (0 - this.delay) // add delay for the state 0 only
451
            )
452
        }
453
    }
454

455
    onPause(time: uint64): void {
456
        if (this.lastState && this.onPausePolicy == OnPause.Reset) {
457
            this.isPauseRequested = true
458
        }
459
        else if (this.lastState && this.onPausePolicy == OnPause.Fade) {
460
            this.isPauseRequested = true
461
            if (this.lastState > 0) {
462
                // tune start time on direction change
463
                this.running = true
464
                this.startTime = time - this.duration * (2 - this.lastState)
465
            }
466
        }
467
        else {
468
            this.running = false
469
            this.startTime = undefined
470
        }
471
    }
472

473
    private onPauseReset(): float64 {
474
        this.running = false
475
        this.startTime = undefined
476
        this.isPauseRequested = false
477
        scheduleCallback(this.onResetCallback)
478
        return 0
479
    }
480

481
    private onEdgeReached(): float64 {
482
        if (this.running) {
483
            this.running = false
484
            scheduleCallback(this.onEndCallback)
485
        }
486
        return 1
487
    }
488
}
489

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.