From ce959dddfd9f892300fdbfd105af0b4f2d70a81a Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 7 Feb 2024 13:38:37 +0100 Subject: [PATCH 1/6] Latest| --- dev/benchmarks/mix-array-framer-motion.html | 56 +++++++++ dev/benchmarks/mix-array-greensock.html | 53 +++++++++ .../mix-color-value-framer-motion.html | 56 +++++++++ dev/benchmarks/mix-color-value-greensock.html | 53 +++++++++ .../mix-complex-value-framer-motion.html | 57 +++++++++ .../mix-complex-value-greensock.html | 53 +++++++++ .../mix-number-value-framer-motion.html | 53 +++++++++ .../mix-number-value-greensock.html | 50 ++++++++ dev/benchmarks/mix-object-greensock.html | 56 +++++++++ .../mix-unit-value-framer-motion.html | 8 +- dev/benchmarks/mix-unit-value-greensock.html | 50 ++++++++ .../src/animation/animators/js/index.ts | 14 ++- .../src/animation/sequence/utils/edit.ts | 4 +- .../components/Reorder/utils/check-reorder.ts | 4 +- .../drag/VisualElementDragControls.ts | 6 +- .../src/gestures/drag/utils/constraints.ts | 16 ++- .../src/projection/animation/mix-values.ts | 16 ++- .../src/projection/geometry/delta-apply.ts | 4 +- .../src/projection/geometry/delta-calc.ts | 6 +- .../src/projection/geometry/delta-remove.ts | 6 +- .../framer-motion/src/projection/index.ts | 2 +- .../projection/node/create-projection-node.ts | 10 +- .../src/projection/styles/scale-box-shadow.ts | 4 +- .../src/utils/__tests__/mix.test.ts | 11 -- packages/framer-motion/src/utils/clamp.ts | 7 +- .../framer-motion/src/utils/interpolate.ts | 23 +--- .../framer-motion/src/utils/mix-complex.ts | 90 -------------- .../src/utils/mix/__tests__/index.test.ts | 31 +++++ .../{ => mix}/__tests__/mix-array.test.ts | 2 +- .../{ => mix}/__tests__/mix-color.test.ts | 2 +- .../{ => mix}/__tests__/mix-complex.test.ts | 10 +- .../utils/mix/__tests__/mix-number.test.ts | 11 ++ .../{ => mix}/__tests__/mix-object.test.ts | 2 +- .../src/utils/{mix-color.ts => mix/color.ts} | 19 +-- .../framer-motion/src/utils/mix/complex.ts | 99 ++++++++++++++++ packages/framer-motion/src/utils/mix/index.ts | 18 +++ .../src/utils/{mix.ts => mix/number.ts} | 8 +- packages/framer-motion/src/utils/mix/types.ts | 3 + .../framer-motion/src/utils/offsets/fill.ts | 4 +- .../src/value/types/complex/index.ts | 112 +++++------------- .../src/value/types/numbers/units.ts | 2 +- 41 files changed, 821 insertions(+), 270 deletions(-) create mode 100644 dev/benchmarks/mix-array-framer-motion.html create mode 100644 dev/benchmarks/mix-array-greensock.html create mode 100644 dev/benchmarks/mix-color-value-framer-motion.html create mode 100644 dev/benchmarks/mix-color-value-greensock.html create mode 100644 dev/benchmarks/mix-complex-value-framer-motion.html create mode 100644 dev/benchmarks/mix-complex-value-greensock.html create mode 100644 dev/benchmarks/mix-number-value-framer-motion.html create mode 100644 dev/benchmarks/mix-number-value-greensock.html create mode 100644 dev/benchmarks/mix-object-greensock.html create mode 100644 dev/benchmarks/mix-unit-value-greensock.html delete mode 100644 packages/framer-motion/src/utils/__tests__/mix.test.ts delete mode 100644 packages/framer-motion/src/utils/mix-complex.ts create mode 100644 packages/framer-motion/src/utils/mix/__tests__/index.test.ts rename packages/framer-motion/src/utils/{ => mix}/__tests__/mix-array.test.ts (89%) rename packages/framer-motion/src/utils/{ => mix}/__tests__/mix-color.test.ts (98%) rename packages/framer-motion/src/utils/{ => mix}/__tests__/mix-complex.test.ts (85%) create mode 100644 packages/framer-motion/src/utils/mix/__tests__/mix-number.test.ts rename packages/framer-motion/src/utils/{ => mix}/__tests__/mix-object.test.ts (92%) rename packages/framer-motion/src/utils/{mix-color.ts => mix/color.ts} (71%) create mode 100644 packages/framer-motion/src/utils/mix/complex.ts create mode 100644 packages/framer-motion/src/utils/mix/index.ts rename packages/framer-motion/src/utils/{mix.ts => mix/number.ts} (69%) create mode 100644 packages/framer-motion/src/utils/mix/types.ts diff --git a/dev/benchmarks/mix-array-framer-motion.html b/dev/benchmarks/mix-array-framer-motion.html new file mode 100644 index 0000000000..82ea0e9012 --- /dev/null +++ b/dev/benchmarks/mix-array-framer-motion.html @@ -0,0 +1,56 @@ + + + + + + +
+ + + + diff --git a/dev/benchmarks/mix-array-greensock.html b/dev/benchmarks/mix-array-greensock.html new file mode 100644 index 0000000000..42de49d9bf --- /dev/null +++ b/dev/benchmarks/mix-array-greensock.html @@ -0,0 +1,53 @@ + + + + + + +
+ + + + diff --git a/dev/benchmarks/mix-color-value-framer-motion.html b/dev/benchmarks/mix-color-value-framer-motion.html new file mode 100644 index 0000000000..ab811e68f3 --- /dev/null +++ b/dev/benchmarks/mix-color-value-framer-motion.html @@ -0,0 +1,56 @@ + + + + + + +
+ + + + diff --git a/dev/benchmarks/mix-color-value-greensock.html b/dev/benchmarks/mix-color-value-greensock.html new file mode 100644 index 0000000000..7e3e34757c --- /dev/null +++ b/dev/benchmarks/mix-color-value-greensock.html @@ -0,0 +1,53 @@ + + + + + + +
+ + + + diff --git a/dev/benchmarks/mix-complex-value-framer-motion.html b/dev/benchmarks/mix-complex-value-framer-motion.html new file mode 100644 index 0000000000..b2483ff09c --- /dev/null +++ b/dev/benchmarks/mix-complex-value-framer-motion.html @@ -0,0 +1,57 @@ + + + + + + +
+ + + + diff --git a/dev/benchmarks/mix-complex-value-greensock.html b/dev/benchmarks/mix-complex-value-greensock.html new file mode 100644 index 0000000000..7e62f55f57 --- /dev/null +++ b/dev/benchmarks/mix-complex-value-greensock.html @@ -0,0 +1,53 @@ + + + + + + +
+ + + + diff --git a/dev/benchmarks/mix-number-value-framer-motion.html b/dev/benchmarks/mix-number-value-framer-motion.html new file mode 100644 index 0000000000..198d8b3f8e --- /dev/null +++ b/dev/benchmarks/mix-number-value-framer-motion.html @@ -0,0 +1,53 @@ + + + + + + +
+ + + + + diff --git a/dev/benchmarks/mix-number-value-greensock.html b/dev/benchmarks/mix-number-value-greensock.html new file mode 100644 index 0000000000..707035c042 --- /dev/null +++ b/dev/benchmarks/mix-number-value-greensock.html @@ -0,0 +1,50 @@ + + + + + + +
+ + + + diff --git a/dev/benchmarks/mix-object-greensock.html b/dev/benchmarks/mix-object-greensock.html new file mode 100644 index 0000000000..82ea0e9012 --- /dev/null +++ b/dev/benchmarks/mix-object-greensock.html @@ -0,0 +1,56 @@ + + + + + + +
+ + + + diff --git a/dev/benchmarks/mix-unit-value-framer-motion.html b/dev/benchmarks/mix-unit-value-framer-motion.html index 35e7c27634..aff0ac6c41 100644 --- a/dev/benchmarks/mix-unit-value-framer-motion.html +++ b/dev/benchmarks/mix-unit-value-framer-motion.html @@ -33,13 +33,14 @@
+ diff --git a/dev/benchmarks/mix-unit-value-greensock.html b/dev/benchmarks/mix-unit-value-greensock.html new file mode 100644 index 0000000000..fba2566380 --- /dev/null +++ b/dev/benchmarks/mix-unit-value-greensock.html @@ -0,0 +1,50 @@ + + + + + + +
+ + + + diff --git a/packages/framer-motion/src/animation/animators/js/index.ts b/packages/framer-motion/src/animation/animators/js/index.ts index e0eec229b2..fd3d02ac60 100644 --- a/packages/framer-motion/src/animation/animators/js/index.ts +++ b/packages/framer-motion/src/animation/animators/js/index.ts @@ -6,7 +6,6 @@ import { AnimationState, KeyframeGenerator } from "../../generators/types" import { DriverControls } from "./types" import { ValueAnimationOptions } from "../../types" import { frameloopDriver } from "./driver-frameloop" -import { interpolate } from "../../../utils/interpolate" import { clamp } from "../../../utils/clamp" import { millisecondsToSeconds, @@ -14,6 +13,8 @@ import { } from "../../../utils/time-conversion" import { calcGeneratorDuration } from "../../generators/utils/calc-duration" import { invariant } from "../../../utils/errors" +import { mix } from "../../../utils/mix" +import { pipe } from "../../../utils/pipe" type GeneratorFactory = ( options: ValueAnimationOptions @@ -32,6 +33,8 @@ export interface MainThreadAnimationControls sample: (t: number) => AnimationState } +const percentToProgress = (percent: number) => percent / 100 + /** * Animate a single value on the main thread. * @@ -92,9 +95,12 @@ export function animateValue({ `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes}` ) } - mapNumbersToKeyframes = interpolate([0, 100], keyframes, { - clamp: false, - }) + + mapNumbersToKeyframes = pipe( + percentToProgress, + mix(keyframes[0], keyframes[1]) + ) as (t: number) => V + keyframes = [0, 100] as any } diff --git a/packages/framer-motion/src/animation/sequence/utils/edit.ts b/packages/framer-motion/src/animation/sequence/utils/edit.ts index 9c2202609c..ae6ec443da 100644 --- a/packages/framer-motion/src/animation/sequence/utils/edit.ts +++ b/packages/framer-motion/src/animation/sequence/utils/edit.ts @@ -1,7 +1,7 @@ import { Easing } from "../../../easing/types" import { getEasingForSegment } from "../../../easing/utils/get-easing-for-segment" import { removeItem } from "../../../utils/array" -import { mix } from "../../../utils/mix" +import { mixNumber } from "../../../utils/mix/number" import { UnresolvedValueKeyframe } from "../../types" import type { ValueSequence } from "../types" @@ -40,7 +40,7 @@ export function addKeyframes( for (let i = 0; i < keyframes.length; i++) { sequence.push({ value: keyframes[i], - at: mix(startTime, endTime, offset[i]), + at: mixNumber(startTime, endTime, offset[i]), easing: getEasingForSegment(easing, i), }) } diff --git a/packages/framer-motion/src/components/Reorder/utils/check-reorder.ts b/packages/framer-motion/src/components/Reorder/utils/check-reorder.ts index 1e26238bb6..db2313fe86 100644 --- a/packages/framer-motion/src/components/Reorder/utils/check-reorder.ts +++ b/packages/framer-motion/src/components/Reorder/utils/check-reorder.ts @@ -1,5 +1,5 @@ import { moveItem } from "../../../utils/array" -import { mix } from "../../../utils/mix" +import { mixNumber } from "../../../utils/mix/number" import { ItemData } from "../types" export function checkReorder( @@ -21,7 +21,7 @@ export function checkReorder( const item = order[index] const nextLayout = nextItem.layout - const nextItemCenter = mix(nextLayout.min, nextLayout.max, 0.5) + const nextItemCenter = mixNumber(nextLayout.min, nextLayout.max, 0.5) if ( (nextOffset === 1 && item.layout.max + offset > nextItemCenter) || diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index d3d76f1ea7..b0f69416d0 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -28,7 +28,7 @@ import { import { LayoutUpdateData } from "../../projection/node/types" import { addDomEvent } from "../../events/add-dom-event" import { calcLength } from "../../projection/geometry/delta-calc" -import { mix } from "../../utils/mix" +import { mixNumber } from "../../utils/mix/number" import { percent } from "../../value/types/numbers/units" import { animateMotionValue } from "../../animation/interfaces/motion-value" import { frame } from "../../frameloop" @@ -489,7 +489,7 @@ export class VisualElementDragControls { if (projection && projection.layout) { const { min, max } = projection.layout.layoutBox[axis] - axisValue.set(point[axis] - mix(min, max, 0.5)) + axisValue.set(point[axis] - mixNumber(min, max, 0.5)) } }) } @@ -552,7 +552,7 @@ export class VisualElementDragControls { */ const axisValue = this.getAxisMotionValue(axis) const { min, max } = this.constraints[axis] - axisValue.set(mix(min, max, boxProgress[axis])) + axisValue.set(mixNumber(min, max, boxProgress[axis])) }) } diff --git a/packages/framer-motion/src/gestures/drag/utils/constraints.ts b/packages/framer-motion/src/gestures/drag/utils/constraints.ts index 05351d0c4a..980acc397c 100644 --- a/packages/framer-motion/src/gestures/drag/utils/constraints.ts +++ b/packages/framer-motion/src/gestures/drag/utils/constraints.ts @@ -7,7 +7,7 @@ import { Point, } from "../../../projection/geometry/types" import { clamp } from "../../../utils/clamp" -import { mix } from "../../../utils/mix" +import { mixNumber } from "../../../utils/mix/number" import { DragElastic, ResolvedConstraints } from "../types" /** @@ -22,10 +22,14 @@ export function applyConstraints( ): number { if (min !== undefined && point < min) { // If we have a min point defined, and this is outside of that, constrain - point = elastic ? mix(min, point, elastic.min) : Math.max(point, min) + point = elastic + ? mixNumber(min, point, elastic.min) + : Math.max(point, min) } else if (max !== undefined && point > max) { // If we have a max point defined, and this is outside of that, constrain - point = elastic ? mix(max, point, elastic.max) : Math.min(point, max) + point = elastic + ? mixNumber(max, point, elastic.max) + : Math.min(point, max) } return point @@ -162,7 +166,11 @@ export function calcPositionFromProgress( progress: number ): Axis { const axisLength = axis.max - axis.min - const min = mix(constraints.min, constraints.max - axisLength, progress) + const min = mixNumber( + constraints.min, + constraints.max - axisLength, + progress + ) return { min, max: min + axisLength } } diff --git a/packages/framer-motion/src/projection/animation/mix-values.ts b/packages/framer-motion/src/projection/animation/mix-values.ts index 0d33b25d20..6fea321028 100644 --- a/packages/framer-motion/src/projection/animation/mix-values.ts +++ b/packages/framer-motion/src/projection/animation/mix-values.ts @@ -2,7 +2,7 @@ import { circOut } from "../../easing/circ" import { EasingFunction } from "../../easing/types" import { ResolvedValues } from "../../render/types" import { progress as calcProgress } from "../../utils/progress" -import { mix } from "../../utils/mix" +import { mixNumber } from "../../utils/mix/number" import { noop } from "../../utils/noop" import { percent, px } from "../../value/types/numbers/units" @@ -24,19 +24,19 @@ export function mixValues( isOnlyMember: boolean ) { if (shouldCrossfadeOpacity) { - target.opacity = mix( + target.opacity = mixNumber( 0, // TODO Reinstate this if only child lead.opacity !== undefined ? (lead.opacity as number) : 1, easeCrossfadeIn(progress) ) - target.opacityExit = mix( + target.opacityExit = mixNumber( follow.opacity !== undefined ? (follow.opacity as number) : 1, 0, easeCrossfadeOut(progress) ) } else if (isOnlyMember) { - target.opacity = mix( + target.opacity = mixNumber( follow.opacity !== undefined ? (follow.opacity as number) : 1, lead.opacity !== undefined ? (lead.opacity as number) : 1, progress @@ -63,7 +63,11 @@ export function mixValues( if (canMix) { target[borderLabel] = Math.max( - mix(asNumber(followRadius), asNumber(leadRadius), progress), + mixNumber( + asNumber(followRadius), + asNumber(leadRadius), + progress + ), 0 ) @@ -79,7 +83,7 @@ export function mixValues( * Mix rotation */ if (follow.rotate || lead.rotate) { - target.rotate = mix( + target.rotate = mixNumber( (follow.rotate as number) || 0, (lead.rotate as number) || 0, progress diff --git a/packages/framer-motion/src/projection/geometry/delta-apply.ts b/packages/framer-motion/src/projection/geometry/delta-apply.ts index 2844daae51..c0d9bf9bb6 100644 --- a/packages/framer-motion/src/projection/geometry/delta-apply.ts +++ b/packages/framer-motion/src/projection/geometry/delta-apply.ts @@ -1,5 +1,5 @@ import { ResolvedValues } from "../../render/types" -import { mix } from "../../utils/mix" +import { mixNumber } from "../../utils/mix/number" import { IProjectionNode } from "../node/types" import { hasTransform } from "../utils/has-transform" import { Axis, Box, Delta, Point } from "./types" @@ -160,7 +160,7 @@ export function transformAxis( const axisOrigin = transforms[originKey] !== undefined ? transforms[originKey] : 0.5 - const originPoint = mix(axis.min, axis.max, axisOrigin as number) + const originPoint = mixNumber(axis.min, axis.max, axisOrigin as number) // Apply the axis delta to the final axis applyAxisDelta( diff --git a/packages/framer-motion/src/projection/geometry/delta-calc.ts b/packages/framer-motion/src/projection/geometry/delta-calc.ts index 6560607dcd..a0c76e9925 100644 --- a/packages/framer-motion/src/projection/geometry/delta-calc.ts +++ b/packages/framer-motion/src/projection/geometry/delta-calc.ts @@ -1,5 +1,5 @@ import { ResolvedValues } from "../../render/types" -import { mix } from "../../utils/mix" +import { mixNumber } from "../../utils/mix/number" import { Axis, AxisDelta, Box, Delta } from "./types" export function calcLength(axis: Axis) { @@ -17,13 +17,13 @@ export function calcAxisDelta( origin: number = 0.5 ) { delta.origin = origin - delta.originPoint = mix(source.min, source.max, delta.origin) + delta.originPoint = mixNumber(source.min, source.max, delta.origin) delta.scale = calcLength(target) / calcLength(source) if (isNear(delta.scale, 1, 0.0001) || isNaN(delta.scale)) delta.scale = 1 delta.translate = - mix(target.min, target.max, delta.origin) - delta.originPoint + mixNumber(target.min, target.max, delta.origin) - delta.originPoint if (isNear(delta.translate) || isNaN(delta.translate)) delta.translate = 0 } diff --git a/packages/framer-motion/src/projection/geometry/delta-remove.ts b/packages/framer-motion/src/projection/geometry/delta-remove.ts index 4f12c223a8..ba401d3f98 100644 --- a/packages/framer-motion/src/projection/geometry/delta-remove.ts +++ b/packages/framer-motion/src/projection/geometry/delta-remove.ts @@ -1,5 +1,5 @@ import { ResolvedValues } from "../../render/types" -import { mix } from "../../utils/mix" +import { mixNumber } from "../../utils/mix/number" import { percent } from "../../value/types/numbers/units" import { scalePoint } from "./delta-apply" import { Axis, Box } from "./types" @@ -38,7 +38,7 @@ export function removeAxisDelta( ): void { if (percent.test(translate)) { translate = parseFloat(translate as string) - const relativeProgress = mix( + const relativeProgress = mixNumber( sourceAxis.min, sourceAxis.max, translate / 100 @@ -48,7 +48,7 @@ export function removeAxisDelta( if (typeof translate !== "number") return - let originPoint = mix(originAxis.min, originAxis.max, origin) + let originPoint = mixNumber(originAxis.min, originAxis.max, origin) if (axis === originAxis) originPoint -= translate axis.min = removePointDelta( diff --git a/packages/framer-motion/src/projection/index.ts b/packages/framer-motion/src/projection/index.ts index 807dd6db5b..7ed2faffff 100644 --- a/packages/framer-motion/src/projection/index.ts +++ b/packages/framer-motion/src/projection/index.ts @@ -6,7 +6,7 @@ export { calcBoxDelta } from "./geometry/delta-calc" * For debugging purposes */ import { frame, frameData } from "../frameloop" -import { mix } from "../utils/mix" +import { mixNumber as mix } from "../utils/mix/number" import { animateValue } from "../animation/animators/js" export { frame, animateValue as animate, mix, frameData } export { buildTransform } from "../render/html/utils/build-transform" diff --git a/packages/framer-motion/src/projection/node/create-projection-node.ts b/packages/framer-motion/src/projection/node/create-projection-node.ts index 6ae756d6c3..a0ee244ce6 100644 --- a/packages/framer-motion/src/projection/node/create-projection-node.ts +++ b/packages/framer-motion/src/projection/node/create-projection-node.ts @@ -44,7 +44,7 @@ import { resolveMotionValue } from "../../value/utils/resolve-motion-value" import { MotionStyle } from "../../motion/types" import { globalProjectionState } from "./state" import { delay } from "../../utils/delay" -import { mix } from "../../utils/mix" +import { mixNumber } from "../../utils/mix/number" import { Process } from "../../frameloop/types" import { ProjectionFrame } from "../../debug/types" import { record } from "../../debug/record" @@ -2080,15 +2080,15 @@ function removeLeadSnapshots(stack: NodeStack) { } export function mixAxisDelta(output: AxisDelta, delta: AxisDelta, p: number) { - output.translate = mix(delta.translate, 0, p) - output.scale = mix(delta.scale, 1, p) + output.translate = mixNumber(delta.translate, 0, p) + output.scale = mixNumber(delta.scale, 1, p) output.origin = delta.origin output.originPoint = delta.originPoint } export function mixAxis(output: Axis, from: Axis, to: Axis, p: number) { - output.min = mix(from.min, to.min, p) - output.max = mix(from.max, to.max, p) + output.min = mixNumber(from.min, to.min, p) + output.max = mixNumber(from.max, to.max, p) } export function mixBox(output: Box, from: Box, to: Box, p: number) { diff --git a/packages/framer-motion/src/projection/styles/scale-box-shadow.ts b/packages/framer-motion/src/projection/styles/scale-box-shadow.ts index 7c1f6619af..c82428d8df 100644 --- a/packages/framer-motion/src/projection/styles/scale-box-shadow.ts +++ b/packages/framer-motion/src/projection/styles/scale-box-shadow.ts @@ -1,4 +1,4 @@ -import { mix } from "../../utils/mix" +import { mixNumber } from "../../utils/mix/number" import { complex } from "../../value/types/complex" import { ScaleCorrectorDefinition } from "./types" @@ -27,7 +27,7 @@ export const correctBoxShadow: ScaleCorrectorDefinition = { * We could potentially improve the outcome of this by incorporating the ratio between * the two scales. */ - const averageScale = mix(xScale, yScale, 0.5) + const averageScale = mixNumber(xScale, yScale, 0.5) // Blur if (typeof shadow[2 + offset] === "number") diff --git a/packages/framer-motion/src/utils/__tests__/mix.test.ts b/packages/framer-motion/src/utils/__tests__/mix.test.ts deleted file mode 100644 index 90de9d0597..0000000000 --- a/packages/framer-motion/src/utils/__tests__/mix.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { mix } from "../mix" - -test("mix", () => { - expect(mix(0, 1, 0.5)).toBe(0.5) - expect(mix(-100, 100, 2)).toBe(300) - expect(mix(10, 20, 0.5)).toBe(15) - expect(mix(-10, -20, 0.5)).toBe(-15) - expect(mix(0, 80, 0.5)).toBe(40) - expect(mix(100, 200, 2)).toBe(300) - expect(mix(-100, 100, 2)).toBe(300) -}) diff --git a/packages/framer-motion/src/utils/clamp.ts b/packages/framer-motion/src/utils/clamp.ts index 3363441ead..88e8c2107c 100644 --- a/packages/framer-motion/src/utils/clamp.ts +++ b/packages/framer-motion/src/utils/clamp.ts @@ -1,2 +1,5 @@ -export const clamp = (min: number, max: number, v: number) => - Math.min(Math.max(v, min), max) +export const clamp = (min: number, max: number, v: number) => { + if (v > max) return max + if (v < min) return min + return v +} diff --git a/packages/framer-motion/src/utils/interpolate.ts b/packages/framer-motion/src/utils/interpolate.ts index 101038560d..1b47a58d95 100644 --- a/packages/framer-motion/src/utils/interpolate.ts +++ b/packages/framer-motion/src/utils/interpolate.ts @@ -1,13 +1,10 @@ import { invariant } from "../utils/errors" import { EasingFunction } from "../easing/types" -import { color } from "../value/types/color" import { clamp } from "./clamp" -import { mix } from "./mix" -import { mixColor } from "./mix-color" -import { mixArray, mixComplex, mixObject } from "./mix-complex" import { pipe } from "./pipe" import { progress } from "./progress" import { noop } from "./noop" +import { mix } from "./mix" type Mix = (v: number) => T export type MixerFactory = (from: T, to: T) => Mix @@ -18,29 +15,13 @@ export interface InterpolateOptions { mixer?: MixerFactory } -const mixNumber = (from: number, to: number) => (p: number) => mix(from, to, p) - -function detectMixerFactory(v: T): MixerFactory { - if (typeof v === "number") { - return mixNumber - } else if (typeof v === "string") { - return color.test(v) ? mixColor : mixComplex - } else if (Array.isArray(v)) { - return mixArray - } else if (typeof v === "object") { - return mixObject - } - return mixNumber -} - function createMixers( output: T[], ease?: EasingFunction | EasingFunction[], customMixer?: MixerFactory ) { const mixers: Array> = [] - const mixerFactory: MixerFactory = - customMixer || detectMixerFactory(output[0]) + const mixerFactory: MixerFactory = customMixer || mix const numMixers = output.length - 1 for (let i = 0; i < numMixers; i++) { diff --git a/packages/framer-motion/src/utils/mix-complex.ts b/packages/framer-motion/src/utils/mix-complex.ts deleted file mode 100644 index e0ee4c3d47..0000000000 --- a/packages/framer-motion/src/utils/mix-complex.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { mix } from "./mix" -import { mixColor } from "./mix-color" -import { pipe } from "./pipe" -import { warning } from "../utils/errors" -import { HSLA, RGBA } from "../value/types/types" -import { color } from "../value/types/color" -import { analyseComplexValue, complex } from "../value/types/complex" - -type MixComplex = (p: number) => string - -type BlendableArray = Array -type BlendableObject = { - [key: string]: string | number | RGBA | HSLA -} - -const mixImmediate = - (origin: number | string, target: number | string) => (p: number) => - `${p > 0 ? target : origin}` - -function getMixer(origin: any, target: any) { - if (typeof origin === "number") { - return (v: number) => mix(origin, target as number, v) - } else if (color.test(origin)) { - return mixColor(origin, target as HSLA | RGBA | string) - } else { - return origin.startsWith("var(") - ? mixImmediate(origin, target) - : mixComplex(origin as string, target as string) - } -} - -export const mixArray = (from: BlendableArray, to: BlendableArray) => { - const output = [...from] - const numValues = output.length - - const blendValue = from.map((fromThis, i) => getMixer(fromThis, to[i])) - - return (v: number) => { - for (let i = 0; i < numValues; i++) { - output[i] = blendValue[i](v) - } - return output - } -} - -export const mixObject = (origin: BlendableObject, target: BlendableObject) => { - const output = { ...origin, ...target } - const blendValue: { [key: string]: (v: number) => any } = {} - - for (const key in output) { - if (origin[key] !== undefined && target[key] !== undefined) { - blendValue[key] = getMixer(origin[key], target[key]) - } - } - - return (v: number) => { - for (const key in blendValue) { - output[key] = blendValue[key](v) - } - return output - } -} - -export const mixComplex = ( - origin: string | number, - target: string | number -): MixComplex => { - const template = complex.createTransformer(target) - const originStats = analyseComplexValue(origin) - const targetStats = analyseComplexValue(target) - - const canInterpolate = - originStats.numVars === targetStats.numVars && - originStats.numColors === targetStats.numColors && - originStats.numNumbers >= targetStats.numNumbers - - if (canInterpolate) { - return pipe( - mixArray(originStats.values, targetStats.values), - template - ) as MixComplex - } else { - warning( - true, - `Complex values '${origin}' and '${target}' too different to mix. Ensure all colors are of the same type, and that each contains the same quantity of number and color values. Falling back to instant transition.` - ) - - return mixImmediate(origin, target) - } -} diff --git a/packages/framer-motion/src/utils/mix/__tests__/index.test.ts b/packages/framer-motion/src/utils/mix/__tests__/index.test.ts new file mode 100644 index 0000000000..35f7772322 --- /dev/null +++ b/packages/framer-motion/src/utils/mix/__tests__/index.test.ts @@ -0,0 +1,31 @@ +import { mix } from "../" + +describe("mix", () => { + test("Supports legacy immediate call syntax", () => { + const output = mix(0, 2, 0.25) + expect(output).toBe(0.5) + }) + + test("mixes numbers", () => { + const mixer = mix(0, 2) + expect(mixer(0.25)).toBe(0.5) + }) + + test("mixes deep array", () => { + const mixer = mix( + { a: [0, 1], b: { c: 0, d: "1px" } }, + { a: [2, 3], b: { c: 1, d: "2px" } } + ) + + expect(mixer(0.5)).toEqual({ a: [1, 2], b: { c: 0.5, d: "1.5px" } }) + }) + + test("mixes deep array", () => { + const mixer = mix( + [[0, 1], { c: 0, d: "1px" }], + [[2, 3], { c: 1, d: "2px" }] + ) + + expect(mixer(0.5)).toEqual([[1, 2], { c: 0.5, d: "1.5px" }]) + }) +}) diff --git a/packages/framer-motion/src/utils/__tests__/mix-array.test.ts b/packages/framer-motion/src/utils/mix/__tests__/mix-array.test.ts similarity index 89% rename from packages/framer-motion/src/utils/__tests__/mix-array.test.ts rename to packages/framer-motion/src/utils/mix/__tests__/mix-array.test.ts index ee80981a53..30c81a2848 100644 --- a/packages/framer-motion/src/utils/__tests__/mix-array.test.ts +++ b/packages/framer-motion/src/utils/mix/__tests__/mix-array.test.ts @@ -1,4 +1,4 @@ -import { mixArray } from "../mix-complex" +import { mixArray } from "../complex" test("mixArray", () => { const a = [0, "100px 0px", "#fff"] diff --git a/packages/framer-motion/src/utils/__tests__/mix-color.test.ts b/packages/framer-motion/src/utils/mix/__tests__/mix-color.test.ts similarity index 98% rename from packages/framer-motion/src/utils/__tests__/mix-color.test.ts rename to packages/framer-motion/src/utils/mix/__tests__/mix-color.test.ts index 8da4490cd8..7bfa7d9786 100644 --- a/packages/framer-motion/src/utils/__tests__/mix-color.test.ts +++ b/packages/framer-motion/src/utils/mix/__tests__/mix-color.test.ts @@ -1,4 +1,4 @@ -import { mixColor, mixLinearColor } from "../mix-color" +import { mixColor, mixLinearColor } from "../color" test("mixColor hex", () => { expect(mixColor("#fff", "#000")(0.5)).toBe("rgba(180, 180, 180, 1)") diff --git a/packages/framer-motion/src/utils/__tests__/mix-complex.test.ts b/packages/framer-motion/src/utils/mix/__tests__/mix-complex.test.ts similarity index 85% rename from packages/framer-motion/src/utils/__tests__/mix-complex.test.ts rename to packages/framer-motion/src/utils/mix/__tests__/mix-complex.test.ts index 88f7e87845..717582ce0f 100644 --- a/packages/framer-motion/src/utils/__tests__/mix-complex.test.ts +++ b/packages/framer-motion/src/utils/mix/__tests__/mix-complex.test.ts @@ -1,4 +1,4 @@ -import { mixComplex } from "../mix-complex" +import { mixComplex } from "../complex" test("mixComplex", () => { expect(mixComplex("20px", "10px")(0.5)).toBe("15px") @@ -33,14 +33,14 @@ test("mixComplex can interpolate out-of-order values", () => { }) test("mixComplex can animate from a value-less prop", () => { - expect(mixComplex("#fff 0 0px", "20px 0px #000")(0.5)).toBe( - "10px 0px rgba(180, 180, 180, 1)" + expect(mixComplex("#fff 0 0px", "#000 20px 0px")(0.5)).toBe( + "rgba(180, 180, 180, 1) 10px 0px" ) }) test("mixComplex can animate from a value with extra zeros", () => { - expect(mixComplex("#fff 0 0px 0px", "20px 0px #000")(0.5)).toBe( - "10px 0px rgba(180, 180, 180, 1)" + expect(mixComplex("#000 #fff 0 0px 0px", "20px 0px")(0.5)).toBe( + "rgba(180, 180, 180, 1) 10px 0px" ) }) diff --git a/packages/framer-motion/src/utils/mix/__tests__/mix-number.test.ts b/packages/framer-motion/src/utils/mix/__tests__/mix-number.test.ts new file mode 100644 index 0000000000..2a5d5d373e --- /dev/null +++ b/packages/framer-motion/src/utils/mix/__tests__/mix-number.test.ts @@ -0,0 +1,11 @@ +import { mixNumber } from "../number" + +test("mixNumber", () => { + expect(mixNumber(0, 1, 0.5)).toBe(0.5) + expect(mixNumber(-100, 100, 2)).toBe(300) + expect(mixNumber(10, 20, 0.5)).toBe(15) + expect(mixNumber(-10, -20, 0.5)).toBe(-15) + expect(mixNumber(0, 80, 0.5)).toBe(40) + expect(mixNumber(100, 200, 2)).toBe(300) + expect(mixNumber(-100, 100, 2)).toBe(300) +}) diff --git a/packages/framer-motion/src/utils/__tests__/mix-object.test.ts b/packages/framer-motion/src/utils/mix/__tests__/mix-object.test.ts similarity index 92% rename from packages/framer-motion/src/utils/__tests__/mix-object.test.ts rename to packages/framer-motion/src/utils/mix/__tests__/mix-object.test.ts index 8bb3c5a93a..e9c6938a1c 100644 --- a/packages/framer-motion/src/utils/__tests__/mix-object.test.ts +++ b/packages/framer-motion/src/utils/mix/__tests__/mix-object.test.ts @@ -1,4 +1,4 @@ -import { mixObject } from "../mix-complex" +import { mixObject } from "../complex" test("mixObject", () => { expect( diff --git a/packages/framer-motion/src/utils/mix-color.ts b/packages/framer-motion/src/utils/mix/color.ts similarity index 71% rename from packages/framer-motion/src/utils/mix-color.ts rename to packages/framer-motion/src/utils/mix/color.ts index 1b2ca6ed88..8de9879079 100644 --- a/packages/framer-motion/src/utils/mix-color.ts +++ b/packages/framer-motion/src/utils/mix/color.ts @@ -1,17 +1,18 @@ -import { mix } from "./mix" -import { invariant } from "../utils/errors" -import { hslaToRgba } from "./hsla-to-rgba" -import { hex } from "../value/types/color/hex" -import { rgba } from "../value/types/color/rgba" -import { hsla } from "../value/types/color/hsla" -import { Color, HSLA, RGBA } from "../value/types/types" +import { mixNumber } from "./number" +import { invariant } from "../errors" +import { hslaToRgba } from "../hsla-to-rgba" +import { hex } from "../../value/types/color/hex" +import { rgba } from "../../value/types/color/rgba" +import { hsla } from "../../value/types/color/hsla" +import { Color, HSLA, RGBA } from "../../value/types/types" // Linear color space blending // Explained https://www.youtube.com/watch?v=LKnqECcg6Gw // Demonstrated http://codepen.io/osublake/pen/xGVVaN export const mixLinearColor = (from: number, to: number, v: number) => { const fromExpo = from * from - return Math.sqrt(Math.max(0, v * (to * to - fromExpo) + fromExpo)) + const expo = v * (to * to - fromExpo) + fromExpo + return expo < 0 ? 0 : Math.sqrt(expo) } const colorTypes = [hex, rgba, hsla] @@ -46,7 +47,7 @@ export const mixColor = (from: Color | string, to: Color | string) => { blended.red = mixLinearColor(fromRGBA.red, toRGBA.red, v) blended.green = mixLinearColor(fromRGBA.green, toRGBA.green, v) blended.blue = mixLinearColor(fromRGBA.blue, toRGBA.blue, v) - blended.alpha = mix(fromRGBA.alpha, toRGBA.alpha, v) + blended.alpha = mixNumber(fromRGBA.alpha, toRGBA.alpha, v) return rgba.transform!(blended) } } diff --git a/packages/framer-motion/src/utils/mix/complex.ts b/packages/framer-motion/src/utils/mix/complex.ts new file mode 100644 index 0000000000..4964d7b87f --- /dev/null +++ b/packages/framer-motion/src/utils/mix/complex.ts @@ -0,0 +1,99 @@ +import { mixNumber as mixNumberImmediate } from "./number" +import { mixColor } from "./color" +import { pipe } from "../pipe" +import { warning } from "../errors" +import { HSLA, RGBA } from "../../value/types/types" +import { color } from "../../value/types/color" +import { analyseComplexValue, complex } from "../../value/types/complex" + +type MixableArray = Array +type MixableObject = { + [key: string]: string | number | RGBA | HSLA +} + +function mixImmediate(a: T, b: T) { + return (p: number) => (p > 0 ? b : a) +} + +function mixNumber(a: number, b: number) { + return (p: number) => mixNumberImmediate(a, b, p) +} + +export function getMixer(a: T) { + if (typeof a === "number") { + return mixNumber + } else if (typeof a === "string") { + if (a.startsWith("var(") || a.startsWith("url(")) { + return mixImmediate + } else if (color.test(a)) { + return mixColor + } + return mixComplex + } else if (Array.isArray(a)) { + return mixArray + } else if (typeof a === "object") { + return mixObject + } + + return mixImmediate +} + +export function mixArray(a: MixableArray, b: MixableArray) { + const output = [...a] + const numValues = output.length + + const blendValue = a.map((v, i) => getMixer(v)(v as any, b[i] as any)) + + return (p: number) => { + for (let i = 0; i < numValues; i++) { + output[i] = blendValue[i](p) as any + } + return output + } +} + +export function mixObject(a: MixableObject, b: MixableObject) { + const output = { ...a, ...b } + const blendValue: { [key: string]: (v: number) => any } = {} + + for (const key in output) { + if (a[key] !== undefined && b[key] !== undefined) { + blendValue[key] = getMixer(a[key])( + a[key] as any, + b[key] as any + ) as any + } + } + + return (v: number) => { + for (const key in blendValue) { + output[key] = blendValue[key](v) + } + return output + } +} + +export const mixComplex = ( + origin: string | number, + target: string | number +) => { + const template = complex.createTransformer(target) + const originStats = analyseComplexValue(origin) + const targetStats = analyseComplexValue(target) + + const canInterpolate = + originStats.values && + targetStats.values && + originStats.values.length === targetStats.values.length + + if (canInterpolate) { + return pipe(mixArray(originStats.values, targetStats.values), template) + } else { + warning( + true, + `Complex values '${origin}' and '${target}' too different to mix. Ensure all colors are of the same type, and that each contains the same quantity of number and color values. Falling back to instant transition.` + ) + + return mixImmediate(origin, target) + } +} diff --git a/packages/framer-motion/src/utils/mix/index.ts b/packages/framer-motion/src/utils/mix/index.ts new file mode 100644 index 0000000000..bdda460ed3 --- /dev/null +++ b/packages/framer-motion/src/utils/mix/index.ts @@ -0,0 +1,18 @@ +import { getMixer } from "./complex" +import { mixNumber as mixNumberImmediate } from "./number" +import { Mixer } from "./types" + +export function mix(from: T, to: T): Mixer +export function mix(from: number, to: number, p: number): number +export function mix(from: T, to: T, p?: T): Mixer | number { + if ( + typeof from === "number" && + typeof to === "number" && + typeof p === "number" + ) { + return mixNumberImmediate(from, to, p) + } + + const mixer = getMixer(from) + return mixer(from as any, to as any) as Mixer +} diff --git a/packages/framer-motion/src/utils/mix.ts b/packages/framer-motion/src/utils/mix/number.ts similarity index 69% rename from packages/framer-motion/src/utils/mix.ts rename to packages/framer-motion/src/utils/mix/number.ts index a57214e8b6..dd5097c3a7 100644 --- a/packages/framer-motion/src/utils/mix.ts +++ b/packages/framer-motion/src/utils/mix/number.ts @@ -19,5 +19,9 @@ @param [number]: The progress between lower and upper limits expressed 0-1 @return [number]: Value as calculated from progress within range (not limited within range) */ -export const mix = (from: number, to: number, progress: number) => - -progress * from + progress * to + from +// export const mixNumber = (from: number, to: number, progress: number) => +// -progress * from + progress * to + from + +export const mixNumber = (from: number, to: number, progress: number) => { + return from + (to - from) * progress +} diff --git a/packages/framer-motion/src/utils/mix/types.ts b/packages/framer-motion/src/utils/mix/types.ts new file mode 100644 index 0000000000..0a4f543ba2 --- /dev/null +++ b/packages/framer-motion/src/utils/mix/types.ts @@ -0,0 +1,3 @@ +export type Mixer = (p: number) => T + +export type MixerFactory = (a: T, b: T) => Mixer diff --git a/packages/framer-motion/src/utils/offsets/fill.ts b/packages/framer-motion/src/utils/offsets/fill.ts index 3e4f9bfe7c..1b87a8ad21 100644 --- a/packages/framer-motion/src/utils/offsets/fill.ts +++ b/packages/framer-motion/src/utils/offsets/fill.ts @@ -1,10 +1,10 @@ -import { mix } from "../mix" +import { mixNumber } from "../mix/number" import { progress } from "../progress" export function fillOffset(offset: number[], remaining: number): void { const min = offset[offset.length - 1] for (let i = 1; i <= remaining; i++) { const offsetProgress = progress(0, remaining, i) - offset.push(mix(min, 1, offsetProgress)) + offset.push(mixNumber(min, 1, offsetProgress)) } } diff --git a/packages/framer-motion/src/value/types/complex/index.ts b/packages/framer-motion/src/value/types/complex/index.ts index ed3154e8b3..4580cb81b7 100644 --- a/packages/framer-motion/src/value/types/complex/index.ts +++ b/packages/framer-motion/src/value/types/complex/index.ts @@ -1,10 +1,5 @@ -import { - cssVariableRegex, - CSSVariableToken, -} from "../../../render/dom/utils/is-css-variable" -import { noop } from "../../../utils/noop" -import { color } from "../color" -import { number } from "../numbers" +import { CSSVariableToken } from "../../../render/dom/utils/is-css-variable" +import { isNumericalString } from "../../../utils/is-numerical-string" import { Color } from "../types" import { colorRegex, floatRegex, isString, sanitize } from "../utils" @@ -18,72 +13,38 @@ function test(v: any) { ) } -export interface ComplexValueInfo { - value: string - values: Array - numVars: number - numColors: number - numNumbers: number - tokenised: string -} - -interface Tokeniser { - regex: RegExp - countKey: string - token: string - parse: (value: string) => any -} - -const cssVarTokeniser: Tokeniser = { - regex: cssVariableRegex, - countKey: "Vars", - token: "${v}", - parse: noop, -} - -const colorTokeniser: Tokeniser = { - regex: colorRegex, - countKey: "Colors", - token: "${c}", - parse: color.parse, -} +type ComplexValues = Array -const numberTokeniser: Tokeniser = { - regex: floatRegex, - countKey: "Numbers", - token: "${n}", - parse: number.parse, +export interface ComplexValueInfo { + values: ComplexValues + split: string[] } -function tokenise( - info: ComplexValueInfo, - { regex, countKey, token, parse }: Tokeniser -) { - const matches = info.tokenised.match(regex) +const complexRegex = + /(var\s*\(\s*--[\w-]+(\s*,\s*(?:(?:[^)(]|\((?:[^)(]+|\([^)(]*\))*\))*)+)?\s*\))|(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))|((-)?([\d]*\.?[\d])+)/gi - if (!matches) return - - info["num" + countKey] = matches.length - info.tokenised = info.tokenised.replace(regex, token) - info.values.push(...(matches.map(parse) as any)) -} +const splitToken = "${}" export function analyseComplexValue(value: string | number): ComplexValueInfo { const originalValue = value.toString() - const info = { - value: originalValue, - tokenised: originalValue, - values: [], - numVars: 0, - numColors: 0, - numNumbers: 0, + + const matchedValues = originalValue.match(complexRegex) + const values: ComplexValues = + matchedValues === null ? [] : (matchedValues as ComplexValues) + + /** + * match() returns strings - convert numerical strings to actual numbers. + */ + for (let i = 0; i < values?.length; i++) { + if (isNumericalString(values[i] as string)) { + values[i] = parseFloat(values[i] as string) + } } - if (info.value.includes("var(--")) tokenise(info, cssVarTokeniser) - tokenise(info, colorTokeniser) - tokenise(info, numberTokeniser) + const tokenised = originalValue.replace(complexRegex, splitToken) + const split = tokenised.split(splitToken) - return info + return { values, split } } function parseComplexValue(v: string | number) { @@ -91,26 +52,15 @@ function parseComplexValue(v: string | number) { } function createTransformer(source: string | number) { - const { values, numColors, numVars, tokenised } = - analyseComplexValue(source) - const numValues = values.length + const { split } = analyseComplexValue(source) + const numSections = split.length return (v: Array) => { - let output = tokenised - - for (let i = 0; i < numValues; i++) { - if (i < numVars) { - output = output.replace(cssVarTokeniser.token, v[i] as any) - } else if (i < numVars + numColors) { - output = output.replace( - colorTokeniser.token, - color.transform(v[i] as any) - ) - } else { - output = output.replace( - numberTokeniser.token, - sanitize(v[i] as number) as any - ) + let output = "" + for (let i = 0; i < numSections; i++) { + output += split[i] + if (v[i] !== undefined) { + output += typeof v === "number" ? sanitize(v[i]) : v[i] } } diff --git a/packages/framer-motion/src/value/types/numbers/units.ts b/packages/framer-motion/src/value/types/numbers/units.ts index c8b6aafdc4..e2f71c28f7 100644 --- a/packages/framer-motion/src/value/types/numbers/units.ts +++ b/packages/framer-motion/src/value/types/numbers/units.ts @@ -4,7 +4,7 @@ const createUnitType = (unit: string) => ({ test: (v: string | number) => isString(v) && v.endsWith(unit) && v.split(" ").length === 1, parse: parseFloat, - transform: (v: number | string) => `${v}${unit}`, + transform: (v: number | string) => v + unit, }) export const degrees = createUnitType("deg") From d3b56fca04256953a78857616132b798077555ce Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 7 Feb 2024 13:44:00 +0100 Subject: [PATCH 2/6] Updating --- packages/framer-motion/src/projection/index.ts | 2 +- packages/framer-motion/src/utils/mix/number.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/framer-motion/src/projection/index.ts b/packages/framer-motion/src/projection/index.ts index 7ed2faffff..807dd6db5b 100644 --- a/packages/framer-motion/src/projection/index.ts +++ b/packages/framer-motion/src/projection/index.ts @@ -6,7 +6,7 @@ export { calcBoxDelta } from "./geometry/delta-calc" * For debugging purposes */ import { frame, frameData } from "../frameloop" -import { mixNumber as mix } from "../utils/mix/number" +import { mix } from "../utils/mix" import { animateValue } from "../animation/animators/js" export { frame, animateValue as animate, mix, frameData } export { buildTransform } from "../render/html/utils/build-transform" diff --git a/packages/framer-motion/src/utils/mix/number.ts b/packages/framer-motion/src/utils/mix/number.ts index dd5097c3a7..8768b339b0 100644 --- a/packages/framer-motion/src/utils/mix/number.ts +++ b/packages/framer-motion/src/utils/mix/number.ts @@ -19,9 +19,6 @@ @param [number]: The progress between lower and upper limits expressed 0-1 @return [number]: Value as calculated from progress within range (not limited within range) */ -// export const mixNumber = (from: number, to: number, progress: number) => -// -progress * from + progress * to + from - export const mixNumber = (from: number, to: number, progress: number) => { return from + (to - from) * progress } From 80d802dccb38354e283eaefdb7973249daff62da Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 12 Feb 2024 11:08:03 +0100 Subject: [PATCH 3/6] Fixing tests --- .../utils/mix/__tests__/mix-complex.test.ts | 4 +- .../framer-motion/src/utils/mix/complex.ts | 44 ++++++++++--- .../src/value/types/__tests__/index.test.ts | 36 +++++------ .../src/value/types/complex/index.ts | 61 ++++++++++++++----- .../src/value/types/numbers/units.ts | 2 +- 5 files changed, 103 insertions(+), 44 deletions(-) diff --git a/packages/framer-motion/src/utils/mix/__tests__/mix-complex.test.ts b/packages/framer-motion/src/utils/mix/__tests__/mix-complex.test.ts index 717582ce0f..077b23c6c6 100644 --- a/packages/framer-motion/src/utils/mix/__tests__/mix-complex.test.ts +++ b/packages/framer-motion/src/utils/mix/__tests__/mix-complex.test.ts @@ -39,8 +39,8 @@ test("mixComplex can animate from a value-less prop", () => { }) test("mixComplex can animate from a value with extra zeros", () => { - expect(mixComplex("#000 #fff 0 0px 0px", "20px 0px")(0.5)).toBe( - "rgba(180, 180, 180, 1) 10px 0px" + expect(mixComplex("#fff 0 0px 0px", "20px 0px #000")(0.5)).toBe( + "10px 0px rgba(180, 180, 180, 1)" ) }) diff --git a/packages/framer-motion/src/utils/mix/complex.ts b/packages/framer-motion/src/utils/mix/complex.ts index 4964d7b87f..3d11707b33 100644 --- a/packages/framer-motion/src/utils/mix/complex.ts +++ b/packages/framer-motion/src/utils/mix/complex.ts @@ -4,7 +4,12 @@ import { pipe } from "../pipe" import { warning } from "../errors" import { HSLA, RGBA } from "../../value/types/types" import { color } from "../../value/types/color" -import { analyseComplexValue, complex } from "../../value/types/complex" +import { + ComplexValueInfo, + ComplexValues, + analyseComplexValue, + complex, +} from "../../value/types/complex" type MixableArray = Array type MixableObject = { @@ -23,7 +28,7 @@ export function getMixer(a: T) { if (typeof a === "number") { return mixNumber } else if (typeof a === "string") { - if (a.startsWith("var(") || a.startsWith("url(")) { + if (a.startsWith("var(")) { return mixImmediate } else if (color.test(a)) { return mixColor @@ -73,6 +78,29 @@ export function mixObject(a: MixableObject, b: MixableObject) { } } +function matchOrder( + origin: ComplexValueInfo, + target: ComplexValueInfo +): ComplexValues { + const orderedOrigin: ComplexValues = [] + + const pointers = { color: 0, var: 0, number: 0 } + + for (let i = 0; i < target.values.length; i++) { + const type = target.types[i] + const originIndex = origin.indexes[type][pointers[type]] + const originValue = origin.values[originIndex] ?? 0 + + orderedOrigin[i] = originValue + + pointers[type]++ + } + + console.log({ orderedOrigin }) + + return orderedOrigin +} + export const mixComplex = ( origin: string | number, target: string | number @@ -80,14 +108,16 @@ export const mixComplex = ( const template = complex.createTransformer(target) const originStats = analyseComplexValue(origin) const targetStats = analyseComplexValue(target) - const canInterpolate = - originStats.values && - targetStats.values && - originStats.values.length === targetStats.values.length + originStats.indexes.var.length === targetStats.indexes.var.length && + originStats.indexes.color.length === targetStats.indexes.color.length && + originStats.indexes.number.length >= targetStats.indexes.number.length if (canInterpolate) { - return pipe(mixArray(originStats.values, targetStats.values), template) + return pipe( + mixArray(matchOrder(originStats, targetStats), targetStats.values), + template + ) } else { warning( true, diff --git a/packages/framer-motion/src/value/types/__tests__/index.test.ts b/packages/framer-motion/src/value/types/__tests__/index.test.ts index f551ccc820..5f4f2f2537 100644 --- a/packages/framer-motion/src/value/types/__tests__/index.test.ts +++ b/packages/framer-motion/src/value/types/__tests__/index.test.ts @@ -82,34 +82,34 @@ describe("complex value type", () => { expect(complex.parse(PATH)).toEqual(PATH_VALUES) expect(complex.parse(GREYSCALE)).toEqual([100]) expect(complex.parse(MIXED)).toEqual([ - { red: 161, green: 0, blue: 246, alpha: 0 }, 0, 0, 0, + { red: 161, green: 0, blue: 246, alpha: 0 }, ]) expect(complex.parse("0px 0px 0px rgba(161 0 246 / 0.5)")).toEqual([ - { red: 161, green: 0, blue: 246, alpha: 0.5 }, 0, 0, 0, + { red: 161, green: 0, blue: 246, alpha: 0.5 }, ]) expect(complex.parse("0px 0px 0px #F00")).toEqual([ - { red: 255, green: 0, blue: 0, alpha: 1 }, 0, 0, 0, + { red: 255, green: 0, blue: 0, alpha: 1 }, ]) expect(complex.parse("0px 0px 0px #F000")).toEqual([ - { red: 255, green: 0, blue: 0, alpha: 0 }, 0, 0, 0, + { red: 255, green: 0, blue: 0, alpha: 0 }, ]) expect(complex.parse("0px 0px 0px #00FF0000")).toEqual([ - { red: 0, green: 255, blue: 0, alpha: 0 }, 0, 0, 0, + { red: 0, green: 255, blue: 0, alpha: 0 }, ]) }) @@ -122,15 +122,15 @@ describe("complex value type", () => { ) expect( transformMixedExpo([ + 0, + 1.5999999547489097e-8, + 3.199999909497819e-8, { red: 161, green: 0, blue: 246, alpha: 6.399999974426862e-10, }, - 0, - 1.5999999547489097e-8, - 3.199999909497819e-8, ]) ).toBe("0px 0px 0px rgba(161, 0, 246, 0)") @@ -410,52 +410,52 @@ describe("combination values", () => { it("should parse into an array", () => { expect(complex.parse("0px 10px #fff")).toEqual([ - { red: 255, green: 255, blue: 255, alpha: 1 }, 0, 10, + { red: 255, green: 255, blue: 255, alpha: 1 }, ]) expect(complex.parse("20px 20px 10px inset #fff")).toEqual([ - { red: 255, green: 255, blue: 255, alpha: 1 }, 20, 20, 10, + { red: 255, green: 255, blue: 255, alpha: 1 }, ]) expect( complex.parse("20px 20px 10px inset rgba(255, 255, 255, 1)") - ).toEqual([{ red: 255, green: 255, blue: 255, alpha: 1 }, 20, 20, 10]) + ).toEqual([20, 20, 10, { red: 255, green: 255, blue: 255, alpha: 1 }]) expect( complex.parse( "20px 20px 10px inset #fff, 20px 20px 10px inset rgba(255, 255, 255, 1)" ) ).toEqual([ - { red: 255, green: 255, blue: 255, alpha: 1 }, - { red: 255, green: 255, blue: 255, alpha: 1 }, 20, 20, 10, + { red: 255, green: 255, blue: 255, alpha: 1 }, 20, 20, 10, + { red: 255, green: 255, blue: 255, alpha: 1 }, ]) expect(complex.parse("linear-gradient(0.25turn, #fff)")).toEqual([ + 0.25, { red: 255, green: 255, blue: 255, alpha: 1, }, - 0.25, ]) expect( complex.parse("linear-gradient(1deg, rgba(255, 255, 255, 1))") ).toEqual([ + 1, { red: 255, green: 255, blue: 255, alpha: 1, }, - 1, ]) expect( @@ -463,6 +463,7 @@ describe("combination values", () => { "linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%)" ) ).toEqual([ + 217, { red: 255, green: 0, @@ -475,7 +476,6 @@ describe("combination values", () => { blue: 0, alpha: 0, }, - 217, 70.71, ]) @@ -484,10 +484,10 @@ describe("combination values", () => { "radial-gradient(circle at 50% 25%, #e66465, #9198e5)" ) ).toEqual([ - { alpha: 1, blue: 101, green: 100, red: 230 }, - { alpha: 1, blue: 229, green: 152, red: 145 }, 50, 25, + { alpha: 1, blue: 101, green: 100, red: 230 }, + { alpha: 1, blue: 229, green: 152, red: 145 }, ]) }) diff --git a/packages/framer-motion/src/value/types/complex/index.ts b/packages/framer-motion/src/value/types/complex/index.ts index 4580cb81b7..e6cfb287eb 100644 --- a/packages/framer-motion/src/value/types/complex/index.ts +++ b/packages/framer-motion/src/value/types/complex/index.ts @@ -1,5 +1,5 @@ import { CSSVariableToken } from "../../../render/dom/utils/is-css-variable" -import { isNumericalString } from "../../../utils/is-numerical-string" +import { color } from "../color" import { Color } from "../types" import { colorRegex, floatRegex, isString, sanitize } from "../utils" @@ -13,11 +13,19 @@ function test(v: any) { ) } -type ComplexValues = Array +export type ComplexValues = Array + +export type ValueIndexes = { + color: number[] + number: number[] + var: number[] +} export interface ComplexValueInfo { values: ComplexValues split: string[] + indexes: ValueIndexes + types: Array } const complexRegex = @@ -28,23 +36,37 @@ const splitToken = "${}" export function analyseComplexValue(value: string | number): ComplexValueInfo { const originalValue = value.toString() - const matchedValues = originalValue.match(complexRegex) - const values: ComplexValues = - matchedValues === null ? [] : (matchedValues as ComplexValues) - - /** - * match() returns strings - convert numerical strings to actual numbers. - */ - for (let i = 0; i < values?.length; i++) { - if (isNumericalString(values[i] as string)) { - values[i] = parseFloat(values[i] as string) + const matchedValues = originalValue.match(complexRegex) || [] + const values: ComplexValues = [] + const indexes: ValueIndexes = { + color: [], + number: [], + var: [], + } + const types: Array = [] + + for (let i = 0; i < matchedValues.length; i++) { + const parsedValue: string | number = matchedValues[i] + + if (color.test(parsedValue)) { + indexes.color.push(i) + types.push("color") + values.push(color.parse(parsedValue)) + } else if (parsedValue.startsWith("var(")) { + indexes.var.push(i) + types.push("var") + values.push(parsedValue) + } else { + indexes.number.push(i) + types.push("number") + values.push(parseFloat(parsedValue)) } } const tokenised = originalValue.replace(complexRegex, splitToken) const split = tokenised.split(splitToken) - return { values, split } + return { values, split, indexes, types } } function parseComplexValue(v: string | number) { @@ -52,7 +74,7 @@ function parseComplexValue(v: string | number) { } function createTransformer(source: string | number) { - const { split } = analyseComplexValue(source) + const { split, types } = analyseComplexValue(source) const numSections = split.length return (v: Array) => { @@ -60,7 +82,14 @@ function createTransformer(source: string | number) { for (let i = 0; i < numSections; i++) { output += split[i] if (v[i] !== undefined) { - output += typeof v === "number" ? sanitize(v[i]) : v[i] + const type = types[i] + if (type === "number") { + output += sanitize(v[i] as number) + } else if (type === "color") { + output += color.transform(v[i] as Color) + } else { + output += v[i] + } } } @@ -68,7 +97,7 @@ function createTransformer(source: string | number) { } } -const convertNumbersToZero = (v: number | Color) => +const convertNumbersToZero = (v: number | string) => typeof v === "number" ? 0 : v function getAnimatableNone(v: string | number) { diff --git a/packages/framer-motion/src/value/types/numbers/units.ts b/packages/framer-motion/src/value/types/numbers/units.ts index e2f71c28f7..c8b6aafdc4 100644 --- a/packages/framer-motion/src/value/types/numbers/units.ts +++ b/packages/framer-motion/src/value/types/numbers/units.ts @@ -4,7 +4,7 @@ const createUnitType = (unit: string) => ({ test: (v: string | number) => isString(v) && v.endsWith(unit) && v.split(" ").length === 1, parse: parseFloat, - transform: (v: number | string) => v + unit, + transform: (v: number | string) => `${v}${unit}`, }) export const degrees = createUnitType("deg") From 9a5f871d86ce012c87227e44a838e67155678f8b Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 13 Feb 2024 13:45:26 +0100 Subject: [PATCH 4/6] Latest --- packages/framer-motion/src/utils/mix/complex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framer-motion/src/utils/mix/complex.ts b/packages/framer-motion/src/utils/mix/complex.ts index 3d11707b33..ebe6818c0a 100644 --- a/packages/framer-motion/src/utils/mix/complex.ts +++ b/packages/framer-motion/src/utils/mix/complex.ts @@ -37,7 +37,7 @@ export function getMixer(a: T) { } else if (Array.isArray(a)) { return mixArray } else if (typeof a === "object") { - return mixObject + return color.test(a) ? mixColor : mixObject } return mixImmediate From f7c2e4835b4a1d41a94f000ef2d667d01999469b Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 13 Feb 2024 14:13:20 +0100 Subject: [PATCH 5/6] Removing console.log --- packages/framer-motion/src/utils/mix/complex.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/framer-motion/src/utils/mix/complex.ts b/packages/framer-motion/src/utils/mix/complex.ts index ebe6818c0a..850fd429b0 100644 --- a/packages/framer-motion/src/utils/mix/complex.ts +++ b/packages/framer-motion/src/utils/mix/complex.ts @@ -96,8 +96,6 @@ function matchOrder( pointers[type]++ } - console.log({ orderedOrigin }) - return orderedOrigin } From 24af5ee71b16d1f6bcf72cf78e9dd89d12c1b89c Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 13 Feb 2024 14:19:37 +0100 Subject: [PATCH 6/6] TOkenising strings --- .../src/value/types/complex/index.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/framer-motion/src/value/types/complex/index.ts b/packages/framer-motion/src/value/types/complex/index.ts index e6cfb287eb..bfb3cd6bea 100644 --- a/packages/framer-motion/src/value/types/complex/index.ts +++ b/packages/framer-motion/src/value/types/complex/index.ts @@ -13,6 +13,12 @@ function test(v: any) { ) } +const NUMBER_TOKEN = "number" +const COLOR_TOKEN = "color" +const VAR_TOKEN = "var" +const VAR_FUNCTION_TOKEN = "var(" +const SPLIT_TOKEN = "${}" + export type ComplexValues = Array export type ValueIndexes = { @@ -31,8 +37,6 @@ export interface ComplexValueInfo { const complexRegex = /(var\s*\(\s*--[\w-]+(\s*,\s*(?:(?:[^)(]|\((?:[^)(]+|\([^)(]*\))*\))*)+)?\s*\))|(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))|((-)?([\d]*\.?[\d])+)/gi -const splitToken = "${}" - export function analyseComplexValue(value: string | number): ComplexValueInfo { const originalValue = value.toString() @@ -50,21 +54,21 @@ export function analyseComplexValue(value: string | number): ComplexValueInfo { if (color.test(parsedValue)) { indexes.color.push(i) - types.push("color") + types.push(COLOR_TOKEN) values.push(color.parse(parsedValue)) - } else if (parsedValue.startsWith("var(")) { + } else if (parsedValue.startsWith(VAR_FUNCTION_TOKEN)) { indexes.var.push(i) - types.push("var") + types.push(VAR_TOKEN) values.push(parsedValue) } else { indexes.number.push(i) - types.push("number") + types.push(NUMBER_TOKEN) values.push(parseFloat(parsedValue)) } } - const tokenised = originalValue.replace(complexRegex, splitToken) - const split = tokenised.split(splitToken) + const tokenised = originalValue.replace(complexRegex, SPLIT_TOKEN) + const split = tokenised.split(SPLIT_TOKEN) return { values, split, indexes, types } } @@ -83,9 +87,9 @@ function createTransformer(source: string | number) { output += split[i] if (v[i] !== undefined) { const type = types[i] - if (type === "number") { + if (type === NUMBER_TOKEN) { output += sanitize(v[i] as number) - } else if (type === "color") { + } else if (type === COLOR_TOKEN) { output += color.transform(v[i] as Color) } else { output += v[i]