@rafaelaugusto/jolt 0.1.0-beta.10 → 0.1.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/driver/android-gradient.d.ts +5 -0
- package/dist/driver/android-gradient.d.ts.map +1 -0
- package/dist/driver/android-gradient.js +89 -0
- package/dist/driver/android-gradient.js.map +1 -0
- package/dist/driver/ios-gradient.d.ts +5 -0
- package/dist/driver/ios-gradient.d.ts.map +1 -0
- package/dist/driver/ios-gradient.js +109 -0
- package/dist/driver/ios-gradient.js.map +1 -0
- package/dist/gradient.d.ts +90 -0
- package/dist/gradient.d.ts.map +1 -0
- package/dist/gradient.js +247 -0
- package/dist/gradient.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/motion-scope.d.ts +1 -1
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-states.d.ts +9 -2
- package/dist/motion-states.d.ts.map +1 -1
- package/dist/motion-states.js +64 -9
- package/dist/motion-states.js.map +1 -1
- package/dist/transition.d.ts +15 -0
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +17 -0
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +195 -0
- package/platforms/android/proguard-rules.pro +2 -0
- package/src/android-plugin.d.ts +29 -0
- package/src/driver/android-gradient.ts +109 -0
- package/src/driver/ios-gradient.ts +114 -0
- package/src/gradient.ts +372 -0
- package/src/index.ts +16 -2
- package/src/motion-scope.ts +2 -2
- package/src/motion-states.ts +73 -19
- package/src/transition.ts +30 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Utils, type View } from '@nativescript/core'
|
|
2
|
+
import type { GradientFrame, GradientPainter, GradientType } from '../gradient.js'
|
|
3
|
+
|
|
4
|
+
const floats = (values: readonly number[]) => {
|
|
5
|
+
const array = Array.create('float', values.length)
|
|
6
|
+
values.forEach((value, i) => (array[i] = value))
|
|
7
|
+
return array
|
|
8
|
+
}
|
|
9
|
+
const ints = (values: readonly number[]) => {
|
|
10
|
+
const array = Array.create('int', values.length)
|
|
11
|
+
values.forEach((value, i) => (array[i] = value | 0))
|
|
12
|
+
return array
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A gradient drawable stacked on the view's own background: above its color, below its children. */
|
|
16
|
+
export function androidGradient(
|
|
17
|
+
view: View,
|
|
18
|
+
type: GradientType,
|
|
19
|
+
): GradientPainter | undefined {
|
|
20
|
+
let layer: org.rafaelaugusto.jolt.GradientLayer
|
|
21
|
+
try {
|
|
22
|
+
layer = new org.rafaelaugusto.jolt.GradientLayer(
|
|
23
|
+
type === 'linear' ? 0 : type === 'radial' ? 1 : 2,
|
|
24
|
+
)
|
|
25
|
+
} catch {
|
|
26
|
+
// A JS-only update cannot install the native plugin.
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
let stack: android.graphics.drawable.LayerDrawable | undefined
|
|
30
|
+
let queued = false
|
|
31
|
+
|
|
32
|
+
const wrap = () => {
|
|
33
|
+
const native = view.nativeViewProtected as android.view.View | undefined
|
|
34
|
+
if (!native) return
|
|
35
|
+
const corner = typeof view.borderRadius === 'number' ? view.borderRadius : 0
|
|
36
|
+
layer.setCorner(corner * Utils.layout.getDisplayDensity())
|
|
37
|
+
const background = native.getBackground()
|
|
38
|
+
if (stack && background === stack) return
|
|
39
|
+
const layers = Array.create(android.graphics.drawable.Drawable, 2)
|
|
40
|
+
layers[0] = background ?? new android.graphics.drawable.ColorDrawable(0)
|
|
41
|
+
layers[1] = layer
|
|
42
|
+
stack = new android.graphics.drawable.LayerDrawable(layers)
|
|
43
|
+
native.setBackground(stack)
|
|
44
|
+
}
|
|
45
|
+
// Core replaces the background drawable when its style changes; stack on the new one.
|
|
46
|
+
const rewrap = () => {
|
|
47
|
+
if (queued) return
|
|
48
|
+
queued = true
|
|
49
|
+
void Promise.resolve().then(() => {
|
|
50
|
+
queued = false
|
|
51
|
+
wrap()
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
view.on('loaded', wrap)
|
|
55
|
+
view.style.on('backgroundInternalChange', rewrap)
|
|
56
|
+
view.style.on('borderTopLeftRadiusChange', rewrap)
|
|
57
|
+
wrap()
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
paint(frame: GradientFrame) {
|
|
61
|
+
layer.paint(
|
|
62
|
+
ints(frame.colors),
|
|
63
|
+
floats(frame.stops),
|
|
64
|
+
frame.angle,
|
|
65
|
+
frame.x,
|
|
66
|
+
frame.y,
|
|
67
|
+
frame.radius,
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
play(frames, times, timing, onEnd) {
|
|
71
|
+
let active = true
|
|
72
|
+
layer.play(
|
|
73
|
+
floats(times),
|
|
74
|
+
ints(frames.flatMap((frame) => frame.colors)),
|
|
75
|
+
floats(frames.flatMap((frame) => frame.stops)),
|
|
76
|
+
floats(frames.flatMap((frame) => [frame.angle, frame.x, frame.y, frame.radius])),
|
|
77
|
+
Math.round(timing.duration),
|
|
78
|
+
Math.round(timing.delay),
|
|
79
|
+
timing.repeat === Infinity ? -1 : timing.repeat,
|
|
80
|
+
timing.reverse,
|
|
81
|
+
new org.rafaelaugusto.jolt.GradientLayer.Listener({
|
|
82
|
+
onEnd: (finished: boolean) => {
|
|
83
|
+
if (!active) return
|
|
84
|
+
active = false
|
|
85
|
+
onEnd(finished)
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
return {
|
|
90
|
+
cancel() {
|
|
91
|
+
if (!active) return
|
|
92
|
+
active = false
|
|
93
|
+
layer.cancel()
|
|
94
|
+
onEnd(false)
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
remove() {
|
|
99
|
+
view.off('loaded', wrap)
|
|
100
|
+
view.style.off('backgroundInternalChange', rewrap)
|
|
101
|
+
view.style.off('borderTopLeftRadiusChange', rewrap)
|
|
102
|
+
layer.cancel()
|
|
103
|
+
const native = view.nativeViewProtected as android.view.View | undefined
|
|
104
|
+
if (native && stack && native.getBackground() === stack)
|
|
105
|
+
native.setBackground(stack.getDrawable(0))
|
|
106
|
+
stack = undefined
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Color, type View } from '@nativescript/core'
|
|
2
|
+
import {
|
|
3
|
+
gradientPoints,
|
|
4
|
+
type GradientFrame,
|
|
5
|
+
type GradientPainter,
|
|
6
|
+
type GradientType,
|
|
7
|
+
} from '../gradient.js'
|
|
8
|
+
|
|
9
|
+
const keys = ['colors', 'locations', 'startPoint', 'endPoint'] as const
|
|
10
|
+
|
|
11
|
+
/** A CAGradientLayer at the back of the view's layer: above its background, below its children. */
|
|
12
|
+
export function iosGradient(view: View, type: GradientType): GradientPainter {
|
|
13
|
+
const layer = CAGradientLayer.new()
|
|
14
|
+
layer.type =
|
|
15
|
+
type === 'linear'
|
|
16
|
+
? kCAGradientLayerAxial
|
|
17
|
+
: type === 'radial'
|
|
18
|
+
? kCAGradientLayerRadial
|
|
19
|
+
: kCAGradientLayerConic
|
|
20
|
+
let current: GradientFrame | undefined
|
|
21
|
+
let generation = 0
|
|
22
|
+
|
|
23
|
+
const native = () => view.nativeViewProtected as UIView | undefined
|
|
24
|
+
const size = () => {
|
|
25
|
+
const bounds = native()?.layer.bounds
|
|
26
|
+
return { width: bounds?.size.width ?? 0, height: bounds?.size.height ?? 0 }
|
|
27
|
+
}
|
|
28
|
+
const values = (frame: GradientFrame) => {
|
|
29
|
+
const { width, height } = size()
|
|
30
|
+
const { start, end: toward } = gradientPoints(type, frame, width, height)
|
|
31
|
+
// Core Animation starts a conic gradient on the side opposite its end point.
|
|
32
|
+
const end =
|
|
33
|
+
type === 'conic' ? { x: 2 * start.x - toward.x, y: 2 * start.y - toward.y } : toward
|
|
34
|
+
return {
|
|
35
|
+
colors: NSArray.arrayWithArray(
|
|
36
|
+
frame.colors.map((argb) => new Color(argb).ios.CGColor),
|
|
37
|
+
),
|
|
38
|
+
locations: NSArray.arrayWithArray(frame.stops),
|
|
39
|
+
startPoint: NSValue.valueWithCGPoint(CGPointMake(start.x, start.y)),
|
|
40
|
+
endPoint: NSValue.valueWithCGPoint(CGPointMake(end.x, end.y)),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const attach = () => {
|
|
44
|
+
const host = native()
|
|
45
|
+
if (!host) return
|
|
46
|
+
CATransaction.begin()
|
|
47
|
+
CATransaction.setDisableActions(true)
|
|
48
|
+
if (layer.superlayer !== host.layer) host.layer.insertSublayerAtIndex(layer, 0)
|
|
49
|
+
layer.frame = host.layer.bounds
|
|
50
|
+
layer.cornerRadius = host.layer.cornerRadius
|
|
51
|
+
if (current) {
|
|
52
|
+
const model = values(current)
|
|
53
|
+
layer.colors = model.colors
|
|
54
|
+
layer.locations = model.locations
|
|
55
|
+
layer.startPoint = model.startPoint.CGPointValue
|
|
56
|
+
layer.endPoint = model.endPoint.CGPointValue
|
|
57
|
+
}
|
|
58
|
+
CATransaction.commit()
|
|
59
|
+
}
|
|
60
|
+
view.on('loaded', attach)
|
|
61
|
+
view.on('layoutChanged', attach)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
paint(frame) {
|
|
65
|
+
current = frame
|
|
66
|
+
generation++
|
|
67
|
+
for (const key of keys) layer.removeAnimationForKey(`jolt:gradient:${key}`)
|
|
68
|
+
attach()
|
|
69
|
+
},
|
|
70
|
+
play(frames, times, timing, onEnd) {
|
|
71
|
+
const instance = ++generation
|
|
72
|
+
current = frames[frames.length - 1]!
|
|
73
|
+
attach()
|
|
74
|
+
const sampled = frames.map(values)
|
|
75
|
+
const now = CACurrentMediaTime()
|
|
76
|
+
CATransaction.begin()
|
|
77
|
+
if (timing.repeat !== Infinity)
|
|
78
|
+
CATransaction.setCompletionBlock(() => {
|
|
79
|
+
if (instance === generation) onEnd(true)
|
|
80
|
+
})
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
const animation = CAKeyframeAnimation.animationWithKeyPath(key)
|
|
83
|
+
animation.values = NSArray.arrayWithArray(sampled.map((value) => value[key]))
|
|
84
|
+
animation.keyTimes = NSArray.arrayWithArray(times as number[])
|
|
85
|
+
animation.duration = Math.max(timing.duration, 1) / 1000
|
|
86
|
+
animation.beginTime = layer.convertTimeFromLayer(now + timing.delay / 1000, null)
|
|
87
|
+
animation.fillMode = kCAFillModeBackwards
|
|
88
|
+
if (timing.repeat > 0) {
|
|
89
|
+
animation.autoreverses = timing.reverse
|
|
90
|
+
animation.repeatCount = timing.reverse
|
|
91
|
+
? (timing.repeat + 1) / 2
|
|
92
|
+
: timing.repeat + 1
|
|
93
|
+
}
|
|
94
|
+
layer.addAnimationForKey(animation, `jolt:gradient:${key}`)
|
|
95
|
+
}
|
|
96
|
+
CATransaction.commit()
|
|
97
|
+
return {
|
|
98
|
+
cancel() {
|
|
99
|
+
if (instance !== generation) return
|
|
100
|
+
generation++
|
|
101
|
+
for (const key of keys) layer.removeAnimationForKey(`jolt:gradient:${key}`)
|
|
102
|
+
onEnd(false)
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
remove() {
|
|
107
|
+
generation++
|
|
108
|
+
view.off('loaded', attach)
|
|
109
|
+
view.off('layoutChanged', attach)
|
|
110
|
+
layer.removeAllAnimations()
|
|
111
|
+
layer.removeFromSuperlayer()
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/gradient.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { isAndroid, isIOS, type View } from '@nativescript/core'
|
|
2
|
+
import { toView } from './animate.js'
|
|
3
|
+
import { colorMixer, parseColor, type ColorValue } from './color.js'
|
|
4
|
+
import { reducedMotion } from './config.js'
|
|
5
|
+
import { getDriver } from './driver/index.js'
|
|
6
|
+
import { androidGradient } from './driver/android-gradient.js'
|
|
7
|
+
import { iosGradient } from './driver/ios-gradient.js'
|
|
8
|
+
import type { Animation } from './engine.js'
|
|
9
|
+
import { createTrack, type Track } from './track.js'
|
|
10
|
+
import {
|
|
11
|
+
resolveTransition,
|
|
12
|
+
springs,
|
|
13
|
+
type SpringName,
|
|
14
|
+
type Transition,
|
|
15
|
+
} from './transition.js'
|
|
16
|
+
import type { Target } from './types.js'
|
|
17
|
+
|
|
18
|
+
export type GradientType = 'linear' | 'radial' | 'conic'
|
|
19
|
+
|
|
20
|
+
export interface GradientValues {
|
|
21
|
+
/** Two to eight colors. Animating keeps the count. */
|
|
22
|
+
colors?: readonly ColorValue[]
|
|
23
|
+
/** Where each color sits, from 0 to 1. Evenly spaced by default. */
|
|
24
|
+
stops?: readonly number[]
|
|
25
|
+
/** Degrees, like CSS: `linear` points this way (180 runs top to bottom), `conic` starts here (0 is up). */
|
|
26
|
+
angle?: number
|
|
27
|
+
/** `radial` and `conic`: the center, in fractions of the view's width and height. */
|
|
28
|
+
center?: { readonly x: number; readonly y: number }
|
|
29
|
+
/** `radial`: the radius, in fractions of the view's shorter side. */
|
|
30
|
+
radius?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GradientOptions extends GradientValues {
|
|
34
|
+
type: GradientType
|
|
35
|
+
colors: readonly ColorValue[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Gradient {
|
|
39
|
+
readonly type: GradientType
|
|
40
|
+
/** Animates to new values from the ones on screen. Colors and stops keep their count. */
|
|
41
|
+
animate(values: GradientValues, transition?: Transition | SpringName): Animation
|
|
42
|
+
/** Applies values at once, stopping the animation in progress. */
|
|
43
|
+
set(values: GradientValues): void
|
|
44
|
+
/** Stops the animation in progress where it is. */
|
|
45
|
+
stop(): void
|
|
46
|
+
/** Takes the gradient off the view. */
|
|
47
|
+
remove(): void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Everything the platform needs to paint one frame. */
|
|
51
|
+
export interface GradientFrame {
|
|
52
|
+
colors: number[]
|
|
53
|
+
stops: number[]
|
|
54
|
+
angle: number
|
|
55
|
+
x: number
|
|
56
|
+
y: number
|
|
57
|
+
radius: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface GradientTiming {
|
|
61
|
+
duration: number
|
|
62
|
+
delay: number
|
|
63
|
+
repeat: number
|
|
64
|
+
reverse: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface GradientPainter {
|
|
68
|
+
paint(frame: GradientFrame): void
|
|
69
|
+
/** Plays sampled frames natively; `onEnd(false)` when cancelled. */
|
|
70
|
+
play(
|
|
71
|
+
frames: readonly GradientFrame[],
|
|
72
|
+
times: readonly number[],
|
|
73
|
+
timing: GradientTiming,
|
|
74
|
+
onEnd: (finished: boolean) => void,
|
|
75
|
+
): { cancel(): void }
|
|
76
|
+
remove(): void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type PainterFactory = (view: View, type: GradientType) => GradientPainter | undefined
|
|
80
|
+
|
|
81
|
+
let factory: PainterFactory | undefined
|
|
82
|
+
|
|
83
|
+
/** Test seam, like `setDriver`. */
|
|
84
|
+
export function setGradientPainter(next: PainterFactory | undefined) {
|
|
85
|
+
factory = next
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function platformPainter(view: View, type: GradientType) {
|
|
89
|
+
if (isIOS) return iosGradient(view, type)
|
|
90
|
+
if (isAndroid) return androidGradient(view, type)
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const types: readonly GradientType[] = ['linear', 'radial', 'conic']
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Paints a native gradient behind a layout's children and animates it.
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* const glow = gradient(halo, { type: 'radial', colors: ['#FF0080', '#7928CA00'], radius: 0.8 })
|
|
101
|
+
* glow.animate({ center: { x: 0.8, y: 0.2 }, radius: 1.2 }, { duration: 2000 })
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function gradient(target: Target, options: GradientOptions): Gradient {
|
|
105
|
+
const view = toView(target)
|
|
106
|
+
if (!options || !types.includes(options.type))
|
|
107
|
+
throw new TypeError(`Gradient type must be 'linear', 'radial' or 'conic'.`)
|
|
108
|
+
const first = resolve(
|
|
109
|
+
{ colors: [], stops: [], angle: 180, x: 0.5, y: 0.5, radius: 0.5 },
|
|
110
|
+
options,
|
|
111
|
+
false,
|
|
112
|
+
)
|
|
113
|
+
return new GradientController(view, options.type, first)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class GradientController implements Gradient {
|
|
117
|
+
private painter: GradientPainter | undefined
|
|
118
|
+
private pending: GradientFrame | undefined
|
|
119
|
+
private removed = false
|
|
120
|
+
private model: GradientFrame
|
|
121
|
+
private running:
|
|
122
|
+
| {
|
|
123
|
+
from: GradientFrame
|
|
124
|
+
to: GradientFrame
|
|
125
|
+
track: Track
|
|
126
|
+
startedAt: number
|
|
127
|
+
handle: { cancel(): void }
|
|
128
|
+
settle: (finished: boolean) => void
|
|
129
|
+
}
|
|
130
|
+
| undefined
|
|
131
|
+
|
|
132
|
+
constructor(
|
|
133
|
+
private readonly view: View,
|
|
134
|
+
readonly type: GradientType,
|
|
135
|
+
frame: GradientFrame,
|
|
136
|
+
) {
|
|
137
|
+
this.model = frame
|
|
138
|
+
this.attach((factory ?? platformPainter)(view, type))
|
|
139
|
+
view.on('unloaded', this.unloaded)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private unloaded = () => this.stop()
|
|
143
|
+
|
|
144
|
+
private attach(painter: GradientPainter | undefined) {
|
|
145
|
+
if (!painter) return
|
|
146
|
+
if (this.removed) return painter.remove()
|
|
147
|
+
this.painter = painter
|
|
148
|
+
painter.paint(this.pending ?? this.model)
|
|
149
|
+
this.pending = undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
animate(values: GradientValues, transition?: Transition | SpringName): Animation {
|
|
153
|
+
this.assertActive()
|
|
154
|
+
const from = this.presented()
|
|
155
|
+
const to = resolve(from, values, true)
|
|
156
|
+
const resolved =
|
|
157
|
+
typeof transition === 'string' ? springOf(transition) : (transition ?? {})
|
|
158
|
+
const track = createTrack(
|
|
159
|
+
'opacity',
|
|
160
|
+
[0, 1],
|
|
161
|
+
resolveTransition(resolved, 'opacity', 2),
|
|
162
|
+
0,
|
|
163
|
+
)
|
|
164
|
+
const reduced = reducedMotion()
|
|
165
|
+
const frameAt = (progress: number) => mix(from, to, progress, reduced)
|
|
166
|
+
this.cancelRunning()
|
|
167
|
+
this.model = frameAt(track.to)
|
|
168
|
+
|
|
169
|
+
let settle!: (finished: boolean) => void
|
|
170
|
+
const finished = new Promise<boolean>((resolve) => (settle = resolve))
|
|
171
|
+
const animation: Animation = {
|
|
172
|
+
finished,
|
|
173
|
+
stop: () => {
|
|
174
|
+
if (this.running?.settle === settle) this.stop()
|
|
175
|
+
},
|
|
176
|
+
then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
|
|
177
|
+
}
|
|
178
|
+
if (track.end === 0 || !this.painter) {
|
|
179
|
+
this.paint(this.model)
|
|
180
|
+
settle(true)
|
|
181
|
+
return animation
|
|
182
|
+
}
|
|
183
|
+
const handle = this.painter.play(
|
|
184
|
+
track.values.map(frameAt),
|
|
185
|
+
track.times,
|
|
186
|
+
{
|
|
187
|
+
duration: track.duration,
|
|
188
|
+
delay: track.delay,
|
|
189
|
+
repeat: track.repeat,
|
|
190
|
+
reverse: track.reverse,
|
|
191
|
+
},
|
|
192
|
+
(completed) => {
|
|
193
|
+
if (this.running?.settle !== settle) return
|
|
194
|
+
this.running = undefined
|
|
195
|
+
settle(completed)
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
this.running = {
|
|
199
|
+
from,
|
|
200
|
+
to,
|
|
201
|
+
track,
|
|
202
|
+
startedAt: getDriver().now(),
|
|
203
|
+
handle,
|
|
204
|
+
settle,
|
|
205
|
+
}
|
|
206
|
+
return animation
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
set(values: GradientValues) {
|
|
210
|
+
this.assertActive()
|
|
211
|
+
const next = resolve(this.presented(), values, false)
|
|
212
|
+
this.cancelRunning()
|
|
213
|
+
this.model = next
|
|
214
|
+
this.paint(next)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
stop() {
|
|
218
|
+
if (!this.running) return
|
|
219
|
+
const current = this.presented()
|
|
220
|
+
this.cancelRunning()
|
|
221
|
+
this.model = current
|
|
222
|
+
this.paint(current)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
remove() {
|
|
226
|
+
if (this.removed) return
|
|
227
|
+
this.cancelRunning()
|
|
228
|
+
this.removed = true
|
|
229
|
+
this.view.off('unloaded', this.unloaded)
|
|
230
|
+
this.painter?.remove()
|
|
231
|
+
this.painter = undefined
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private paint(frame: GradientFrame) {
|
|
235
|
+
if (this.painter) this.painter.paint(frame)
|
|
236
|
+
else this.pending = frame
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private presented(): GradientFrame {
|
|
240
|
+
const running = this.running
|
|
241
|
+
if (!running) return this.model
|
|
242
|
+
const elapsed = getDriver().now() - running.startedAt
|
|
243
|
+
return mix(running.from, running.to, running.track.valueAt(elapsed), reducedMotion())
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private cancelRunning() {
|
|
247
|
+
const running = this.running
|
|
248
|
+
if (!running) return
|
|
249
|
+
this.running = undefined
|
|
250
|
+
running.handle.cancel()
|
|
251
|
+
running.settle(false)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private assertActive() {
|
|
255
|
+
if (this.removed) throw new Error('This gradient was removed.')
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function springOf(name: SpringName): Transition {
|
|
260
|
+
const spring = springs[name]
|
|
261
|
+
if (!spring || !Object.hasOwn(springs, name))
|
|
262
|
+
throw new Error(`Unknown spring "${name}".`)
|
|
263
|
+
return spring
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Validates values and fills in what they leave out from `base`. */
|
|
267
|
+
function resolve(
|
|
268
|
+
base: GradientFrame,
|
|
269
|
+
values: GradientValues,
|
|
270
|
+
sameCount: boolean,
|
|
271
|
+
): GradientFrame {
|
|
272
|
+
if (!values || typeof values !== 'object')
|
|
273
|
+
throw new TypeError('Gradient values must be an object.')
|
|
274
|
+
const colors = values.colors ? values.colors.map(parseColor) : base.colors
|
|
275
|
+
if (colors.length < 2 || colors.length > 8)
|
|
276
|
+
throw new RangeError('A gradient needs between two and eight colors.')
|
|
277
|
+
if (sameCount && values.colors && colors.length !== base.colors.length)
|
|
278
|
+
throw new RangeError(
|
|
279
|
+
`Animating a gradient keeps its ${base.colors.length} colors; use set() to change the count.`,
|
|
280
|
+
)
|
|
281
|
+
const stops = values.stops
|
|
282
|
+
? [...values.stops]
|
|
283
|
+
: values.colors || base.stops.length !== colors.length
|
|
284
|
+
? colors.map((_, i) => i / (colors.length - 1))
|
|
285
|
+
: base.stops
|
|
286
|
+
if (
|
|
287
|
+
stops.length !== colors.length ||
|
|
288
|
+
stops.some(
|
|
289
|
+
(stop, i) =>
|
|
290
|
+
!Number.isFinite(stop) || stop < 0 || stop > 1 || stop < (stops[i - 1] ?? 0),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
throw new RangeError(
|
|
294
|
+
'Gradient stops need one value per color, from 0 to 1, without going back.',
|
|
295
|
+
)
|
|
296
|
+
const angle = values.angle ?? base.angle
|
|
297
|
+
const x = values.center?.x ?? base.x
|
|
298
|
+
const y = values.center?.y ?? base.y
|
|
299
|
+
const radius = values.radius ?? base.radius
|
|
300
|
+
for (const [name, value] of [
|
|
301
|
+
['angle', angle],
|
|
302
|
+
['center', x],
|
|
303
|
+
['center', y],
|
|
304
|
+
['radius', radius],
|
|
305
|
+
] as const)
|
|
306
|
+
if (!Number.isFinite(value))
|
|
307
|
+
throw new RangeError(`Invalid gradient ${name}: ${value}.`)
|
|
308
|
+
if (radius < 0)
|
|
309
|
+
throw new RangeError(`Gradient radius must not be negative, got ${radius}.`)
|
|
310
|
+
return { colors, stops, angle, x, y, radius }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** One frame between two states. Geometry follows the curve, overshoot included; colors and stops stay between their ends. */
|
|
314
|
+
export function mix(
|
|
315
|
+
from: GradientFrame,
|
|
316
|
+
to: GradientFrame,
|
|
317
|
+
progress: number,
|
|
318
|
+
reduced = false,
|
|
319
|
+
): GradientFrame {
|
|
320
|
+
const p = Math.min(Math.max(progress, 0), 1)
|
|
321
|
+
const g = reduced ? 1 : progress
|
|
322
|
+
const lerp = (a: number, b: number, t: number) => a + (b - a) * t
|
|
323
|
+
return {
|
|
324
|
+
colors: to.colors.map((color, i) => colorMixer(from.colors[i] ?? color, color)(p)),
|
|
325
|
+
stops: to.stops.map((stop, i) => lerp(from.stops[i] ?? stop, stop, p)),
|
|
326
|
+
angle: lerp(from.angle, to.angle, g),
|
|
327
|
+
x: lerp(from.x, to.x, g),
|
|
328
|
+
y: lerp(from.y, to.y, g),
|
|
329
|
+
radius: Math.max(0, lerp(from.radius, to.radius, g)),
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The points a platform gradient needs, in unit coordinates of a `width` × `height` box.
|
|
335
|
+
* Linear follows CSS: the line runs through the center at `angle` and is long enough
|
|
336
|
+
* for the corners to get the first and last colors.
|
|
337
|
+
*/
|
|
338
|
+
export function gradientPoints(
|
|
339
|
+
type: GradientType,
|
|
340
|
+
frame: GradientFrame,
|
|
341
|
+
width: number,
|
|
342
|
+
height: number,
|
|
343
|
+
) {
|
|
344
|
+
const radians = (frame.angle * Math.PI) / 180
|
|
345
|
+
const sin = Math.sin(radians)
|
|
346
|
+
const cos = Math.cos(radians)
|
|
347
|
+
if (type === 'linear') {
|
|
348
|
+
const length = Math.abs(width * sin) + Math.abs(height * cos)
|
|
349
|
+
const dx = width > 0 ? (sin * length) / (2 * width) : 0
|
|
350
|
+
const dy = height > 0 ? (-cos * length) / (2 * height) : 0
|
|
351
|
+
return { start: { x: 0.5 - dx, y: 0.5 - dy }, end: { x: 0.5 + dx, y: 0.5 + dy } }
|
|
352
|
+
}
|
|
353
|
+
const center = { x: frame.x, y: frame.y }
|
|
354
|
+
const shorter = Math.min(width, height)
|
|
355
|
+
if (type === 'radial') {
|
|
356
|
+
const r = frame.radius * shorter
|
|
357
|
+
return {
|
|
358
|
+
start: center,
|
|
359
|
+
end: {
|
|
360
|
+
x: center.x + (width > 0 ? r / width : 0),
|
|
361
|
+
y: center.y + (height > 0 ? r / height : 0),
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
start: center,
|
|
367
|
+
end: {
|
|
368
|
+
x: center.x + (width > 0 ? (sin * shorter) / (2 * width) : 0),
|
|
369
|
+
y: center.y + (height > 0 ? (-cos * shorter) / (2 * height) : 0),
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -42,12 +42,26 @@ export type {
|
|
|
42
42
|
SymbolEffectName,
|
|
43
43
|
SymbolEffectOptions,
|
|
44
44
|
} from './symbol-effect.js'
|
|
45
|
+
export { gradient } from './gradient.js'
|
|
46
|
+
export type {
|
|
47
|
+
Gradient,
|
|
48
|
+
GradientOptions,
|
|
49
|
+
GradientType,
|
|
50
|
+
GradientValues,
|
|
51
|
+
} from './gradient.js'
|
|
45
52
|
export { configure, onReducedMotion, reducedMotion } from './config.js'
|
|
46
53
|
export type { JoltConfig } from './config.js'
|
|
47
54
|
export { definePreset, presets } from './presets.js'
|
|
48
55
|
export type { Preset, PresetName, Presets } from './presets.js'
|
|
49
|
-
export { springs } from './transition.js'
|
|
50
|
-
export type {
|
|
56
|
+
export { origamiSpring, springs } from './transition.js'
|
|
57
|
+
export type {
|
|
58
|
+
OrigamiSpring,
|
|
59
|
+
Spring,
|
|
60
|
+
SpringName,
|
|
61
|
+
Timing,
|
|
62
|
+
Transition,
|
|
63
|
+
Tween,
|
|
64
|
+
} from './transition.js'
|
|
51
65
|
export { cubicBezier } from './easing.js'
|
|
52
66
|
export type { CubicBezier, Easing, EasingFunction, EasingName } from './easing.js'
|
|
53
67
|
export type { PlaybackAnimation } from './playback.js'
|
package/src/motion-scope.ts
CHANGED
|
@@ -40,13 +40,13 @@ export interface MotionScope {
|
|
|
40
40
|
press: typeof press
|
|
41
41
|
drag: typeof drag
|
|
42
42
|
states<
|
|
43
|
-
T extends Record<string, Target>,
|
|
43
|
+
T extends Record<string, Target | null>,
|
|
44
44
|
const S extends Record<string, MotionState<keyof T>>,
|
|
45
45
|
>(
|
|
46
46
|
targets: T,
|
|
47
47
|
definitions: MotionDefinitions<T, S>,
|
|
48
48
|
options?: Omit<MotionStatesOptions, 'scope'>,
|
|
49
|
-
): MotionStates<Extract<keyof S, string>>
|
|
49
|
+
): MotionStates<Extract<keyof S, string>, Extract<keyof T, string>>
|
|
50
50
|
/** Adopt an existing handle. Completed animations are released automatically. */
|
|
51
51
|
add<T extends MotionResource>(resource: T): T
|
|
52
52
|
/** Attach a subscription, detach on suspension, and recreate on resume. */
|