@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.
Files changed (38) hide show
  1. package/README.md +18 -0
  2. package/dist/driver/android-gradient.d.ts +5 -0
  3. package/dist/driver/android-gradient.d.ts.map +1 -0
  4. package/dist/driver/android-gradient.js +89 -0
  5. package/dist/driver/android-gradient.js.map +1 -0
  6. package/dist/driver/ios-gradient.d.ts +5 -0
  7. package/dist/driver/ios-gradient.d.ts.map +1 -0
  8. package/dist/driver/ios-gradient.js +109 -0
  9. package/dist/driver/ios-gradient.js.map +1 -0
  10. package/dist/gradient.d.ts +90 -0
  11. package/dist/gradient.d.ts.map +1 -0
  12. package/dist/gradient.js +247 -0
  13. package/dist/gradient.js.map +1 -0
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/motion-scope.d.ts +1 -1
  19. package/dist/motion-scope.d.ts.map +1 -1
  20. package/dist/motion-states.d.ts +9 -2
  21. package/dist/motion-states.d.ts.map +1 -1
  22. package/dist/motion-states.js +64 -9
  23. package/dist/motion-states.js.map +1 -1
  24. package/dist/transition.d.ts +15 -0
  25. package/dist/transition.d.ts.map +1 -1
  26. package/dist/transition.js +17 -0
  27. package/dist/transition.js.map +1 -1
  28. package/package.json +1 -1
  29. package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +195 -0
  30. package/platforms/android/proguard-rules.pro +2 -0
  31. package/src/android-plugin.d.ts +29 -0
  32. package/src/driver/android-gradient.ts +109 -0
  33. package/src/driver/ios-gradient.ts +114 -0
  34. package/src/gradient.ts +372 -0
  35. package/src/index.ts +16 -2
  36. package/src/motion-scope.ts +2 -2
  37. package/src/motion-states.ts +73 -19
  38. 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
+ }
@@ -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 { Spring, SpringName, Timing, Transition, Tween } from './transition.js'
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'
@@ -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. */