(config: T) {
if (Array.isArray(config.to)) {
/**
* If to is defined as a keyframes array we want to force this to be a keyframes
* animation. In the future it might be possible to allow spring keyframes.
*/
return keyframes
} else if (types[config.type]) {
/**
* Or if the user has explicity defined their own animation type, return that.
*/
return types[config.type]
}
/**
* Attempt to detect which animation to use based on the options provided
*/
const keys = new Set(Object.keys(config))
if (
keys.has("ease") ||
(keys.has("duration") && !keys.has("dampingRatio"))
) {
return keyframes
} else if (
keys.has("dampingRatio") ||
keys.has("stiffness") ||
keys.has("mass") ||
keys.has("damping") ||
keys.has("restSpeed") ||
keys.has("restDelta")
) {
return spring
}
return keyframes
}
================================================
FILE: packages/popmotion/src/animations/utils/elapsed.ts
================================================
export function loopElapsed(elapsed: number, duration: number, delay = 0) {
return elapsed - duration - delay
}
export function reverseElapsed(
elapsed: number,
duration: number,
delay = 0,
isForwardPlayback = true
) {
return isForwardPlayback
? loopElapsed(duration + -elapsed, duration, delay)
: duration - (elapsed - duration) + delay
}
export function hasRepeatDelayElapsed(
elapsed: number,
duration: number,
delay: number,
isForwardPlayback: boolean
) {
return isForwardPlayback ? elapsed >= duration + delay : elapsed <= -delay
}
================================================
FILE: packages/popmotion/src/animations/utils/find-spring.ts
================================================
import { warning } from "hey-listen"
import { clamp } from "../../utils/clamp"
import { SpringOptions } from "../types"
/**
* This is ported from the Framer implementation of duration-based spring resolution.
*/
type Resolver = (num: number) => number
const safeMin = 0.001
export const minDuration = 0.01
export const maxDuration = 10.0
export const minDamping = 0.05
export const maxDamping = 1
export function findSpring({
duration = 800,
bounce = 0.25,
velocity = 0,
mass = 1,
}: SpringOptions) {
let envelope: Resolver
let derivative: Resolver
warning(
duration <= maxDuration * 1000,
"Spring duration must be 10 seconds or less"
)
let dampingRatio = 1 - bounce
/**
* Restrict dampingRatio and duration to within acceptable ranges.
*/
dampingRatio = clamp(minDamping, maxDamping, dampingRatio)
duration = clamp(minDuration, maxDuration, duration / 1000)
if (dampingRatio < 1) {
/**
* Underdamped spring
*/
envelope = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio
const delta = exponentialDecay * duration
const a = exponentialDecay - velocity
const b = calcAngularFreq(undampedFreq, dampingRatio)
const c = Math.exp(-delta)
return safeMin - (a / b) * c
}
derivative = (undampedFreq) => {
const exponentialDecay = undampedFreq * dampingRatio
const delta = exponentialDecay * duration
const d = delta * velocity + velocity
const e =
Math.pow(dampingRatio, 2) * Math.pow(undampedFreq, 2) * duration
const f = Math.exp(-delta)
const g = calcAngularFreq(Math.pow(undampedFreq, 2), dampingRatio)
const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1
return (factor * ((d - e) * f)) / g
}
} else {
/**
* Critically-damped spring
*/
envelope = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration)
const b = (undampedFreq - velocity) * duration + 1
return -safeMin + a * b
}
derivative = (undampedFreq) => {
const a = Math.exp(-undampedFreq * duration)
const b = (velocity - undampedFreq) * (duration * duration)
return a * b
}
}
const initialGuess = 5 / duration
const undampedFreq = approximateRoot(envelope, derivative, initialGuess)
duration = duration * 1000
if (isNaN(undampedFreq)) {
return {
stiffness: 100,
damping: 10,
duration,
}
} else {
const stiffness = Math.pow(undampedFreq, 2) * mass
return {
stiffness,
damping: dampingRatio * 2 * Math.sqrt(mass * stiffness),
duration,
}
}
}
const rootIterations = 12
function approximateRoot(
envelope: Resolver,
derivative: Resolver,
initialGuess: number
): number {
let result = initialGuess
for (let i = 1; i < rootIterations; i++) {
result = result - envelope(result) / derivative(result)
}
return result
}
export function calcAngularFreq(undampedFreq: number, dampingRatio: number) {
return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio)
}
================================================
FILE: packages/popmotion/src/easing/__tests__/cubic-bezier.test.ts
================================================
import { cubicBezier } from "../cubic-bezier"
describe("cubicBezier", () => {
test("correctly generates easing functions from curve definitions", () => {
const linear = cubicBezier(0, 0, 1, 1)
expect(linear(0)).toBe(0)
expect(linear(1)).toBe(1)
expect(linear(0.5)).toBe(0.5)
const curve = cubicBezier(0.5, 0.1, 0.31, 0.96)
expect(curve(0)).toBe(0)
expect(curve(0.01)).toBeCloseTo(0.002, 2)
expect(curve(0.25)).toBeCloseTo(0.164, 2)
expect(curve(0.75)).toBeCloseTo(0.935, 2)
expect(curve(0.99)).toBeCloseTo(0.999, 2)
expect(curve(1)).toBe(1)
})
})
================================================
FILE: packages/popmotion/src/easing/__tests__/index.test.ts
================================================
import { circIn, bounceOut, bounceInOut } from "../"
describe("circIn", () => {
test("correctly eases", () => {
expect(circIn(0)).toEqual(0)
expect(circIn(0.25)).toBeCloseTo(0.0317)
expect(circIn(0.75)).toBeCloseTo(0.3385)
expect(circIn(1)).toEqual(1)
})
})
describe("bounceOut", () => {
test("correctly eases", () => {
expect(bounceOut(0)).toEqual(0)
expect(bounceOut(0.25)).toBeCloseTo(0.472)
expect(bounceOut(0.75)).toBeCloseTo(0.9588)
expect(bounceOut(1)).toEqual(1)
})
})
describe("bounceInOut", () => {
test("correctly eases", () => {
expect(bounceInOut(0)).toEqual(0)
expect(bounceInOut(0.25)).toBeCloseTo(0.1406)
expect(bounceInOut(0.75)).toBeCloseTo(0.85937)
expect(bounceInOut(1)).toEqual(1)
})
})
================================================
FILE: packages/popmotion/src/easing/__tests__/steps.test.ts
================================================
import { steps } from "../steps"
test("steps", () => {
const stepEnd = steps(4)
expect(stepEnd(0)).toBe(0)
expect(stepEnd(0.2)).toBe(0)
expect(stepEnd(0.249)).toBe(0)
expect(stepEnd(0.25)).toBe(0.25)
expect(stepEnd(0.49)).toBe(0.25)
expect(stepEnd(0.5)).toBe(0.5)
expect(stepEnd(0.99)).toBe(0.75)
expect(stepEnd(1)).toBe(0.75)
const stepStart = steps(4, "start")
expect(stepStart(0)).toBe(0.25)
expect(stepStart(0.2)).toBe(0.25)
expect(stepStart(0.249)).toBe(0.25)
expect(stepStart(0.25)).toBe(0.25)
expect(stepStart(0.49)).toBe(0.5)
expect(stepStart(0.5)).toBe(0.5)
expect(stepStart(0.51)).toBe(0.75)
expect(stepStart(0.99)).toBe(1)
expect(stepStart(1)).toBe(1)
expect(stepStart(2)).toBe(1)
})
================================================
FILE: packages/popmotion/src/easing/__tests__/utils.test.ts
================================================
import {
reverseEasing,
mirrorEasing,
createExpoIn,
createBackIn,
createAnticipate,
} from "../utils"
import { easeOut, easeIn, easeInOut } from "../"
describe("reverseEasing", () => {
test("correctly reverses an easing curve", () => {
expect(reverseEasing(easeOut)(0.25)).toEqual(1 - easeOut(0.75))
})
})
describe("mirrorEasing", () => {
test("correctly mirrors an easing curve", () => {
expect(mirrorEasing(easeIn)(0.25)).toEqual(easeInOut(0.25))
expect(mirrorEasing(easeIn)(0.75)).toEqual(easeInOut(0.75))
})
})
describe("createExpoIn", () => {
test("creates an expoIn curve according to the provided power", () => {
expect(createExpoIn(2)(0.5)).toEqual(0.25)
expect(createExpoIn(3)(0.5)).toEqual(0.25 / 2)
expect(createExpoIn(4)(0.5)).toEqual(0.25 / 4)
})
})
describe("createBackIn", () => {
test("creates an backIn curve according to the provided power", () => {
expect(createBackIn(2)(0.5)).toEqual(-0.125)
expect(createBackIn(3)(0.5)).toEqual(-0.25)
expect(createBackIn(4)(0.5)).toEqual(-0.375)
})
})
describe("createAnticipate", () => {
test("creates an createAnticipate curve according to the provided power", () => {
expect(createAnticipate(2)(0.25)).toEqual(-0.0625)
expect(createAnticipate(3)(0.25)).toEqual(-0.125)
expect(createAnticipate(4)(0.25)).toEqual(-0.1875)
})
})
================================================
FILE: packages/popmotion/src/easing/cubic-bezier.ts
================================================
/*
Bezier function generator
Gaëtan Renaudeau's BezierEasing
https://github.com/gre/bezier-easing/blob/master/src/index.js
https://github.com/gre/bezier-easing/blob/master/LICENSE
You're a hero
Use
const easeOut = cubicBezier(.17,.67,.83,.67);
const x = easeOut(0.5); // returns 0.627...
*/
import { linear } from "."
const a = (a1: number, a2: number) => 1.0 - 3.0 * a2 + 3.0 * a1
const b = (a1: number, a2: number) => 3.0 * a2 - 6.0 * a1
const c = (a1: number) => 3.0 * a1
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
const calcBezier = (t: number, a1: number, a2: number) =>
((a(a1, a2) * t + b(a1, a2)) * t + c(a1)) * t
// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
const getSlope = (t: number, a1: number, a2: number) =>
3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1)
const subdivisionPrecision = 0.0000001
const subdivisionMaxIterations = 10
function binarySubdivide(
aX: number,
aA: number,
aB: number,
mX1: number,
mX2: number
) {
let currentX: number
let currentT: number
let i: number = 0
do {
currentT = aA + (aB - aA) / 2.0
currentX = calcBezier(currentT, mX1, mX2) - aX
if (currentX > 0.0) {
aB = currentT
} else {
aA = currentT
}
} while (
Math.abs(currentX) > subdivisionPrecision &&
++i < subdivisionMaxIterations
)
return currentT
}
const newtonIterations = 8
const newtonMinSlope = 0.001
function newtonRaphsonIterate(
aX: number,
aGuessT: number,
mX1: number,
mX2: number
) {
for (let i = 0; i < newtonIterations; ++i) {
const currentSlope = getSlope(aGuessT, mX1, mX2)
if (currentSlope === 0.0) {
return aGuessT
}
const currentX = calcBezier(aGuessT, mX1, mX2) - aX
aGuessT -= currentX / currentSlope
}
return aGuessT
}
const kSplineTableSize = 11
const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
export function cubicBezier(
mX1: number,
mY1: number,
mX2: number,
mY2: number
) {
// If this is a linear gradient, return linear easing
if (mX1 === mY1 && mX2 === mY2) return linear
// Precompute samples table
const sampleValues = new Float32Array(kSplineTableSize)
for (let i = 0; i < kSplineTableSize; ++i) {
sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2)
}
function getTForX(aX: number) {
let intervalStart = 0.0
let currentSample = 1
const lastSample = kSplineTableSize - 1
for (
;
currentSample !== lastSample && sampleValues[currentSample] <= aX;
++currentSample
) {
intervalStart += kSampleStepSize
}
--currentSample
// Interpolate to provide an initial guess for t
const dist =
(aX - sampleValues[currentSample]) /
(sampleValues[currentSample + 1] - sampleValues[currentSample])
const guessForT = intervalStart + dist * kSampleStepSize
const initialSlope = getSlope(guessForT, mX1, mX2)
if (initialSlope >= newtonMinSlope) {
return newtonRaphsonIterate(aX, guessForT, mX1, mX2)
} else if (initialSlope === 0.0) {
return guessForT
} else {
return binarySubdivide(
aX,
intervalStart,
intervalStart + kSampleStepSize,
mX1,
mX2
)
}
}
// If animation is at start/end, return t without easing
return (t: number) =>
t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2)
}
================================================
FILE: packages/popmotion/src/easing/index.ts
================================================
import {
createExpoIn,
reverseEasing,
mirrorEasing,
createBackIn,
createAnticipate,
} from "./utils"
import { Easing } from "./types"
const DEFAULT_OVERSHOOT_STRENGTH = 1.525
const BOUNCE_FIRST_THRESHOLD = 4.0 / 11.0
const BOUNCE_SECOND_THRESHOLD = 8.0 / 11.0
const BOUNCE_THIRD_THRESHOLD = 9.0 / 10.0
export const linear: Easing = p => p
export const easeIn = createExpoIn(2)
export const easeOut = reverseEasing(easeIn)
export const easeInOut = mirrorEasing(easeIn)
export const circIn: Easing = p => 1 - Math.sin(Math.acos(p))
export const circOut = reverseEasing(circIn)
export const circInOut = mirrorEasing(circOut)
export const backIn = createBackIn(DEFAULT_OVERSHOOT_STRENGTH)
export const backOut = reverseEasing(backIn)
export const backInOut = mirrorEasing(backIn)
export const anticipate = createAnticipate(DEFAULT_OVERSHOOT_STRENGTH)
// helper constants
const ca = 4356.0 / 361.0
const cb = 35442.0 / 1805.0
const cc = 16061.0 / 1805.0
export const bounceOut = (p: number) => {
if (p === 1 || p === 0) return p
const p2 = p * p
return p < BOUNCE_FIRST_THRESHOLD
? 7.5625 * p2
: p < BOUNCE_SECOND_THRESHOLD
? 9.075 * p2 - 9.9 * p + 3.4
: p < BOUNCE_THIRD_THRESHOLD
? ca * p2 - cb * p + cc
: 10.8 * p * p - 20.52 * p + 10.72
}
export const bounceIn = reverseEasing(bounceOut)
export const bounceInOut = (p: number) =>
p < 0.5
? 0.5 * (1.0 - bounceOut(1.0 - p * 2.0))
: 0.5 * bounceOut(p * 2.0 - 1.0) + 0.5
================================================
FILE: packages/popmotion/src/easing/steps.ts
================================================
import { clamp } from '../utils/clamp';
import { Easing } from './types';
/*
Create stepped version of 0-1 progress
@param [int]: Number of steps
@param [number]: Current value
@return [number]: Stepped value
*/
export type Direction = 'start' | 'end';
export const steps = (steps: number, direction: Direction = 'end'): Easing => (
progress: number
) => {
progress =
direction === 'end' ? Math.min(progress, 0.999) : Math.max(progress, 0.001);
const expanded = progress * steps;
const rounded =
direction === 'end' ? Math.floor(expanded) : Math.ceil(expanded);
return clamp(0, 1, rounded / steps);
};
================================================
FILE: packages/popmotion/src/easing/types.ts
================================================
export type Easing = (v: number) => number;
export type EasingModifier = (easing: Easing) => Easing;
================================================
FILE: packages/popmotion/src/easing/utils.ts
================================================
import { Easing, EasingModifier } from "./types"
// Accepts an easing function and returns a new one that outputs reversed values.
// Turns easeIn into easeOut.
export const reverseEasing: EasingModifier = easing => p => 1 - easing(1 - p)
// Accepts an easing function and returns a new one that outputs mirrored values for
// the second half of the animation. Turns easeIn into easeInOut.
export const mirrorEasing: EasingModifier = easing => p =>
p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2
// Creates an easing function that is based on the exponent of the provided `power`.
// The higher the `power`, the stronger the easing.
export const createExpoIn = (power: number): Easing => p => p ** power
// Creates an easing function that has a stronger overshoot the higher the provided `power`.
export const createBackIn = (power: number): Easing => p =>
p * p * ((power + 1) * p - power)
// Creates an easing function that pulls back a little before moving, and then
// has a `createBackIn`-based overshoot
export const createAnticipate = (power: number): Easing => {
const backEasing = createBackIn(power)
return p =>
(p *= 2) < 1
? 0.5 * backEasing(p)
: 0.5 * (2 - Math.pow(2, -10 * (p - 1)))
}
================================================
FILE: packages/popmotion/src/index.ts
================================================
export { animate } from "./animations"
export { inertia } from "./animations/inertia"
// Animators
export { decay } from "./animations/generators/decay"
export { spring } from "./animations/generators/spring"
export { keyframes } from "./animations/generators/keyframes"
// Utilities
export { angle } from "./utils/angle"
export { applyOffset } from "./utils/apply-offset"
export { createAttractor, attract, attractExpo } from "./utils/attract"
export { clamp } from "./utils/clamp"
export { degreesToRadians } from "./utils/degrees-to-radians"
export { distance } from "./utils/distance"
export { interpolate } from "./utils/interpolate"
export { isPoint3D } from "./utils/is-point-3d"
export { isPoint } from "./utils/is-point"
export { mixColor } from "./utils/mix-color"
export { mixComplex } from "./utils/mix-complex"
export { mix } from "./utils/mix"
export { pipe } from "./utils/pipe"
export { pointFromVector } from "./utils/point-from-vector"
export { progress } from "./utils/progress"
export { radiansToDegrees } from "./utils/radians-to-degrees"
export { smoothFrame } from "./utils/smooth-frame"
export { smooth } from "./utils/smooth"
export { snap } from "./utils/snap"
export { toDecimal } from "./utils/to-decimal"
export { velocityPerFrame } from "./utils/velocity-per-frame"
export { velocityPerSecond } from "./utils/velocity-per-second"
export { wrap } from "./utils/wrap"
// Easing
export {
linear,
easeIn,
easeInOut,
easeOut,
circIn,
circInOut,
circOut,
backIn,
backInOut,
backOut,
anticipate,
bounceIn,
bounceInOut,
bounceOut,
} from "./easing"
export { cubicBezier } from "./easing/cubic-bezier"
export { steps } from "./easing/steps"
export {
mirrorEasing,
reverseEasing,
createExpoIn,
createBackIn,
createAnticipate,
} from "./easing/utils"
// Types
export * from "./animations/types"
export * from "./easing/types"
// // Worklet
// export { animate } from './worklet/animate';
// export { workletReady } from './worklet/load-worklet';
================================================
FILE: packages/popmotion/src/types.ts
================================================
export type Point2D = {
x: number;
y: number;
};
export type Point3D = Point2D & {
z: number;
};
export type Point = Point2D | Point3D;
================================================
FILE: packages/popmotion/src/utils/__tests__/angle.test.ts
================================================
import { angle } from "../angle"
const a = { x: 0, y: 0 }
const b = { x: 1, y: 0 }
const c = { x: 1, y: 1 }
const d = { x: 0, y: 1 }
test("angle", () => {
expect(angle(a, a)).toBe(0)
expect(angle(a, d)).toBe(90)
expect(angle(d, a)).toBe(-90)
expect(angle(d, b)).toBe(-45)
expect(angle(a, c)).toBe(45)
expect(angle(c)).toBe(-135)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/apply-offset.test.ts
================================================
import { applyOffset } from "../apply-offset"
test("applyOffset", () => {
const withInitialOffset = applyOffset(10, 20)
expect(withInitialOffset(15)).toBe(25)
const withoutInitialOffset = applyOffset(20)
expect(withoutInitialOffset(10)).toBe(20)
expect(withoutInitialOffset(15)).toBe(25)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/attract.test.ts
================================================
import { createAttractor, attract, attractExpo } from "../attract"
describe("createAttractor", () => {
test("Will create an attractor", () => {
const attractor = createAttractor()
expect(attractor(0.5, 10, 5)).toEqual(7.5)
})
})
describe("attract", () => {
test("Attracts to origin with a linear curve", () => {
expect(attract(0.5, 10, 5)).toEqual(7.5)
expect(attract(0.5, 10, 15)).toEqual(12.5)
expect(attract(1, 10, 5)).toEqual(10)
expect(attract(0, 10, 5)).toEqual(5)
})
})
describe("attractExpo", () => {
test("Attracts to origin with an exponential curve", () => {
expect(attractExpo(0.5, 10, 5)).toBeCloseTo(8.9, 1)
expect(attractExpo(0.5, 10, 15)).toBeCloseTo(11.1, 1)
expect(attractExpo(1, 10, 5)).toEqual(10)
expect(attractExpo(0, 10, 5)).toBeCloseTo(7.8, 1)
})
})
================================================
FILE: packages/popmotion/src/utils/__tests__/clamp.test.ts
================================================
import { clamp } from "../clamp"
test("clamp", () => {
expect(clamp(100, 200, 99)).toBe(100)
expect(clamp(100, 200, 201)).toBe(200)
expect(clamp(100, 200, 150)).toBe(150)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/degrees-to-radians.test.ts
================================================
import { degreesToRadians } from "../degrees-to-radians"
test("degreesToRadians", () => {
expect(degreesToRadians(45)).toBe(0.7853981633974483)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/distance.test.ts
================================================
import { distance } from "../distance"
test("distance", () => {
expect(distance(-100, 100)).toBe(200)
expect(distance(100, -100)).toBe(200)
})
test("should return the correct distance between two 2D points", () => {
expect(
distance(
{
x: 0,
y: 0,
},
{
x: 1,
y: 1,
}
)
).toBe(1.4142135623730951)
})
test("should return the correct distance between two 3D points", () => {
expect(distance({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 10 })).toBe(10)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/hsla-to-rgba.test.ts
================================================
import { hslaToRgba } from "../hsla-to-rgba"
describe("hslaToRgba", () => {
test("Correctly converts hsla to rgba", () => {
expect(
hslaToRgba({
hue: 190,
saturation: 100,
lightness: 80,
alpha: 1,
})
).toEqual({ red: 153, green: 238, blue: 255, alpha: 1 })
})
})
================================================
FILE: packages/popmotion/src/utils/__tests__/interpolate.test.ts
================================================
import { interpolate } from "../interpolate"
const invert = (v: number) => -v
test("interpolate numbers", () => {
const f = interpolate([0, 100], [0, 1])
expect(f(50)).toBe(0.5)
const s = interpolate([-100, 100, 200], [0, 100, 0])
expect(s(-200)).toBe(0)
expect(s(0)).toBe(50)
expect(s(201)).toBe(0)
})
test("interpolate - ease", () => {
const f = interpolate([0, 100], [0, 100], { ease: invert })
expect(f(50)).toBe(-50)
})
test("interpolate - negative", () => {
const f = interpolate([-500, 500], [0, 1])
expect(f(-250)).toBe(0.25)
const s = interpolate([-500, 500, 600], [0, 1, 2])
expect(s(-250)).toBe(0.25)
})
test("interpolate - out of range", () => {
const f = interpolate([0, 100], [200, 100])
expect(f(50)).toBe(150)
expect(f(150)).toBe(100)
expect(f(0)).toBe(200)
expect(f(-100)).toBe(200)
const s = interpolate([0, 100, 200], [100, 200, 100])
expect(s(-50)).toBe(100)
expect(s(250)).toBe(100)
})
test("interpolate - unclamped", () => {
const f = interpolate([0, 100], [200, 100], { clamp: false })
expect(f(50)).toBe(150)
expect(f(150)).toBe(50)
expect(f(0)).toBe(200)
expect(f(-100)).toBe(300)
const s = interpolate([0, 100, 200], [1, 2, 3], { clamp: false })
expect(s(-100)).toBe(0)
})
test("interpolate - complex", () => {
const a = interpolate([0, 100, 200], [1000, 500, 1000])
expect(a(100)).toBe(500)
})
test("interpolate - reverse", () => {
const a = interpolate([1000, 0], [500, 600])
expect(a(500)).toBe(550)
expect(a(1000)).toBe(500)
expect(a(0)).toBe(600)
})
test("interpolate - reverse complex", () => {
const a = interpolate([0, 100, 200], [1000, 500, 1000])
expect(a(100)).toBe(500)
expect(a(0)).toBe(1000)
expect(a(200)).toBe(1000)
})
test("interpolate complex strings", () => {
const a = interpolate(
[0, 1, 2],
[
"20px, rgba(0, 0, 0, 0)",
"10px, rgba(255, 255, 255, 1)",
"40px, rgba(100, 100, 100, 0.5)",
]
)
expect(a(0)).toBe("20px, rgba(0, 0, 0, 0)")
expect(a(1.5)).toBe("25px, rgba(194, 194, 194, 0.75)")
const b = interpolate([0, 1], ["invert(0)", "invert(1)"])
expect(b(0.5)).toBe("invert(0.5)")
})
test("interpolate colors", () => {
const a = interpolate([0, 1], ["#000", "#fff"])
expect(a(0.5)).toBe("rgba(180, 180, 180, 1)")
})
test("interpolate objects", () => {
const a = interpolate([0, 100], [{ opacity: 0 }, { opacity: 1 }])
expect(a(50)).toEqual({ opacity: 0.5 })
})
test("interpolate arrays", () => {
const a = interpolate(
[0, 100],
[
[100, 300],
[0, 600],
]
)
expect(a(50)).toEqual([50, 450])
})
test("custom mixer", () => {
const a = interpolate([0, 1], [0, 1], {
mixer: (from: number, to: number) => (p: number) => 42,
})
expect(a(0.5)).toBe(42)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/is-point-3d.test.ts
================================================
import { isPoint3D } from "../is-point-3d"
test("isPoint3D", () => {
expect(isPoint3D(9 as any)).toBe(false)
expect(isPoint3D({ x: 0, y: 0 })).toBe(false)
expect(isPoint3D({ x: 0, y: 0, z: 0 })).toBe(true)
expect(isPoint3D({ z: 0 } as any)).toBe(false)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/is-point.test.ts
================================================
import { isPoint } from "../is-point"
test("isPoint", () => {
expect(isPoint(9)).toBe(false)
expect(isPoint({ x: 0, y: 0 })).toBe(true)
expect(isPoint({ x: 0, y: 0, z: 0 })).toBe(true)
expect(isPoint({ z: 0 })).toBe(false)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/mix-array.test.ts
================================================
import { mixArray } from "../mix-complex"
test("mixArray", () => {
const a = [0, "100px 0px", "#fff"]
const b = [50, "200px 100px", "#000"]
const blender = mixArray(a, b)
expect(blender(0)).toEqual([0, "100px 0px", "rgba(255, 255, 255, 1)"])
expect(blender(1)).toEqual([50, "200px 100px", "rgba(0, 0, 0, 1)"])
expect(blender(0.5)).toEqual([25, "150px 50px", "rgba(180, 180, 180, 1)"])
})
================================================
FILE: packages/popmotion/src/utils/__tests__/mix-color.test.ts
================================================
import { mixColor, mixLinearColor } from "../mix-color"
test("mixColor hex", () => {
expect(mixColor("#fff", "#000")(0.5)).toBe("rgba(180, 180, 180, 1)")
})
test("mixColor rgba", () => {
expect(mixColor("rgba(0, 0, 0, 0)", "rgba(255, 255, 255, 1)")(0.5)).toBe(
"rgba(180, 180, 180, 0.5)"
)
})
test("mixColor rgba out of bounds", () => {
expect(mixColor("rgba(0, 0, 0, 0)", "rgba(255, 255, 255, 1)")(2)).toBe(
"rgba(255, 255, 255, 1)"
)
expect(mixColor("rgba(0, 0, 0, 0)", "rgba(255, 255, 255, 1)")(-1)).toBe(
"rgba(0, 0, 0, 0)"
)
})
test("mixColor rgb", () => {
expect(mixColor("rgb(0, 0, 0)", "rgba(255, 255, 255)")(0.5)).toBe(
"rgba(180, 180, 180, 1)"
)
})
test("mixColor rgb out of bounds", () => {
expect(mixColor("rgb(0, 0, 0)", "rgba(255, 255, 255)")(2)).toBe(
"rgba(255, 255, 255, 1)"
)
expect(mixColor("rgb(0, 0, 0)", "rgba(255, 255, 255)")(-1)).toBe(
"rgba(0, 0, 0, 1)"
)
})
test("mixColor hsla", () => {
expect(mixColor("hsla(0, 0%, 0%, 0)", "hsla(0, 0%, 100%, 1)")(0.5)).toBe(
"rgba(180, 180, 180, 0.5)"
)
expect(
mixColor(
"hsla(177, 37.4978%, 76.66804%, 1)",
"hsla(0, 0%, 100%, 1)"
)(0.5)
).toBe("rgba(218, 237, 236, 1)")
})
test("mixColor hsla out of bounds", () => {
expect(mixColor("hsla(120, 0%, 0%, 0)", "hsla(360, 100%, 50%, 1)")(2)).toBe(
"rgba(255, 0, 0, 1)"
)
expect(
mixColor("hsla(120, 0%, 0%, 0)", "hsla(360, 100%, 50%, 1)")(-1)
).toBe("rgba(0, 0, 0, 0)")
})
test("mixColor hsl", () => {
expect(mixColor("hsl(120, 0%, 0%)", "hsl(360, 100%, 50%)")(0.5)).toBe(
"rgba(180, 0, 0, 1)"
)
})
test("mixColor hsl out of bounds", () => {
expect(mixColor("hsl(120, 0%, 0%)", "hsl(360, 100%, 50%)")(2)).toBe(
"rgba(255, 0, 0, 1)"
)
expect(mixColor("hsl(120, 0%, 0%)", "hsl(360, 100%, 50%)")(-1)).toBe(
"rgba(0, 0, 0, 1)"
)
})
test("mixColor rgb to hex", () => {
expect(mixColor("rgb(255, 255, 255)", "#000")(0.5)).toBe(
"rgba(180, 180, 180, 1)"
)
})
test("mixColor hsla to rgba", () => {
expect(mixColor("hsla(0, 0, 0, 0)", "rgba(255, 255, 255, 1)")(0.5)).toBe(
"rgba(180, 180, 180, 0.5)"
)
})
test("mixColor hsla to hex", () => {
expect(mixColor("hsla(0, 0, 0, 0)", "#fff")(0.5)).toBe(
"rgba(180, 180, 180, 0.5)"
)
})
test("mixColor hex to hsla", () => {
expect(mixColor("#fff", "hsla(0, 0, 0, 0)")(0.5)).toBe(
"rgba(180, 180, 180, 0.5)"
)
})
test("mixColor rgba to hsla", () => {
expect(mixColor("rgba(255, 255, 255, 1)", "hsla(0, 0, 0, 0)")(0.5)).toBe(
"rgba(180, 180, 180, 0.5)"
)
})
test("mixColor rgba with slash to rgba without", () => {
expect(mixColor("rgb(255, 255, 255 / 1)", "rgba(0, 0, 0, 0)")(0.5)).toBe(
"rgba(180, 180, 180, 0.5)"
)
})
test("mixColor rgba with slash (without slash spaces) to rgba without", () => {
expect(mixColor("rgb(255 255 255/1)", "rgba(0, 0, 0, 0)")(0.5)).toBe(
"rgba(180, 180, 180, 0.5)"
)
})
test("doesn't return NaN", () => {
expect(mixLinearColor(255, 0, 2)).not.toBeNaN()
})
================================================
FILE: packages/popmotion/src/utils/__tests__/mix-complex.test.ts
================================================
import { mixComplex } from "../mix-complex"
test("mixComplex", () => {
expect(mixComplex("20px", "10px")(0.5)).toBe("15px")
expect(
mixComplex(
"20px, rgba(0, 0, 0, 0)",
"10px, rgba(255, 255, 255, 1)"
)(0.5)
).toBe("15px, rgba(180, 180, 180, 0.5)")
})
test("mixComplex gracefully handles numbers", () => {
expect(mixComplex(20, "10")(0.5)).toBe("15")
})
test("mixComplex errors", () => {
expect(mixComplex("hsla(100%, 100, 100, 1)", "#fff")(0)).toBe(
"hsla(100%, 100, 100, 1)"
)
expect(mixComplex("hsla(100%, 100, 100, 1)", "#fff")(0.1)).toBe("#fff")
})
test("mixComplex can interpolate out-of-order values", () => {
expect(mixComplex("#fff 0px 0px", "20px 0px #000")(0.5)).toBe(
"10px 0px rgba(180, 180, 180, 1)"
)
})
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)"
)
})
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)"
)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/mix-object.test.ts
================================================
import { mixObject } from "../mix-complex"
test("mixObject", () => {
expect(
mixObject(
{
x: 0,
y: "0px",
color: "#fff",
shadow: "#000 0px 20px 0px",
},
{
x: 100,
y: "100px",
color: "#000",
shadow: "0px 10px #fff",
}
)(0.5)
).toEqual({
x: 50,
y: "50px",
color: "rgba(180, 180, 180, 1)",
shadow: "0px 15px rgba(180, 180, 180, 1)",
})
})
================================================
FILE: packages/popmotion/src/utils/__tests__/mix.test.ts
================================================
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)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/point-from-vector.test.ts
================================================
import { pointFromVector } from "../point-from-vector"
test("pointFromVector", () => {
expect(pointFromVector({ x: 0, y: 0 }, 45, 100)).toEqual({
x: 70.71067811865476,
y: 70.71067811865474,
})
})
================================================
FILE: packages/popmotion/src/utils/__tests__/progress.test.ts
================================================
import { progress } from "../progress"
test("progress", () => {
expect(progress(0, 100, 50)).toBe(0.5)
expect(progress(100, -100, 50)).toBe(0.25)
expect(progress(100, -100, -300)).toBe(2)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/radians-to-degrees.test.ts
================================================
import { radiansToDegrees } from "../radians-to-degrees"
test("radiansToDegrees", () => {
expect(radiansToDegrees(0.7853981633974483)).toBe(45)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/smooth-frame.test.ts
================================================
import { smoothFrame } from "../smooth-frame"
test("smoothFrame", () => {
expect(smoothFrame(0, 100, 16.7, 0)).toBe(100)
expect(smoothFrame(0, 100, 16.7, 50)).toBe(33.4)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/smooth.test.ts
================================================
import { smooth } from "../smooth"
import sync from "framesync"
test("smooth", () => {
const smoother = smooth()
return new Promise(resolve => {
sync.update(() => {
expect(smoother(100)).toBe(100)
sync.update(() => {
expect(smoother(200)).toBeGreaterThan(100)
resolve()
})
})
})
})
================================================
FILE: packages/popmotion/src/utils/__tests__/snap.test.ts
================================================
import { snap } from "../snap"
it("should snap a number to the nearest in the provided array", () => {
const snapTo = snap([-100, -50, 100, 200])
expect(snapTo(-200)).toBe(-100)
expect(snapTo(-100)).toBe(-100)
expect(snapTo(-76)).toBe(-100)
expect(snapTo(-74)).toBe(-50)
expect(snapTo(0)).toBe(-50)
expect(snapTo(99)).toBe(100)
expect(snapTo(150)).toBe(200)
expect(snapTo(200)).toBe(200)
expect(snapTo(300)).toBe(200)
})
it("should snap a number to a regular interval", () => {
const snapTo = snap(45)
expect(snapTo(1)).toBe(0)
expect(snapTo(44)).toBe(45)
expect(snapTo(45)).toBe(45)
expect(snapTo(46)).toBe(45)
expect(snapTo(89)).toBe(90)
expect(snapTo(-44)).toBe(-45)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/to-decimal.test.ts
================================================
import { toDecimal } from "../to-decimal"
test("toDecimal", () => {
expect(toDecimal(3.33333)).toBe(3.33)
expect(toDecimal(3.3)).toBe(3.3)
expect(toDecimal(6.66666, 3)).toBe(6.667)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/velocity-per-frame.test.ts
================================================
import { velocityPerFrame } from "../velocity-per-frame"
test("velocityPerFrame", () => {
expect(velocityPerFrame(50, 16.7)).toBe(0.835)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/velocity-per-second.test.ts
================================================
import { velocityPerSecond } from "../velocity-per-second"
test("velocityPerSecond", () => {
expect(velocityPerSecond(0.835, 16.7)).toBe(50)
expect(velocityPerSecond(0.835, 0)).toBe(0)
})
================================================
FILE: packages/popmotion/src/utils/__tests__/wrap.test.ts
================================================
import { wrap } from "../wrap"
test("wrap", () => {
expect(wrap(-100, 100, -100)).toBe(-100)
expect(wrap(-100, 100, 0)).toBe(0)
expect(wrap(-100, 100, -200)).toBe(0)
expect(wrap(-100, 100, 101)).toBe(-99)
})
================================================
FILE: packages/popmotion/src/utils/angle.ts
================================================
import { Point } from '../types';
import { radiansToDegrees } from './radians-to-degrees';
import { zeroPoint } from './inc';
/*
Angle between points
@param [object]: X and Y coordinates of from point
@param [object]: X and Y coordinates of to point
@return [radian]: Angle between the two points in radians
*/
export const angle = (a: Point, b: Point = zeroPoint) =>
radiansToDegrees(Math.atan2(b.y - a.y, b.x - a.x));
================================================
FILE: packages/popmotion/src/utils/apply-offset.ts
================================================
/**
* Apply offset
* A function that, given a value, will get the offset from `from`
* and apply it to `to`
* @param {number} from
* @param {number} to
* @return {function}
*/
export const applyOffset = (from: number, to?: number) => {
let hasReceivedFrom = true;
if (to === undefined) {
to = from;
hasReceivedFrom = false;
}
return (v: number) => {
if (hasReceivedFrom) {
return v - from + to;
} else {
from = v;
hasReceivedFrom = true;
return to;
}
};
};
================================================
FILE: packages/popmotion/src/utils/attract.ts
================================================
const identity = (v: any): any => v
/**
* Creates an attractor that, given a strength constant, origin and value,
* will calculate value as attracted to origin.
*/
export const createAttractor = (alterDisplacement: Function = identity) => (
constant: number,
origin: number,
v: number
) => {
const displacement = origin - v
const springModifiedDisplacement =
-(0 - constant + 1) * (0 - alterDisplacement(Math.abs(displacement)))
return displacement <= 0
? origin + springModifiedDisplacement
: origin - springModifiedDisplacement
}
export const attract = createAttractor()
export const attractExpo = createAttractor(Math.sqrt)
================================================
FILE: packages/popmotion/src/utils/clamp.ts
================================================
export const clamp = (min: number, max: number, v: number) =>
Math.min(Math.max(v, min), max);
================================================
FILE: packages/popmotion/src/utils/degrees-to-radians.ts
================================================
/*
Convert degrees to radians
@param [number]: Value in degrees
@return [number]: Value in radians
*/
export const degreesToRadians = (degrees: number) => (degrees * Math.PI) / 180;
================================================
FILE: packages/popmotion/src/utils/distance.ts
================================================
import { Point } from "../types"
import { isPoint } from "./is-point"
import { isPoint3D } from "./is-point-3d"
import { isNum } from "./inc"
const distance1D = (a: number, b: number) => Math.abs(a - b)
/*
Distance
Returns the distance between two n dimensional points.
@param [object/number]: x and y or just x of point A
@param [object/number]: (optional): x and y or just x of point B
@return [number]: The distance between the two points
*/
export function distance(a: P, b: P): number {
if (isNum(a) && isNum(b)) {
// 1D dimensions
return distance1D(a, b)
} else if (isPoint(a) && isPoint(b)) {
// Multi-dimensional
const xDelta = distance1D(a.x, b.x)
const yDelta = distance1D(a.y, b.y)
const zDelta = isPoint3D(a) && isPoint3D(b) ? distance1D(a.z, b.z) : 0
return Math.sqrt(xDelta ** 2 + yDelta ** 2 + zDelta ** 2)
}
}
================================================
FILE: packages/popmotion/src/utils/hsla-to-rgba.ts
================================================
import { HSLA, RGBA } from "style-value-types"
// Adapted from https://gist.github.com/mjackson/5311256
function hueToRgb(p: number, q: number, t: number) {
if (t < 0) t += 1
if (t > 1) t -= 1
if (t < 1 / 6) return p + (q - p) * 6 * t
if (t < 1 / 2) return q
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
return p
}
export function hslaToRgba({ hue, saturation, lightness, alpha }: HSLA): RGBA {
hue /= 360
saturation /= 100
lightness /= 100
let red = 0
let green = 0
let blue = 0
if (!saturation) {
red = green = blue = lightness
} else {
const q =
lightness < 0.5
? lightness * (1 + saturation)
: lightness + saturation - lightness * saturation
const p = 2 * lightness - q
red = hueToRgb(p, q, hue + 1 / 3)
green = hueToRgb(p, q, hue)
blue = hueToRgb(p, q, hue - 1 / 3)
}
return {
red: Math.round(red * 255),
green: Math.round(green * 255),
blue: Math.round(blue * 255),
alpha,
}
}
================================================
FILE: packages/popmotion/src/utils/inc.ts
================================================
import { Point } from '../types';
export const zeroPoint: Point = {
x: 0,
y: 0,
z: 0
};
export const isNum = (v: any): v is number => typeof v === 'number';
================================================
FILE: packages/popmotion/src/utils/interpolate.ts
================================================
import { Easing } from '../easing/types';
import { progress } from './progress';
import { mix } from './mix';
import { mixColor } from './mix-color';
import { mixComplex, mixArray, mixObject } from './mix-complex';
import { color } from 'style-value-types';
import { clamp } from './clamp';
import { pipe } from './pipe';
import { invariant } from 'hey-listen';
type MixEasing = Easing | Easing[];
type InterpolateOptions = {
clamp?: boolean;
ease?: MixEasing;
mixer?: MixerFactory;
};
type Mix = (v: number) => T;
export type MixerFactory = (from: T, to: T) => Mix;
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') {
if (color.test(v)) {
return mixColor;
} else {
return mixComplex;
}
} else if (Array.isArray(v)) {
return mixArray;
} else if (typeof v === 'object') {
return mixObject;
}
}
function createMixers(
output: T[],
ease?: MixEasing,
customMixer?: MixerFactory
) {
const mixers: Array> = [];
const mixerFactory: MixerFactory =
customMixer || detectMixerFactory(output[0]);
const numMixers = output.length - 1;
for (let i = 0; i < numMixers; i++) {
let mixer = mixerFactory(output[i], output[i + 1]);
if (ease) {
const easingFunction = Array.isArray(ease) ? ease[i] : ease;
mixer = pipe(easingFunction, mixer) as Mix;
}
mixers.push(mixer);
}
return mixers;
}
function fastInterpolate([from, to]: number[], [mixer]: Array>) {
return (v: number) => mixer(progress(from, to, v));
}
function slowInterpolate(input: number[], mixers: Array>) {
const inputLength = input.length;
const lastInputIndex = inputLength - 1;
return (v: number) => {
let mixerIndex = 0;
let foundMixerIndex = false;
if (v <= input[0]) {
foundMixerIndex = true;
} else if (v >= input[lastInputIndex]) {
mixerIndex = lastInputIndex - 1;
foundMixerIndex = true;
}
if (!foundMixerIndex) {
let i = 1;
for (; i < inputLength; i++) {
if (input[i] > v || i === lastInputIndex) {
break;
}
}
mixerIndex = i - 1;
}
const progressInRange = progress(
input[mixerIndex],
input[mixerIndex + 1],
v
);
return mixers[mixerIndex](progressInRange);
};
}
/**
* Create a function that maps from a numerical input array to a generic output array.
*
* Accepts:
* - Numbers
* - Colors (hex, hsl, hsla, rgb, rgba)
* - Complex (combinations of one or more numbers or strings)
*
* ```jsx
* const mixColor = interpolate([0, 1], ['#fff', '#000'])
*
* mixColor(0.5) // 'rgba(128, 128, 128, 1)'
* ```
*
* @public
*/
export function interpolate(
input: number[],
output: T[],
{ clamp: isClamp = true, ease, mixer }: InterpolateOptions = {}
) {
const inputLength = input.length;
invariant(
inputLength === output.length,
'Both input and output ranges must be the same length'
);
invariant(
!ease || !Array.isArray(ease) || ease.length === inputLength - 1,
'Array of easing functions must be of length `input.length - 1`, as it applies to the transitions **between** the defined values.'
);
// If input runs highest -> lowest, reverse both arrays
if (input[0] > input[inputLength - 1]) {
input = [].concat(input);
output = [].concat(output);
input.reverse();
output.reverse();
}
const mixers = createMixers(output, ease, mixer);
const interpolator =
inputLength === 2
? fastInterpolate(input, mixers)
: slowInterpolate(input, mixers);
return isClamp
? (v: number) => interpolator(clamp(input[0], input[inputLength - 1], v))
: interpolator;
}
================================================
FILE: packages/popmotion/src/utils/is-point-3d.ts
================================================
import { isPoint } from './is-point';
import { Point, Point3D } from '../types';
export const isPoint3D = (point: Point): point is Point3D =>
isPoint(point) && point.hasOwnProperty('z');
================================================
FILE: packages/popmotion/src/utils/is-point.ts
================================================
import { Point } from '../types';
export const isPoint = (point: Object): point is Point =>
point.hasOwnProperty('x') && point.hasOwnProperty('y');
================================================
FILE: packages/popmotion/src/utils/mix-color.ts
================================================
import { mix } from "./mix"
import { hsla, rgba, hex, Color } from "style-value-types"
import { invariant } from "hey-listen"
import { hslaToRgba } from "./hsla-to-rgba"
// 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
const toExpo = to * to
return Math.sqrt(Math.max(0, v * (toExpo - fromExpo) + fromExpo))
}
const colorTypes = [hex, rgba, hsla]
const getColorType = (v: Color | string) =>
colorTypes.find((type) => type.test(v))
const notAnimatable = (color: Color | string) =>
`'${color}' is not an animatable color. Use the equivalent color code instead.`
export const mixColor = (from: Color | string, to: Color | string) => {
let fromColorType = getColorType(from)
let toColorType = getColorType(to)
invariant(!!fromColorType, notAnimatable(from))
invariant(!!toColorType, notAnimatable(to))
let fromColor = fromColorType.parse(from)
let toColor = toColorType.parse(to)
if (fromColorType === hsla) {
fromColor = hslaToRgba(fromColor)
fromColorType = rgba
}
if (toColorType === hsla) {
toColor = hslaToRgba(toColor)
toColorType = rgba
}
const blended = { ...fromColor }
return (v: number) => {
for (const key in blended) {
if (key !== "alpha") {
blended[key] = mixLinearColor(fromColor[key], toColor[key], v)
}
}
blended.alpha = mix(fromColor.alpha, toColor.alpha, v)
return fromColorType.transform(blended)
}
}
================================================
FILE: packages/popmotion/src/utils/mix-complex.ts
================================================
import { color, complex, RGBA, HSLA } from "style-value-types"
import { mix } from "./mix"
import { mixColor } from "./mix-color"
import { isNum } from "./inc"
import { pipe } from "./pipe"
import { warning } from "hey-listen"
type MixComplex = (p: number) => string
type BlendableArray = Array
type BlendableObject = {
[key: string]: string | number | RGBA | HSLA
}
function getMixer(origin: any, target: any) {
if (isNum(origin)) {
return (v: number) => mix(origin, target as number, v)
} else if (color.test(origin)) {
return mixColor(origin, target as HSLA | RGBA | string)
} else {
return 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
}
}
function analyse(value: string | number) {
const parsed = complex.parse(value)
const numValues = parsed.length
let numNumbers = 0
let numRGB = 0
let numHSL = 0
for (let i = 0; i < numValues; i++) {
// Parsed complex values return with colors first, so if we've seen any number
// we're already past that part of the array and don't need to continue running typeof
if (numNumbers || typeof parsed[i] === "number") {
numNumbers++
} else {
if ((parsed[i] as HSLA).hue !== undefined) {
numHSL++
} else {
numRGB++
}
}
}
return { parsed, numNumbers, numRGB, numHSL }
}
export const mixComplex = (
origin: string | number,
target: string | number
): MixComplex => {
const template = complex.createTransformer(target)
const originStats = analyse(origin)
const targetStats = analyse(target)
const canInterpolate =
originStats.numHSL === targetStats.numHSL &&
originStats.numRGB === targetStats.numRGB &&
originStats.numNumbers >= targetStats.numNumbers
if (canInterpolate) {
return pipe(
mixArray(originStats.parsed, targetStats.parsed),
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 (p: number) => `${p > 0 ? target : origin}`
}
}
================================================
FILE: packages/popmotion/src/utils/mix.ts
================================================
/*
Value in range from progress
Given a lower limit and an upper limit, we return the value within
that range as expressed by progress (usually a number from 0 to 1)
So progress = 0.5 would change
from -------- to
to
from ---- to
E.g. from = 10, to = 20, progress = 0.5 => 15
@param [number]: Lower limit of range
@param [number]: Upper limit of range
@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;
================================================
FILE: packages/popmotion/src/utils/pipe.ts
================================================
/**
* Pipe
* Compose other transformers to run linearily
* pipe(min(20), max(40))
* @param {...functions} transformers
* @return {function}
*/
const combineFunctions = (a: Function, b: Function) => (v: any) => b(a(v));
export const pipe = (...transformers: Function[]) =>
transformers.reduce(combineFunctions);
================================================
FILE: packages/popmotion/src/utils/point-from-vector.ts
================================================
import { Point2D } from '../types';
import { degreesToRadians } from './degrees-to-radians';
/*
Point from angle and distance
@param [object]: 2D point of origin
@param [number]: Angle from origin
@param [number]: Distance from origin
@return [object]: Calculated 2D point
*/
export const pointFromVector = (
origin: Point2D,
angle: number,
distance: number
) => {
angle = degreesToRadians(angle);
return {
x: distance * Math.cos(angle) + origin.x,
y: distance * Math.sin(angle) + origin.y
};
};
================================================
FILE: packages/popmotion/src/utils/progress.ts
================================================
/*
Progress within given range
Given a lower limit and an upper limit, we return the progress
(expressed as a number 0-1) represented by the given value, and
limit that progress to within 0-1.
@param [number]: Lower limit
@param [number]: Upper limit
@param [number]: Value to find progress within given range
@return [number]: Progress of value within range as expressed 0-1
*/
export const progress = (from: number, to: number, value: number) => {
const toFromDifference = to - from;
return toFromDifference === 0 ? 1 : (value - from) / toFromDifference;
};
================================================
FILE: packages/popmotion/src/utils/radians-to-degrees.ts
================================================
/*
Convert radians to degrees
@param [number]: Value in radians
@return [number]: Value in degrees
*/
export const radiansToDegrees = (radians: number) => (radians * 180) / Math.PI;
================================================
FILE: packages/popmotion/src/utils/smooth-frame.ts
================================================
import { toDecimal } from './to-decimal';
/*
Framerate-independent smoothing
@param [number]: New value
@param [number]: Old value
@param [number]: Frame duration
@param [number] (optional): Smoothing (0 is none)
*/
export const smoothFrame = (
prevValue: number,
nextValue: number,
duration: number,
smoothing: number = 0
) =>
toDecimal(
prevValue +
(duration * (nextValue - prevValue)) / Math.max(smoothing, duration)
);
================================================
FILE: packages/popmotion/src/utils/smooth.ts
================================================
import { smoothFrame } from './smooth-frame';
import { getFrameData } from 'framesync';
export const smooth = (strength: number = 50) => {
let previousValue = 0;
let lastUpdated = 0;
return (v: number) => {
const currentFramestamp = getFrameData().timestamp;
const timeDelta =
currentFramestamp !== lastUpdated ? currentFramestamp - lastUpdated : 0;
const newValue = timeDelta
? smoothFrame(previousValue, v, timeDelta, strength)
: previousValue;
lastUpdated = currentFramestamp;
previousValue = newValue;
return newValue;
};
};
================================================
FILE: packages/popmotion/src/utils/snap.ts
================================================
export const snap = (points: number | number[]) => {
if (typeof points === 'number') {
return (v: number) => Math.round(v / points) * points;
} else {
let i = 0;
const numPoints = points.length;
return (v: number) => {
let lastDistance = Math.abs(points[0] - v);
for (i = 1; i < numPoints; i++) {
const point = points[i];
const distance = Math.abs(point - v);
if (distance === 0) return point;
if (distance > lastDistance) return points[i - 1];
if (i === numPoints - 1) return point;
lastDistance = distance;
}
};
}
};
================================================
FILE: packages/popmotion/src/utils/to-decimal.ts
================================================
export const toDecimal = (num: number, precision: number = 2) => {
precision = 10 ** precision;
return Math.round(num * precision) / precision;
};
================================================
FILE: packages/popmotion/src/utils/velocity-per-frame.ts
================================================
/*
Convert x per second to per frame velocity based on fps
@param [number]: Unit per second
@param [number]: Frame duration in ms
*/
export function velocityPerFrame(xps: number, frameDuration: number) {
return xps / (1000 / frameDuration)
}
================================================
FILE: packages/popmotion/src/utils/velocity-per-second.ts
================================================
/*
Convert velocity into velocity per second
@param [number]: Unit per frame
@param [number]: Frame duration in ms
*/
export function velocityPerSecond(velocity: number, frameDuration: number) {
return frameDuration ? velocity * (1000 / frameDuration) : 0
}
================================================
FILE: packages/popmotion/src/utils/wrap.ts
================================================
export const wrap = (min: number, max: number, v: number) => {
const rangeSize = max - min;
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
};
================================================
FILE: packages/popmotion/src/worklet/animate.ts.wip
================================================
import { registerCustomProperties, namespace } from './custom-properties';
import { whenWorkletReady } from './load-worklet';
type Target = {
[key: string]: string | number;
};
interface AnimationOptions {}
registerCustomProperties();
export async function animate(
element: HTMLElement | SVGElement,
values: Target,
options: AnimationOptions
) {
await whenWorkletReady();
// TODO: Dynamically map transforms and filters to CSS variable name and type
const keyframes = [
{
[namespace('translate-x')]:
element.style.getPropertyValue(namespace('translate-x')) || '0px' // TODO: Read from element and fallback to defailt of not set
},
{
[namespace('translate-x')]: `${values.x}px` // TODO dynamically convert numbers to default type
}
];
// TODO: Dynamically generate this based on the values set on this element potentially within a weakmap
element.style.transform = `translateX(var(${namespace('translate-x')}))`;
const effect = new KeyframeEffect(element, keyframes, options);
// TODO Replace ts ignore
// @ts-ignore
const animation = new WorkletAnimation('tween', effect, document.timeline, {
documentTimeline: new DocumentTimeline()
});
return animation.play();
}
export async function parallax(
element: HTMLElement | SVGElement,
values: Target,
source: HTMLElement,
options: AnimationOptions
) {
await whenWorkletReady();
const keyframes: any = [];
const effect = new KeyframeEffect(element, keyframes, options);
// @ts-ignore
const timeline = new ScrollTimeline({
scrollSource: source,
timeRange: 1000,
orientation: 'vertical'
});
// @ts-ignore
const animation = new WorkletAnimation('parallax', effect, timeline);
return animation.play();
}
================================================
FILE: packages/popmotion/src/worklet/custom-properties.ts
================================================
interface CustomProperty {
name: string;
syntax: string;
initial: string | number;
}
const transforms: CustomProperty[] = [
{
name: 'translate',
syntax: 'length-percentage',
initial: '0px'
},
{
name: 'scale',
syntax: 'number',
initial: 1
},
{
name: 'rotate',
syntax: 'angle',
initial: '0deg'
}
];
const axes = ['x', 'y', 'z'];
const customProperties: CustomProperty[] = [];
transforms.forEach(({ name, syntax, initial }) => {
customProperties.push({ name, syntax, initial });
axes.forEach(axis =>
customProperties.push({
name: `${name}-${axis}`,
syntax,
initial
})
);
});
export function namespace(name: string) {
return `--pm-${name}`;
}
export function registerCustomProperties() {
// TODO we need to check for registerProperty's presence
customProperties.forEach(({ name, syntax, initial }) => {
// TODO declare this with types
(CSS as any).registerProperty({
name: namespace(name),
syntax: `<${syntax}>`,
inherits: false,
initialValue: initial
});
});
}
================================================
FILE: packages/popmotion/src/worklet/index.ts.wip
================================================
//import { tween } from '../animations/tween';
import { ForT } from '../types';
import { spring } from '../animations/spring';
interface WorkletAnimationEffect {
localTime: number;
}
// extends StatelessAnimator when browsers implement the spec
class StatelessPopmotionAnimator {
animation: ForT;
a: any;
documentTimeline: DocumentTimeline;
constructor(animation: ForT) {
this.animation = animation;
// this.state = state;
//console.log('constructor');
}
animate(currentTime: number, effect: WorkletAnimationEffect) {
// TODO: Why does this snap back once the duration is reached?
effect.localTime = this.animation(currentTime);
// this.a = { velocity: 'localTime ' };
}
// state() {
// return this.a;
// }
}
// interface TweenOptions {
// documentTimeline: DocumentTimeline;
// }
// class Tween extends StatelessPopmotionAnimator {
// constructor(options: TweenOptions) {
// super(tween({ from: 0, to: 3000, duration: 3000 }));
// }
// }
interface SpringOptions {}
class Spring extends StatelessPopmotionAnimator {
constructor(options: SpringOptions) {
super(spring({}));
}
}
// @ts-ignore
registerAnimator(
'tween',
class {
constructor() {
console.log('made');
}
animate(currentTime: number, effect: WorkletAnimationEffect) {
effect.localTime = currentTime;
}
}
);
// @ts-ignore
registerAnimator('spring', Spring);
interface ParallaxOptions {
factor: number;
}
class Parallax {
factor = 0.5;
constructor(options: ParallaxOptions) {
this.factor = options.factor;
}
animate(currentTime: number, effect: WorkletAnimationEffect) {
if (isNaN(currentTime)) return;
effect.localTime = this.factor * currentTime;
}
}
// @ts-ignore
registerAnimator('parallax', Parallax);
================================================
FILE: packages/popmotion/src/worklet/load-worklet.ts
================================================
type Resolve = () => void
let isReady = false
const awaitingReady: Resolve[] = []
export async function whenWorkletReady() {
if (!isReady) {
return new Promise((resolve) => {
awaitingReady.push(resolve)
})
}
}
function flushAwaiting() {
awaitingReady.forEach((resolve) => resolve())
}
export function workletReady() {
isReady = true
flushAwaiting()
}
================================================
FILE: packages/popmotion/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib"
},
"include": ["src/**/*"]
}
================================================
FILE: packages/popmotion/tslint.json
================================================
{
"extends": [
"tslint-circular-dependencies"
]
}
================================================
FILE: packages/popmotion-pose/docs/api/dom/config.md
================================================
---
title: Config
description: Configure a poser
category: vanilla
---
# Config options
### init
`init?: Pose`
A [pose config](#config-options-pose-config) to default to if no `initialPose` is defined.
### initialPose
`initialPose?: string | string[] = 'init'`
The name of the initial pose (or poses if provided as an array).
### draggable
`draggable?: true | 'x' | 'y'`
If `true`, will make the element draggable on both axis. Setting to either `'x'` or `'y'` will restrict movement to that axis.
If defined, allows use of the special `drag` pose for styling the element while dragging is active.
A `dragEnd` pose can be **optionally** set for animating on drag end.
```javascript
const config = {
draggable: 'x',
init: { scale: 1 },
drag: { scale: 1.2 }
}
```
The `drag` and `dragEnd` poses will travel through any posed children.
### dragBounds
`dragBounds?: { [key: string]: number }`
An object that defines `top`, `right`, `bottom` and/or `left` drag boundaries in pixels.
Currently, these boundaries are enforced by a hard clamp.
### onDragStart/onDragEnd
`onDragStart/onDragEnd: (e: MouseEvent | TouchEvent) => any`
Lifecycle callbacks for drag events.
## hoverable
`hoverable?: boolean`
If `true`, this element will receive `hover` poses when a pointer hovers over it.
There's also an **optional** `hoverEnd` pose, for providing a different pose when hovering ends.
```javascript
const config = {
hoverable: true,
init: { scale: 1 },
hover: { scale: 1.2 }
}
```
The `hover` and `hoverEnd` poses will travel through any posed children.
### focusable
`focusable?: boolean`
If `true`, this element will receive `focus` poses when the element receives focus, and `blur` poses when it loses focus.
```javascript
const config = {
focusable: true,
init: { scale: 1 },
focus: { scale: 1.2 },
blur: {
scale: 1,
transition: {
type: 'spring',
stiffness: 800
}
}
}
```
### pressable
`pressable?: boolean`
If `true`, this element will receive `press` poses when the element is pressed, and **optionally** `pressEnd` when pressing stops.
```javascript
const config = {
pressable: true,
init: { scale: 1 },
press: { scale: 0.8 }
};
```
### onPressStart/onPressEnd
`onPressStart/onPressEnd: (e: MouseEvent | TouchEvent) => any`
Lifecycle callbacks for press events.
### passive
`passive: { [key: string]: PassiveValue }`
```typescript
type PassiveValue = [
subscribedKey: string,
transform: (subscribedValue: any) => any,
fromParent?: true | string
]
```
Map of values that are passively changed when other values, either on this Poser or an ancestor, change.
`fromParent` can be set either as `true` or as a `string`:
- `true`: Link to value from immediate parent.
- `string`: Link to the nearest ancestor with this `label` prop.
#### Example
The `transform` function here is composed with Popmotion [transformers](/api/transformers):
```javascript
const config = {
draggable: 'x',
passive: {
backgroundColor: ['x', pipe(
clamp(0, 300),
interpolate([0, 300], [0, 1]),
blendColor('#f00', '#0f0')
)]
}
}
```
### label
`label: string`
Set a label on this poser. Currently, this allows a `passive` value on a child poser to refer to this ancestor value.
### props
`props: { [key: string]: any }`
Properties to provide to entered pose `transition` methods and dynamic pose props. These can be updated with the `setProps` method or, in React Pose, by providing props to the posed component.
### onChange
`onChange?: { [key: string]: (v: any) => any }`
Map of callbacks, one for each animated value, that will fire whenever that value changes.
**Note:** For React Pose, instead use the `onValueChange` property on the posed component.
#### Example
```javascript
const config = {
draggable: 'x',
onChange: {
x: (x) => // you do you
}
}
```
### ...poses
`...poses: { [key: string]: Pose }`
Any other config props will be treated as poses (see [Pose config](#pose-config)).
## Pose config
You can call a pose anything, and animate to it by calling `poser.set('poseName')` or setting ``.
A pose is defined by style attributes like `x` or `backgroundColor`, and the following optional props:
### delay
`delay?: number | (props: Props) => number`
A duration, in milliseconds, to delay this transition. Does **not** affect children.
### delayChildren
`delayChildren?: number | (props: Props) => number`
A duration, in milliseconds, to delay the transition of direct children.
### flip
`flip?: boolean = false`
If `true`, will convert this animation to a [FLIP animation](https://aerotwist.com/blog/flip-your-animations/).
### staggerChildren
`staggerChildren?: number | (props: Props) => number`
A duration, in milliseconds, between transitioning each children.
### staggerDirection
`staggerDirection?: 1 | -1 | (props: Props) => 1 | -1`
If `1`, staggers from the first child to the last. If `-1`, from last to first.
### beforeChildren
`beforeChildren?: boolean | (props: Props) => boolean`
If `true`, will ensure this animation completes before firing any child animations.
### afterChildren
`afterChildren?: boolean | (props: Props) => boolean`
If `true`, will ensure this animation only fires after all child animations have completed.
### applyAtStart/applyAtEnd
`applyAtStart/applyAtEnd?: { [string]: any | (props: Props) => any }`
`applyAtStart` and `applyAtEnd` accept style properties to apply either at the start or end of an animation.
For instance, you might have an element that you want to flip between `display: block` before it fades in, and `display: none` after it fades out:
```javascript
const config = {
visible: {
applyAtStart: { display: 'block' },
opacity: 1
},
hidden: {
applyAtEnd: { display: 'none' },
opacity: 0
}
};
```
### transition
`transition?`
The `transition` prop can be used to create custom transitions.
It can be set as a transition definition:
```javascript
transition: { type: 'spring' }
```
A function that returns a transition definition **or** a Popmotion animation:
```javascript
transition: (props) => spring({...props})
transition: (props) => ({ type: 'spring' })
```
Or finally, a named map where each prop is either a transition definition, or a function returning a transition definition/Popmotion animation:
```javascript
visible: {
x: 0,
opacity: 1,
transition: {
x: { type: 'spring' },
default: (props) => tween(props)
}
}
```
#### Transition definitions
A transition definition describes the type of animation Pose should use to move to the value defined in the Pose.
There are many types, and each has its own specific configuration props available.
##### Tween (default)
Transitions between one value and another over a set duration of time.
- `duration?: number = 300`: Total duration of animation, in milliseconds.
- `elapsed?: number = 0`: Duration of animation already elapsed, in milliseconds.
- `ease?: string | number[] | Function`: The name of an easing function, a cubic bezier definition, or an easing function. The following easings are included with Pose:
- 'linear'
- 'easeIn', 'easeOut', 'easeInOut'
- 'circIn', 'circOut', 'circInOut'
- 'backIn', 'backOut', 'backInOut'
- 'anticipate'
- `loop?: number = 0`: Number of times to loop animation.
- `flip?: number = 0`: Number of times to flip animation.
- `yoyo?: number = 0`: Number of times to reverse tween.
##### Spring
A spring animation based on `stiffness`, `damping` and `mass`.
- `type: 'spring'`: Set transition to spring.
- `stiffness?: number = 100`: Spring stiffness.
- `damping?: number = 10`: Strength of opposing force.
- `mass?: number = 1.0`: Mass of the moving object.
- `restDelta?: number = 0.01`: End animation if distance to `to` is below this value **and** `restSpeed` is `true`.
- `restSpeed?: number = 0.01`: End animation if speed drops below this value **and** `restDelta` is `true`.
##### Physics
Integrated simulation of velocity, acceleration, friction and springs.
- `type: 'physics'`: Set transition to physics.
- `acceleration?: number = 0`: Increase `velocity` by this amount every second.
- `restSpeed?: number = 0.001`: When absolute speed drops below this value, `complete` is fired.
- `friction?: number = 0`: Amount of friction to apply per frame, from `0` to `1`.
- `springStrength?: number = 0`: If set with `to`, will spring towards target with this strength.
##### Keyframes
Keyframes accepts an array of `values` and will animate between each in sequence.
Timing is defined with a combination of `duration`, `easings` and `times` properties (see [Methods](#methods))
- `type: 'keyframes'`: Set transition to keyframes.
- `values: number[]`: An array of numbers to animate between. To use the value defined in the Pose as the final destination value, set `transition` as a function: `transition: ({ to }) => { type: 'keyframes', values: [0, to] }`
- `duration?: number = 300`: Total duration of animation, in milliseconds.
- `easings?: Easing | Easing[]`: An array of easing functions for each generated tween, or a single easing function applied to all tweens. This array should be `values.length - 1`. Defaults to `easeOut`. (This doesn't yet support named easings)
- `times?: number[]`: An array of numbers between `0` and `1`, representing `0` to `duration`, that represent at which point each number should be hit. Defaults to an array of evenly-spread durations will be calculated.
- `elapsed?: number = 0`: Duration of animation already elapsed, in milliseconds.
- `ease?: Easing = easeOut`: A function, given a progress between `0` and `1`, that returns a new progress value. Used to affect the rate of playback across the duration of the animation. (This doesn't yet support named easings)
- `loop?: number = 0`: Number of times to loop animation.
- `flip?: number = 0`: Number of times to flip animation.
- `yoyo?: number = 0`: Number of times to reverse tween.
##### Decay
`decay` exponentially decelerates a number and velocity to an automatically generated target value. This target can be modified by the user.
This animation is particularly useful for implementing momentum scrolling.
- `type: 'decay'`: Set transition to decay.
- `power?: number = 0.8`: A constant with which to calculate a target value. Higher power = further target. `0.8` should be okay.
- `timeConstant?: number = 350`: Adjusting the time constant will change the duration of the deceleration, thereby affecting its feel.
- `restDelta?: number = 0.5`: Automatically completes the action when the calculated value is this far away from the target.
- `modifyTarget?: (v: number) => number`: A function that receives the calculated target and returns a new one. Useful for snapping the target to a grid, for example.
##### General transition props
The following props can be set on any transition:
- `from?: number | Color`: Start value of animation (overrides Pose-generated value).
- `to?: number | Color`: End value of animation (overrides Pose-generated value).
- `velocity?: number`: Initial velocity of animation (overrides Pose-generated value).
- `delay?: number`: Delay the execution of the transition by this amount of time (in milliseconds).
- `min?: number`: Restrict output to numbers larger than this.
- `max?: number`: Restrict output to numbers smaller than this.
- `round?: boolean`: If `true`, output numbers will be rounded.
#### Transition props
If set as a function, `transition` received the same user-defined props as other dynamic pose properties, with some generated by Pose:
```typescript
type TransitionsProps = {
from: any,
to: any,
velocity: number,
key: string,
prevPoseKey: string
}
```
- `from`: The current state of the value
- `velocity`: The current velocity of the value, if it's a number
- `to`: The state we're animating to, as defined in the current pose. **Note:** You're under no obligation to actually animate to this value (for instance for non-deterministic animations)
- `key`: The name of the value
- `prevPoseKey`: The name of the pose this value was previously in.
### ...values
`...values: any | (props: Props) => any`
Any remaining properties are treated as stylistic values and will be animated.
================================================
FILE: packages/popmotion-pose/docs/api/dom/dom-pose.md
================================================
---
title: Poser
description: Animate and HTML or SVG element with a poser.
category: vanilla
---
# Poser
A poser is used to animate an element and its poser children.
**Note:** For React, use [posed components](/pose/api/posed).
## Import
```javascript
import pose from 'popmotion-pose'
```
## Usage
### Create
Posers are created with the `pose` function.
`pose` accepts two arguments: A HTML or SVG element, and an object of [pose config](/pose/api/config).
```javascript
const poser = pose(element, config)
```
### Animate
Animating to a pose defined in the [pose config](/pose/api/config) is a matter of calling the poser's `set` method:
```javascript
poser.set('nameOfPose')
```
### Sequence
`set` returns a Promise, which resolves once all animations, and animations of any child posers, are complete.
```javascript
// Promise
poser.set('nameOfPose').then(() => /* Do other thing */)
// Async
await poser.set('nameOfPose')
/* Do other thing */
```
### Multiple animations
When a pose is set on a poser, it is only set on the values defined **in that pose**. Which means, if we have two poses with different sets of properties, we can animate both at the same time:
```javascript
const poser = pose(element, {
flash: {
backgroundColor: '#f00',
transition: ({ from, to }) => tween({ from, to, yoyo: Infinity })
},
shake: {
x: true, // We're not using `to` in the transition but want to animate x
transition: ({ from, velocity }) => spring({
from, velocity,
stiffness: 1000,
damping: 100
})
}
})
poser.set('flash')
poser.set('shake')
```
### Transitions
By default, Pose will choose a transition depending on the property being animated:
- `x`, `y`, `z`: [Spring](https://popmotion.io/api/spring)
- `scale`, `scaleX`, `scaleY`: [Spring](https://popmotion.io/api/spring) (overdamped)
- Other props: [Tween](https://popmotion.io/api/tween)
**Note:** In a future release, these default animations will be configurable via `personality` settings.
#### Custom transitions
Every pose has an optional `transition` property that allows you to define a custom transition:
```javascript
const config = {
attention: {
scale: 1.2,
transition: ({ from, to }) => tween({ from, to, yoyo: Infinity })
},
rest: { scale: 1 }
}
```
This function is run **once for each animating property** and must return a [Popmotion animation](https://popmotion.io/api) (or `false` for no animation).
The `transition` function receives a single argument, an object containing:
- Information about the current transition: `from`, `to`, `velocity`, `key` and `prevPoseKey` properties.
- Transition props. These can be set statefully as `config.props` or via the `setProps` method. Or they can be set temporarily, as the second argument provided to `set`.
- **React Pose:** All props set on the posed component.
You can use these props to create different animations for different values.
### Dynamic values
Values on a pose can be set as a function that returns the `to` property for a transition.
This function is passed all the same properties as the `transition` function **except** for `to`, which this function is responsible for returning.
```javascript
const config = {
open: { x: 0 },
closed: {
x: ({ i }) => Math.sin(i * 0.5) * 100
}
}
// Vanilla
// --------------------
itemPosers = items.map((item, i) => pose(item, {
...config,
props: { i }
}))
// React
// --------------------
const Item = posed.li(config)
// In component render
{items.map((item, i) => }
```
### Draggable
Any element can be made draggable by passing the `draggable` property to `config`:
```javascript
const config = { draggable: true };
```
Dragging can be locked to a single axis by passing the name of that axis instead:
```javascript
const config = { draggable: 'x' };
```
#### dragEnd pose
When an element is draggable and a user stops dragging, a special pose called `dragEnd` is automatically set.
You can decide what animation fires by using the `transition` property:
```javascript
const config = {
draggable: 'x',
dragEnd: {
transition: ({ from }) => // return custom animation
}
};
```
#### Drag lifecycle events
`onDragStart` and `onDragEnd` functions can be defined to fire when a user starts and stops dragging:
```javascript
const config {
draggable: true,
onDragStart: (e) => // not our business!
}
```
#### Bound drag movement
We can limit pointer-driven movement with the `dragBounds` object.
It can restrict movement in both dimensions with optional `left`, `right`, `top`, and `bottom` properties:
```javascript
const config = {
draggable: 'x',
dragBounds: { left: 0, right: 500 }
}
```
### onChange events
We can append `onChange` callbacks to any value with the `onChange` map:
```javascript
const config = {
draggable: true,
onChange: {
x: v => // Do your thing!
}
}
```
### Children
With a poser's `addChild` method, we can spawn a new poser as a child.
When we call `set` on the parent poser, the same `set` will be passed down to its children. Like this, we can orchestrate multiple animations with a single call.
#### Add children
`addChild` is called exactly like `pose`:
```javascript
const childPoser = parentPoser.addChild(element, childConfig)
```
Now, all `set` calls on `parentPoser` will be passed down to all of its children. It doesn't even need a pose with that label of its own to do so.
```javascript
parentPoser.set('open')
```
We can still call set on the child posers without affecting the parent or its siblings:
```javascript
childPoser.set('hover')
```
#### Delay and stagger children
We can delay the propagation of a set call `delayChildren` on the parent pose:
```javascript
const parentConfig = {
initialPose: 'close',
open: {
x: '0%',
delayChildren: 200
},
close: { x: '100%' }
}
```
Or if we wanted to stagger over the children, we can do so with `staggerChildren`, and **optionally** `staggerDirection`:
```javascript
const sidebarConfig = {
initialPose: 'close',
open: {
x: '0%',
delayChildren: 200,
staggerChildren: 50,
staggerDirection: -1 // stagger from the last child
},
close: { x: '100%' }
}
```
### Passive values
Not all values have to be actively animated via a pose. The `passive` property can be used to define values that simply subscribe to actively animated values and update when they do.
For instance, we could update `y` when `x` updates like so:
```javascript
const config = {
draggable: 'x',
passive: {
y: ['x', x => x]
}
}
```
Each passive value is defined as a tuple:
```javascript
[subscribeKey: string, transform: v => v, fromParent?: boolean]
```
- `subscribeKey`: The key of the value we'll subscribe to.
- `transform`: Receives the latest subscribed value and returns the passive value.
- `fromParent`: If `true`, will subscribe to this value in the direct parent instead of the current poser.
### FLIP
Animating positional and dimensional properties like `width` and `top` is tasking for browsers and can cause stuttering in animations.
The [FLIP technique](https://aerotwist.com/blog/flip-your-animations/) was developed to animate these performantly by replacing them with transforms.
When animating these properties, `flip: true` can be set to use FLIP:
```javascript
// Pose will automatically measure the difference
// in element size and animate `scaleX` instead:
const config = {
open: { width: 200, flip: true }, // Will FLIP
closed: { width: 0 } // Will not FLIP
}
```
#### Explicit FLIP methods
Alternatively, we might want to transition to a new state where we don't know the new position or size.
For instance, if we change the children of the element, we might change the height. We can smoothly transition to the new height with the `measure` and `flip` methods:
```javascript
const poser = pose(element, config)
// Measure the current bounding box
poser.measure()
// Do stuff, like swap the element's children
doStuff()
// FLIP!
poser.flip()
```
Alternatively, we can just pass a callback to `flip`:
```javascript
poser.flip(doStuff)
```
## Methods
### set
`set(poseName: string, props?: Object): Promise`
Sets the current pose to `poseName`. If `Poser` has children, this will get set on those, too. Returns a `Promise`.
If `props` is defined, these will be passed through to the selected pose's `transition` function.
### setProps
`setProps(props: Object)`
Sets props on the poser that will be passed to any functions set on a pose.
### measure
`measure()`
Measures the current bounding box. Use this before making a change to the element that will affect physical dimensions (like adding new children, or moving it in the DOM), and then use `flip` to animate it to the new size.
### flip
`flip()`
Performs a FLIP animation between the previously `measure`d bounding box and the latest one.
You can add `flip` as a custom pose use a custom transition for this:
```javascript
const config = {
flip: {
transition: () => // your custom transition
}
};
```
### addChild
`addChild(element: HTMLElement | SVGElement, config: PoseConfig): Poser`
Creates and returns a new `Poser` as a child.
### removeChild
`removeChild(poser: Poser)`
Removes a child.
### clearChildren
`clearChildren()`
Removes all child posers and destroys them.
### destroy
`destroy()`
Stops all active transitions of this `Poser` and its children.
================================================
FILE: packages/popmotion-pose/docs/api/faqs.md
================================================
---
title: FAQs
description: Pose frequently asked questions
---
# FAQs
## Browser support?
Pose and React Pose support all major browsers.
For legacy IE11 support, the following polyfills are required:
- [String.endsWith](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith#Polyfill)
- [Array.from](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Polyfill)
- [WeakSet](https://github.com/dy/weakset)
## Filesize?
As of version 3.0:
- Pose is **15.7kb**
- React Pose is **18.8kb**, or **17kb** for pre-existing users of Styled Components or Emotion due to shared modules.
## Server-side rendering?
Currently React Pose doesn't apply any styles on the server, and you may need to apply initial styles via your CSS solution.
================================================
FILE: packages/popmotion-pose/docs/api/react/posed.md
================================================
---
title: Posed
description: Create a posed component
category: react
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# posed
`posed` is used to create animated and interactive components that you can reuse throughout your React site.
## Import
```javascript
import posed from 'react-pose'
```
## Usage
### Create a posed component
`posed` can be used to create posed components in two ways:
- **Recommended:** Create HTML & SVG elements (eg `posed.div`)
- **Advanced:** Convert existing components (eg `posed(Component)`)
#### HTML & SVG elements
`pose` isn't called directly, instead we pass [a pose config object](/pose/api/react-config) to `posed.div`, `posed.button` etc. Every HTML and SVG element is supported:
```javascript
const DraggableCircle = posed.circle({
draggable: 'x',
dragBounds: { left: 0, right: 100 }
})
export default ({ radius }) =>
```
#### Existing components
Existing components can be converted to posed components by calling `posed` directly:
```javascript
const PosedComponent = posed(MyComponent)(poseProps)
```
For performance and layout calculations, React Pose requires a reference to the underlying DOM element. So, the component to be animated **must pass forward a ref using the `React.forwardRef` function**:
```javascript
const MyComponent = forwardRef((props, ref) => (
));
const PosedComponent = posed(MyComponent)({
draggable: true
});
export default () =>
```
Many CSS-in-JS libraries like Styled Components will automatically do this for you.
For FLIP support in a `PoseGroup` component, it **optionally** needs to pass on the `style` prop:
```javascript
const MyComponent = forwardRef(({ style }, ref) => (
));
```
### Set a pose
Poses can be set via the `pose` property. This can either be a string, or an array of strings to reference multiple poses:
```javascript
const Sidebar = posed.nav({
open: { x: '0%' },
closed: { x: '-100%' }
})
export default ({ isOpen }) =>
```
### Children
Using a posed component creates a new tree of posed components. Any children that are also posed components are automatically added to this tree.
This means that orchestrating animations through React trees becomes trivial, as a pose only has to be set on a parent. Any children with that pose defined will also animate:
```javascript
const Sidebar = posed.nav({
open: { x: '0%', staggerChildren: 100 },
closed: { x: '-100%' }
})
const NavItem = posed.li({
open: { opacity: 1 },
closed: { opacity: 0 }
})
export default ({ isOpen, navItems }) => (
{navItems.map(({ url, name }) => (
{name}
))}
)
```
In tandem with the [`PoseGroup`](/pose/api/posegroup) component, this capability can be used to orchestrate sophisticated `enter` and `exit` animations.
This behaviour can be overridden by passing `newTree` to a posed component, which will ignore any parent posed components and create a new tree.
### Styling
Posed components are normal components, so they can be used with any CSS styling solution. For instance:
#### Styled Components
```javascript
const sidebarProps = {
open: { x: '0%' },
closed: { x: '-100%' }
}
const Sidebar = styled(posed.nav(sidebarProps))`
```
#### className
```javascript
() =>
```
## Props
### pose
`pose?: string | string[]`
The name or names of the current pose.
### initialPose
`initialPose?: string | string[]`
The name of one or more poses to set to before the component mounts. Once the component mounts, it will transition from this pose into `pose`.
### poseKey
`poseKey?: string | number`
If `poseKey` changes, it'll force the posed component to transition to the current `pose`, even if it hasn't changed.
This won't be required for the majority of use-cases. But we might have something like a paginated where we pass the x offset to the component but the pose itself doesn't change:
```javascript
const Slider = posed.div({
nextItem: {
x: ({ target }) => target
}
})
({ target }) =>
```
### withParent
`withParent?: boolean = true`
If set to `false`, this component won't subscribe to its parent posed component and create root for any further child components.
### onPoseComplete
`onPoseComplete?: Function`
A callback that fires whenever a pose has finished transitioning.
### onValueChange
`onValueChange?: { [key: string]: any }`
`onValueChange` is a map of functions, each corresponding to a value being animated by the posed component and will fire when that value changes.
### onDragStart/onDragEnd
`onDragStart/onDragEnd: (e: Event) => void`
Callbacks that fire when dragging starts or ends. **Note:** These props are immutable and can't be changed after mounting.
### onPressStart/onPressEnd
`onPressStart/onPressEnd: (e: Event) => void`
Callbacks that fire when pressing starts or ends. **Note:** These props are immutable and can't be changed after mounting.
### ref
`ref?: RefObject | (ref: Element) => void`
An optional ref property. If a function, will call with the posed DOM element when it mounts, and `null` when it unmounts.
Alternatively, it can be passed a ref object (created from `React.createRef`).
### values/parentValues
`values?: { [key: string]: Value }`
Normally, Pose generates [Popmotion `value`](/api/value) for each animating property. It then passes these down to any posed children so `passive` values can link to them.
Novel hierarchies, where the posed tree and the React component tree diverge, can be achieved by creating your own values map, and passing that to a single posed component.
This same map can then be passed to multiple other posed component's `parentValues` property in order to establish a parent-children relationship even between sibling components.
### ...props
`...props: { [key: string]: any }`
When a new pose is entered, any remaining props set on a component will be used to resolve that pose's dynamic props:
```javascript
const Component = posed.div({
visible: { opacity: 1, y: 0 },
hidden: {
opacity: 0,
y: ({ i }) => i * 50
}
})
// Later
({ isVisibile, i }) => (
)
```
================================================
FILE: packages/popmotion-pose/docs/api/react/posegroup.md
================================================
---
title: PoseGroup
description: Animate a group of posed components as they're added and removed.
category: react
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# PoseGroup
The `PoseGroup` component manages `enter` and `exit` animations on its direct children as they enter and exit the component tree.
## Import
```javascript
import posed, { PoseGroup } from 'react-pose'
```
## Usage
By adding a [posed component](/pose/api/posed) as a direct child of `PoseGroup`, it will gain two new poses: `enter` and `exit`.
```javascript
const Item = posed.div({
enter: { opacity: 1 },
exit: { opacity: 0 }
})
const ItemList = ({ items }) => (
{items.map((item) => )}
)
```
**Notes:**
- Every child must be provided a unique `key` property for `PoseGroup` to track entering and exiting children.
- Entering children transition from their pre-enter pose to `'enter'`. The pre-enter pose is `'exit'` by default, and can be edited with the [`preEnterPose` prop](#posegroup-props-preenterpose).
### Animating children
As with any posed component, the `enter`/`exit` pose will propagate throughout any of its posed component children.
In the case of the `exit` pose, `PoseGroup` will only unmount the animating component once **all of its children** have also finished their `exit` animation.
### Passing props to children
A common problem with transition components is passing props to components that have been removed from the tree. They might be animating out, but as far as React is concerned, they've already left (so props don't get updated).
With Pose for React, any props you provide to `PoseGroup` will be forwarded to all children, even ones that are leaving the tree.
This allows you to use the latest props in dynamic poses:
```javascript
const Item = posed.li({
enter: { opacity: 1, x: 0 },
exit: {
opacity: 0,
x: ({ selectedItemId, id }) =>
id === selectedItemId ? 100 : -100
}
});
export default ({ items, selectedItemId }) => (
{items.map(({ id }) => )}
);
```
## Props
### animateOnMount
`animateOnMount: boolean = false`
By default, only children added to the `PoseGroup` **after** it has mounted are animated to `enter`.
By setting `animateOnMount` to `true`, all children elements will animate in on mount.
### enterPose
`enterPose: string = 'enter'`
The name of the pose to use when a component enters.
### exitPose
`exitPose: string = 'exit'`
The name of the pose to use when a component leaves.
### preEnterPose
`preEnterPose: string = 'exit'`
The name of the pose to set before a component enters. This can be used to configure where a components animates in **from**.
### flipMove
`flipMove: boolean = true`
When an element exits, Pose takes it out of the layout and applies `position: absolute` so it can detect the new position of surrounding elements and animate via FLIP.
While it attempts to figure out the correct matching `transform-origin` there are times when this fails. Setting `flipMove={false}` will prevent these issues.
### onRest
`onRest: Function`
When a component finishes exiting, it isn't removed immediately. Instead, it's kept in the DOM until **all** currently leaving components have finished animating out to prevent expensive layout thrashing.
`onRest` will fire when all exit transitions are complete.
If new exit transitions begin in the meantime, `onRest` won't be fired until these have also finished.
### ...props
All remaining props passed to `PoseGroup` will be forwarded to its immediate children, for use in dynamic props.
This is useful because if you're removing a component, it's impossible in React to update its props without doing a two-pass render.
By providing them to the `PoseGroup` prop, Pose can pass these to the posed components to change the `exit` animation:
```javascript
const Box = posed.div({
enter: { x: 0 },
exit: { x: ({ delta }) => - delta * 100 + 'vw' }
});
export default ({ isVisible }) => (
{isVisible && }
)
```
================================================
FILE: packages/popmotion-pose/docs/api/react/react-config.md
================================================
---
title: Config
description: Configure a posed component
category: react
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Config options
Options to configure [posed components](/pose/api/posed) in React Pose.
## Options
### draggable
`draggable?: true | 'x' | 'y'`
If `true`, will make the element draggable on both axis. Setting to either `'x'` or `'y'` will restrict movement to that axis.
If defined, allows use of the special `drag` pose for styling the element while dragging is active.
A `dragEnd` pose can be **optionally** set for animating on drag end.
```javascript
const config = {
draggable: 'x',
init: { scale: 1 },
drag: { scale: 1.2 }
}
```
The `drag` and `dragEnd` poses will travel through any posed children.
### dragBounds
`dragBounds?: { [key: string]: number } | (props: Props) => { [key: string]: number }`
An object that defines `top`, `right`, `bottom` and/or `left` drag boundaries in pixels.
Currently, these boundaries are enforced by a hard clamp.
### hoverable
`hoverable?: boolean`
If `true`, this element will receive `hover` poses when a pointer hovers over it.
There's also an **optional** `hoverEnd` pose, for providing a different pose when hovering ends.
```javascript
const config = {
hoverable: true,
init: { scale: 1 },
hover: { scale: 1.2 }
}
```
The `hover` and `hoverEnd` poses will travel through any posed children.
### focusable
`focusable?: boolean`
If `true`, this element will receive `focus` poses when the element receives focus, and `blur` poses when it loses focus.
```javascript
const config = {
focusable: true,
init: { scale: 1 },
focus: { scale: 1.2 },
blur: {
scale: 1,
transition: {
type: 'spring',
stiffness: 800
}
}
};
```
### pressable
`pressable?: boolean`
If `true`, this element will receive `press` poses when the element is pressed, and **optionally** `pressEnd` when pressing stops.
```javascript
const config = {
pressable: true,
init: { scale: 1 },
press: { scale: 0.8 }
};
```
### passive
`passive: { [key: string]: PassiveValue }`
```typescript
type PassiveValue = [
subscribedKey: string,
transform: (subscribedValue: any) => any,
fromParent?: true | string
]
```
Map of values that are passively changed when other values, either on this Poser or an ancestor, change.
`fromParent` can be set either as `true` or as a `string`:
- `true`: Link to value from immediate parent.
- `string`: Link to the nearest ancestor with this `label` prop.
#### Example
The `transform` function here is composed with Popmotion [transformers](/api/transformers):
```javascript
const config = {
draggable: 'x',
passive: {
backgroundColor: ['x', pipe(
clamp(0, 300),
interpolate([0, 300], [0, 1]),
blendColor('#f00', '#0f0')
)]
}
}
```
### label
`label: string`
Set a label on this poser. Currently, this allows a `passive` value on a child poser to refer to this ancestor value.
### props
`props: { [key: string]: any }`
Default properties to provide to entered pose `transition` methods and dynamic pose props. These can be overridden by providing props to the posed component.
### ...poses
`...poses: { [key: string]: Pose }`
Any other config props will be treated as poses (see [Pose config](#pose-config)).
## Poses
You can call a pose anything, and animate to it by setting `` (or multiple poses with an array).
A pose is defined by style attributes like `x` or `backgroundColor`, and the following optional props:
### delay
`delay?: number | (props: Props) => number`
A duration, in milliseconds, to delay this transition. Does **not** affect children.
### delayChildren
`delayChildren?: number | (props: Props) => number`
A duration, in milliseconds, to delay the transition of direct children.
### flip
`flip?: boolean = false`
If `true`, will convert this animation to a [FLIP animation](https://aerotwist.com/blog/flip-your-animations/).
### staggerChildren
`staggerChildren?: number | (props: Props) => number`
A duration, in milliseconds, between transitioning each children.
### staggerDirection
`staggerDirection?: 1 | -1 | (props: Props) => 1 | -1`
If `1`, staggers from the first child to the last. If `-1`, from last to first.
### beforeChildren
`beforeChildren?: boolean | (props: Props) => boolean`
If `true`, will ensure this animation completes before firing any child animations.
### afterChildren
`afterChildren?: boolean | (props: Props) => boolean`
If `true`, will ensure this animation only fires after all child animations have completed.
### applyAtStart/applyAtEnd
`applyAtStart/applyAtEnd?: { [string]: any | (props: Props) => any }`
`applyAtStart` and `applyAtEnd` accept style properties to apply either at the start or end of an animation.
For instance, you might have an element that you want to flip between `display: block` before it fades in, and `display: none` after it fades out:
```javascript
const config = {
visible: {
applyAtStart: { display: 'block' },
opacity: 1
},
hidden: {
applyAtEnd: { display: 'none' },
opacity: 0
}
};
```
### transition
`transition?`
The `transition` prop can be used to create custom transitions.
It can be set as a transition definition:
```javascript
transition: { type: 'spring' }
```
A function that returns a transition definition **or** a Popmotion animation:
```javascript
transition: (props) => spring({...props})
transition: (props) => ({ type: 'spring' })
```
Or finally, a named map, where a separate `transition` is defined for each animating value. `default` can be used to define a transition for all remaining values.
```javascript
visible: {
x: 0,
opacity: 1,
transition: {
x: { type: 'spring' },
default: (props) => tween(props)
}
}
```
#### Transition definitions
A transition definition describes the type of animation Pose should use to transition to the value defined in the Pose.
There are many types, and each has its own specific configuration props available.
##### Tween (default)
Transitions between one value and another over a set duration of time.
- `duration?: number = 300`: Total duration of animation, in milliseconds.
- `elapsed?: number = 0`: Duration of animation already elapsed, in milliseconds.
- `ease?: string | number[] | Function`: The name of an easing function, a cubic bezier definition (as an array of numbers), or an easing function. The following easings are included with Pose:
- 'linear'
- 'easeIn', 'easeOut', 'easeInOut'
- 'circIn', 'circOut', 'circInOut'
- 'backIn', 'backOut', 'backInOut'
- 'anticipate'
- `loop?: number = 0`: Number of times to loop animation.
- `flip?: number = 0`: Number of times to flip animation.
- `yoyo?: number = 0`: Number of times to reverse tween.
##### Spring
A spring animation based on `stiffness`, `damping` and `mass`.
- `type: 'spring'`: Set transition to spring.
- `stiffness?: number = 100`: Spring stiffness.
- `damping?: number = 10`: Strength of opposing force.
- `mass?: number = 1.0`: Mass of the moving object.
- `restDelta?: number = 0.01`: End animation if distance to `to` is below this value **and** `restSpeed` is `true`.
- `restSpeed?: number = 0.01`: End animation if speed drops below this value **and** `restDelta` is `true`.
##### Physics
Integrated simulation of velocity, acceleration, friction and springs.
- `type: 'physics'`: Set transition to physics.
- `acceleration?: number = 0`: Increase `velocity` by this amount every second.
- `restSpeed?: number = 0.001`: When absolute speed drops below this value, `complete` is fired.
- `friction?: number = 0`: Amount of friction to apply per frame, from `0` to `1`.
- `springStrength?: number = 0`: If set with `to`, will spring towards target with this strength.
##### Keyframes
Keyframes accepts an array of `values` and will animate between each in sequence.
Timing is defined with a combination of `duration`, `easings` and `times` properties.
- `type: 'keyframes'`: Set transition to keyframes.
- `values: number[]`: An array of numbers to animate between. To use the value defined in the Pose as the final target value, set `transition` as a function: `transition: ({ to }) => { type: 'keyframes', values: [0, to] }`
- `duration?: number = 300`: Total duration of animation, in milliseconds.
- `easings?: Easing | Easing[]`: An array of easing functions for each generated tween, or a single easing function applied to all tweens. This array should be `values.length - 1`. Defaults to `easeOut`. (This doesn't yet support named easings)
- `times?: number[]`: An array of numbers between `0` and `1`, representing `0` to `duration`, that represent at which point each number should be hit. Defaults to an array of evenly-spread durations will be calculated.
- `elapsed?: number = 0`: Duration of animation already elapsed, in milliseconds.
- `ease?: Easing = easeOut`: A function, given a progress between `0` and `1`, that returns a new progress value. Used to affect the rate of playback across the duration of the animation. (This doesn't yet support named easings)
- `loop?: number = 0`: Number of times to loop animation.
- `flip?: number = 0`: Number of times to flip animation.
- `yoyo?: number = 0`: Number of times to reverse tween.
##### Decay
`decay` exponentially decelerates a number and velocity to an automatically generated target value. This target can be modified by the user.
This animation is particularly useful for implementing momentum scrolling.
- `type: 'decay'`: Set transition to decay.
- `power?: number = 0.8`: A constant with which to calculate a target value. Higher power = further target. `0.8` should be okay.
- `timeConstant?: number = 350`: Adjusting the time constant will change the duration of the deceleration, thereby affecting its feel.
- `restDelta?: number = 0.5`: Automatically completes the action when the calculated value is this far away from the target.
- `modifyTarget?: (v: number) => number`: A function that receives the calculated target and returns a new one. Useful for snapping the target to a grid, for example.
##### General transition props
The following props can be set on any transition:
- `from?: number | Color`: Start value of animation (overrides Pose-generated value).
- `to?: number | Color`: End value of animation (overrides Pose-generated value).
- `velocity?: number`: Initial velocity of animation (overrides Pose-generated value).
- `delay?: number`: Delay the execution of the transition by this amount of time (in milliseconds).
- `min?: number`: Restrict output to numbers larger than this.
- `max?: number`: Restrict output to numbers smaller than this.
- `round?: boolean`: If `true`, output numbers will be rounded.
#### Transition props
If set as a function, `transition` receives the same user-defined props as other dynamic pose properties, with some generated by Pose:
```typescript
type TransitionsProps = {
from: any,
to: any,
velocity: number,
key: string,
prevPoseKey: string
}
```
- `from`: The current state of the value
- `velocity`: The current velocity of the value, if it's a number
- `to`: The state we're animating to, as defined in the current pose. **Note:** You're under no obligation to actually animate to this value (for instance for non-deterministic animations)
- `key`: The name of the value
- `prevPoseKey`: The name of the pose this value was previously in.
### values
`...values: any | (props: Props) => any`
Any remaining properties are treated as stylistic values and will be animated.
================================================
FILE: packages/popmotion-pose/docs/api/react/react-pose-text.md
================================================
---
title: SplitText
description: Animate words and characters using the power of React Pose animations.
category: react
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# React Pose Text
React Pose Text automatically splits strings of text into individual words and/or characters. These can then be animated with the full power of Pose:
- Magic animations
- Stagger across words and characters
- Animate any style property, including `color` and `text-shadow`
- Make words and characters individually draggable, or respond to a parent's dragging.
- `enter`/`exit` animations with `PoseGroup`
- Only 1kb!
## License
React Pose Text is free for **non-profit** use under a GPL-v3 license.
A permissive, commercial license is exclusive to backers of the [Popmotion Patreon](https://patreon.com/popmotion)!
## Usage
### Install
```
npm install react-pose react-pose-text
```
### SplitText
React Pose Text exports a single component, `SplitText`.
```javascript
import SplitText from 'react-pose-text';
export default () => (
Hello world!
);
```
Strings wrapped with this component will be split into posed components for every word and character.
Poses can be defined for both words and characters by providing [Pose configs](https://popmotion.io/pose/api/react-config/) to the `wordPoses` and `charPoses` props, respectively:
```javascript
const charPoses = {
enter: { opacity: 1 },
exit: { opacity: 0 }
};
export default () => (
Hello world!
);
```
`SplitText` acts like a regular posed component, which means we can animate between poses using the `pose` property:
```javascript
export default ({ isVisible }) => (
Hello world!
);
```
It also responds to pose changes further up the tree.
#### Special pose props
Like normal posed components, all props provided to `SplitText` are sent through to dynamic pose properties:
```javascript
const charPoses = {
enter: { y: 0 },
exit: { y: ({ initialOffset }) => initialOffset }
};
export default () => (
Hello world!
);
```
But `SplitText` also provides a series of special props.
Words receive:
- `wordIndex`
- `numWords`
Characters receive:
- `wordIndex`
- `numWords`
- `charIndex`
- `numChars`
- `charInWordIndex`
- `numCharsInWord`
You can use these props in various ways, for instance to create a variety of staggering effects by dynamically generating `delay`:
```javascript
const charPoses = {
enter: {
y: 0,
delay: ({ charIndex }) => charIndex * 50
}
};
```
#### Pointer events
You can use Pose's pointer events as usual. For instance, you can make every word draggable by setting `draggable: true`:
Those poses still cascade down, too. So by setting `dragging` and `dragEnd` poses to our characters, we can make our characters animate while dragging words:
================================================
FILE: packages/popmotion-pose/docs/api/react/supported-values.md
================================================
---
title: Supported values
description: A list of supported values in Pose for React.
category: react
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Supported values
## Overview
**Any value that contains a number or color, or multiple numbers and colors, is animatable.**
Pose doesn't need specific support for a value to animate it. The only limitation is that if you're animating a complex value like CSS's `background-image` or SVG's `d`, **the non-animating parts of the value must match**.
For instance, from `'linear-gradient(to bottom, #1e5799 0%, #7db9e8 100%)'` you could animate to `'linear-gradient(to bottom, #D32A2D 25%, #FF3236 75%);'`, but not to `'linear-gradient(to top, #D32A2D 25%, #000 50%, #FF3236 75%);'`
## CSS
### Enhanced values
Pose has enhanced support for `x`, `y`, `width`, `height`, `top`, `left`, `right` and `bottom` values.
All of these can be animated between different value units, for instance `x` could be animated like so:
```javascript
{
closed: { x: 0 },
open: { x: '100vw' }
}
```
They can also be animated with `calc()`:
```javascript
{
closed: { x: 0 },
open: { x: 'calc(50vw - 50%)' }
}
```
`width` and `height` specifically can also be animated to and from `'auto'`.
### Transforms
Transform values can be animated as separate values:
- `x`, `y`, `z`,
- `rotate`, `rotateX`, `rotateY`, `rotateZ`,
- `scale`, `scaleX`, `scaleY`, `scaleZ`,
- `skewX`, `skewY`,
- `perspective`
## SVG
All SVG attributes like `fill` and `stroke` can be animated, and Pose attempts to replace the confusing SVG transformation model with CSS.
### Path drawing
Path drawing is the process of animating `stroke-dasharray` and `stroke-dashoffset` to emulate a pen drawing a line. [This blog post](https://css-tricks.com/svg-line-animation-works/) explains the effect in more detail.
Path drawing works with these properties works with `circle`, `ellipse`, `path`, `polygon`, `polyline`, `rect` and `text` SVG elements.
But for `path`, there's three special properties:
* `pathLength`
* `pathSpacing`
* `pathOffset`
These are all set as a **percentage of the total path length, from `0` to `100`**, which is automatically measured by Pose.
So you can define an animation from `0` to `100`:
```javascript
{
empty: { pathLength: 0 },
fill: { pathLength: 100 }
}
```
## Automatic animations
Every value, if no `transition` prop is defined, will have an animation automatically generated for it.
Currently, these are:
### Movement
Translation and rotational transforms use an overdamped spring that retain any existing velocity and incorporate that into the next animation.
This gives the animation a playful, snappy and robust feel.
```javascript
{
type: 'spring',
stiffness: 500,
damping: 25,
restDelta: 0.5,
restSpeed: 10
}
```
### Scale
Scale transforms use an overdamped spring that ensure that scale isn't inverted.
```javascript
{
type: 'spring',
stiffness: 700,
damping: to === 0 ? 100 : 35
}
```
### Opacity
Opacity uses a linear easing tween. Opacity looks odd when eased, so we transition straight from one to the other.
```javascript
{
ease: 'linear'
}
```
### Everything else
All other properties use [Popmotion Pure's default tween](/api/tween) settings.
In the future it'll be possible to define automatic animations by defining the type of interface you wish to have (ie 'confident', 'playfull' etc).
In the nearer future we'll intelligently combine automatic animations to ensure they all finish at the same time.
================================================
FILE: packages/popmotion-pose/docs/api/react-native/native-config.md
================================================
---
title: Config
description: Options to create a posed component
category: react-native
---
# Config
Every posed component is created via a config object:
```javascript
const PosedComponent = posed.View(config)
```
## Available options
### draggable
`draggable?: true | x | y`
If `true`, will make the component draggable on both axis. Setting to either `'x'` or `'y'` will restrict movement to that axis.
If defined, will allow the use of a special `dragging` and `dragEnd` poses.
### passive
`passive?: { [key: string]: PassiveValue }`
```typescript
type PassiveValue = [
bindKey: string,
interpolate: InterpolateConfig,
fromParent?: true | string
]
```
Map of values that are passively changed when other values, either on this Poser or an ancestor, change.
`bindKey` is the name of the value to interpolate from.
`InterpolateConfig` is an object with any [valid `Animated.Value.interpolate` props](https://facebook.github.io/react-native/docs/animations.html#interpolation).
`fromParent` can be set either as `true` or as a `string`:
- `true`: Link to value from immediate parent.
- `string`: Link to the nearest ancestor with this `label` prop.
### label
`label?: string`
Set a label on this poser. Currently, this allows a `passive` value on a child poser to refer to this ancestor value.
### props
`props?: { [key: string]: any }`
Props to provide to the `transition` method and dynamic props of entered poses.
### ...poses
`...poses: { [key: string]: Pose }`
Remaining keys will be treated as poses:
## Poses
You can call a pose anything, and animate to it by providing its name to the posed component's `pose` property:
```javascript
```
A pose is defined by style attributes like `x` or `opacity`, and the following optional props:
### transition
`transition?`
The `transition` prop can be used to create custom transitions.
It can be set as transition definition:
```javascript
transition: { type: 'spring' }
```
A function that returns a transition definition **or** a React Animated animation:
```javascript
transition: ({ value, ...props }) => spring(value, props)
transition: (props) => ({ type: 'spring' })
```
Or finally, a named map, where a separate `transition` is defined for each animating value. `default` can be used to define a transition for all remaining values.
```javascript
visible: {
x: 0,
opacity: 1,
transition: {
x: { type: 'spring' },
default: ({ value, ...props }) =>
Animated.timing(value, props)
}
}
```
#### Transition definitions
A transition definition describes the type of animation Pose should use to transition to the value defined in the Pose.
There are many types, and each has its own specific configuration props available.
##### Tween (default)
Transitions between one value and another over a set duration of time.
- `duration?: number = 300`: Total duration of animation, in milliseconds.
- `ease?: string | number[] | Function`: The name of an easing function, a cubic bezier definition (as an array of numbers), or an easing function. The following easings are included with Pose:
- 'linear'
- 'easeIn', 'easeOut', 'easeInOut'
- 'circIn', 'circOut', 'circInOut'
- 'backIn', 'backOut', 'backInOut'
- 'anticipate'
##### Spring
A spring animation based on `stiffness`, `damping` and `mass`.
- `type: 'spring'`: Set transition to spring.
- `stiffness?: number = 100`: Spring stiffness.
- `damping?: number = 10`: Strength of opposing force.
- `mass?: number = 1.0`: Mass of the moving object.
- `velocity?: number = 0`: Initial velocity.
- `restDelta?: number = 0.01`: End animation if distance to `to` is below this value **and** `restSpeed` is `true`.
- `restSpeed?: number = 0.01`: End animation if speed drops below this value **and** `restDelta` is `true`.
- `overshootClamping?: boolean = false`: Clamps any overshoot beyond the target value.
##### Keyframes
Keyframes accepts an array of `values` and will animate to each, in sequence.
Timing is defined with a combination of `duration`, `easings` and `times` properties.
- `type: 'keyframes'`: Set transition to keyframes.
- `values: number[]`: An array of numbers to animate between. To use the value defined in the Pose as the final target value, set `transition` as a function: `transition: ({ toValue }) => { type: 'keyframes', values: [0, toValue] }`. To use the current value as the current value as the initial value, skip definition that value: `values: [, 45, 90]`
- `duration?: number = 300`: Total duration of animation, in milliseconds.
- `easings?: Easing | Easing[]`: An array of easings (see tween for options) to provide to each generated tween, or a single easing applied to all tweens. This array should be `values.length - 1`. Defaults to `'easeOut'`.
- `times?: number[]`: An array of numbers between `0` and `1`, representing `0` to `duration`, that represent at which point each number should be hit. Defaults to an array of evenly-spread durations will be calculated.
##### General transition props
The following props can be set on any transition:
- `loop?: number = 0`: If set, defines how many times transition will replay.
- `delay?: number = 0`: Delay the execution of the transition by this amount of time (in milliseconds).
- `isInteraction?: boolean = true`: Defines whether this animation creates an "interaction handle" on React Native's `InteractionManager`.
#### Transition props
If set as a function, `transition` receives the same user-defined props as other dynamic pose properties, with some generated by Pose:
```javascript
type Props = {
value: Animated.Value,
toValue: number,
key: string,
prevPoseKey: string,
useNativeDriver: boolean,
...props: any
}
```
- `value`: The React Animated `Value` being animated.
- `toValue`: The state we're animating to, as defined in the current pose. **Note:** You're under no obligation to actually animate to this value (for instance for non-deterministic animations)
- `key`: The name of the value.
- `prevPoseKey`: The name of the pose this value was previously in.
- `useNativeDriver`: Whether to use the native animation driver for better performance. If returning an Animated animation (rather than a transition definition), this **must** be passed to that animation.
### delay
`delay?: number | (props: Props) => number`
A duration, in milliseconds, to delay this transition. Does **not** affect children.
### delayChildren
`delayChildren?: number | (props: Props) => number`
A duration, in milliseconds, to delay the transition of direct children.
### staggerChildren
`staggerChildren?: number | (props: Props) => number`
A duration, in milliseconds, between transitioning each children.
### staggerDirection
`staggerDirection?: 1 | -1 | (props: Props) => 1 | -1`
If `1`, staggers from the first child to the last. If `-1`, from last to first.
### beforeChildren
`beforeChildren?: boolean | (props: Props) => boolean`
If `true`, will ensure this animation completes before firing any child animations.
### afterChildren
`afterChildren?: boolean | (props: Props) => boolean`
If `true`, will ensure this animation only fires after all child animations have completed.
### ...values
`...values: any | (props: TransitionProps) => any`
Any remaining properties are treated as stylistic values and will be animated.
================================================
FILE: packages/popmotion-pose/docs/api/react-native/native-posed.md
================================================
---
title: Posed
description: Create posed components
category: react-native
---
# `posed`
React Native Pose exports a single function, `posed`.
```javascript
import posed from 'react-native-pose';
```
## Posed components
`posed` is a factory function that creates posed components. These are components [configured with a series of states that it can animate between and other options](/pose/api/native-config).
We can use `posed` to create three different kinds of posed component:
- Included components (ie `posed.View`)
- Custom components
- Function as child components
### Included components
[React Animated](https://facebook.github.io/react-native/docs/animations.html) ships with four animatable components: `View`, `Text`, `Image` and `ScrollView`.
Likewise, `posed` has shortcuts for each of these components:
- `posed.View(config)`
- `posed.Text(config)`
- `posed.Image(config)`
- `posed.ScrollView(config)`
### Custom components
Animated also has a helper function that you can use to create animated components from any normal component: `createAnimatedComponent(Component)`.
If `posed` is called as a function, it can also create an animated component from any other:
`posed(Component)(config)`
This makes `posed.View` practically the same as `posed(View)`.
### Function as children components
By creating posed components with the previous two methods, React Native Pose will automatically handle the application of the generated `Animated.Value`s.
In return for this simplicity, you lose a little flexibility. For instance, you don't get to choose the `transform` order, and you can't create arbitrary values (as they all get applied as `style`s).
By calling `posed` as a function without any `Component`, the returned posed component will use the "function as children pattern" to provide the `Animated.Value`s to children:
```javascript
const PosedComponent = posed()({
open: { x: 0, scaleY: 1 },
closed: { x: 100, scaleY: 0 }
});
export default ({ isOpen }) => (
{({ x, scaleY }) => (
)}
)
```
## Props
### pose
`pose?: string | string[]`
The name of one or more poses to set to.
### initialPose
`initialPose?: string | string[]`
The name of one or more poses to set to before the component mounts. Once the component mounts, it will transition from this pose into `pose`.
### poseKey
`poseKey?: string | number`
If `poseKey` changes, it'll force the posed component to transition to the current `pose`, even if it hasn't changed.
This won't be required for the majority of use-cases. But we might have something like a paginated where we pass the x offset to the component but the pose itself doesn't change:
```javascript
const Slider = posed.View({
nextItem: {
x: ({ target }) => target
}
})
({ target }) =>
```
### onDragStart/onDragEnd
`onDragStart/onDragEnd?: (e: NativeEvent, gestureState: GestureState) => any`
Lifecycle callbacks for drag events. Provided the same arguments as [PanResponder's lifecycle events](https://facebook.github.io/react-native/docs/panresponder.html).
### withParent
`withParent?: boolean = true`
If explicitly set to `false`, this posed component will become a new root for any posed children components.
### values
`values?: { [key: string]: Animated.Value }`
Optional way of providing the posed component the `Animated.Value`s rather than letting it create them itself. In case you want to retain ownership for whatever reason.
================================================
FILE: packages/popmotion-pose/docs/api/react-native/native-supported-values.md
================================================
---
title: Supported values
description: A list of supported values in Pose for React Native.
category: react-native
---
# Supported values
## Transforms
Pose for React Native supports the animation of all `transform` values.
`rotate`, `rotateX` and `rotateY` must (currently) be defined as `'deg'` values:
```javascript
posed.View({
init: { rotate: '0deg' },
flip: { rotate: '180deg' }
})
```
## Colors
Color animations (`backgroundColor`, `color` etc) are **currently** only supported as [passive values](/pose/learn/native-passive):
```javascript
posed.View({
draggable: 'x',
passive: {
backgroundColor: ['x', {
inputRange: [-200, 200],
outputRange: ['#f00', '#0f0']
}]
}
})
```
## Other values
All other values, defined either as a number or unit type, are animatable.
**However,** React Animated (the underlying animation library) doesn't currently support native animation of these values, and animating them will disable native animation of **all** values on that component.
================================================
FILE: packages/popmotion-pose/docs/api/react-native/native-transition.md
================================================
---
title: Transition
description: Control enter/exit animations with Pose for React Native's Transition component
category: react-native
---
# `Transition`
The `Transition` component is used to animate components as they enter and exit the component tree.
## Import
```javascript
import posed, { Transition } from 'react-native-pose';
```
## Usage
To animate posed components as they're added and removed from the React tree, provide them as **direct** children of `Transition`.
Set `enter` and `exit` poses to define how they should animate in and out.
### Switching visibility
```javascript
const Box = posed.View({
enter: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 50 }
});
export default ({ isVisible }) => (
{isVisible && }
)
```
### Switching components
```javascript
export default ({ toShow }) => (
{toShow === 'a' ? : }
)
```
### Multiple children
```javascript
export default ({ items }) => (
{items.map(item => )}
)
```
### Passing props to children
A common problem with transition components is passing props to components that have been removed from the tree. They might be animating out, but as far as React is concerned, they've already left (so props don't get updated).
With Pose for React Native, any props you provide to `Transition` will be forwarded to all children, even ones that are leaving the tree.
This allows you to use the latest props in dynamic poses:
```javascript
const Item = posed.li({
enter: { opacity: 1, x: 0 },
exit: {
opacity: 0,
x: ({ selectedItemId, id }) =>
id === selectedItemId ? 100 : -100
}
});
export default ({ items, selectedItemId }) => (
{items.map(({ id }) => )}
);
```
### Notes
- Posed components must be direct children of `Transition`.
- Enter and exit poses will propagate throughout posed components, allowing you to animate multiple children components when their parent enters/exits.
- Every child must have a unique `key` property.
- If components affect each other's layout, some snapping will occur when exiting components are removed. Position with `position: 'absolute'` or ensure exiting components animate out **before** animating new components in via the `enterAfterExit` prop.
## Props
### animateOnMount
`animateOnMount: boolean = false`
By default, only children added to the `Transition` **after** it has mounted are animated to `enter`.
By setting `animateOnMount` to `true`, all children elements will animate in on mount.
### enterPose
`enterPose: string = 'enter'`
The name of the pose to use when a component enters.
### exitPose
`exitPose: string = 'exit'`
The name of the pose to use when a component leaves.
### preEnterPose
`preEnterPose: string = 'exit'`
The name of the pose to set before a component enters. This can be used to configure where a components animates in **from**.
### enterAfterExit
`enterAfterExit: boolean = false`
If `true`, `Transition` will finish animating exiting components out **before** animating entering components in.
This can be useful when animating two or more components that affect each other's layout, which can lead to snapping when the exiting components are removed.
### onRest
`onRest: Function`
When a component finishes exiting, it isn't removed immediately. Instead, it's kept in the DOM until **all** currently leaving components have finished animating out to prevent expensive layout thrashing.
`onRest` will fire when all exit transitions are complete.
If new exit transitions begin in the meantime, `onRest` won't be fired until these have also finished.
### ...props
All remaining props passed to `Transition` will be forwarded to its immediate children, for use in dynamic props.
This is useful because if you're removing a component, it's impossible in React to update its props without doing a two-pass render.
By providing them to the `Transition` prop, Pose can pass these to the posed components to change the `exit` animation:
```javascript
const Box = posed.View({
enter: { x: 0 },
exit: { x: ({ delta }) => - delta * 100 + 'vw' }
});
export default ({ isVisible }) => (
{isVisible && }
)
```
================================================
FILE: packages/popmotion-pose/docs/api/vue/vue-config.md
================================================
---
title: Config
description: Configure a posed component
category: vue
---
# Config options
Options to configure [posed components](/pose/api/vue-posed) in Pose for Vue.
## Options
### draggable
`draggable?: true | 'x' | 'y'`
If `true`, will make the element draggable on both axis. Setting to either `'x'` or `'y'` will restrict movement to that axis.
If defined, allows use of the special `drag` pose for styling the element while dragging is active.
A `dragEnd` pose can be **optionally** set for animating on drag end.
```javascript
const config = {
draggable: 'x',
init: { scale: 1 },
drag: { scale: 1.2 }
}
```
The `drag` and `dragEnd` poses will travel through any posed children.
### dragBounds
`dragBounds?: { [key: string]: number }`
An object that defines `top`, `right`, `bottom` and/or `left` drag boundaries in pixels.
Currently, these boundaries are enforced by a hard clamp.
### hoverable
`hoverable?: boolean`
If `true`, this element will receive `hover` poses when a pointer hovers over it.
There's also an **optional** `hoverEnd` pose, for providing a different pose when hovering ends.
```javascript
const config = {
hoverable: true,
init: { scale: 1 },
hover: { scale: 1.2 }
}
```
The `hover` and `hoverEnd` poses will travel through any posed children.
### focusable
`focusable?: boolean`
If `true`, this element will receive `focus` poses when the element receives focus, and `blur` poses when it loses focus.
```javascript
const config = {
focusable: true,
init: { scale: 1 },
focus: { scale: 1.2 },
blur: {
scale: 1,
transition: {
type: 'spring',
stiffness: 800
}
}
};
```
### pressable
`pressable?: boolean`
If `true`, this element will receive `press` poses when the element is pressed, and **optionally** `pressEnd` when pressing stops.
```javascript
const config = {
pressable: true,
init: { scale: 1 },
press: { scale: 0.8 }
};
```
### passive
`passive: { [key: string]: PassiveValue }`
```typescript
type PassiveValue = [
subscribedKey: string,
transform: (subscribedValue: any) => any,
fromParent?: true | string
]
```
Map of values that are passively changed when other values, either on this Poser or an ancestor, change.
`fromParent` can be set either as `true` or as a `string`:
- `true`: Link to value from immediate parent.
- `string`: Link to the nearest ancestor with this `label` prop.
#### Example
The `transform` function here is composed with Popmotion [transformers](/api/transformers):
```javascript
const config = {
draggable: 'x',
passive: {
backgroundColor: ['x', pipe(
clamp(0, 300),
interpolate([0, 300], [0, 1]),
blendColor('#f00', '#0f0')
)]
}
}
```
### label
`label: string`
Set a label on this poser. Currently, this allows a `passive` value on a child poser to refer to this ancestor value.
### props
`props: { [key: string]: any }`
Default properties to provide to entered pose `transition` methods and dynamic pose props. These can be overridden by providing props to the posed component.
### ...poses
`...poses: { [key: string]: Pose }`
Any other config props will be treated as poses (see [Pose config](#pose-config)).
## Poses
You can call a pose anything, and animate to it by setting `` (or multiple poses with an array).
A pose is defined by style attributes like `x` or `backgroundColor`, and the following optional props:
### delay
`delay?: number | (props: Props) => number`
A duration, in milliseconds, to delay this transition. Does **not** affect children.
### delayChildren
`delayChildren?: number | (props: Props) => number`
A duration, in milliseconds, to delay the transition of direct children.
### flip
`flip?: boolean = false`
If `true`, will convert this animation to a [FLIP animation](https://aerotwist.com/blog/flip-your-animations/).
### staggerChildren
`staggerChildren?: number | (props: Props) => number`
A duration, in milliseconds, between transitioning each children.
### staggerDirection
`staggerDirection?: 1 | -1 | (props: Props) => 1 | -1`
If `1`, staggers from the first child to the last. If `-1`, from last to first.
### beforeChildren
`beforeChildren?: boolean | (props: Props) => boolean`
If `true`, will ensure this animation completes before firing any child animations.
### afterChildren
`afterChildren?: boolean | (props: Props) => boolean`
If `true`, will ensure this animation only fires after all child animations have completed.
### applyAtStart/applyAtEnd
`applyAtStart/applyAtEnd?: { [string]: any | (props: Props) => any }`
`applyAtStart` and `applyAtEnd` accept style properties to apply either at the start or end of an animation.
For instance, you might have an element that you want to flip between `display: block` before it fades in, and `display: none` after it fades out:
```javascript
const config = {
visible: {
applyAtStart: { display: 'block' },
opacity: 1
},
hidden: {
applyAtEnd: { display: 'none' },
opacity: 0
}
};
```
### transition
`transition?`
The `transition` prop can be used to create custom transitions.
It can be set as a transition definition:
```javascript
transition: { type: 'spring' }
```
A function that returns a transition definition **or** a Popmotion animation:
```javascript
transition: (props) => spring({...props})
transition: (props) => ({ type: 'spring' })
```
Or finally, a named map where each prop is either a transition definition, or a function returning a transition definition/Popmotion animation:
```javascript
visible: {
x: 0,
opacity: 1,
transition: {
x: { type: 'spring' },
default: (props) => tween(props)
}
}
```
#### Transition definitions
A transition definition describes the type of animation Pose should use to move to the value defined in the Pose.
There are many types, and each has its own specific configuration props available.
##### Tween (default)
Transitions between one value and another over a set duration of time.
- `duration?: number = 300`: Total duration of animation, in milliseconds.
- `elapsed?: number = 0`: Duration of animation already elapsed, in milliseconds.
- `ease?: string | number[] | Function`: The name of an easing function, a cubic bezier definition, or an easing function. The following easings are included with Pose:
- 'linear'
- 'easeIn', 'easeOut', 'easeInOut'
- 'circIn', 'circOut', 'circInOut'
- 'backIn', 'backOut', 'backInOut'
- 'anticipate'
- `loop?: number = 0`: Number of times to loop animation.
- `flip?: number = 0`: Number of times to flip animation.
- `yoyo?: number = 0`: Number of times to reverse tween.
##### Spring
A spring animation based on `stiffness`, `damping` and `mass`.
- `type: 'spring'`: Set transition to spring.
- `stiffness?: number = 100`: Spring stiffness.
- `damping?: number = 10`: Strength of opposing force.
- `mass?: number = 1.0`: Mass of the moving object.
- `restDelta?: number = 0.01`: End animation if distance to `to` is below this value **and** `restSpeed` is `true`.
- `restSpeed?: number = 0.01`: End animation if speed drops below this value **and** `restDelta` is `true`.
##### Physics
Integrated simulation of velocity, acceleration, friction and springs.
- `type: 'physics'`: Set transition to physics.
- `acceleration?: number = 0`: Increase `velocity` by this amount every second.
- `restSpeed?: number = 0.001`: When absolute speed drops below this value, `complete` is fired.
- `friction?: number = 0`: Amount of friction to apply per frame, from `0` to `1`.
- `springStrength?: number = 0`: If set with `to`, will spring towards target with this strength.
##### Keyframes
Keyframes accepts an array of `values` and will animate between each in sequence.
Timing is defined with a combination of `duration`, `easings` and `times` properties (see [Methods](#methods))
- `type: 'keyframes'`: Set transition to keyframes.
- `values: number[]`: An array of numbers to animate between. To use the value defined in the Pose as the final destination value, set `transition` as a function: `transition: ({ to }) => { type: 'keyframes', values: [0, to] }`
- `duration?: number = 300`: Total duration of animation, in milliseconds.
- `easings?: Easing | Easing[]`: An array of easing functions for each generated tween, or a single easing function applied to all tweens. This array should be `values.length - 1`. Defaults to `easeOut`. (This doesn't yet support named easings)
- `times?: number[]`: An array of numbers between `0` and `1`, representing `0` to `duration`, that represent at which point each number should be hit. Defaults to an array of evenly-spread durations will be calculated.
- `elapsed?: number = 0`: Duration of animation already elapsed, in milliseconds.
- `ease?: Easing = easeOut`: A function, given a progress between `0` and `1`, that returns a new progress value. Used to affect the rate of playback across the duration of the animation. (This doesn't yet support named easings)
- `loop?: number = 0`: Number of times to loop animation.
- `flip?: number = 0`: Number of times to flip animation.
- `yoyo?: number = 0`: Number of times to reverse tween.
##### Decay
`decay` exponentially decelerates a number and velocity to an automatically generated target value. This target can be modified by the user.
This animation is particularly useful for implementing momentum scrolling.
- `type: 'decay'`: Set transition to decay.
- `power?: number = 0.8`: A constant with which to calculate a target value. Higher power = further target. `0.8` should be okay.
- `timeConstant?: number = 350`: Adjusting the time constant will change the duration of the deceleration, thereby affecting its feel.
- `restDelta?: number = 0.5`: Automatically completes the action when the calculated value is this far away from the target.
- `modifyTarget?: (v: number) => number`: A function that receives the calculated target and returns a new one. Useful for snapping the target to a grid, for example.
##### General transition props
The following props can be set on any transition:
- `from?: number | Color`: Start value of animation (overrides Pose-generated value).
- `to?: number | Color`: End value of animation (overrides Pose-generated value).
- `velocity?: number`: Initial velocity of animation (overrides Pose-generated value).
- `delay?: number`: Delay the execution of the transition by this amount of time (in milliseconds).
- `min?: number`: Restrict output to numbers larger than this.
- `max?: number`: Restrict output to numbers smaller than this.
- `round?: boolean`: If `true`, output numbers will be rounded.
#### Transition props
If set as a function, `transition` received the same user-defined props as other dynamic pose properties, with some generated by Pose:
```typescript
type TransitionsProps = {
from: any,
to: any,
velocity: number,
key: string,
prevPoseKey: string
}
```
- `from`: The current state of the value
- `velocity`: The current velocity of the value, if it's a number
- `to`: The state we're animating to, as defined in the current pose. **Note:** You're under no obligation to actually animate to this value (for instance for non-deterministic animations)
- `key`: The name of the value
- `prevPoseKey`: The name of the pose this value was previously in.
### values
`...values: any | (props: Props) => any`
Any remaining properties are treated as stylistic values and will be animated.
================================================
FILE: packages/popmotion-pose/docs/api/vue/vue-posed.md
================================================
---
title: Posed
description: Create a posed component with Pose for Vue
category: vue
---
# `posed`
`posed` is used to create animated and interactive components that you can reuse throughout your Vue site.
## Import
```javascript
import posed from 'vue-pose';
```
## Usage
### Create a posed component
`posed` can be used to create posed HTML and SVG elements.
It isn't called directly, instead we call it via the name of the element we want to create, along with a [posed component config](/pose/api/vue-config).
```
```
Every HTML and SVG element is supported.
### Set a pose
Poses can be set via the `pose` property.
```
```
### Children
Using a posed component creates a new tree of posed components. Any children that are also posed components are automatically added to this tree.
This means that orchestrating animations through React trees becomes trivial, as a pose only has to be set on a parent. Any children with that pose defined will also animate:
```
```
### Styling
Posed components return regular HTML elements, so they can be used with any CSS styling solution.
## Props
### pose
`pose?: string`
The name of the current pose.
### preEnterPose
`preEnterPose?: string`
The name of a pose to set before the component mounts. If present, once the component mounts it will transition from this pose into `pose`.
### poseKey
`poseKey?: string | number`
If `poseKey` changes, it'll force the posed component to transition to the current `pose`, even if it hasn't changed.
This won't be required for the majority of use-cases. But we might have something like a paginated where we pass the x offset to the component but the pose itself doesn't change:
```
```
### withParent
`withParent?: boolean = true`
If set to `false`, this component won't subscribe to its parent posed component and create root for any further child components.
### onPoseComplete
`onPoseComplete?: Function`
A callback that fires whenever a pose has finished transitioning.
### onValueChange
`onValueChange?: { [key: string]: any }`
`onValueChange` is a map of functions, each corresponding to a value being animated by the posed component and will fire when that value changes.
### ...props
`...props: { [key: string]: any }`
When a new pose is entered, any remaining props set on a component will be used to resolve that pose's dynamic props:
```
```
## Events
The following events can be subscribed to with `v-on`:
### drag-start/drag-end
`drag-start/drag-end: (e: Event) => void`
Callbacks that fire when dragging starts or ends. **Note:** These props are immutable and can't be changed after mounting.
### press-start/press-end
`press-start/press-end: (e: Event) => void`
Callbacks that fire when pressing starts or ends. **Note:** These props are immutable and can't be changed after mounting.
================================================
FILE: packages/popmotion-pose/docs/api/vue/vue-posetransition.md
================================================
---
title: PoseTransition
description: A component for managing entering and exiting Vue components
category: vue
---
# `PoseTransition`
`PoseTransition` is a component that can animate a single element as its added to, and removed from, the DOM.
## Import
```javascript
import { PoseTransition } from 'vue-pose';
```
## Usage
### Animating a single element
By default, `PoseTransition` will automatically animate a child element in and out using a simple `opacity` fade. We can control whether the child is rendered using `v-if`:
```javascript
export default {
props: ['isVisible'],
components: { PoseTransition },
template: `
Hello world
`
};
```
### Custom transition
We can change the enter and exit animations by making the child animation a posed component:
```javascript
export default {
props: ['isVisible'],
components: {
PoseTransition,
Message: posed.div({
enter: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 20 }
})
},
template: `
Hello world
`
}
```
### Animating children
Enter and exit poses are propagated like any other:
```javascript
export default {
props: ['isVisible'],
components: {
PoseTransition,
Shade: posed.div({
enter: { opacity: 1, beforeChildren: true },
exit: { opacity: 0, afterChildren: true }
}),
Modal: posed.div({
enter: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 20 }
})
},
template: `
Hello world
`
}
```
### Animating between components
`PoseTransition` uses Vue's `transition` component under the hood, so many of the [same techniques for animating between components](https://vuejs.org/v2/guide/transitions.html#Transitioning-Between-Components) work here.
## Props
### appear
`appear: boolean = false`
By adding `appear`, any elements present when the component mounts will be animated in from their `'exit'` pose.
```javascript
/* children */
```
### enterPose/exitPose
`enterPose: string = 'enter'`
`exitPose: string = 'exit'`
By default, the poses used for entering and exiting are `'enter'` and `'exit'`, respectfully.
By providing strings as props to `PoseTransition`, you can name these whatever you like.
```javascript
```
### mode
`mode: 'in-out' | 'out-in'`
By default, if one element is animating in and another is animating out, they'll both be rendered and animated simultaneously.
This can sometimes look odd, or not be the desired effect. By setting `mode`, we can change this behaviour:
- `'in-out'`: New element transitions in, then the current element transitions out.
- `'out-in'`: Current element transitions out, then the new element transitions in.
```javascript
```
================================================
FILE: packages/popmotion-pose/docs/api/vue/vue-supported-values.md
================================================
---
title: Supported values
description: A list of supported values in Pose for Vue
category: vue
---
# Supported values
## Overview
**Any value that contains a number or color, or multiple numbers and colors, is animatable.**
Pose doesn't need specific support for a value to animate it. The only limitation is that if you're animating a complex value like CSS's `background-image` or SVG's `d`, **the non-animating parts of the value must match**.
For instance, from `'linear-gradient(to bottom, #1e5799 0%, #7db9e8 100%)'` you could animate to `'linear-gradient(to bottom, #D32A2D 25%, #FF3236 75%);'`, but not to `'linear-gradient(to top, #D32A2D 25%, #000 50%, #FF3236 75%);'`
## CSS
### Enhanced values
Pose has enhanced support for `x`, `y`, `width`, `height`, `top`, `left`, `right` and `bottom` values.
All of these can be animated between different value units, for instance `x` could be animated like so:
```javascript
{
closed: { x: 0 },
open: { x: '100vw' }
}
```
They can also be animated with `calc()`:
```javascript
{
closed: { x: 0 },
open: { x: 'calc(50vw - 50%)' }
}
```
`width` and `height` specifically can also be animated to and from `'auto'`.
### Transforms
Transform values can be animated as separate values:
- `x`, `y`, `z`,
- `rotate`, `rotateX`, `rotateY`, `rotateZ`,
- `scale`, `scaleX`, `scaleY`, `scaleZ`,
- `skewX`, `skewY`,
- `perspective`
## SVG
All SVG attributes like `fill` and `stroke` can be animated, and Pose attempts to replace the confusing SVG transformation model with CSS.
### Path drawing
Path drawing is the process of animating `stroke-dasharray` and `stroke-dashoffset` to emulate a pen drawing a line. [This blog post](https://css-tricks.com/svg-line-animation-works/) explains the effect in more detail.
Path drawing works with these properties works with `circle`, `ellipse`, `path`, `polygon`, `polyline`, `rect` and `text` SVG elements.
But for `path`, there's three special properties:
* `pathLength`
* `pathSpacing`
* `pathOffset`
These are all set as a **percentage of the total path length, from `0` to `100`**, which is automatically measured by Pose.
So you can define an animation from `0` to `100`:
```javascript
{
empty: { pathLength: 0 },
fill: { pathLength: 100 }
}
```
## Automatic animations
Every value, if no `transition` prop is defined, will have an animation automatically generated for it.
Currently, these are:
### Movement
Translation and rotational transforms use an overdamped spring that retain any existing velocity and incorporate that into the next animation.
This gives the animation a playful, snappy and robust feel.
```javascript
{
type: 'spring',
stiffness: 500,
damping: 25,
restDelta: 0.5,
restSpeed: 10
}
```
### Scale
Scale transforms use an overdamped spring that ensure that scale isn't inverted.
```javascript
{
type: 'spring',
stiffness: 700,
damping: to === 0 ? 100 : 35
}
```
### Opacity
Opacity uses a linear easing tween. Opacity looks odd when eased, so we transition straight from one to the other.
```javascript
{
ease: 'linear'
}
```
### Everything else
All other properties use [Popmotion Pure's default tween](/api/tween) settings.
In the future it'll be possible to define automatic animations by defining the type of interface you wish to have (ie 'confident', 'playfull' etc).
In the nearer future we'll intelligently combine automatic animations to ensure they all finish at the same time.
================================================
FILE: packages/popmotion-pose/docs/examples/react/accordion.md
================================================
---
title: Accordion
description: Make accordion animations in React by animating height between 0 and 'auto'
category: react
---
# Accordion
Pose supports animating between any unit type for `width` and `height`, including `'auto'`.
This makes it trivial to create accordion animations.
```javascript
const Content = posed.div({
closed: { height: 0 },
open: { height: 'auto' }
});
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/css-variables.md
================================================
---
title: CSS variables
description: A live example of Pose declaratively animating CSS variables
category: react
---
# CSS variables
Pose can animate CSS variables like normal style props.
In the following example, all the boxes have the class `.box` with these styles:
```css
.box {
background: var(--color);
}
```
So to animate their background color, we can animate the `--color` property of one of their ancestors. In the following example, that's the `Container` posed component.
[Read the full tutorial](/pose/learn/css-variables/)
================================================
FILE: packages/popmotion-pose/docs/examples/react/enter-exit.md
================================================
---
title: "Enter/exit: Single component"
description: React Pose's powerful PoseGroup component can be used to animate single children in and out of the DOM.
category: react
---
# Enter/exit: Single component
`PoseGroup` can be used to animate `enter` and `exit` states of a single component when it's added/removed to and from the component tree.
```javascript
const Modal = posed.div({
enter: { opacity: 1 },
exit: { opacity: 0 }
});
export default ({ isVisible }) => (
{isVisible ? : null}
)
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/event-drag-boundaries.md
================================================
---
title: "Events: Drag with boundaries"
description: Animate hover events with Pose
category: react
---
# Events: Drag with boundaries
Boundaries can be imposed on dragging by using the `dragBounds` property.
It accepts `top`, `left`, `right`, and `bottom` properties in either pixels or percentages.
```javascript
const config = {
draggable: 'x',
dragBounds: { left: '-100%', right: '100%' }
}
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/event-drag.md
================================================
---
title: "Events: Drag"
description: Easily make elements draggable with Popmotion Pose
category: react
---
# Events: Drag
Making an element draggable is as easy as setting `draggable: true`.
```javascript
const config = {
draggable: true
}
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/event-focus.md
================================================
---
title: "Events: Focus"
description: Easily make elements focusable with Popmotion Pose
category: react
---
# Events: Focus
To animate an element on focus, set `focusable: true` and a `focus` prop.
```javascript
const config = {
focusable: true,
init: {
color: '#aaa',
outlineWidth: '0px',
outlineOffset: '0px',
scale: 1
},
focus: {
color: '#000',
outlineWidth: '12px',
outlineOffset: '5px',
outlineColor: '#AB36FF',
scale: 1.2
}
}
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/event-hover.md
================================================
---
title: "Events: Hover"
description: Easily make elements hoverable with Popmotion Pose
category: react
---
# Events: Hover
To animate an element on hover, set `hoverable: true` and a `pose` prop.
```javascript
const config = {
hoverable: true,
init: { scale: 1 },
hover: { scale: 1.2 }
}
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/event-press.md
================================================
---
title: "Events: Press"
description: Easily make elements pressable with Popmotion Pose
category: react
---
# Events: Press
To animate an element on press, set `pressable: true` and a `press` prop.
```javascript
const config = {
pressable: true,
init: { scale: 1 },
press: { scale: 0.8 }
}
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/filter.md
================================================
---
title: CSS filters
description: A live example of animating CSS filters with React Pose
category: react
---
# CSS filters
CSS filters can be animated by settings `filter` as you [would in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/filter):
```javascript
const config = {
hoverable: true,
init: { filter: 'grayscale(80%) blur(2px)' },
hover: { filter: 'grayscale(0%) blur(0px)' }
};
```
Hover over, or tap, the image in this example to see the effect in action.
================================================
FILE: packages/popmotion-pose/docs/examples/react/outline.md
================================================
---
title: CSS outline
description: A live example of animating CSS outline with React Pose
category: react
---
# CSS outline
The outline properties can be animated with Pose either individually:
```javascript
const config = {
focusable: true,
init: {
color: '#aaa',
outlineWidth: '0px',
outlineOffset: '0px',
scale: 1
},
focus: {
color: '#000',
outlineWidth: '12px',
outlineOffset: '5px',
outlineColor: '#AB36FF',
scale: 1.2
}
};
```
Or as a single property:
```javascript
const config = {
focusable: true,
init: {
outline: '0px ridge rgba(170, 50, 220, 0)'
},
focus: {
outline: '8px ridge rgba(170, 50, 220, .6)'
}
};
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/posegroup-reordering.md
================================================
---
title: 'PoseGroup: Reordering items'
description: React Pose's powerful PoseGroup component can automatically animate reordered items.
category: react
---
# PoseGroup: Reordering items
When posed items in a `PoseGroup` component are reordered, they're automatically animated to their new position:
```javascript
const Item = posed.li();
export default ({ items }) => (
{items.map(({ id }) => )}
)
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/react-medium-style-image-zoom.md
================================================
---
title: Medium-style image zoom
description: Pose for React makes it easy to write Medium-style image zoom.
category: react
---
# Medium-style image zoom
By leveraging Pose's FLIP capabilities, and the `applyAtStart` and `applyAtEnd` properties, it becomes trivial to implement [Medium](https://medium.com)-style image zooming.
```javascript
const Frame = posed.div({
zoomedOut: {
applyAtEnd: { display: 'none' },
opacity: 0
},
zoomedIn: {
applyAtStart: { display: 'block' },
opacity: 1
}
});
const Image = posed.img({
zoomedOut: {
position: 'static',
width: 'auto',
height: 'auto',
flip: true
},
zoomedIn: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
flip: true
}
});
```
================================================
FILE: packages/popmotion-pose/docs/examples/react/route-transitions-reach-router.md
================================================
---
title: 'Route transitions: Reach Router'
description: A live example of route transitions with Pose and Reach Router
category: react
---
# Route transitions: Reach Router
[Read the full tutorial](/pose/learn/route-transitions-reach-router/)
================================================
FILE: packages/popmotion-pose/docs/examples/react/route-transitions-react-router.md
================================================
---
title: 'Route transitions: React Router'
description: A live example of route transitions with Pose and React Router
category: react
---
# Route transitions: React Router
[Read the full tutorial](/pose/learn/route-transitions-react-router/)
================================================
FILE: packages/popmotion-pose/docs/examples/react/splittext-as-children.md
================================================
---
title: SplitText as children
description: Animate individual words and characters with Pose animations
category: react
---
# SplitText as children
Pose changes flow through to the children posed components made by [React Pose Text](/pose/api/react-pose-text)'s `SplitText` component.
================================================
FILE: packages/popmotion-pose/docs/examples/react/splittext-custom-animations.md
================================================
---
title: SplitText custom animations
description: Animate individual words and characters with Pose animations
category: react
---
# SplitText custom animations
Using the special properties provided by [React Pose Text](/pose/api/react-pose-text), you can create custom animations and staggers per word and character.
================================================
FILE: packages/popmotion-pose/docs/examples/react/splittext-ui-events.md
================================================
---
title: SplitText UI events
description: Animate individual words and characters with Pose animations
category: react
---
# SplitText UI events
Using [React Pose Text](/pose/api/react-pose-text), individual words and characters can be respond to all the same [UI events](/pose/api/ui-events) as other posed components.
These poses are all passed through both words and characters.
================================================
FILE: packages/popmotion-pose/docs/examples/react/splittext.md
================================================
---
title: SplitText
description: Animate individual words and characters with Pose animations
category: react
---
# SplitText
With [React Pose Text](/pose/api/react-pose-text), you can use Pose animations across individual words and characters.
================================================
FILE: packages/popmotion-pose/docs/examples/react/svg-morphing.md
================================================
---
title: SVG Morphing
description: With Pose and a path morphing library like Flubber, you can perform declarative SVG path morphing animations.
category: react
---
# SVG Morphing
By combining a path morphing library like [Flubber](https://github.com/veltman/flubber) with Pose, we can declaratively animate between any two SVG paths.
By importing animations from [Popmotion Pure](/pure), which is included with Pose, we can use their powerful `pipe` method with Flubber's `interpolate` to create a special `transition` prop that animates between paths:
```javascript
import { tween } from 'popmotion';
import { interpolate } from 'flubber';
const morphTransition = ({ from, to }) =>
tween().pipe(interpolate(from, to));
```
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-animating-children.md
================================================
---
title: Animating children
description: An live example of animating across children using Pose for Vue.
category: vue
---
# Animating children
Pose changes propagate across posed components, so you only have to set `pose` on a parent to animate all its children.
```javascript
```
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-enter-exit.md
================================================
---
title: Enter/exit transitions
description: An live example of using the PoseTransition component to make enter and exit transitions with Pose for Vue
category: vue
---
# Enter/exit transitions
Pose for Vue offers a special version of Vue's `transition` component.
Used to animate between single children, `PoseTransition` automatically creates opacity animations as child DOM elements are added and removed.
By providing it posed components, custom animations can be defined. They even flow down through children!
```javascript
export default {
data: () => ({ isVisible: true }),
components: {
PoseTransition,
Shade: posed.div({
enter: {
opacity: 1,
beforeChildren: true,
transition: { duration: 200, ease: "linear" }
},
exit: {
opacity: 0,
afterChildren: true,
transition: { duration: 200, ease: "linear" }
}
}),
Modal: posed.div({
enter: { opacity: 1, z: 0 },
exit: { opacity: 0, z: -150 }
})
},
template: ``
};
```
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-flip.md
================================================
---
title: FLIP
description: An live example of making FLIP animations in Pose for Vue
category: vue
---
# FLIP
The FLIP technique, [fully explained here](https://aerotwist.com/blog/flip-your-animations/), is a way of animating expensive, layout-breaking animations like `width` and `top` by using quick transforms.
In Pose for Vue, performing a FLIP animation is as simple as providing a pose the `flip: true` property.
```javascript
Box: posed.div({
fullscreen: {
width: '100vw',
height: '100vh',
transition: tween,
flip: true
},
thumbnail: {
width: 100,
height: 100,
transition: tween,
flip: true
}
})
```
Click on this box to see it in action:
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-magic-animations.md
================================================
---
title: Magic animations
description: An live example of magic animations in Pose for Vue.
category: vue
---
# Magic animations
Even with no `transition` defined, Pose for Vue will automatically create snappy and responsive transitions between to states.
```javascript
posed.div({
left: { x: -100 },
right: { x: 100 }
})
```
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-medium-style-image-zoom.md
================================================
---
title: Medium-style image zoom
description: Pose for Vue makes it easy to write Medium-style image zoom.
category: vue
---
# Medium-style image zoom
By leveraging Pose's FLIP capabilities, and the `applyAtStart` and `applyAtEnd` properties, it becomes trivial to implement [Medium](https://medium.com)-style image zooming.
```javascript
const Frame = posed.div({
zoomedOut: {
applyAtEnd: { display: 'none' },
opacity: 0
},
zoomedIn: {
applyAtStart: { display: 'block' },
opacity: 1
}
});
const Image = posed.img({
zoomedOut: {
position: 'static',
width: 'auto',
height: 'auto',
flip: true
},
zoomedIn: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
flip: true
}
});
```
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-passive-values.md
================================================
---
title: Passive values
description: An live example of making passive values in Pose for Vue
category: vue
---
# Passive values
A passive value is a value that isn't animated directly. It only changes when another value changes.
```javascript
posed.div({
draggable: 'x',
passive: {
opacity: ['x', interpolate(
[-200, -100, 100, 200],
[0, 1, 1, 0]
)]
}
})
```
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-ui-events-dragging.md
================================================
---
title: "UI events: Drag"
description: An live example of making an element draggable with a single prop in Pose for Vue
category: vue
---
# UI events: Drag
Making an element draggable in Pose for Vue is a simple matter of setting `draggable` to `true`.
```javascript
posed.div({ draggable: true })
```
To restrict to a single axis, use either `'x'` or `'y'`.
```javascript
posed.div({ draggable: 'x' })
```
You can use the special `drag` and `dragEnd` poses to animate the element while dragging is in progress:
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-ui-events-focus.md
================================================
---
title: "UI events: Focus"
description: An live example of making an element focusable with a single prop in Pose for Vue
category: vue
---
# UI events: Focus
Making an element focusable in Pose for Vue is a simple matter of setting `press` to `true`, with associated poses:
```javascript
posed.input({
focusable: true,
init: {
color: '#aaa',
outlineWidth: '0px',
outlineOffset: '0px',
scale: 1
},
focus: {
color: '#000',
outlineWidth: '12px',
outlineOffset: '5px',
outlineColor: '#AB36FF',
scale: 1.2
}
})
```
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-ui-events-hover.md
================================================
---
title: "UI events: Hover"
description: An live example of making an element hoverable with a single prop in Pose for Vue
category: vue
---
# UI events: Hover
Making an element hoverable in Pose for Vue is a simple matter of setting `press` to `true`, with associated poses:
```javascript
posed.div({
hoverable: true,
init: {
scale: 1,
boxShadow: '0px 0px 0px rgba(0,0,0,0)'
},
hover: {
scale: 1.2,
boxShadow: '0px 5px 10px rgba(0,0,0,0.2)'
}
})
```
================================================
FILE: packages/popmotion-pose/docs/examples/vue/vue-ui-events-press.md
================================================
---
title: "UI events: Press"
description: An live example of making an element pressable with a single prop in Pose for Vue
category: vue
---
# UI events: Press
Making an element pressable in Pose for Vue is a simple matter of setting `press` to `true`, with associated poses:
```javascript
posed.div({
pressable: true,
init: { scale: 1 },
press: { scale: 0.8 }
})
```
================================================
FILE: packages/popmotion-pose/docs/learn/get-started.md
================================================
---
title: Get started
description: Get started with the Pose animation system for HTML, SVG, React and React Native
---
# Get started
## Choose your adventure:
### [React](/pose/learn/popmotion-get-started)
### [React Native](/pose/learn/native-get-started)
### [Vue](/pose/learn/vue-get-started)
================================================
FILE: packages/popmotion-pose/docs/learn/how-to/css-variables.md
================================================
---
title: CSS variables
description: How to declaratively animate CSS variables
category: how-to
---
# Animate CSS variables
[CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables) are variables that can be defined on a parent element and then referenced throughout its children:
```css
body {
--brand-color: #f00;
}
a {
color: var(--brand-color);
}
```
The benefit of CSS variables vs variables in CSS pre-processors like SASS is that they're **dynamic**, which means they can be updated at runtime.
So in the above example, to change the link color we'd simply change the value of `--brand-color`.
Because these values can be changed, they can be animated. In this tutorial we'll demonstrate how to animate multiple elements on a page by animating a single CSS variable.
## CSS
First, open this [CodePen](https://codepen.io/popmotion/pen/mxGZNx?editors=0110) to follow along with the tutorial.
As a simple example, we're going to animate a `distance` property. Add that to `body`:
```css
body {
--distance: 0px;
}
```
Now, on the `.box` rule add a `transform` that uses this `distance`:
```css
transform: translateX(var(--distance));
```
## JavaScript
In all the previous Pose tutorials, if we've wanted to change a property of an element, we've selected the element itself.
With CSS variables, we're instead going to animate the parent element where the variable is set. Which, in this case, is `body`:
```javascript
const body = document.querySelector('body')
```
Now define the pose config with a single pose, called `right`:
```javascript
const config = {
right: { '--distance': '100px' }
}
```
Bring the two together by creating a new poser:
```javascript
const bodyPoser = pose(body, config)
```
To trigger the animation, set the pose to "right":
```javascript
bodyPoser.set('right')
```
And that's it! All four boxes will animate to the right.
## Gotchas
There are a couple of points to keep in mind when you animate CSS variables.
### Pose can't infer types
In the example above, we explicitly set `--distance` to `'100px'`. Usually when animating `x`, we'd simply use `100` as a raw number and Pose would infer that we should use pixels.
When you're animating CSS variables, **Pose doesn't know what you're going to do with the variable**. So we need to manually tell it if we want specific measurements.
### CSS variables sometimes contain whitespace
In [this CodePen](https://codepen.io/popmotion/pen/JLaBNG?editors=0010), we're animating using the `--color` variable as initially read directly from the CSS.
Oddly, this read value is sometimes returned with extra whitespace. If you then set the value with the extra whitespace, it breaks! In this example we're using `from` as `from.trim()` to fix this, but there is an [open issue in the Stylefire](https://github.com/Popmotion/stylefire/issues/16) to have this fixed.
================================================
FILE: packages/popmotion-pose/docs/learn/how-to/framer-x.md
================================================
---
title: "Use Pose in Framer X (WIP)"
description: Add declarative animations to your Framer X prototypes
category: how-to
---
# Use Pose in Framer X (WIP)
Framer X beta invites are going out and it's already possible to use Pose in Framer X prototypes.
It's early days at the moment so integration isn't seamless but it's already possible to add UI events like `drag`, `hover` and `press`.
You can also already use Framer X to test animations by exposing pose logic via the UI.
But there are currently limitations, and we'll take a look at those, too.
## Install Pose
First we need to add Pose to our Framer project.
With our project open in Framer X, go to File > Show Project Folder.

You should have the `container` folder highlighted. Open this folder in your terminal of choice.
> If you're using VS Code, drag and drop the highlighted folder onto the VS Code dock icon. Then, in VS Code's file browser, right click in the project root and choose "Open in Terminal".
This folder is a typical yarn package, so you can add Pose by running `yarn add react-pose`.
The current limitation with this process is you have to repeat it for every project you want to use Pose in. I'm also unclear as to whether yarn dependencies are saved to the project file itself, as `Show Project Folder` opens a directory in the app cache.
## Create a posed Framer component
At the bottom of the Framer X Component panel, there's a button labelled "New Component". Click that, give your component a name, choose "from code" and press "Create and edit".

You'll be greeted with a TypeScript file containing some simple React code.
The only Framer X-specific code in here is a static property called `propertyControls`, which we'll explore in a moment.
First, import Pose and make a simple hoverable component:
```javascript
import posed from 'react-pose';
const Container = posed.div({
hoverable: true,
init: { scale: 1 },
hover: { scale: 1.2 }
});
```
Replace the render function with the following:
```javascript
return {this.props.text};
```
Save, and return to the Framer X editor.
In the components panel you'll see your new component. You can add this to the Framer X editor by dragging and dropping it in place.
Click the play icon in the top and hover over your new component. It'll react to your hovering as expected.

## Expose pose controls
Go back to your editor and highlight your component. Notice in the right-hand inspector there's a section named after your component with a text input labelled "Text". If you replace "Hello world!", your component will update with the input text.
Go to the source code of your component and take a look at the `render` function. It's outputting `this.props.text`.
Now look at `propertyControls`. It has a single property, `text`, with a title of "Text". This is how you expose control over `props` via the Framer X UI.
We're going to use this to control Pose animations.
First, cmd-click on `propertyControls`' `PropertyControls` type. This will take you through to its type definition, and we can take a look at some of the control types available to us.
Let's use a simple control, a `Boolean`. Add this to `propertyControls`:
```javascript
isVisible: {
type: ControlType.Boolean,
title: "Is visible"
}
```
We also need to add a default for this property to `defaultProps`:
```javascript
static defaultProps = {
text: "Hello World!",
isVisible: false
};
```
And a type for the property to the `Props` interface:
```javascript
interface Props {
text: string;
isVisible: boolean;
}
```
Now, this control will be available to use in the inspector panel.
But we still need to use this prop in our pose logic. Add two new poses, `visible` and `hidden`:
```javascript
const Container = posed.div({
hoverable: true,
init: { scale: 1 },
hover: { scale: 1.2 },
visible: { opacity: 1 },
hidden: { opacity: 0 }
});
```
And set `Container`'s `pose` prop to either of these poses based the value of `isVisible`:
```javascript
{this.props.text}
```
Now, back in our editor, the "Is visible" toggle control will make our component fade in and out.
## Dynamic pose props
We can use the Framer UI to affect the actual transition used, too.
Repeating the steps above, add a new `fadeDuration` property to the right-hand inspector. This time, we want it to be a `Number`.
By cmd-clicking `PropertyControls` again we can see that `NumberControl` has a `max` property available which, if we don't set, the range slider will max out at `100`. So lets change that to something higher like `10000`:
```javascript
fadeDuration: {
type: ControlType.Number,
max: 10000,
title: "Fade duration"
}
```
Then make a new fade transition creator:
```javascript
const fadeTransition = ({ fadeDuration }) => ({
type: 'tween',
duration: fadeDuration
});
```
And apply this to our `visible` and `hidden` poses:
```javascript
const Container = posed.div({
hoverable: true,
init: { scale: 1 },
hover: { scale: 1.2 },
visible: {
opacity: 1,
transition: fadeTransition
},
hidden: {
opacity: 0,
transition: fadeTransition
}
});
```
Finally, ensure we're passing the `fadeDuration` prop to `Container`:
```javascript
{this.props.text}
```
Now, you can change the duration of the transition with the new "Fade duration" number slider in the inspector panel. Play with the visibility toggle to check it out!

## Conclusion
This level of integration is already possible, but in the near future it'd be great to figure out a way of triggering local state/prop changes from other components within the Framer prototype itself.
I'd also like to be able to expose the entire pose API via the Framer UI, as well as figure out `PoseGroup` integration. Framer will be a great tool for closing down the feedback loop when playing around with interactions.
Some of the above may already be possible, and just requires further exploration. [Hit me up](https://twitter.com/popmotionjs) if you have any ideas!
================================================
FILE: packages/popmotion-pose/docs/learn/react/animating-children.md
================================================
---
title: Animating children
description: How to orchestrate animations across multiple elements
category: react
next: ui-events
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Animating children
Traditionally, coordinating animation across multiple children has been an involved process. Especially with React.
With Pose, it's as simple as animating just one. It looks like this:
```javascript
const Parent = posed.ul();
const Child = posed.li();
({items}) => (
{items.map(item => )}
)
```
Whenever a posed component changes `pose`, that change is communicated throughout all of its children posed components via React's context API. So even if they're not direct children, they still update accordingly.
This makes it super-simple to, for instance, make page-wide [route transitions](/pose/learn/route-transitions-react-router).
## Setup
To demonstrate animating children, we're going to create this sidebar animation (if the animation has already played, refresh the iframe):
Follow along by forking this [CodeSandbox](https://codesandbox.io/s/0q10o2xlyl).
## Poses
First, we need to describe two poses, "open" and "closed", for both the sidebar and the items within it.
At the top of your `index.js`, create your posed components:
```javascript
const Sidebar = posed.ul({
open: { x: '0%' },
closed: { x: '-100%' }
});
const Item = posed.li({
open: { y: 0, opacity: 1 },
closed: { y: 20, opacity: 0 }
});
```
## Render
In our `render` function, replace `return null` with our `Sidebar`, setting our `pose` prop to either the `open` or `closed` poses defined above:
```javascript
return (
);
```
Now when the `isOpen` is `true`, our sidebar animates out. All we have to do to also animate the `Item` components is add those as children:
```javascript
return (
);
```
We only provide `pose` to `Sidebar`, yet `Item` components also animate!
## Scheduling animations
Currently, our children animations are being fired at the exact same time as the parent. But, often we'd prefer the child animations to be delayed or staggered.
Luckily, we've got properties for that!
### delay
The `delay` property can be used to delay the animation on the **current** poser, without affecting the execution of child animations.
So by setting `delay: 300` on the sidebar's `closed` pose, the children will all animate out before the sidebar itself.
```javascript
const Sidebar = posed.ul({
open: { x: '0%' },
closed: { x: '-100%', delay: 300 }
});
```
### delayChildren
Conversely, the `delayChildren` property can be used to delay all the children animations.
By setting `delayChildren` on the sidebar's `open` pose, we can animate the sidebar out and **then** animate the children in:
```javascript
const Sidebar = posed.ul({
open: { x: '0%', delayChildren: 200 },
closed: { x: '-100%', delay: 300 }
});
```
### staggerChildren
Rather than animating all the children in at once, it's possible to stagger them in individually. The `staggerChildren` prop can be used to determine the delay between each one, starting from **after** the `delayChildren` duration:
```javascript
const Sidebar = posed.ul({
open: {
x: '0%',
delayChildren: 200,
staggerChildren: 50
},
closed: { x: '-100%', delay: 300 },
initialPose: 'closed'
});
```
### staggerDirection
`staggerDirection` can be used to determine which order we stagger over the children in. It can either be `1` (first to last, default), or `-1` (last to first).
### beforeChildren/afterChildren
Setting either `beforeChildren` or `afterChildren` props to `true` will make the parent animation play **before** or **after** any children animations.
================================================
FILE: packages/popmotion-pose/docs/learn/react/custom-transitions.md
================================================
---
title: Custom transitions
description: How to use Pose for React to define custom transitions
category: react
next: dynamic-props
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Custom transitions
With [automatic animations](/pose/learn/get-started), it's easy to create snappy and playful animations just by defining poses.
But there's plenty of instances where we want full control over our animation. For this, we can use the `transition` property.
## Basic usage
Just like CSS, every pose can have a [`transition` property](/pose/api/react-config/#config-options-poses-transition). This property describes how each value should transition to its new pose:
```javascript
posed.div({
visible: {
opacity: 1,
transition: { duration: 300 }
}
})
```
If we're animating multiple properties, we can **optionally** provide different animations for each by providing a named map.
```javascript
posed.div({
visible: {
opacity: 1,
scaleY: 1,
transition: {
opacity: { ease: 'easeOut', duration: 300 },
default: { ease: 'linear', duration: 500 }
}
}
});
```
**By default**, if we define a `transition`, it'll be a `tween`. This is an animation between two values over a specific duration of time.
By providing a `type` property, we can select a different animation to use:
## Transitions
Pose ships with five types of animation from [Popmotion Pure](/pure). Tween, spring, decay, keyframes, and physics.
### Tween
A tween animates from one value to another over a set duration of time.
```javascript
transition: {
duration: 400,
ease: 'linear'
}
```
#### Easing
The `ease` property can be used to affect the speed of the tween over the course of its duration.
This property can be the name of a [Popmotion easing function](/api/easing):
- 'linear'
- 'easeIn', 'easeOut', 'easeInOut'
- 'circIn', 'circOut', 'circInOut'
- 'backIn', 'backOut', 'backInOut'
- 'anticipate'
Or an array of four numbers to create a cubic bezier easing function:
```javascript
transition: {
ease: [.01, .64, .99, .56]
}
```
[Full `tween` documentation](/api/tween)
### Spring
Spring animations maintain velocity between animations to create visceral, engaging motion.
It makes them perfect for animations that happen as a result of user interaction.
By adjusting their `stiffness`, `mass` and `damping` properties, a wide-variety of spring feels can be created.
```javascript
transition: { type: 'spring', stiffness: 100 }
```
[Full `spring` documentation](/api/spring)
### Decay
Decay reduces the velocity of an animation over a duration of time.
It's a perfect match for the special `dragEnd` pose that fires when a user stops [dragging](/pose/learn/ui-events) something, as it can replicate the momentum-scrolling common on smart phones.
The end value is automatically calculated by Pose at the start of the animation, but with the `modifyTarget` prop, you can adjust this, allowing you to do things like snap to a grid.
```javascript
transition: {
type: 'decay',
modifyTarget: v => Math.ceil(v / 100) * 100 // Snap to nearest 100px
}
```
[Full `decay` documentation](/api/decay)
### Keyframes
Keyframes allows you to schedule a series of values to tween between.
```javascript
transition: ({ from, to }) => ({
type: 'keyframes',
values: [from, 100, to],
times: [0, 0.25, 1]
})
```
[Full `keyframes` documentation](/api/keyframes)
### Physics
Physics allows you to simulate things like velocity, friction, and acceleration.
```javascript
transition: {
type: 'physics',
velocity: 1000
}
```
[Full `physics` documentation](/api/physics)
## Transition props
There are a number of other properties that can be used with any transition:
### Delay
If set, will delay the execution of the transition by the specified amount:
```javascript
transition: {
type: 'physics',
delay: 400
}
```
### Min/Max
If set, will ensure values are capped to no less than `min` and no more than `max`.
```javascript
transition: {
type: 'keyframes',
values: [0, 3, 10],
min: 2,
max: 9
}
```
### Round
If set to `true`, `round` will ensure that values output from the animation will be rounded.
```javascript
transition: {
type: 'spring',
round: true
}
```
================================================
FILE: packages/popmotion-pose/docs/learn/react/dynamic-props.md
================================================
---
title: Dynamic props
description: Set props as dynamic functions
category: react
next: animating-children
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Dynamic pose props
Each pose property can be set as a function that resolves when the pose is entered:
```javascript
const Box = posed.div({
visible: {
x: 0,
y: (props) => 100, // Resolved on `visible` enter
transition: {
x: { type: 'tween' },
y: (props) => ({ type: 'spring' }) // Resolved on `visible` enter
}
}
})
```
By using the provided `props` argument, this allows us to create dynamic properties that will react to changes in your app.
## Props
So what are props? They're just component props! Any props you provide to your posed component will be forwarded to these dynamic pose props.
For instance, you could use a component's `i` index prop to write a dynamic `delay` prop:
```javascript
const Item = posed.li({
visible: {
opacity: 1,
transition: ({ i }) => ({ delay: i * 50 })
},
props: { i: 0 }
});
export default ({ i, isVisible }) =>
```
## Transition props
`transition` works a little differently than other pose props.
If set as a function, the function is run **once each for every property being animated**.
That function is provided a few extra props, automatically generated by Pose:
- `from`: The current state of this value
- `to`: The target state defined in the pose
- `velocity`: If a numerical value, the current velocity of the value
- `key`: The name of the current value
- `prevPoseKey`: The name of the pose this value was previously set to
These props can be used to return a different transition definition based on the state of the value:
```javascript
const Sidebar = posed.div({
open: {
x: '-100%',
transition: ({ velocity, to }) => velocity < 0
? { to: 0 }
: { to }
}
});
```
If `transition` is a named map, **some or all** of these can be defined as functions:
```javascript
const Sidebar = posed.div({
open: {
x: 0,
opacity: 1,
transition: {
x: ({ velocity, to }) => velocity < 0 ? { to: -300 } : { to },
opacity: { type: 'spring' }
}
}
});
```
================================================
FILE: packages/popmotion-pose/docs/learn/react/flip.md
================================================
---
title: FLIP
description: A look at Pose's powerful FLIP API
category: react
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# FLIP
The FLIP technique, [fully explained here](https://aerotwist.com/blog/flip-your-animations/), is a way of animating expensive, layout-breaking animations like `width` and `top` by using quick transforms.
Pose provides a couple of methods for performing FLIP:
1. Animate `width`/`top`/etc and adding `flip: true` to a pose
2. Via the [`PoseGroup` component](/pose/api/posegroup)
In this tutorial, we'll take a look at each of these.
## width/top
The problem with animating size and position properties is that they break layout. Recalculating layout is expensive, which can slow animations to below 60fps.
So, when you set a pose with `flip: true` and any of `width`, `height`, `top`, `left`, `right`, or `bottom` values, these will applied at the start of the animation. Pose will measure the size and position of the element before and after, and animate from one to the other using transform properties instead.
For instance, we can switch a `div` to fullscreen and back using the following config:
```javascript
const Panel = posed.div({
fullscreen: {
width: '100vw',
height: '100vh',
transition: tween,
flip: true
},
thumbnail: {
width: 100,
height: 100,
transition: tween,
flip: true
}
});
```
## PoseGroup
React Pose includes a [component called `PoseGroup`](/pose/api/posegroup).
When wrapping a group of posed components, it enables two things:
1. `enter`/`exit` poses
2. FLIP-powered re-ordering
When an item enters or exits the group, or changes position in it, all the other items automatically measure their position before the change and then FLIP into to the new position:
This all happens automatically. Open [this CodePen template](https://codepen.io/popmotion/pen/mxqobd?editors=0010)
At the top of the JS, create a new posed component, a list item:
```javascript
const Item = posed.li()
```
Now, replace `null` in the render function of `Example` with:
```javascript
(
{this.state.items.map(item => (
))}
);
```
This component is pre-set to shuffle the items every two seconds. Notice how, without even defining any animations, all the items automatically animate to their new home.
Plus, the `flip` pose is enabled. So we can define the animation they use to move position by changing `Item` to:
```javascript
const { tween } = popmotion
const Item = posed.li({
flip: {
transition: tween
}
})
```
================================================
FILE: packages/popmotion-pose/docs/learn/react/install.md
================================================
---
title: Install
description: Overview of Pose's installation options.
category: react
next: popmotion-get-started
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Install
**React Pose requires React 16.3.0.**
## Package managers (recommended)
### npm
```bash
npm install react-pose --save
```
### yarn
```bash
yarn add react-pose
```
## File include
**Note:** The Pose documentation uses the `import` syntax for importing individual modules.
**If you use one of the following installation methods, top-level React Pose exports will be available as the global `posed` and `PoseGroup` variables.**
So, when you see in the docs `import posed, { PoseGroup } from 'react-pose'`, you can instead simply use the global `posed` and `PoseGroup` variables.
### Download
You can download the latest version of Pose at https://unpkg.com/react-pose/dist/react-pose.js
### Script include
You can include it in your HTML with this `script` tag:
```
```
## CodePen
You can fork the [React Pose playground on CodePen](https://codepen.io/popmotion/pen/mxmrPZ?editors=0010), which is set up with the latest version of React Pose.
You an also add React Pose to any existing CodePen project by clicking on Settings > JavaScript and then pasting `https://unpkg.com/react-pose/dist/react-pose.js` into the "Add External JavaScript" field.
================================================
FILE: packages/popmotion-pose/docs/learn/react/passive.md
================================================
---
title: Passive values
description: Learn how to create passive values that only change when others do
category: react
next: flip
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Passive values
Sometimes we don't want to explicitly define a state for a value, we might just want it to change whenever another value does.
For instance, we might want an element to disappear as it moves beyond certain boundaries:
For this, we can use **passive values**. In this tutorial we'll see how to define them, and how to make them respond to changes in parent values too.
## Defining a passive value
Open the [this draggable example](https://codepen.io/popmotion/pen/dmWWdp?editors=0010) and replace the posed component config with this:
```javascript
const Box = posed.div({
draggable: 'x'
});
```
The dragging motion of the element is locked to the `x` axis. We can actually lock movement to the diagonal by defining `y` as a `passive` value.
Passive values are defined a tuples, like this:
```javascript
const Box = posed.div({
draggable: 'x',
passive: {
y: ['x', v => v]
}
});
```
The first item in the tuple is the name of the value we want to link to. In this case, that's `'x'`.
The second item is a mapping function. This takes the output of the linked value and returns our passive value. In this example, we're simply returning `x`, and creating this motion:
By using this mapping function we can start creating new effects. For instance, returning the negative of `x` creates diagonal movement in the opposite direction:
```javascript
y: ['x', v => -v]
```
Or by using `Math.sin` we can make wavey behaviour:
```javascript
y: ['x', v => v * Math.sin(v * 0.01)]
```
## Changing non-numerical values
So far, we've mapped two pixel values. But we can set any kind of value with any other.
Long-time users of Popmotion will recognise the signature of the mapping function. It accepts one value, and returns another. Which means we can compose this function using [Popmotion's transformers](/api/transformers).
For instance, instead of `y` let's create a function that will map `x` to `backgroundColor`.
For this we'll need to import four functions from `popmotion.transform`:
```javascript
const { pipe, clamp, interpolate, blendColor } = popmotion.transform
```
Our steps will be:
1) Convert the output of `x` from pixels to a `0` to `1` range
2) Clamp that output to within `0` and `1`
3) Use that progress number to blend between two colors
Which means our function will look like this:
```javascript
backgroundColor: ['x', pipe(
interpolate([-200, 200], [0, 1]),
clamp(0, 1),
blendColor('#FF1C68', '#09f')
)]
```
## Linking to ancestors
We can also link a passive value to a value in one of the poser's ancestors.
Let's revist our [sidebar example](https://codepen.io/popmotion/pen/LdybdN?editors=0010) from earlier.
Currently, we're actively animating the children by setting poses on both the parent and the children.
But, it's possible to change the opacity of the items as the `x` of their sidebar parent changes.
To do this, we pass `true` as the third and final argument of the tuple.
Add a slower `transition` to `sidebarConfig.open` to help us see this in effect.
```javascript
transition: (props) => tween({ ...props, duration: 1000 })
```
Now, replace `itemConfig` with this:
```javascript
const Box = posed.li({
passive: {
opacity: ['x', pipe(
parseFloat,
interpolate([-100, 0], [0, 1])
), true]
}
});
```
As you can see, we're passing in a third parameter to the passive tuple, `true`. This says "listen to the `x` value, but do so on my immediate parent".
### Distant ancestors
Using `true` is fine if we want to look just one part up the ancestor chain. But it's also possible to go much further up using `label`.
By explicitly naming our posers with a `label`, we can refer to any poser in the ancestor chain.
Add the label `'sidebar'` to our `sidebarConfig`:
```javascript
const Sidebar = posed.ul({
label: 'sidebar',
/* other props */
});
```
Now replace `true` in `itemConfig` with `'sidebar'`. It still works, and it will still work if you decide to put a poser between sidebar and items.
================================================
FILE: packages/popmotion-pose/docs/learn/react/popmotion-get-started.md
================================================
---
title: Get started
description: Introduction to Pose for React's declarative animation interface
category: react
next: custom-transitions
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Get started
Pose is a declarative motion system that combines the simplicity of CSS transitions with the power and flexibility of JavaScript.
In this series of tutorials, we'll learn how to use Pose for React DOM. We'll gradually introduce each of its features, starting with this simple `opacity` animation:
## Setup
The easiest way to play around with Pose is to [fork this CodeSandbox playground](https://codesandbox.io/s/qz0zyqwnqq).
For local development, all installation options can be found on the [install](/pose/learn/install) page.
## The "Hello World" animation
In Pose for React, we create animated components by importing `posed` from `react-pose`:
```javascript
import posed from 'react-pose';
```
`posed` can create animated versions of any HTML or SVG element. Use it to create a `div`:
```javascript
const Box = posed.div();
```
Change the `render` function to return an instance of `Box` instead of `div`:
```javascript
return ;
```
With Pose, we define possible states, or **poses**, that the `Box` can be in. It looks a lot like CSS:
```javascript
const Box = posed.div({
visible: { opacity: 1 },
hidden: { opacity: 0 }
});
```
Now to animate `Box` between its `visible` and `hidden` poses, we just pass it a `pose` prop:
```javascript
return (
);
```
The box is now animating between the two poses!
## But wait, where did we define the animation?
Short answer: we didn't.
More helpful answer: By default, Pose **doesn't require you to explicitly define the animations** used to transition between two states.
Instead, it automatically creates an animation based on the properties being animated.
These animations have been designed to create snappy and playful interfaces. Physical motion uses `spring` to maintain velocity between animations, whereas properties like `opacity` use a `tween`.
However, there will always be situations where we want greater control. For that, we can define [custom transitions](/pose/learn/custom-transitions).
================================================
FILE: packages/popmotion-pose/docs/learn/react/react-exit-enter-transitions.md
================================================
---
title: Enter/exit transitions
description: Learn how to animate React components as they mount and unmount with Pose for React's PoseGroup component
category: react
next: passive
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Enter/exit transitions
Animating components when they unmount has been historically tricky with React.
[Pose for React's `PoseGroup` component](/pose/api/posegroup) makes it trivial to animate one or more children components as they enter and exit.
In this tutorial, we'll see how it can be used to make this modal animation:
## Setup
To start, fork [this CodeSandbox](https://codesandbox.io/s/842823w17j).
It's set up with a `setInterval` that switches the visibility of two components, the modal, and its background overlay.
```javascript
render() {
return (
this.state.isVisible && [
,
]
);
}
```
Currently, both components snap in and out, so let's make them smoothly animate instead!
## Import Pose for React
First, we need to import Pose for React. This time, we're going to be importing both `posed` and the `PoseGroup` component:
```javascript
import posed, { PoseGroup } from 'react-pose';
```
## Add PoseGroup
To start triggering animations when components enter and exit, we need to wrap them in `PoseGroup`.
`PoseGroup` needs to stay rendered at all times, and its children can be added and removed via logic. So wrap our `isVisible` check like so:
```javascript
return (
{this.state.isVisible && [
,
]}
);
```
## Create posed components
As the `shade` and `modal` children are currently plain `div` components, they won't animate. We need to replace them with posed components.
After the block of imports, add:
```javascript
const Modal = posed.div();
const Shade = posed.div();
```
Then replace the two `div`s with our new components:
```javascript
,
```
## Animate!
By default, `PoseGroup` will fire a component's `enter` pose when they enter (transitioning from its `exit` pose), and `exit` when they exit.
So let's give our new `Shade` and `Modal` components `enter` and `exit` poses:
```javascript
const Modal = posed.div({
enter: { y: 0, opacity: 1 },
exit: { y: 50, opacity: 0 }
});
const Shade = posed.div({
enter: { opacity: 1 },
exit: { opacity: 0 }
});
```
Now, your modal and its shade will animate in and out!
As usual you can play around with these poses to tweak the animation. For instance you can add a `delay` to `Modal`'s `enter` pose to make it animate in a little after the `Shade`. Or provide it a custom `transition` to `exit` faster:
```javascript
const Modal = posed.div({
enter: { y: 0, opacity: 1, delay: 300 },
exit: {
y: 50,
opacity: 0,
transition: { duration: 200 }
}
});
```
================================================
FILE: packages/popmotion-pose/docs/learn/react/react-tutorial-medium-style-image-zoom.md
================================================
---
title: "Tutorial: Medium-style image zoom"
description: How to make Medium-style image zooming with Pose for React
category: react
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Tutorial: Medium-style image zoom
[Medium](https://medium.com) have an beautiful zoom effect on their images. When clicked, they pop out of the page as a white background fades in behind them. Then, if clicked again, or if a user scrolls away, they pop back into place.
Take a look:
In this tutorial, we'll learn how to achieve this same effect using Pose for React.
## Setup
To get started, fork this [CodeSandbox template](https://codesandbox.io/s/y2k00vx22x).
It contains a mock article that contains a couple of images. These are being rendered via the component we're going to work on, `ZoomImage`.
Open `components/ZoomImage.js`, and let's get started!
## State
First, we need to create `state` so we know whether the image is zoomed or not. At the top of the `ZoomImage` class, add the following:
```javascript
state = { isZoomed: false };
```
Of course, this state is useless on its own. We're going to need a couple of functions to set the zoom status. On the next line, add the following `zoomIn` and `zoomOut` methods:
```javascript
zoomIn() {
this.setState({ isZoomed: true });
}
zoomOut() {
this.setState({ isZoomed: false });
}
```
Finally, we want to toggle the zoomed state when someone clicks the image container (as this will also later contain the white background):
```javascript
this.state.isZoomed ? this.zoomOut() : this.zoomIn()}
style={{ width: imageWidth, height: imageHeight }}
>
```
Now, when the image is clicked, the component will change zoom status. But we're not responding to this in our `render` function. Let's make some animations!
## Image zoom animation
When the image zooms in, it needs to animate from its place in the document, smoothly into the center of the screen. To do this, we're going to use Pose's FLIP capabilities.
You can read the gritty details about FLIP in this [blog post by Paul Lewis](https://aerotwist.com/blog/flip-your-animations/). In essence, it's a way of *performantly* animating between two states that would otherwise be expensive, for instance where `position`, `top`, `width`, or other layout-changing properties have changed.
In Pose, you simply have to add `flip: true` to a pose, and it'll automatically perform the usually complicated steps to perform this animation.
Import Pose for React:
```javascript
import posed from 'react-pose';
```
Now make a posed `img` component:
```javascript
const Image = posed.img();
```
We're going to provide the component two poses, one for each zoom state: `zoomedIn` and `zoomedOut`.
Our `zoomedIn` pose is going to set `position: fixed` and every positional prop to `0`. This will pop the content out of the layout and lock it to the viewport.
In our `styles.css` file, `img` has a style of `margin: auto` which centers the image when it's being stretched across the screen in this way.
```javascript
const Image = posed.img({
zoomedIn: {
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
flip: true
}
});
```
`zoomedOut` sets `position: static` to pop it back into the DOM, as well as setting `width` and `height` to `auto` to make it fill its layout container:
```javascript
const Image = posed.img({
zoomedIn: {
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
flip: true
},
zoomedOut: {
position: 'static',
width: 'auto',
height: 'auto',
flip: true
}
});
```
We've now got our posed `img` component fully configured. Replace the `img` component in the `render` function with it:
```javascript
```
To animate `Image` between the two poses, we need to provide it a `pose` property.
At the top of the `render` function, set our `pose`:
```javascript
const { isZoomed } = this.state;
const pose = isZoomed ? 'zoomedIn' : 'zoomedOut';
```
And provide it to `Image`:
```javascript
```
Now, when we click our image, it zooms in and out!
I find the automatically generated animation a little bouncy for this purpose. We can define a new `transition` with a `ease` curve generated at [Lea Verou's cubic bezier generator](http://cubic-bezier.com/#.08,.69,.2,.99).
```javascript
const transition = {
duration: 400,
ease: [0.08, 0.69, 0.2, 0.99]
};
```
Provide this as a `transition` prop to both poses, and the animation becomes a little slicker.
## Background animation
That's the (usually) difficult bit out of the way. It's looking pretty good but the Medium example fades a background in behind the image as it zooms in and out.
Make a new posed component called `Frame`:
```javascript
const Frame = posed.div();
```
In our `styles.css` add a new rule for `.frame`. We're going to make the background of this frame white, and set `translateZ(0)` to ensure its fade animation is hardware-accelerated:
```css
.frame {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
background: white;
transform: translateZ(0);
}
```
And now add `Frame` as a sibling of `Image`, also passing it the `pose` prop:
```javascript
```
Now we can animate it! We just want to fade the overlay in and out, so first add those poses:
```javascript
const Frame = posed.div({
zoomedIn: { opacity: 1 },
zoomedOut: { opacity: 0 }
});
```
By itself this won't do anything, as we've got `display` set to `none` in the CSS.
For this we can use the `applyAtStart` and `applyAtEnd` props. They allow you to define styles to set at the start and at the end of the pose transition, respectively.
```javascript
const Frame = posed.div({
zoomedIn: {
applyAtStart: { display: 'block' },
opacity: 1
},
zoomedOut: {
applyAtEnd: { display: 'none' },
opacity: 0
}
});
```
Now your background will fade in and out behind the image as it zooms in!
## Scroll to zoom out
The original Medium image zoom has a nice feature where if a user starts scrolling, the image zooms out back into its original place.
We can accomplish the same thing by adding a `'scroll'` event listener to `zoomIn`:
```javascript
zoomIn() {
window.addEventListener('scroll', this.zoomOut);
this.setState({ isZoomed: true });
}
```
By itself, this isn't going to work. When `this.zoomOut` is called, it'll be in the execution context of the event caller rather than our React component. We can bind `zoomOut` to our component by changing it to an arrow function:
```javascript
zoomOut = () => {
this.setState({ isZoomed: false });
};
```
Finally, we need to remove the event listener when a user does zoom out:
```javascript
zoomOut = () => {
window.removeEventListener('scroll', this.zoomOut);
this.setState({ isZoomed: false });
};
```
## Conclusion
Here's our finished example:
There's plenty of fun things you can do to improve accessibility and aesthetics.
Have a think about:
- Closing the image via the `esc` key
- Changing the background animation. You could even incorporate SVGs.
- Adding a "scroll delay" where a user has to scroll a minimum distance before we close the image.
- Changing the cursor to show a zoom in or zoom out icon.
================================================
FILE: packages/popmotion-pose/docs/learn/react/route-transitions-reach-router.md
================================================
---
title: "Tutorial: Reach Router"
description: How to make route transition animations with React Pose and Reach Router
category: react
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Route transitions with Reach Router
> **Note:** This tutorial is for **Reach** Router. Users of **React** Router will want to use the [React Router tutorial](/pose/learn/route-transitions-react-router).
Route transitions in React are notoriously fiddly. With [Pose](/pose) and the accessibility-first [Reach Router](https://reach.tech/router), they can be pretty simple.
We're going first learn how to make a simple fade transition between two routes.
Then, as Pose has the ability to coordinate animations throughout the component tree, we'll show how to animate each route differently, with content staggering in and out.
Here's what we'll be making:
## Sandbox
We've created a [CodeSandbox](https://codesandbox.io/s/mzx1jz521p) example preloaded with React Pose and Reach Router, for you to fork and follow along.
It's set up in a standard way according to the [Reach Router docs](https://reach.tech/router), so if you're not already familiar with Reach Router, it's worth reading the overview there.
## Fade transition
So, let's animate! The first animation we'll add is a simple fade transition. When a user clicks a link, we want to fade the existing content out, and the new content in.
We'll start by importing React Pose. We're going to be using both `posed` and `PoseGroup`. So at the end of your imports, add:
```javascript
import posed, { PoseGroup } from 'react-pose';
```
Now, let's create a posed component with our two visual states, `enter` and `exit`. After the line above, add:
```javascript
const RoutesContainer = posed.div({
enter: { opacity: 1 },
exit: { opacity: 0 }
});
```
We can now use this to wrap our `Router` component:
```javascript
{({ location }) => (
{children}
)}
```
To animate this component between the two `enter` and `exit` states, we can use the `PoseGroup` component.
`PoseGroup` tracks the entering and exiting of child components (as well as reordering), and will animate them in an out. Any components exiting will only be physically removed from the tree once they've finished their `exit` animation.
```javascript
{({ location }) => (
{children}
)}
```
Ah, but wait! If you try and click between pages, there's still no animation. What's up?
Without passing `RoutesContainer` a `key`, `PoseGroup` doesn't know that it has a new child, so it can't animate anything. Let's use Reach Router's `location.key` prop as our key:
```javascript
```
Now, when you change routes, the content will fade in and out!
The two pieces of content currently fade on top of each other. By adding a small delay to the `enter` state, we can optionally separate the animations:
```javascript
const RoutesContainer = posed.div({
enter: { opacity: 1, delay: 300 },
exit: { opacity: 0 }
});
```
## Content transitions
With that animation in place, we can go a step further and animate the entering and exiting content.
First, add a `beforeChildren: true` property to the `RoutesContainer` `enter` pose. This will ensure that it finishes fading in before we animate any of its children:
```javascript
const RoutesContainer = posed.div({
enter: {
opacity: 1,
delay: 300,
beforeChildren: true
},
exit: { opacity: 0 }
});
```
Let's animate our first page. Open `pages/about.js`. You'll see that we've pre-made two posed components, `Container` and `P`, and used those to markup the `About` component.
In the markup, the `P` components are children of `Container`, and `Container` is a child of `RoutesContainer`. So when `RoutesContainer` changes to `enter` and `exit` poses, these will flow through each of the posed components in turn, allowing us to animate them.
Add `enter` and `exit` poses to `P`:
```javascript
const P = posed.p({
enter: { x: 0, opacity: 1 },
exit: { x: 50, opacity: 0 }
});
```
Now, when you enter and exit the `About` page, all the paragraphs will animate in and out. But they all animate in together. It'd be nice to stagger these animations instead.
That's why we've made `Container` a posed component too. It's not going to do animation itself, it's just going to control the animation of its children with the `staggerChildren` property:
```javascript
const Container = posed.div({
enter: { staggerChildren: 50 }
});
```
Try entering and exiting the `About` page again. The paragraphs stagger in.
We can try the same trick on the `Home` page. Open `pages/home.js` and replace the `ListContainer` and `Item` posed components with this:
```javascript
const ListContainer = posed.ul({
enter: { staggerChildren: 50 },
exit: { staggerChildren: 20, staggerDirection: -1 }
});
const Item = posed.li({
enter: { y: 0, opacity: 1 },
exit: { y: 50, opacity: 0 }
});
```
This time, `ListContainer`'s `exit` pose has a new property, `staggerDirection`. Setting this to `-1` reverses the stagger, so elements animate out from the bottom up.
## Conclusion
We've learned how to use Pose with Reach Router to do a quick and simple fade transition, as well as animating across children to provide unique effects for every page.
We've also seen how posed components can be used not only to animate, but to sequence the animations of their children.
================================================
FILE: packages/popmotion-pose/docs/learn/react/route-transitions-react-router.md
================================================
---
title: "Tutorial: React Router 4"
description: How to make route transition animations with React Pose and React Router
category: react
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# Route transitions with React Router
> **Note:** This tutorial is for **React** Router. Users of **Reach** Router will want to use the [Reach Router tutorial](/pose/learn/route-transitions-reach-router).
Route transitions in React are notoriously fiddly. With [Pose](/pose) and [React Router](https://reacttraining.com/react-router/), they can be pretty simple.
We're going first learn how to make a simple fade transition between two routes.
Then, as Pose has the ability to coordinate animations throughout the component tree, we'll show how to animate each route differently, with content staggering in and out.
Here's what we'll be making:
## Sandbox
We've created a [CodeSandbox](https://codesandbox.io/s/m3z4pm0myx) example preloaded with React Pose and React Router, for you to fork and follow along.
It's set up in a standard way according to the [React Router docs](https://reacttraining.com/react-router/web/guides/philosophy), so if you're not already familiar with React Router, it's worth reading the overview there.
## Fade transition
So, let's animate! The first animation we'll add is a simple fade transition. When a user clicks a link, we want to fade the existing content out, and the new content in.
We'll start by importing React Pose. We're going to be using both `posed` and `PoseGroup`. So at the end of your imports, add:
```javascript
import posed, { PoseGroup } from 'react-pose';
```
Now, let's create a posed component with our two visual states, `enter` and `exit`. After the line above, add:
```javascript
const RoutesContainer = posed.div({
enter: { opacity: 1 },
exit: { opacity: 0 }
});
```
We can now use this to wrap our `Switch` component:
```javascript
{/* ...routes */}
```
To animate this component between the two `enter` and `exit` states, we can use the `PoseGroup` component.
`PoseGroup` tracks the entering and exiting of child components (as well as reordering), and will animate them in an out. Any components exiting will only be physically removed from the tree once they've finished their `exit` animation.
```javascript
{/* ...routes */}
```
Ah, but wait! If you try and click between pages, there's still no animation. What's up?
Without passing `RoutesContainer` a `key`, `PoseGroup` doesn't know that it has a new child, so it can't animate anything. Let's use React Router's `location.key` prop as our key:
```javascript
```
Now, when you change routes, the content will fade in and out!
The two pieces of content currently fade on top of each other. By adding a small delay to the `enter` state, we can optionally separate the animations:
```javascript
const RoutesContainer = posed.div({
enter: { opacity: 1, delay: 300 },
exit: { opacity: 0 }
});
```
## Content transitions
With that animation in place, we can go a step further and animate the entering and exiting content.
First, add a `beforeChildren: true` property to the `RoutesContainer` `enter` pose. This will ensure that it finishes fading in before we animate any of its children:
```javascript
const RoutesContainer = posed.div({
enter: {
opacity: 1,
delay: 300,
beforeChildren: true
},
exit: { opacity: 0 }
});
```
Let's animate our first page. Open `pages/about.js`. You'll see that we've pre-made two posed components, `Container` and `P`, and used those to markup the `About` component.
In the markup, the `P` components are children of `Container`, and `Container` is a child of `RoutesContainer`. So when `RoutesContainer` changes to `enter` and `exit` poses, these will flow through each of the posed components in turn, allowing us to animate them.
Add `enter` and `exit` poses to `P`:
```javascript
const P = posed.p({
enter: { x: 0, opacity: 1 },
exit: { x: 50, opacity: 0 }
});
```
Now, when you enter and exit the `About` page, all the paragraphs will animate in and out. But they all animate in together. It'd be nice to stagger these animations instead.
That's why we've made `Container` a posed component too. It's not going to do animation itself, it's just going to control the animation of its children with the `staggerChildren` property:
```javascript
const Container = posed.div({
enter: { staggerChildren: 50 }
});
```
Try entering and exiting the `About` page again. The paragraphs stagger in.
We can try the same trick on the `Home` page. Open `pages/home.js` and replace the `ListContainer` and `Item` posed components with this:
```javascript
const ListContainer = posed.ul({
enter: { staggerChildren: 50 },
exit: { staggerChildren: 20, staggerDirection: -1 }
});
const Item = posed.li({
enter: { y: 0, opacity: 1 },
exit: { y: 50, opacity: 0 }
});
```
This time, `ListContainer`'s `exit` pose has a new property, `staggerDirection`. Setting this to `-1` reverses the stagger, so elements animate out from the bottom up.
## Conclusion
We've learned how to use Pose with React Router to do a quick and simple fade transition, as well as animating across children to provide unique effects for every page.
We've also seen how posed components can be used not only to animate, but to sequence the animations of their children.
================================================
FILE: packages/popmotion-pose/docs/learn/react/ui-events.md
================================================
---
title: UI events and interactions
description: Trigger animations based on UI events like drag, press, hover and focus
category: react
next: react-exit-enter-transitions
---
> React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/)
# UI events and interactions
Pose can be used to power and animate interactions. Currently, it supports the following events: drag, press, hover and focus.
In this tutorial, we'll take a look at each.
## Drag
With Pose, making an element draggable is as simple as passing `draggable: true` to the config:
```javascript
const Box = posed.div({
draggable: true
});
```
`true` sets both axis to draggable, but we can select a single axis to drag by setting it to `'x'` or `'y'`.
### Boundaries
We can add boundaries to the range of motion with the `dragBounds` property.
It accepts `top`, `left`, `bottom` and/or `right`, measured in pixels or percentages:
```javascript
const Box = posed.div({
draggable: 'x',
dragBounds: { left: '-100%', right: '100%' }
});
```
### Poses
When a posed component is draggable, two new poses are fired.
While dragging is active, the `drag` pose takes effect.
```javascript
const Box = posed.div({
draggable: true,
init: { scale: 1 },
drag: { scale: 1.2 }
});
```
**Note:** A current limitation with the `drag` pose is you must leave `transition` **undefined**.
When dragging finishes, by default all values revert to their previous pose. Optionally, a `dragEnd` pose can be defined:
```javascript
const Box = posed.div({
draggable: true,
init: { scale: 1 },
drag: { scale: 1.2 },
dragEnd: { scale: 0.5 }
});
```
You can use this pose to animate `x` and `y`, too. For instance, you could make the object spring back to its origin:
```javascript
const Box = posed.div({
draggable: true,
init: { scale: 1 },
drag: { scale: 1.2 },
dragEnd: {
x: 0,
y: 0,
transition: { type: 'spring' }
}
});
```
### Events
We can respond to drag start, drag end, and value change events to trigger updates in our UI.
You can provide callbacks to the `onDragStart` and `onDragEnd` props. These fire with the originating mouse or touch events.
To track changes in `x` and `y`, the `onValueChange` prop accepts a map of callbacks, one for each animating value.
```javascript
const Box = posed.div({ draggable: 'x' })
export default ({ onStart, onEnd, onDrag }) => (
);
```
## Press
With `pressable` set to `true`, you can respond to mouse and touch down events with the `press` pose:
```javascript
const Box = posed.div({
pressable: true,
init: { scale: 1 },
press: { scale: 0.8 }
});
```
By default, when pressing ends, values will return to their previous pose. You can **optionally** set a `pressEnd` pose to override this.
### Events
We can respond to press start and end events with the `onPressStart` and `onPressEnd` callbacks:
```javascript
const Box = posed.div({
pressable: true,
init: { scale: 1 },
press: { scale: 0.8 }
});
export default ({ onStart, onEnd }) => (
);
```
You might prefer to use this over React's in-built `onMouseDown`/`onMouseUp` etc events because Pose's event handling method avoids the annoying situation where a user presses an element, and only stops pressing once they've moved their pointer outside the element.
Those `onMouseUp`/`onTouchEnd` callbacks only fire if the pointer is still on the triggering element, whereas `onPressEnd` will fire anywhere in the page.
## Hover
Components can respond to hovers by settings `hoverable` to `true`. This will enable use of the `hover` pose:
```javascript
const Box = posed.div({
hoverable: true,
init: {
scale: 1,
boxShadow: '0px 0px 0px rgba(0,0,0,0)'
},
hover: {
scale: 1.2,
boxShadow: '0px 5px 10px rgba(0,0,0,0.2)'
}
});
```
By default, when hovering ends, values will return to their previous pose. You can **optionally** set a `hoverEnd` pose to override this.
## Focus
Focusable elements can animate into the `focus` pose by setting `focusable: true`:
```javascript
const Box = posed.div({
focusable: true,
init: {
color: '#aaa',
outlineWidth: '0px',
outlineOffset: '0px',
scale: 1
},
focus: {
color: '#000',
outlineWidth: '12px',
outlineOffset: '5px',
outlineColor: '#AB36FF',
scale: 1.2
}
});
```
By default, when hovering ends, values will return to their previous pose. You can **optionally** set a `blur` pose to override this.
================================================
FILE: packages/popmotion-pose/docs/learn/react-native/native-animating-children.md
================================================
---
title: Animating children
description: How to orchestrate animations across multiple elements
category: react-native
next: native-dragging
---
# Animating children
Traditionally, coordinating animations across multiple children has been a delicate, involved process.
With Pose, animating multiple components is as simple as animating one.
It looks like this:
```javascript
export default ({items}) => (
{items.map(item => )}
)
```
Here's how it's done.
## Child animation
We're going to make an animation of an overlay sliding in from the bottom of the screen. As it animates in, its children items will fade and slide in from the right.
First, we need to define the posed components:
```javascript
const Overlay = posed.View({
open: { y: 0 },
closed: { y: '100vh' }
});
const Item = posed.View({
open: { x: 0, opacity: 1 },
closed: { x: 100, opacity: 0 }
})
```
To make the transition between the two states, we make `Item` children components of `Overlay`:
```javascript
export default ({ isOpen }) => (
)
```
When `pose` changes on `Overlay`, that will be propagated through to `Item`.
For simplicity's sake, they're shown here as direct children. But it's important to note that they don't need to be direct children, or rendered in the same component. Plus, `Item` could have posed children that also had `open` and `closed` poses, and the animations would propagate through to those too.
## Block pose propagation
If you have a posed component that's a child of another posed component, and you **don't** want pose changes propagating down to it, you can start a new ancestor chain by passing `withParent={false}`:
```javascript
```
## Schedule parent and child animations
Currently, our child animations are being fired at the exact same time as the parent. We can change that with some props that can delay, stagger or rearrange animations.
### delay
The `delay` property can be used to delay the animation on the current poser, without affecting the execution of child animations.
So by setting `delay: 300` on the overlay's `closed` pose, the children animations will wait 300 milliseconds before animating out.
```javascript
const Overlay = posed.View({
open: { y: 0 },
closed: { y: '100vh', delay: 300 }
});
```
### delayChildren
Conversely, the `delayChildren` property can be used to delay all the children animations.
By setting `delayChildren` on the overlay's `open` pose, we can animate the overlay out and **then** animate the children in:
```javascript
const Overlay = posed.View({
open: { y: 0, delayChildren: 200 },
closed: { y: '100vh', delay: 300 }
});
```
### staggerChildren
Rather than animating all the children in at once, it's possible to stagger them in individually. The `staggerChildren` prop can be used to determine the delay between each one, starting from **after** the `delayChildren` duration:
```javascript
const Overlay = posed.View({
open: {
y: 0,
delayChildren: 200,
staggerChildren: 50
},
closed: { y: '100vh', delay: 300 }
});
```
### staggerDirection
`staggerDirection` can be used to determine which order we stagger over the children in. It can either be `1` (first to last, default), or `-1` (last to first).
### beforeChildren/afterChildren
Setting either `beforeChildren` or `afterChildren` props to `true` will make the parent animation play **before** or **after** any children animations.
================================================
FILE: packages/popmotion-pose/docs/learn/react-native/native-custom-transitions.md
================================================
---
title: Custom transitions
description: How to use custom transitions with Pose for React Native
category: react-native
next: native-animating-children
---
# Custom transitions
With [automatic animations](/pose/learn/native-get-started), it's easy to create snappy and playful animations just by defining poses.
But there's plenty of instances where we want full control over our animation. For this, we can use the [`transition` property](/pose/api/native-config/#config-poses-transition).
## Basic usage
`transition` can be defined as an object that describes how each value should transition to its new pose:
```javascript
posed.View({
visible: {
opacity: 1,
transition: { duration: 300 }
}
})
```
If we're animating multiple properties, we can **optionally** provide different animations for each by providing a named map.
```javascript
posed.View({
visible: {
opacity: 1,
scaleY: 1,
transition: {
opacity: { ease: 'easeOut', duration: 300 },
default: { ease: 'linear', duration: 500 }
}
}
});
```
**By default**, if we define a `transition`, it'll be a `tween`. This is an animation between two values over a specific duration of time.
By providing a `type` property, we can select a different animation to use:
```javascript
transition: { type: 'spring', stiffness: 100 }
```
Pose for React Native currently supports tween, spring, and keyframes animations.
## Advanced usage
It's possible to set a `transition` prop as a function.
This can be used to generate a transition definition using dynamic props:
```javascript
transition: ({ toValue }) => ({
type: 'keyframes',
values: [0, 10, toValue]
})
```
Or to return any [React Animated](https://facebook.github.io/react-native/docs/animated) animation:
```javascript
posed.View({
draggable: 'x',
dragEnd: {
x: 0,
transition: ({ value, toValue, gestureState, useNativeDriver }) =>
gestureState.dx > 50 || gestureState.dx < -50
? Animated.decay(value, { velocity: gestureState.vx, useNativeDriver })
: Animated.spring(value, { toValue, useNativeDriver })
}
})
```
**Note:** When returning a React Animated animation, you **must** pass it the `useNativeDriver` prop as provided by the transition function.
================================================
FILE: packages/popmotion-pose/docs/learn/react-native/native-dragging.md
================================================
---
title: Dragging
description: Make elements draggable with React Native Pose
category: react-native
next: native-passive
---
# Dragging (experimental)
Simplifying interactivity is a core aim for Pose on the web, and the same is true for React Native Pose.
Currently, it offers experimental dragging support. In this tutorial, we'll:
- Make a component draggable
- Hook into the special `dragging` and `dragEnd` poses
- Use the `onDragStart` and `onDragEnd` callbacks
## Dragging
To make a component draggable set `draggable: true` in the posed component config:
```javascript
const config = {
draggable: true
};
```
`true` sets both axis to draggable, but we can select a single axis to drag on by setting it to `'x'` or `'y'`:
```javascript
const config = {
draggable: 'x'
}
```
## Special poses
When dragging, two special poses become available. `dragging`, and `dragEnd`.
These two poses will be set automatically and will propagate throughout the component's children as normal.
For instance, we could make a component that increases in scale while the user's dragging:
```javascript
const config = {
draggable: true,
dragging: { scale: 1.2 },
dragEnd: { scale: 1 }
};
```
Both of these poses gets provided [PanResponder's](https://facebook.github.io/react-native/docs/panresponder.html) `gestureState` object, so we can make different animations based on the behaviour of the drag:
```javascript
const config = {
draggable: 'x',
dragEnd: {
x: 0,
transition: ({ value, toValue, gestureState }) => {
return gestureState.dx > 50 || gestureState.dx < -50
? Animated.decay(value, { velocity: gestureState.vx })
: Animated.spring(value, { toValue })
}
}
}
```
## onDragStart/onDragEnd
If `onDragStart` or `onDragEnd` callbacks are provided to the component, they'll be called with the same arguments as [PanResponder's](https://facebook.github.io/react-native/docs/panresponder.html) `onPanResponderGrant` and `onPanResponderRelease` callbacks.
```javascript
{}} />
```
## Coming soon
Pose for the web has a `dragBounds` property that can clamp movement to within a specified range. This feature will come to React Native Pose in the coming weeks.
In the longer term we want to introduce a range of properties like snap points, but the serialisable nature of Animated's API makes this difficult compared to the functional API of Popmotion.
## Dragging via Interactable
Wix's [Interactable](https://github.com/wix/react-native-interactable) library is a declarative way of introducing interactions at the component level and is compatible with React Native Pose. If you use it be careful **not** to set `draggable: true` on the posed component otherwise Pose will disable the native driver.
```javascript
const PosedComponent = posed()(config);
export default () => (
{({ x, y }) => (
)}
)
```
================================================
FILE: packages/popmotion-pose/docs/learn/react-native/native-get-started.md
================================================
---
title: Get started
description: Introduction to Pose for React Native's declarative animation interface
category: react-native
next: native-custom-transitions
---
# Get started
Pose is a declarative motion system that combines the simplicity of CSS syntax with the power and flexibility of JavaScript animations and interactions.
In this series of tutorials, we'll learn how to use Pose for React Native. We'll gradually introduce each of its features, starting with this simple `opacity` animation:
## Setup
Install Pose for React Native in your React Native project:
### npm
```bash
npm install react-native-pose
```
### yarn
```bash
yarn add react-native-pose
```
## The "Hello World" animation
In Pose for React Native, we create animated components by importing `posed` from `react-native-posed`:
```javascript
import posed from 'react-native-pose';
```
`posed` can create [animated versions of any component](/pose/api/native-posed), but it has built-in support for `View`, `Text`, `Image`, and `ScrollView`:
```javascript
const Box = posed.View();
```
We can pass a [a configuration object](/pose/api/native-config) to the posed component that defines visual states, or "poses", that our component can be in:
```javascript
const Box = posed.View({
visible: { opacity: 1 },
hidden: { opacity: 0 }
});
```
This `Box` component can be animated between `'hidden'` and `'visible'` poses by passing it a `pose` property on render:
```javascript
export default ({ isVisible }) => (
)
```
And that's it! By switching `isVisible`, your `Box` component will animate in and out.
## But wait, where did we define the animation?
Short answer: we didn't.
More helpful answer: By default, Pose **doesn't require you to explicitly define the animations** used to transition between two states.
Instead, it automatically selects a React Animated animation based on the property being animated.
These animations have been designed to create snappy and playful interfaces. Physical motion uses `spring` to maintain velocity between animations, whereas properties like `opacity` use a `tween`.
However, there will always be situations where we need greater control over our animations. For that, we can define [custom transitions](/pose/learn/native-custom-transitions).
================================================
FILE: packages/popmotion-pose/docs/learn/react-native/native-passive.md
================================================
---
title: Passive values
description: Learn how to create passive values that only change when others do
category: react-native
---
# Passive values
Passive values bind to a value defined in your poses, and change when they change using `Animated.Value.interpolate`.
They're currently the **only** way to animate colors in React Native Pose.
## Defining a passive value
Passive values are defined via the `passive` config prop.
They're defined as tuples, and look like this:
```javascript
const config = {
draggable: 'x',
passive: {
opacity: ['x', {
inputRange: [-200, 0, 200],
outputRange: [0, 1, 0]
}]
}
}
```
The first property in the tuple is the name of the value to bind to.
The second is [the interpolation definition](https://facebook.github.io/react-native/docs/animations.html#interpolation). It maps from the bound value to our passive value.
## Animating color
We currently use `passive` to animate colors (though the ability to define them in poses is on the roadmap).
```javascript
const config = {
open: { scale: 1 },
closed: { scale: 0 },
passive: {
backgroundColor: ['scale', {
inputRange: [0.5, 1],
outputRange: ['#f00', '#0f0']
}]
}
};
```
## Binding to ancestors
So far we've bound passive values to other values on the same posed components.
We can also look back up the ancestor chain and link to values defined in parent posed components.
### First posed parent
To link to the first ancestor in the posed component ancestor chain, we just pass `true` as the third and final argument of the tuple.
```javascript
const Sidebar = posed.View({
open: { x: 0 },
closed: { x: -300 }
})
const Item = posed.View({
passive: {
opacity: ['x', {
inputRange: [-300, 0],
outputRange: [0, 1]
}, true]
}
})
export default ({ isOpen }) => (
)
```
### Further ancestors
To go further up the chain, we can use the `label` prop instead of `true`.
First, provide a label to the ancestor:
```javascript
const Sidebar = posed.View({
label: 'sidebar',
open: { x: 0 },
closed: { x: 300 }
})
```
Then we provide this label to a child component:
```javascript
const Item = posed.View({
passive: {
opacity: ['x', {
inputRange: [0, 300],
outputRange: [1, 0]
}, 'sidebar']
}
})
```
`Item` could now be many posed components deep and it'll still bind to the Sidebar component.
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-animating-children.md
================================================
---
title: Animating children
description: How to orchestrate animations across multiple elements in Pose for Vue
category: vue
next: vue-ui-events
---
# Animating children
Traditionally, coordinating animation across multiple children has been an involved process.
With Pose for Vue, it's as simple as animating just one. It looks like this:
```javascript
const Component = {
components: {
Parent: posed.ul(ulPoses),
Child: posed.li(liPoses)
},
template: `
`
};
```
Whenever a posed component changes `pose`, that change is communicated throughout all of its children components. Even if they're not direct children, they still update accordingly.
This makes it super-simple to, for instance, make page-wide route transitions.
## Setup
To demonstrate animating children, we're going to create this sidebar animation:
Follow along by forking this [CodeSandbox](https://codesandbox.io/s/n36vyq63vm?module=%2Fsrc%2FApp.vue).
## Sidebar (parent)
In our sandbox, look in the `script` section. We're exporting a component that simply toggles an `isVisible` boolean every two seconds.
Our template is currently empty, so let's add a `Sidebar` that open and closes depending on the status of `isVisible`.
Add a new property to our exported component called `components`. In it, add a `Sidebar` posed component with a couple of poses, `'visible'` and `'hidden'`:
```javascript
components: {
Sidebar: posed.ul({
visible: { x: 0 },
hidden: { x: '-100%' }
})
}
```
In our `template` section, lets render this `Sidebar` component, passing it either a `'visible'` or `'hidden'` pose depending on `isVisible`:
```javascript
```
Already, we can see our sidebar peeking in and out of our page. Let's add some items to it.
## Items (children)
We need a new posed component, this time called `Item`. Add it to `components`:
```javascript
Item: posed.li({
visible: { opacity: 1, y: 0 },
hidden: { opacity: 0, y: 20 }
})
```
We can render this by iterating over the `items` array being created our exported component's `data` function. Pass this as a child of `Sidebar`:
```javascript
```
We've only passed `pose` to `Sidebar`, but all our `Item`s are animating in and out, too!
In this example, `Item` is a direct child of `Sidebar`, but this will still work if it was a far descendant of `Sidebar`. You could even add children to `Item` and those would be animated correctly, too.
## Scheduling animations
Currently, our children animations are being fired at the exact same time as the parent. But, often we'd prefer the child animations to be delayed or staggered.
Luckily, we've got properties for that!
### delay
The `delay` property can be used to delay the animation on the **current** poser, without affecting the execution of child animations.
So by setting `delay: 300` on the sidebar's `closed` pose, the children will all animate out before the sidebar itself.
```javascript
Sidebar: posed.ul({
open: { x: '0%' },
closed: { x: '-100%', delay: 300 }
});
```
### delayChildren
Conversely, the `delayChildren` property can be used to delay all the children animations.
By setting `delayChildren` on the sidebar's `open` pose, we can animate the sidebar out and **then** animate the children in:
```javascript
Sidebar: posed.ul({
open: { x: '0%', delayChildren: 200 },
closed: { x: '-100%', delay: 300 }
});
```
### staggerChildren
Rather than animating all the children in at once, it's possible to stagger them in individually. The `staggerChildren` prop can be used to determine the delay between each one, starting from **after** the `delayChildren` duration:
```javascript
Sidebar: posed.ul({
open: {
x: '0%',
delayChildren: 200,
staggerChildren: 50
},
closed: { x: '-100%', delay: 300 },
initialPose: 'closed'
});
```
### staggerDirection
`staggerDirection` can be used to determine which order we stagger over the children in. It can either be `1` (first to last, default), or `-1` (last to first).
### beforeChildren/afterChildren
Setting either `beforeChildren` or `afterChildren` props to `true` will make the parent animation play **before** or **after** any children animations.
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-custom-transitions.md
================================================
---
title: Custom transitions
description: How to use Pose for Vue to define custom transitions.
category: vue
next: vue-dynamic-props
---
# Custom transitions
With [automatic animations](/pose/learn/vue-get-started), it's easy to create snappy and playful animations just by defining poses.
But there's plenty of instances where we want full control over our animation. For this, we can use the `transition` property.
## Basic usage
Just like CSS, every pose can have a `transition` property. This property describes how each value should transition to its new pose:
```javascript
const config = {
visible: {
opacity: 1,
transition: { duration: 300 }
}
}
```
If we're animating multiple properties, we can **optionally** provide different animations for each by providing a named map.
```javascript
const config = {
visible: {
opacity: 1,
scaleY: 1,
transition: {
opacity: { ease: 'easeOut', duration: 300 },
default: { ease: 'linear', duration: 500 }
}
}
};
```
**By default**, if we define a `transition`, it'll be a `tween`. This is an animation between two values over a specific duration of time.
By providing a `type` property, we can select a different animation to use:
## Transitions
Pose ships with five types of animation from [Popmotion Pure](/pure). Tween, spring, decay, keyframes, and physics.
### Tween
A tween animates from one value to another over a set duration of time.
```javascript
transition: {
duration: 400,
ease: 'linear'
}
```
#### Easing
The `ease` property can be used to affect the speed of the tween over the course of its duration.
This property can be the name of a [Popmotion easing function](/api/easing):
- 'linear'
- 'easeIn', 'easeOut', 'easeInOut'
- 'circIn', 'circOut', 'circInOut'
- 'backIn', 'backOut', 'backInOut'
- 'anticipate'
Or an array of four numbers to create a cubic bezier easing function:
```javascript
transition: {
ease: [.01, .64, .99, .56]
}
```
[Full `tween` documentation](/api/tween)
### Spring
Spring animations maintain velocity between animations to create visceral, engaging motion.
It makes them perfect for animations that happen as a result of user interaction.
By adjusting their `stiffness`, `mass` and `damping` properties, a wide-variety of spring feels can be created.
```javascript
transition: { type: 'spring', stiffness: 100 }
```
[Full `spring` documentation](/api/spring)
### Decay
Decay reduces the velocity of an animation over a duration of time.
It's a perfect match for the special `dragEnd` pose that fires when a user stops [dragging](/pose/learn/vue-ui-events) something, as it can replicate the momentum-scrolling common on smart phones.
The end value is automatically calculated by Pose at the start of the animation, but with the `modifyTarget` prop, you can adjust this, allowing you to do things like snap to a grid.
```javascript
transition: {
type: 'decay',
modifyTarget: v => Math.ceil(v / 100) * 100 // Snap to nearest 100px
}
```
[Full `decay` documentation](/api/decay)
### Keyframes
Keyframes allows you to schedule a series of values to tween between.
```javascript
transition: ({ from, to }) => ({
type: 'keyframes',
values: [from, 100, to],
times: [0, 0.25, 1]
})
```
[Full `keyframes` documentation](/api/keyframes)
### Physics
Physics allows you to simulate things like velocity, friction, and acceleration.
```javascript
transition: {
type: 'physics',
velocity: 1000
}
```
[Full `physics` documentation](/api/physics)
## Transition props
There are a number of other properties that can be used with any transition:
### Delay
If set, will delay the execution of the transition by the specified amount:
```javascript
transition: {
type: 'physics',
delay: 400
}
```
### Min/Max
If set, will ensure values are capped to no less than `min` and no more than `max`.
```javascript
transition: {
type: 'keyframes',
values: [0, 3, 10],
min: 2,
max: 9
}
```
### Round
If set to `true`, `round` will ensure that values output from the animation will be rounded.
```javascript
transition: {
type: 'spring',
round: true
}
```
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-dynamic-props.md
================================================
---
title: Dynamic pose props
description: Use props in Pose for Vue to create dynamic poses.
category: vue
next: vue-animating-children
---
# Dynamic pose props
Each pose property can be set as a function that resolves when the pose is entered:
```javascript
const Box = posed.div({
visible: {
x: 0,
y: (props) => 100, // Resolved on `visible` enter
transition: {
x: { type: 'tween' },
y: (props) => ({ type: 'spring' }) // Resolved on `visible` enter
}
}
})
```
By using the provided `props` argument, this allows us to create dynamic properties that will react to changes in your app.
## Props
So what are props? In Pose for Vue, they're any taken from any [non-prop attributes](https://vuejs.org/v2/guide/components-props.html#Non-Prop-Attributes) set on the posed component.
For instance, you could use a component's `i` index attribute to write a dynamic `delay` prop:
```javascript
const Component = {
components: {
Item: posed.li({
visible: {
opacity: 1,
transition: ({ i }) => ({ delay: i * 50 })
}
})
},
template: ``
};
```
## Transition props
`transition` works a little differently than other pose props.
If set as a function, the function is run **once each for every property being animated**.
That function is provided a few extra props, automatically generated by Pose:
- `from`: The current state of this value
- `to`: The target state defined in the pose
- `velocity`: If a numerical value, the current velocity of the value
- `key`: The name of the current value
- `prevPoseKey`: The name of the pose this value was previously set to
These props can be used to return a different transition definition based on the state of the value:
```javascript
const Sidebar = posed.div({
open: {
x: '-100%',
transition: ({ velocity, to }) => velocity < 0
? { to: 0 }
: { to }
}
});
```
If `transition` is a named map, **some or all** of these can be defined as functions:
```javascript
const Sidebar = posed.div({
open: {
x: 0,
opacity: 1,
transition: {
x: ({ velocity, to }) => velocity < 0 ? { to: -300 } : { to },
opacity: { type: 'spring' }
}
}
});
```
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-enter-exit-transitions.md
================================================
---
title: Enter/exit transitions
description: Learn how to make enter and exit transitions with Pose for Vue's PoseTransition component
category: vue
next: vue-passive
---
# Enter/exit transitions
Pose for Vue includes a spin on the [Vue `transition` component](https://vuejs.org/v2/guide/transitions.html), `PoseTransition`.
Its used for animating a single component as it's added or removed from the DOM.
In this tutorial we'll learn how to use it to animate a single component as it enters and leaves the DOM.
## Setup
Begin by forking this [CodeSandbox playground](https://codesandbox.io/s/r7wv84wmnn?module=%2Fsrc%2FApp.vue).
It's set up with some basic styles, and a `button` that sets an `isVisible` data property to `true`.
## Adding the modal
First lets add a modal that gets conditionally rendered when `isVisible` is `true`.
In our template, add after the `button`:
```html
```
Now, when you click the button, the modal is added to the DOM.
To animate this, we can use `PoseTransition`. At the top of the `script` section, import it:
```javascript
import { PoseTransition } from 'vue-pose';
```
Pass this through to our template by providing it in the `components` property of our exported component:
```javascript
export default {
data: () => ({ isVisible: false }),
components: { PoseTransition }
};
```
Now in our `template` section we can wrap our newly-added HTML with the `PoseTransition` component:
```html
```
Awesome! Our modal now fades in when we click the button.
If we add a new event to our `shade` div, we can see it also fades out when we remove the modal:
```javascript
```
## Custom transition
In many cases, we'll want to customise the animation used as components animate in and out.
We can do this by using posed components.
Import `posed`:
```javascript
import posed, { PoseTransition } from 'vue-pose';
```
Now made a new component for `Shade`. It's already a `div`, so let's use a `div`. We can define anything we want for the `'enter'` and `'exit'` poses, but for now lets just animate `opacity` with a quicker transition:
```javascript
components: {
PoseTransition,
Shade: posed.div({
enter: {
opacity: 1,
transition: { duration: 200 }
},
exit: { opacity: 0 }
})
}
```
Now replace the direct child of `PoseTransition` with `Shade`:
```html
```
And now the modal fades in faster! But wait, it doesn't fade out any more...
We can fix this by changing `v-on:click` with `v-on:click.native`. This tells Vue we want to add the event listener to the underlying element.
## Animating children
It'd be cooler if we could do an animation that involves the white modal box, too. Well, we can! As we saw before, poses get propagated throughout the DOM, and components under `PoseTransition` are no different.
Replace our `modal` div with:
```html
```
And add a new posed component called `Modal` to `components`:
```javascript
Modal: posed.div({
enter: { opacity: 1, z: 0 },
exit: { opacity: 0, z: -150 }
})
```
Now the modal animates too! But all the animations happen at the same time. We can adjust the timings by providing some extra props to `Shade`.
By adding `beforeChildren` to `enter` and `afterChildren` to `exit`, we can ensure that `Modal` animates separately to `Shade`:
```javascript
Shade: posed.div({
enter: {
opacity: 1,
beforeChildren: true,
transition: { duration: 200, ease: "linear" }
},
exit: {
opacity: 0,
afterChildren: true,
transition: { duration: 200, ease: "linear" }
}
}),
```
## Conclusion
There's more to `PoseTransition`, like animating between sibling components, and animating on mount. Find out its full capabilities with the [full API docs](/pose/api/vue-posetransition).
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-flip.md
================================================
---
title: FLIP
description: A look at Pose for Vue's powerful FLIP API
category: vue
---
# FLIP
The FLIP technique, [fully explained here](https://aerotwist.com/blog/flip-your-animations/), is a way of animating expensive, layout-breaking animations like `width` and `top` by using quick transforms.
Pose for Vue makes animating using the FLIP technique as simple as `flip: true`. Let's take a look.
## width/top
The problem with animating size and position properties is that they break layout. Recalculating layout is expensive, which can slow animations to below 60fps.
So, when you set a pose with `flip: true` and any of `width`, `height`, `top`, `left`, `right`, or `bottom` values, these will applied at the start of the animation. Pose will measure the size and position of the element before and after, and animate from one to the other using transform properties instead.
For instance, we can switch a `div` to fullscreen and back using the following config:
```javascript
Panel: posed.div({
fullscreen: {
width: '100vw',
height: '100vh',
transition: tween,
flip: true
},
thumbnail: {
width: 100,
height: 100,
transition: tween,
flip: true
}
});
```
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-get-started.md
================================================
---
title: Get started
description: Introduction to Pose for Vue's declarative animation interface
category: vue
next: vue-custom-transitions
---
# Get started
Pose for Vue is a declarative motion system that combines the simplicity of CSS transitions with the power and flexibility of JavaScript.
In this series of tutorials, we'll learn how to use Pose for Vue. We'll gradually introduce each of its features, starting with with this simple `opacity` animation:
## Setup
The easiest way to follow this tutorial is to [fork this CodeSandbox playground](https://codesandbox.io/s/9l3vknjv74?module=%2Fsrc%2FApp.vue).
For local development, all installation options can be found on the [install](/pose/learn/vue-install) page.
## The "Hello World" animation
In Pose for Vue, we create animated components by importing `posed` from `vue-pose`.
In the head of the `script` section, add:
```javascript
import posed from 'vue-pose';
```
`posed` can create animated versions of any HTML or SVG element.
Add a `components` property to our exported component that uses `posed` to create a `div`:
```javascript
components: {
Box: posed.div()
}
```
Now change the `div` in our `template` section to be an instance of `Box`:
```javascript
```
With Pose, we define possible states, or **poses**, that a component can be in. It looks a lot like CSS:
```javascript
components: {
Box: posed.div({
visible: { opacity: 1 },
hidden: { opacity: 0 }
})
}
```
Now to animate `Box` between its `visible` and `hidden` poses, we just pass it a `pose` prop.
Our exported component has an interval set on it that toggles `isVisible` between `true` and `false`. We can use that variable in our template to define a pose:
```javascript
```
The box is now animating between the two poses!
## But wait, where did we define the animation?
Short answer: we didn't.
More helpful answer: By default, Pose **doesn't require you to explicitly define the animations** used to transition between two states.
Instead, it automatically creates an animation based on the properties being animated.
These animations have been designed to create snappy and playful interfaces. Physical motion uses `spring` to maintain velocity between animations, whereas properties like `opacity` use a `tween`.
However, there will always be situations where we want greater control. For that, we can define [custom transitions](/pose/learn/vue-custom-transitions).
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-install.md
================================================
---
title: Install
description: Overview of Pose for Vue's installation options.
category: vue
next: vue-get-started
---
# Install
## Package managers (recommended)
### npm
```bash
npm install vue-pose --save
```
### yarn
```bash
yarn add vue-pose
```
## File include
**Note:** The Pose documentation uses the `import` syntax for importing individual modules.
**If you use one of the following installation methods, top-level Pose for Vue exports will be available as the global `posed` variable.**
So, when you see in the docs `import posed from 'vue-pose'`, you can instead simply use the global `posed` variable.
### Download
You can download the latest version of Pose for Vue at https://unpkg.com/vue-pose/dist/vue-pose.js
### Script include
You can include it in your HTML with this `script` tag:
```
```
## CodeSandbox
You can fork the [Pose for Vue playground on CodeSandbox](https://codesandbox.io/s/74471lpxqx), which is set up with the latest version of Pose for Vue.
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-passive.md
================================================
---
title: Passive values
description: Learn how to create passive values that only change when others do
category: vue
next: vue-flip
---
# Passive values
Sometimes we don't want to explicitly define a state for a value, we might just want it to change whenever another value does.
For instance, we might want an element to disappear as it moves beyond certain boundaries:
For this, we can use **passive values**. In this tutorial we'll see how to define them, and how to make them respond to changes in parent values too.
## Defining a passive value
Open the [this draggable example](https://codesandbox.io/s/848v06y8yj?module=%2Fsrc%2FApp.vue) and replace the posed component config with this:
```javascript
Box: posed.div({
draggable: 'x'
});
```
The dragging motion of the element is locked to the `x` axis. We can actually lock movement to the diagonal by defining `y` as a `passive` value.
The `passive` syntax will become clearer in the future but for now they're defined as tuples, like this:
```javascript
const Box = posed.div({
draggable: 'x',
passive: {
y: ['x', v => v]
}
});
```
The first item in the tuple is the name of the value we want to link to. In this case, that's `'x'`.
The second item is a mapping function. This takes the output of the linked value and returns our passive value. In this example, we're simply returning `x`, and creating this motion:
By using this mapping function we can start creating new effects. For instance, returning the negative of `x` creates diagonal movement in the opposite direction:
```javascript
y: ['x', v => -v]
```
Or by using `Math.sin` we can make wavey behaviour:
```javascript
y: ['x', v => v * Math.sin(v * 0.01)]
```
## Changing non-numerical values
So far, we've mapped two pixel values. But we can set any kind of value with any other.
Long-time users of [Popmotion Pure](/pure) will recognise the signature of the mapping function. It accepts one value, and returns another. Which means we can compose this function using [Popmotion's transformers](/api/transformers).
For instance, instead of `y` let's create a function that will map `x` to `backgroundColor`.
For this we'll need to import four functions from `popmotion.transform`:
```javascript
import transform from 'popmotion';
const { pipe, clamp, interpolate, blendColor } = transform;
```
Our steps will be:
1) Convert the output of `x` from pixels to a `0` to `1` range
2) Clamp that output to within `0` and `1`
3) Use that progress number to blend between two colors
Which means our function will look like this:
```javascript
backgroundColor: ['x', pipe(
interpolate([-200, 200], [0, 1]),
clamp(0, 1),
blendColor('#FF1C68', '#09f')
)]
```
## Linking to ancestors
We can also link a passive value to a value in one of the poser's ancestors.
Let's revist our [sidebar example](https://codesandbox.io/s/qq667ljpz4?module=%2Fsrc%2FApp.vue) from earlier.
Currently, we're actively animating the children by setting poses on both the parent and the children.
But, it's possible to change the opacity of the items as the `x` of their sidebar parent changes.
To do this, we pass `true` as the third and final argument of the tuple. `true` says "link to my parent".
Add a slower `transition` to `Sidebar`'s `'visible'` pose to help us see this in effect.
```javascript
transition: { duration: 1000 }
```
Now, import `pipe` and `interpolate` from `'popmotion'`:
```javascript
import { transform } from 'popmotion';
const { pipe, interpolate } = transform;
```
And replace `Item`'s config with this:
```javascript
Item: posed.li({
passive: {
opacity: ['x', pipe(
parseFloat,
interpolate([-100, 0], [0, 1])
), true]
}
});
```
As you can see, we're passing in a third parameter to the passive tuple, `true`. This says "listen to the `x` value, but do so on my immediate parent".
### Distant ancestors
Using `true` is fine if we want to look just one part up the ancestor chain. But it's also possible to go much further up using `label`.
By explicitly naming our posers with a `label`, we can refer to any poser in the ancestor chain.
Add the label `'sidebar'` to our `Sidebar` config:
```javascript
Sidebar: posed.ul({
label: 'sidebar',
/* other props */
});
```
Now replace `true` in the `Item` config with `'sidebar'`. It still works, and it will still work if you decide to put a different posed component between `Sidebar` and `Item`.
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-tutorial-medium-style-image-zoom.md
================================================
---
title: "Tutorial: Medium-style image zoom"
description: How to make Medium-style image zooming with Pose for Vue
category: vue
---
# Tutorial: Medium-style image zoom
[Medium](https://medium.com) have an beautiful zoom effect on their images. When clicked, they pop out of the page as a white background fades in behind them. Then, if clicked again, or if a user scrolls away, they pop back into place.
Take a look:
In this tutorial, we'll learn how to achieve this same effect using Pose for Vue.
## Setup
To get started, fork this [CodeSandbox template](https://codesandbox.io/s/n4n8opjjvj?module=%2Fsrc%2FApp.vue).
`App.vue` contains a mock article that contains a couple of images. These are being rendered via the component we're going to work on, `ZoomImage`.
Open `components/ZoomImage.vue`, and let's get started!
## State
First, we need to create some state to keep track of whether the image is zoomed or not.
In the component exported from the `script` section, add a `data` property that returns our initial state:
```javascript
export default {
props: ['imageWidth', 'imageHeight', 'src'],
data: () => ({ isZoomed: false })
}
```
Of course, this state is useless on its own. We're going to need a couple of functions to set the zoom status. After the `data` prop, add a `methods` prop with `zoomIn` and `zoomOut` functions:
```javascript
methods: {
zoomIn() {
this.isZoomed = true;
},
zoomOut() {
this.isZoomed = false;
}
}
```
Finally, we want to toggle the zoomed state when someone clicks the image or, when zoomed in, its white background.
Add a `computed` property to the component with a `toggleZoom` property. This will return either `zoomIn` or `zoomOut` depending on whether `isZoomed` is `true` or `false`:
```javascript
computed: {
toggleZoom() {
return this.isZoomed ? this.zoomOut : this.zoomIn;
}
}
```
In the `template` section, we can now provide this `toggleZoom` property to the `div`:
```javascript
```
Now, when the image is clicked, the component will change zoom status. But we're not responding to this in our `template`. Let's make some animations!
## Image zoom animation
When the image zooms in, it needs to animate from its place in the document, smoothly into the center of the screen. To do this, we're going to use Pose's FLIP capabilities.
You can read the gritty details about FLIP in this [blog post by Paul Lewis](https://aerotwist.com/blog/flip-your-animations/). In essence, it's a way of *performantly* animating between two states that would otherwise be expensive, for instance where `position`, `top`, `width`, or other layout-changing properties have changed.
In Pose, you simply have to add `flip: true` to a pose, and it'll automatically perform the usually complicated steps to perform this animation.
Import Pose for Vue:
```javascript
import posed from 'vue-pose';
```
Now add a `components` prop to our exported component, and give it a posed `img` component named `ZoomImage`:
```javascript
components: {
ZoomImage: posed.img()
}
```
We're going to provide the component two poses, one for each zoom state: `zoomedIn` and `zoomedOut`.
Our `zoomedIn` pose is going to set `position: fixed` and every positional prop to `0`. This will pop the content out of the layout and lock it to the viewport.
In our `style` section, `img` has a style of `margin: auto` which centers the image when it's being stretched across the screen in this way.
```javascript
ZoomImage: posed.img({
zoomedIn: {
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
flip: true
}
})
```
`zoomedOut` sets `position: static` to pop it back into the DOM, as well as setting `width` and `height` to `auto` to make it fill its layout container:
```javascript
ZoomImage: posed.img({
zoomedOut: {
position: 'static',
width: 'auto',
height: 'auto',
flip: true
},
zoomedIn: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
flip: true
}
})
```
We've now got our posed `img` component fully configured. Replace the `img` component in the `render` function with it:
```html
```
To animate `ZoomImage` between the two poses, we need to provide it a `pose` property.
Add a new `computed` property, `pose`. This will return the name of one of our defined poses, `'zoomedIn'` or `'zoomedOut'`, depending on whether `isZoomed` is `true` or `false`:
```javascript
pose() {
return this.isZoomed ? 'zoomedIn' : 'zoomedOut';
}
```
We can now use this `pose` prop in our `template`:
```html
```
Now, when we click our image, it zooms in and out!
I find the automatically generated animation a little bouncy for this purpose. We can define a new `transition` with a `ease` curve generated at [Lea Verou's cubic bezier generator](http://cubic-bezier.com/#.08,.69,.2,.99).
```javascript
const transition = {
duration: 400,
ease: [0.08, 0.69, 0.2, 0.99]
};
```
Provide this as a `transition` prop to both `ZoomImage` poses, and the animation becomes a little slicker.
## Background animation
That's the (usually) difficult bit out of the way. It's looking pretty good but the Medium example fades a background in behind the image as it zooms in and out.
Make a new posed component in `components` called `Frame`:
```javascript
Frame: posed.div();
```
In our `style` section, add a new rule for `.frame`. We're going to make the background of this frame white, and set `translateZ(0)` to ensure its fade animation is hardware-accelerated:
```css
.frame {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
background: white;
transform: translateZ(0);
}
```
And now add `Frame` as a sibling of `Image`, also passing it the `pose` prop:
```html
```
Now we can animate it! We just want to fade the overlay in and out, so first add those poses:
```javascript
Frame: posed.div({
zoomedIn: { opacity: 1 },
zoomedOut: { opacity: 0 }
});
```
By itself this won't do anything, as we've got `display` set to `none` in the CSS.
For this we can use the `applyAtStart` and `applyAtEnd` props. They allow you to define styles to set at the start and at the end of the pose transition, respectively.
```javascript
Frame: posed.div({
zoomedIn: {
applyAtStart: { display: 'block' },
opacity: 1
},
zoomedOut: {
applyAtEnd: { display: 'none' },
opacity: 0
}
});
```
Now your background will fade in and out behind the image as it zooms in!
## Scroll to zoom out
The original Medium image zoom has a nice feature where if a user starts scrolling, the image zooms out back into its original place.
We can accomplish the same thing by adding a `'scroll'` event listener to `zoomIn`, and remove that event listener on `zoomOut`:
```javascript
zoomIn() {
window.addEventListener('scroll', this.zoomOut);
this.isZoomed = true;
},
zoomOut() {
window.removeEventListener('scroll', this.zoomOut);
this.isZoomed = false;
}
```
## Conclusion
Here's our finished example:
There's plenty of fun things you can do to improve accessibility and aesthetics.
Have a think about:
- Closing the image via the `esc` key
- Changing the background animation. You could even incorporate SVGs.
- Adding a "scroll delay" where a user has to scroll a minimum distance before we close the image.
- Changing the cursor to show a zoom in or zoom out icon.
================================================
FILE: packages/popmotion-pose/docs/learn/vue/vue-ui-events.md
================================================
---
title: UI events and interactions
description: Trigger animations based on UI events like drag, press, hover and focus in Pose for Vue
category: vue
next: vue-enter-exit-transitions
---
# UI events and interactions
Pose for Vue can be used to power and animate interactions. Currently, it supports the following events: drag, press, hover and focus.
In this tutorial, we'll take a look at each.
## Drag
With Pose for Vue, making an element draggable is as simple as passing `draggable: true` to the config:
```javascript
const Box = posed.div({
draggable: true
});
```
`true` sets both axis to draggable, but we can select a single axis to drag by setting it to `'x'` or `'y'`.
### Boundaries
We can add boundaries to the range of motion with the `dragBounds` property.
It accepts `top`, `left`, `bottom` and/or `right`, measured in pixels or percentages:
```javascript
const Box = posed.div({
draggable: 'x',
dragBounds: { left: '-100%', right: '100%' }
});
```
### Poses
When a posed component is draggable, two new poses are fired.
While dragging is active, the `drag` pose takes effect.
```javascript
const Box = posed.div({
draggable: true,
init: { scale: 1 },
drag: { scale: 1.2 }
});
```
**Note:** A current limitation with the `drag` pose is you must leave `transition` **undefined**.
When dragging finishes, by default all values revert to their previous pose. Optionally, a `dragEnd` pose can be defined:
```javascript
const Box = posed.div({
draggable: true,
init: { scale: 1 },
drag: { scale: 1.2 },
dragEnd: { scale: 0.5 }
});
```
You can use this pose to animate `x` and `y`, too. For instance, you could make the object spring back to its origin:
```javascript
const Box = posed.div({
draggable: true,
init: { scale: 1 },
drag: { scale: 1.2 },
dragEnd: {
x: 0,
y: 0,
transition: { type: 'spring' }
}
});
```
### Events
We can respond to drag start, drag end, and value change events to trigger updates in our UI.
You can attach listeners for the `drag-start` and `drag-end` events, as well as a map of listeners for each animated property to `on-value-change`:
```vue
```
## Press
With `pressable` set to `true`, you can respond to mouse and touch down events with the `press` pose:
```javascript
const Box = posed.div({
pressable: true,
init: { scale: 1 },
press: { scale: 0.8 }
});
```
By default, when pressing ends, values will return to their previous pose. You can **optionally** set a `pressEnd` pose to override this.
### Events
We can respond to press start and end events with the `press-start` and `press-end` listeners:
```vue
```
You might prefer to use this over Vue's in-built events because Pose's event handling method avoids the annoying situation where a user presses an element, and only stops pressing once they've moved their pointer outside the element.
Those `mouse-up`/`touch-end` callbacks only fire if the pointer is still on the triggering element, whereas `press-end` will fire anywhere in the page.
## Hover
Components can respond to hovers by settings `hoverable` to `true`. This will enable use of the `hover` pose:
```javascript
const Box = posed.div({
hoverable: true,
init: {
scale: 1,
boxShadow: '0px 0px 0px rgba(0,0,0,0)'
},
hover: {
scale: 1.2,
boxShadow: '0px 5px 10px rgba(0,0,0,0.2)'
}
});
```
By default, when hovering ends, values will return to their previous pose. You can **optionally** set a `hoverEnd` pose to override this.
## Focus
Focusable elements can animate into the `focus` pose by setting `focusable: true`:
```javascript
const Box = posed.input({
focusable: true,
init: {
color: '#aaa',
outlineWidth: '0px',
outlineOffset: '0px',
scale: 1
},
focus: {
color: '#000',
outlineWidth: '12px',
outlineOffset: '5px',
outlineColor: '#AB36FF',
scale: 1.2
}
});
```
By default, when hovering ends, values will return to their previous pose. You can **optionally** set a `blur` pose to override this.
================================================
FILE: packages/projection/CHANGELOG.md
================================================
# Changelog
Projection adheres to [Semantic Versioning](http://semver.org/).
## [2.0.0] Unreleased
================================================
FILE: packages/projection/LICENSE.md
================================================
The MIT License (MIT)
Copyright © 2020 Matt Perry
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: packages/projection/README.md
================================================

A lightweight layout projection library
## Introduction
Layout projection is a method for animating layout at 60fps.
In essence, it's the ability to project any element from its browser-computed layout to a size and position on screen of your choosing.
[Learn more about layout projection's capabilities.](https://mattperry.is/writing-code/layout-projection-animate-browser-layout-60fps-3)
**Projection** is an experimental, lightweight layout projection library. It currently clocks in at 1.8kb, and I expect it to land somewhere in the 2.5kb range.
[Demo](https://codesandbox.io/s/floral-frog-hv1k7?file=/src/index.js)
## Install
**Projection** is currently in alpha. Expect bugs, and breaking changes as the scope of the library is better defined.
```bash
npm install projection@alpha
```
## Usage
### Projecting an element
```javascript
import { layoutNode, updateProjectionStyle } from "projection"
const element = document.getElementById("element-id")
/**
* Create a layoutNode for each element you want to project
*/
const node = layoutNode({
onProjectionUpdate: () => updateProjectionStyle(node, element)
})
/**
* To project, we need an accurate measurement of the element. To do this
* accurately, the measured element can't currently have a transform applied.
* Future releases will handle this automatically.
*
* Ensure that that `setLayout` is called every time the element's layout is recomputed.
*/
const bbox = element.getBoundingClientRect()
node.setLayout(bbox)
/**
* Now we can project. Setting a target will project an element into
* the provided bounding box.
*/
node.setTarget({
top: 100,
left: 100,
right: 400,
bottom: 400
})
```
### Projecting trees
A major difficulty of layout animations is once you apply a transform to a parent, you distort children elements that you might wish to be of a particular size or position throughout the animation.
The calculations involved in correcting this distortion become increasingly complex for deeper layout animation trees.
**Projection** removes this complexity by allowing `layoutNode` to accept another `layoutNode` as a parent. In this way, a tree can be formed. Projection will ensure all child transforms are calculated to compensate for parent transforms.
```javascript
const childNode = layoutNode(options, parentNode)
```
#### Relative projection
Set relative to parent:
```javascript
childNode.setRelativeTarget({ top: 10, left: 10 })
```
Change relative parent:
```javascript
childNode.setRelativeParent(node)
```
### Animations
By using a low-level animation library like [Popmotion](https://popmotion.io), layout animations are a matter of calling `setTarget` once per frame.
```javascript
import { animate, mix } from "popmotion"
// Just before layout change:
const prev = element.getBoundingClientRect()
// Immediately after layout change:
element.style.transform = ""
const next = element.getBoundingClientRect()
node.setLayout(next)
animate({
from: 0,
to: 1,
onUpdate: progress => {
node.setTarget({
left: mix(prev.left, next.left, progress),
right: mix(prev.right, next.right, progress),
top: mix(prev.top, next.top, progress),
bottom: mix(prev.bottom, next.bottom, progress),
})
}
})
```
## TODO
- [ ] Add helper functions for scale correcting box shadow, border etc.
- [ ] Make more DOM-specific API.
- [ ] Develop relative target support to include pinned width/height animations.
- [ ] Add support for `rotate`.
- [ ] Add support for custom origins.
- [ ] Add support for additional transforms.
---
Thanks to [Alex Nault](https://alexnault.dev) for donating the `projection` package name!
================================================
FILE: packages/projection/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: packages/projection/cypress/integration/index.spec.js
================================================
///
/* eslint-disable */
describe("Layout projection", () => {
it("Correctly projects DOM elements", () => {
cy.visit("cypress/tests/index.html")
.wait(50)
.get("#test")
.should(($test) => {
const element = $test[0]
const {
left,
top,
bottom,
right,
} = element.getBoundingClientRect()
expect(left).to.equal(200)
expect(top).to.equal(200)
expect(right).to.equal(400)
expect(bottom).to.equal(500)
})
})
it("Correctly projects absolute children", () => {
cy.visit("cypress/tests/index.html")
.wait(50)
.get("#absolute-child")
.should(($test) => {
const element = $test[0]
const {
left,
top,
bottom,
right,
} = element.getBoundingClientRect()
expect(left).to.equal(250)
expect(top).to.equal(300)
expect(right).to.equal(800)
expect(bottom).to.equal(330)
})
})
it("Correctly projects relative children", () => {
cy.visit("cypress/tests/index.html")
.wait(50)
.get("#relative-child")
.should(($test) => {
const element = $test[0]
const {
left,
top,
bottom,
right,
} = element.getBoundingClientRect()
expect(left).to.equal(210)
expect(top).to.equal(210)
expect(right).to.equal(310)
expect(bottom).to.equal(310)
})
})
})
================================================
FILE: packages/projection/cypress/plugins/index.js
================================================
///
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
================================================
FILE: packages/projection/cypress/support/commands.js
================================================
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
================================================
FILE: packages/projection/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: packages/projection/cypress/tests/index.html
================================================
================================================
FILE: packages/projection/cypress.json
================================================
{}
================================================
FILE: packages/projection/package.json
================================================
{
"name": "projection",
"version": "2.0.0-alpha.4",
"description": "The animator's toolbox",
"author": "Matt Perry",
"homepage": "https://popmotion.io",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"module": "dist/es/index.mjs",
"jsnext:main": "dist/es/index.mjs",
"unpkg": "./dist/projection.min.js",
"exports": {
".": {
"types": "./lib/index.d.ts",
"import": "./dist/es/index.mjs",
"require": "./dist/projection.cjs.js",
"default": "./dist/projection.cjs.js"
},
"./package.json": "./package.json"
},
"sideEffects": false,
"repository": {
"type": "git",
"url": "https://github.com/Popmotion/popmotion/tree/master/packages/projection"
},
"bugs": {
"url": "https://github.com/Popmotion/popmotion/issues"
},
"keywords": [
"animation",
"ux",
"ui",
"flip",
"layout animation"
],
"analyze": true,
"license": "MIT",
"scripts": {
"build": "rollup -c && tsc --emitDeclarationOnly && yarn measure",
"postbuild": "yarn babel $npm_package_module --out-file $npm_package_module --no-babelrc --plugins annotate-pure-calls",
"dev": "rollup -c -w",
"prepublishOnly": "yarn test && yarn build",
"measure": "gzip -c $npm_package_unpkg | wc -c",
"test": "jest --coverage --maxWorkers=2 && cypress run",
"publish-beta": "npm publish --tag beta"
},
"dependencies": {
"framesync": "5.0.0",
"hey-listen": "^1.0.8",
"popmotion": "9.0.1",
"style-value-types": "^3.2.0",
"tslib": "^2.1.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^11.0.1",
"cypress": "^6.1.0",
"tslint-circular-dependencies": "^0.1.0"
},
"prettier": {
"printWidth": 80,
"tabWidth": 4,
"semi": false,
"trailingComma": "es5"
},
"jest": {
"moduleFileExtensions": [
"ts",
"js"
],
"moduleNameMapper": {
"style-value-types": "
/../../style-value-types/src",
"framesync": "/../../framesync/src"
},
"transform": {
"\\.(ts)$": "../../../node_modules/ts-jest/preprocessor.js"
},
"testRegex": "/__tests__/.*\\.test.(ts|js)$",
"rootDir": "src",
"collectCoverageFrom": [
"**/*.{js,jsx,ts,tsx}",
"!**/node_modules/**",
"!**/__tests__/**",
"!**/worklet/**"
],
"coverageDirectory": "/../coverage"
}
}
================================================
FILE: packages/projection/rollup.config.js
================================================
import generateConfig from "../../rollup-generate-config"
import pkg from "./package.json"
export default [...generateConfig(pkg)]
================================================
FILE: packages/projection/src/__tests__/index.test.ts
================================================
import { layoutNode } from ".."
import { Projection } from "../geometry/types"
describe("layoutNode", () => {
test("Correctly assigns root node", () => {
const parent = layoutNode()
const child = layoutNode({}, parent)
const grandChild = layoutNode({}, child)
expect(grandChild.scheduleUpdateProjection).toBe(
parent.scheduleUpdateProjection
)
})
test("Correctly projects tree", async () => {
const promise = new Promise((resolve) => {
const parent = layoutNode()
const grandChild = layoutNode(
{
onProjectionUpdate: () => {
resolve([parent.projection, grandChild.projection])
},
},
parent
)
parent.setLayout({
left: 100,
right: 200,
top: 100,
bottom: 300,
})
grandChild.setLayout({
left: 110,
right: 210,
top: 110,
bottom: 210,
})
parent.setTarget({
left: 300,
right: 350,
top: 300,
bottom: 700,
})
grandChild.setTarget({
left: 300,
right: 400,
top: 300,
bottom: 400,
})
})
return expect(promise).resolves.toEqual([
{
x: {
origin: 0.5,
originPoint: 150,
scale: 0.5,
translate: 175,
},
y: {
origin: 0.5,
originPoint: 200,
scale: 2,
translate: 300,
},
},
{
x: {
origin: 0.5,
originPoint: 330,
scale: 2,
translate: 20,
},
y: {
origin: 0.5,
originPoint: 420,
scale: 0.5,
translate: -70,
},
},
])
})
test("Correctly projects tree with relative nodes", async () => {
const promise = new Promise((resolve) => {
const parent = layoutNode()
const childA = layoutNode(
{
onProjectionUpdate: () => {
resolve([parent.projection, childA.projection])
},
},
parent
)
parent.setLayout({
left: 100,
right: 200,
top: 100,
bottom: 300,
})
childA.setLayout({
left: 110,
right: 210,
top: 110,
bottom: 210,
})
parent.setTarget({
left: 300,
right: 350,
top: 300,
bottom: 700,
})
childA.setRelativeTarget({
top: 10,
left: 10,
})
})
return expect(promise).resolves.toEqual([
{
x: {
origin: 0.5,
originPoint: 150,
scale: 0.5,
translate: 175,
},
y: {
origin: 0.5,
originPoint: 200,
scale: 2,
translate: 300,
},
},
{
x: {
origin: 0.5,
originPoint: 330,
scale: 2,
translate: 30,
},
y: {
origin: 0.5,
originPoint: 420,
scale: 0.5,
translate: -60,
},
},
])
})
test("Only fires onProjectionUpdate when projection has updated", () => {
const target = {
left: 300,
right: 400,
top: 300,
bottom: 400,
}
const promise = new Promise((resolve) => {
let updateCount = 0
const parent = layoutNode()
const grandChild = layoutNode(
{
onProjectionUpdate: () => {
updateCount++
},
},
parent
)
parent.setLayout({
left: 100,
right: 200,
top: 100,
bottom: 300,
})
grandChild.setLayout({
left: 110,
right: 210,
top: 110,
bottom: 210,
})
parent.setTarget({
left: 300,
right: 350,
top: 300,
bottom: 700,
})
grandChild.setTarget(target)
requestAnimationFrame(() => {
grandChild.setTarget(target)
requestAnimationFrame(() => {
grandChild.setTarget(target)
requestAnimationFrame(() => {
resolve(updateCount)
})
})
})
})
return expect(promise).resolves.toBe(1)
})
})
================================================
FILE: packages/projection/src/dom/correct-border-radius.ts
================================================
import { Axis } from "../geometry/types"
import { LayoutNode } from "../types"
export function pixelsToPercent(pixels: number, axis: Axis): number {
return (pixels / (axis.max - axis.min)) * 100
}
/**
* We always correct borderRadius as a percentage rather than pixels to reduce paints.
* For example, if you are projecting a box that is 100px wide with a 10px borderRadius
* into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
* borderRadius in both states. If we animate between the two in pixels that will trigger
* a paint each time. If we animate between the two in percentage we'll avoid a paint.
*
* Currently accepts a single value for all border radius rather than space-delimited.
*/
export function correctBorderRadius(latest: string | number, node: LayoutNode) {
/**
* If latest is a string, if it's a percentage we can return immediately as it's
* going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
*/
if (typeof latest === "string") {
if (latest.endsWith("px")) {
latest = parseFloat(latest)
} else {
return latest
}
}
/**
* If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
* pixel value as a percentage of each axis
*/
const { x, y } = node.getTarget()
return `${pixelsToPercent(latest, x)}% ${pixelsToPercent(latest, y)}%`
}
================================================
FILE: packages/projection/src/dom/correct-box-shadow.ts
================================================
import { LayoutNode } from "../types"
import { complex } from "style-value-types"
import { mix } from "popmotion"
/**
* TODO: This is doubled up with Framer Motion
*/
const cssVariableRegex = /var\((--[a-zA-Z0-9-_]+),? ?([a-zA-Z0-9 ()%#.,-]+)?\)/
const varToken = "_$css"
/**
* Note: Currently only supports single shadows
*/
export function correctBoxShadow(latest: string, node: LayoutNode) {
const original = latest
/**
* We need to first strip and store CSS variables from the string.
*/
const containsCSSVariables = latest.includes("var(")
const cssVariables: string[] = []
if (containsCSSVariables) {
latest = latest.replace(cssVariableRegex, (match) => {
cssVariables.push(match)
return varToken
})
}
const shadow = complex.parse(latest)
// TODO: Doesn't support multiple shadows
if (shadow.length > 5) return original
const template = complex.createTransformer(latest)
const offset = typeof shadow[0] !== "number" ? 1 : 0
/**
* TODO: Maybe replace with getters
*/
const projection = node.projection
const treeScale = node.treeScale
// Calculate the overall context scale
const xScale = projection.x.scale * treeScale.x
const yScale = projection.y.scale * treeScale.y
// Scale x/y
;(shadow[0 + offset] as number) /= xScale
;(shadow[1 + offset] as number) /= yScale
/**
* Ideally we'd correct x and y scales individually, but because blur and
* spread apply to both we have to take a scale average and apply that instead.
* We could potentially improve the outcome of this by incorporating the ratio between
* the two scales.
*/
const averageScale = mix(xScale, yScale, 0.5)
// Blur
if (typeof shadow[2 + offset] === "number")
(shadow[2 + offset] as number) /= averageScale
// Spread
if (typeof shadow[3 + offset] === "number")
(shadow[3 + offset] as number) /= averageScale
let output = template(shadow)
if (containsCSSVariables) {
let i = 0
output = output.replace(varToken, () => {
const cssVariable = cssVariables[i]
i++
return cssVariable
})
}
return output
}
================================================
FILE: packages/projection/src/dom/style.ts
================================================
import { LayoutNode } from "../types"
export function updateProjectionStyle(
element: HTMLElement,
{ projection, treeScale }: LayoutNode
) {
const { x, y } = projection
const xTranslate = x.translate / treeScale.x
const yTranslate = y.translate / treeScale.y
element.style.transformOrigin = `${x.origin * 100}% ${y.origin * 100}% 0`
element.style.transform = `translate3d(${xTranslate}px, ${yTranslate}px, 0) scale(${x.scale}, ${y.scale})`
}
================================================
FILE: packages/projection/src/geometry/__tests__/apply.test.ts
================================================
import {
applyAxisProjection,
applyPointProjection,
resetAxis,
resetBox,
scalePoint,
} from "../apply"
describe("resetAxis", () => {
test("resets axis a using the values in axis b", () => {
const a = { min: 0, max: 0 }
const b = { min: 1, max: 2 }
resetAxis(a, b)
expect(a).toEqual(b)
})
})
describe("resetBox", () => {
test("reset box a using the values in axis b", () => {
const a = {
x: { min: 0, max: 0 },
y: { min: 0, max: 0 },
}
const b = {
x: { min: 1, max: 2 },
y: { min: 3, max: 4 },
}
resetBox(a, b)
expect(a).toEqual(b)
})
})
describe("scalePoint", () => {
test("correctly scales a point based on a factor and an originPoint", () => {
expect(scalePoint(100, 2, 50)).toBe(150)
expect(scalePoint(100, 0.5, 50)).toBe(75)
expect(scalePoint(100, 2, 150)).toBe(50)
expect(scalePoint(100, 0.5, 150)).toBe(125)
})
})
describe("applyPointProjection", () => {
test("correctly applies a delta to a point", () => {
expect(applyPointProjection(100, 100, 2, 50)).toBe(250)
expect(applyPointProjection(100, 100, 2, 150)).toBe(150)
})
test("correctly applies a delta to a point with an additional boxScale", () => {
expect(applyPointProjection(100, 100, 2, 50, 2)).toBe(350)
expect(applyPointProjection(100, 100, 2, 150, 2)).toBe(50)
})
})
describe("applyAxisProjection", () => {
test("correctly applies a delta to an axis", () => {
const axis = { min: 100, max: 200 }
applyAxisProjection(axis, 100, 2, 150)
expect(axis).toEqual({ min: 150, max: 350 })
})
test("correctly applies a delta to an axis with an additional boxScale", () => {
const axis = { min: 100, max: 200 }
applyAxisProjection(axis, 100, 2, 150, 2)
expect(axis).toEqual({ min: 50, max: 450 })
})
})
// describe("applyAxisTransforms", () => {
// test("correctly applies transforms to an axis", () => {
// const target = { min: 0, max: 0 }
// const axis = { min: 100, max: 200 }
// applyAxisTransforms(
// target,
// axis,
// {
// x: 100,
// scaleX: 2,
// originX: -0.5,
// },
// ["x", "scaleX", "originX"]
// )
// expect(target).toEqual({ min: 250, max: 450 })
// })
// test("correctly applies transforms with missing scale", () => {
// const target = { min: 0, max: 0 }
// const axis = { min: 100, max: 200 }
// applyAxisTransforms(target, axis, { x: 100, originX: -0.5 }, [
// "x",
// "scaleX",
// "originX",
// ])
// expect(target).toEqual({ min: 200, max: 300 })
// })
// })
// describe("applyBoxTransform", () => {
// test("correctly applies a transform to a box", () => {
// const target = {
// x: { min: 0, max: 0 },
// y: { min: 0, max: 0 },
// }
// const box = {
// x: { min: 100, max: 200 },
// y: { min: 300, max: 400 },
// }
// applyBoxTransforms(target, box, {
// x: 100,
// y: 200,
// scaleX: 2,
// scaleY: 0.5,
// scale: 2,
// })
// expect(target).toEqual({
// x: { min: 50, max: 450 },
// y: { min: 500, max: 600 },
// })
// })
// })
// describe("removePointDelta", () => {
// test("correctly removes a delta from a point", () => {
// expect(removePointDelta(250, 100, 2, 50)).toBe(100)
// })
// test("correctly removes a delta from a point with an additional boxScale", () => {
// expect(removePointDelta(350, 100, 2, 50, 2)).toBe(100)
// })
// })
// describe("removeAxisDelta", () => {
// test("correctly removes a delta from an axis", () => {
// const axis = { min: 150, max: 350 }
// removeAxisDelta(axis, 100, 2, 0.5)
// expect(axis).toEqual({ min: 100, max: 200 })
// })
// test("correctly removes a delta from an axis with an additional boxScale", () => {
// const axis = { min: 50, max: 450 }
// removeAxisDelta(axis, 100, 2, 0.5, 2)
// expect(axis).toEqual({ min: 100, max: 200 })
// })
// })
// describe("removeAxisTransforms", () => {
// test("correctly removes transforms from an axis", () => {
// const axis = { min: 250, max: 450 }
// removeAxisTransforms(
// axis,
// {
// x: 100,
// scaleX: 2,
// originX: -0.5,
// },
// ["x", "scaleX", "originX"]
// )
// expect(axis).toEqual({ min: 100, max: 200 })
// })
// })
// describe("removeBoxTransforms", () => {
// const box = {
// x: { min: 50, max: 450 },
// y: { min: 500, max: 600 },
// }
// removeBoxTransforms(box, {
// x: 100,
// y: 200,
// scaleX: 2,
// scaleY: 0.5,
// scale: 2,
// })
// expect(box).toEqual({
// x: { min: 100, max: 200 },
// y: { min: 300, max: 400 },
// })
// })
================================================
FILE: packages/projection/src/geometry/__tests__/calc.test.ts
================================================
import { box } from ".."
import {
isNear,
calcOrigin,
updateAxisProjection,
calcRelativeAxis,
} from "../calc"
describe("isNear", () => {
test("Correctly indicate when the provided value is within maxDistance of the provided target", () => {
expect(isNear(10.1, 10, 0.1)).toBe(true)
expect(isNear(9.9, 10, 0.1)).toBe(true)
expect(isNear(10.2, 10, 0.1)).toBe(false)
expect(isNear(9.8, 10, 0.1)).toBe(false)
})
})
describe("calcOrigin", () => {
test("Correctly calculates an origin ", () => {
expect(calcOrigin({ min: 0, max: 100 }, { min: 0, max: 100 })).toBe(0.5)
expect(calcOrigin({ min: -100, max: 100 }, { min: -50, max: 50 })).toBe(
0.5
)
expect(calcOrigin({ min: -50, max: 50 }, { min: -100, max: 100 })).toBe(
0.5
)
expect(calcOrigin({ min: 200, max: 200 }, { min: 0, max: 100 })).toBe(1)
expect(calcOrigin({ min: 200, max: 200 }, { min: 300, max: 500 })).toBe(
0
)
})
})
describe("updateAxisProjection", () => {
test("Correctly updates the axis delta with a delta that will, when applied, project source onto delta", () => {
const axisDelta = { scale: 1, translate: 0, origin: 0, originPoint: 0 }
updateAxisProjection(
axisDelta,
{ min: 100, max: 200 },
{ min: 300, max: 500 }
)
expect(axisDelta).toEqual({
origin: 0,
originPoint: 100,
scale: 2,
translate: 200,
})
})
test("Correctly updates the axis delta with a delta that will, when applied, project source onto delta with a defined origin", () => {
const axisDelta = { scale: 1, translate: 0, origin: 0, originPoint: 0 }
updateAxisProjection(
axisDelta,
{ min: 100, max: 200 },
{ min: 300, max: 500 },
1
)
expect(axisDelta).toEqual({
origin: 1,
originPoint: 200,
scale: 2,
translate: 300,
})
})
})
describe("calcRelativeAxis", () => {
test("Correctly calculates an axis relative to the parent target", () => {
const target = box()
calcRelativeAxis(
target.x,
{ min: 100, max: 200 },
{ min: 10 },
{ min: 600, max: 800 }
)
expect(target.x).toEqual({ min: 110, max: 310 })
})
})
================================================
FILE: packages/projection/src/geometry/apply.ts
================================================
import { LayoutNode } from "../types"
import { Axis, Box, Point, Projection } from "./types"
export function resetAxis(axis: Axis, originAxis: Axis) {
axis.min = originAxis.min
axis.max = originAxis.max
}
export function resetBox(box: Box, originBox: Box) {
resetAxis(box.x, originBox.x)
resetAxis(box.y, originBox.y)
}
/**
* Scales a point based on a factor and an originPoint
*/
export function scalePoint(point: number, scale: number, originPoint: number) {
const distanceFromOrigin = point - originPoint
const scaled = scale * distanceFromOrigin
return originPoint + scaled
}
/**
* Applies a translate/scale delta to a point
*/
export function applyPointProjection(
point: number,
translate: number,
scale: number,
originPoint: number,
boxScale?: number
): number {
if (boxScale !== undefined) {
point = scalePoint(point, boxScale, originPoint)
}
return scalePoint(point, scale, originPoint) + translate
}
/**
* Applies a translate/scale delta to an axis
*/
export function applyAxisProjection(
axis: Axis,
translate: number = 0,
scale: number = 1,
originPoint: number,
boxScale?: number
): void {
axis.min = applyPointProjection(
axis.min,
translate,
scale,
originPoint,
boxScale
)
axis.max = applyPointProjection(
axis.max,
translate,
scale,
originPoint,
boxScale
)
}
/**
* Applies a translate/scale delta to a box
*/
export function applyBoxProjection(box: Box, { x, y }: Projection): void {
applyAxisProjection(box.x, x.translate, x.scale, x.originPoint)
applyAxisProjection(box.y, y.translate, y.scale, y.originPoint)
}
/**
* Apply a tree of deltas to a box. We do this to calculate the effect of all the transforms
* in a tree upon our box before then calculating how to project it into our desired viewport-relative box
*/
export function applyTreeProjection(
box: Box,
treeScale: Point,
path: LayoutNode[]
) {
const pathLength = path.length
if (!pathLength) return
// Reset the treeScale
treeScale.x = treeScale.y = 1
/**
* TODO: We already traverse the tree from top-down. Look into whether it's possible to do this
* work culmulatively.
*/
for (let i = 0; i < pathLength; i++) {
const { projection } = path[i]
// Incoporate each ancestor's scale into a culmulative treeScale for this component
treeScale.x *= projection.x.scale
treeScale.y *= projection.y.scale
// Apply each ancestor's calculated projection into this component's recorded layout box
applyBoxProjection(box, projection)
}
}
================================================
FILE: packages/projection/src/geometry/calc.ts
================================================
import { Axis, AxisProjection, Box, Projection, RelativeBox } from "./types"
import { clamp, mix, progress } from "popmotion"
/**
* Returns true if the provided value is within maxDistance of the provided target
*/
export function isNear(value: number, target = 0, maxDistance = 0.01): boolean {
return Math.abs(value - target) < maxDistance
}
function calcLength(axis: Axis) {
return axis.max - axis.min
}
/**
* Calculate a transform origin relative to the source axis, between 0-1, that results
* in an asthetically pleasing scale/transform needed to project from source to target.
*/
export function calcOrigin(source: Axis, target: Axis): number {
let origin = 0.5
const sourceLength = calcLength(source)
const targetLength = calcLength(target)
if (targetLength > sourceLength) {
origin = progress(target.min, target.max - sourceLength, source.min)
} else if (sourceLength > targetLength) {
origin = progress(source.min, source.max - targetLength, target.min)
}
return clamp(0, 1, origin)
}
/**
* Update the AxisDelta with a transform that projects source into target.
*
* The transform `origin` is optional. If not provided, it'll be automatically
* calculated based on the relative positions of the two bounding boxes.
*/
export function updateAxisProjection(
projection: AxisProjection,
source: Axis,
target: Axis,
origin?: number
) {
projection.origin =
origin === undefined ? calcOrigin(source, target) : origin
projection.originPoint = mix(source.min, source.max, projection.origin)
projection.scale = calcLength(target) / calcLength(source)
if (isNear(projection.scale, 1, 0.0001)) projection.scale = 1
projection.translate =
mix(target.min, target.max, projection.origin) - projection.originPoint
if (isNear(projection.translate)) projection.translate = 0
}
/**
* Update the projection with a transform that projects the source into the target.
*
* The transform `origin` is optional. If not provided, it'll be automatically
* calculated based on the relative positions of the two bounding boxes.
*/
export function updateBoxProjection(
projection: Projection,
source: Box,
target: Box,
origin?: number
): void {
updateAxisProjection(projection.x, source.x, target.x, origin)
updateAxisProjection(projection.y, source.y, target.y, origin)
}
export function calcRelativeAxis(
target: Axis,
parent: Axis,
relative: Partial,
layout: Axis
) {
target.min = parent.min + relative.min
target.max = target.min + (layout.max - layout.min)
}
export function calcRelativeBox(
target: Box,
parent: Box,
relative: RelativeBox,
layout: Box
) {
calcRelativeAxis(target.x, parent.x, relative.x, layout.x)
calcRelativeAxis(target.y, parent.y, relative.y, layout.y)
}
================================================
FILE: packages/projection/src/geometry/index.ts
================================================
import { BoundingBox, Box, Projection } from "./types"
const identityProjection = () => ({
translate: 0,
scale: 1,
origin: 0,
originPoint: 0,
})
export const projection = (): Projection => ({
x: identityProjection(),
y: identityProjection(),
})
export const box = (): Box => ({
x: { min: 0, max: 0 },
y: { min: 0, max: 0 },
})
export const convertBoundingBox = ({
top,
left,
right,
bottom,
}: BoundingBox): Box => ({
x: { min: left, max: right },
y: { min: top, max: bottom },
})
================================================
FILE: packages/projection/src/geometry/types.ts
================================================
export interface Axis {
min: number
max: number
length?: number
}
export interface Box {
x: Axis
y: Axis
}
export interface RelativeBox {
x: Partial
y: Partial
}
export interface BoundingBox {
top: number
right: number
bottom: number
left: number
}
export interface Point {
x: number
y: number
}
export interface AxisProjection {
translate: number
scale: number
origin: number
originPoint: number
}
export interface Projection {
x: AxisProjection
y: AxisProjection
}
================================================
FILE: packages/projection/src/index.ts
================================================
export { LayoutNode } from "./types"
export { layoutNode } from "./node"
export { updateProjectionStyle } from "./dom/style"
export { correctBorderRadius } from "./dom/correct-border-radius"
export { correctBoxShadow } from "./dom/correct-box-shadow"
================================================
FILE: packages/projection/src/node.ts
================================================
import { LayoutNode, NodeOptions } from "./types"
import sync, { cancelSync } from "framesync"
import { applyTreeProjection, resetBox } from "./geometry/apply"
import { calcRelativeBox, updateBoxProjection } from "./geometry/calc"
import { BoundingBox, Box, RelativeBox } from "./geometry/types"
import { box, convertBoundingBox, projection } from "./geometry"
export function layoutNode(
{ onLayoutMeasure, onProjectionUpdate }: NodeOptions = {},
parent?: LayoutNode
): LayoutNode {
/**
*
*/
let layout: Box
/**
*
*/
let layoutCorrected: Box
/**
*
*/
let target: Box
/**
*
*/
let relativeTarget: RelativeBox
/**
*
*/
let projectionString = ""
/**
*
*/
let resolveRelativeToParent = false
/**
*
*/
let relativeParent = parent
/**
*
*/
function setBoundingBoxToTarget(
target: Box | RelativeBox,
{ left, right, top, bottom }: Partial
) {
target.x.min = left
target.x.max = right
target.y.min = top
target.y.max = bottom
node.scheduleUpdateProjection()
}
function updateTreeProjection() {
node.treeNodes.forEach(fireUpdateProjection)
}
const node: LayoutNode = {
parent,
treeNodes: parent ? parent.treeNodes : new Set(),
path: parent ? [...parent.path, parent] : [],
depth: parent ? parent.depth + 1 : 0,
projection: projection(),
treeScale: { x: 1, y: 1 },
/**
*
*/
scheduleUpdateProjection: parent
? parent.scheduleUpdateProjection
: () => sync.preRender(updateTreeProjection, false, true),
/**
*
*/
updateProjection() {
resetBox(layoutCorrected, layout)
const prevProjectionString = projectionString
const { x: prevTreeScaleX, y: prevTreeScaleY } = node.treeScale
/**
* Apply all the parent deltas to this box to produce the corrected box. This
* is the layout box, as it will appear on screen as a result of the transforms of its parents.
*/
applyTreeProjection(layoutCorrected, node.treeScale, node.path)
/**
*
*/
if (resolveRelativeToParent) {
calcRelativeBox(
target,
relativeParent.getTarget(),
relativeTarget,
layout
)
}
/**
* Update the delta between the corrected box and the target box before user-set transforms were applied.
* This will allow us to calculate the corrected borderRadius and boxShadow to compensate
* for our layout reprojection, but still allow them to be scaled correctly by the user.
* It might be that to simplify this we may want to accept that user-set scale is also corrected
* and we wouldn't have to keep and calc both deltas, OR we could support a user setting
* to allow people to choose whether these styles are corrected based on just the
* layout reprojection or the final bounding box.
*/
updateBoxProjection(node.projection, layoutCorrected, target, 0.5)
projectionString = JSON.stringify(node.projection)
if (
prevProjectionString !== projectionString ||
// Also compare calculated treeScale, for values that rely on only this for scale correction.
prevTreeScaleX !== node.treeScale.x ||
prevTreeScaleY !== node.treeScale.y
) {
onProjectionUpdate?.()
}
},
/**
*
*/
setLayout(newLayout) {
layout = convertBoundingBox(newLayout)
if (!target) {
layoutCorrected = box()
target = box()
relativeTarget = box()
}
// TODO: Might need to rebase target box here if not animating
},
invalidateLayout() {},
/**
* This will only provide accurate measurements if projection transform
* and all parent transforms have been temporarily disabled.
*/
measureLayout() {
node.setLayout(element.getBoundingClientRect())
onLayoutMeasure?.(layout)
},
/**
*
*/
setTarget(newTarget) {
resolveRelativeToParent = false
setBoundingBoxToTarget(target, newTarget)
},
/**
*
*/
getTarget() {
return target
},
/**
*
*/
setRelativeTarget(newTarget) {
resolveRelativeToParent = true
setBoundingBoxToTarget(relativeTarget, newTarget)
},
setRelativeParent(newRelativeParent: LayoutNode) {
// TODO: Check that this is in the path
relativeParent = newRelativeParent
},
/**
*
*/
destroy() {
node.treeNodes.delete(node)
cancelSync.preRender(node.updateProjection)
},
}
node.treeNodes.add(node)
return node
}
function fireUpdateProjection(node: LayoutNode) {
node.updateProjection()
}
================================================
FILE: packages/projection/src/types.ts
================================================
import { BoundingBox, Box, Point, Projection } from "./geometry/types"
export interface LayoutNode {
parent?: LayoutNode
path: LayoutNode[]
treeNodes: Set
depth: number
projection: Projection
treeScale: Point
scheduleUpdateProjection(): void
updateProjection(): void
setLayout(box: BoundingBox): void
setTarget(box: BoundingBox): void
getTarget(): Box
setRelativeTarget(box: Partial): void
setRelativeParent(parent: LayoutNode): void
destroy(): void
}
export interface NodeOptions {
onLayoutMeasure?: (layout: Box) => void
onProjectionUpdate?: () => void
}
================================================
FILE: packages/projection/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib"
},
"include": ["src/**/*"]
}
================================================
FILE: packages/site/.babelrc
================================================
// {
// "presets": [
// "next/babel",
// "react",
// "stage-0"
// ],
// "plugins": [
// "transform-react-remove-prop-types",
// "transform-export-extensions",
// "transform-react-jsx",
// "transform-react-constant-elements",
// "transform-strict-mode",
// ["babel-plugin-root-import"],
// ["transform-runtime", {
// "helpers": false,
// "polyfill": false
// }],
// ],
// "env": {
// "development": {
// "sourceMaps": "inline"
// }
// }
// }
{
"presets": ["next/babel"],
"plugins": [
["babel-plugin-root-import"],
["styled-components", {
"ssr": true
}]
]
}
================================================
FILE: packages/site/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: packages/site/components/examples/Ball.js
================================================
import styled from 'styled-components';
import { ActionButton } from '~/templates/global/styled';
import { verticalGradient, PINK, PINK_BURN, cols } from '~/styles/vars';
const Container = styled.div`
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
align-content: space-around;
padding: ${cols(1)};
`;
const Box = styled.span`
width: 50px;
height: 50px;
background: ${verticalGradient(PINK, PINK_BURN)};
border-radius: 50px;
`;
export default ({ autostart, start, id }) => (
{!autostart ? Start : null}
);
================================================
FILE: packages/site/components/examples/CodePen.js
================================================
import { Centered } from '~/templates/global/grid';
import { cols, LIGHT_GREY } from '~/styles/vars';
import styled from 'styled-components';
const FrameContainer = styled(Centered.withComponent('div'))`
margin-top: ${cols(2)};
margin-bottom: ${cols(2)};
border: 2px solid ${LIGHT_GREY};
`;
export default ({ id, height = 447 }) => (
);
================================================
FILE: packages/site/components/examples/CodeSandbox.js
================================================
import React from 'react';
import styled from 'styled-components';
import { MajorCentered } from '~/templates/global/grid';
import { cols } from '~/styles/vars';
import { IntersectionElement } from 'react-intersection';
const FrameContainer = styled(MajorCentered.withComponent('div'))`
margin-top: ${cols(2)};
margin-bottom: ${cols(2)};
`;
export default class CodeSandbox extends React.Component {
state = { isVisible: false };
setVisibility = ({ isIntersecting }) => {
isIntersecting &&
!this.state.isVisible &&
this.setState({ isVisible: true });
};
render() {
const { id, height = 500, vue = false, ...props } = this.props;
const suffix = vue ? '?module=%2Fsrc%2FApp.vue' : '';
return (
{this.state.isVisible && (
)}
);
}
}
================================================
FILE: packages/site/components/examples/Counter.js
================================================
import styled from 'styled-components';
import { ActionButton } from '~/templates/global/styled';
import { fontBold } from '~/styles/fonts';
const CounterContainer = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
align-content: space-around;
`;
const Counter = styled.span`
font-size: 42px;
text-align: center;
${fontBold}
`;
export default ({ code, autostart, start, id }) => (
0
{!autostart ? Start : null}
);
================================================
FILE: packages/site/components/examples/Example.js
================================================
import React from 'react';
export default () => {
return Live demos of legacy libraries have been removed.
;
// const Component = templates[template];
// return (
//
//
//
//
// Live editor
//
//
//
//
// );
};
================================================
FILE: packages/site/components/examples/Swatch.js
================================================
import styled from 'styled-components';
import { ActionButton } from '~/templates/global/styled';
const Container = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
align-content: space-around;
`;
const Swatch = styled.div`
flex: 0 0 30%;
`;
export default ({ start, id }) => (
Start
);
================================================
FILE: packages/site/components/examples/templates.js
================================================
import Counter from './Counter';
import Ball from './Ball';
import Swatch from './Swatch';
export default {
Counter,
Ball,
Swatch
};
================================================
FILE: packages/site/components/icons/BrandGradientDef.js
================================================
import { BRAND, BRAND_BURN } from '~/styles/vars';
export default ({ id }) => (
);
================================================
FILE: packages/site/components/icons/DropDownArrow.js
================================================
import { ENTITY } from '~/styles/vars';
export default ({ color = ENTITY, className }) => (
);
================================================
FILE: packages/site/components/icons/FramesyncLogo.js
================================================
export default ({ width = 195, height = 46 }) => (
);
================================================
FILE: packages/site/components/icons/GitHub.js
================================================
import { ACTION, ACTION_BURN } from '~/styles/vars';
export default ({ className }) => (
);
================================================
FILE: packages/site/components/icons/Logo.js
================================================
import BrandGradient from './BrandGradientDef';
export default ({
className,
id='logo-gradient',
pathId,
width=165,
height=41,
...pathAttrs
}) => {
return (
);
};
================================================
FILE: packages/site/components/icons/PopcornLogo.js
================================================
export default ({ width, height }) => (
);
================================================
FILE: packages/site/components/icons/PopmotionIcon.js
================================================
import BrandGradient from "./BrandGradientDef";
export default ({ className, width = 41, height = 37 }) => (
);
================================================
FILE: packages/site/components/icons/PopmotionPure.js
================================================
import BrandGradient from './BrandGradientDef';
export default ({ width = 510, height = 238 }) => (
);
================================================
FILE: packages/site/components/icons/PoseLogo.js
================================================
export default ({ className, width=613, height=192 }) => (
);
================================================
FILE: packages/site/components/icons/StylefireLogo.js
================================================
export default ({ width, height }) => (
);
================================================
FILE: packages/site/components/icons/Twitter.js
================================================
import { ACTION, ACTION_BURN } from '~/styles/vars';
export default ({ className }) => (
);
================================================
FILE: packages/site/components/layout/Content.js
================================================
import GlobalTemplate from '~/components/layout/GlobalTemplate';
export default ({ children, id, section, category, title, description }) => (
{children}
);
================================================
FILE: packages/site/components/layout/SiteLink.js
================================================
import { withTheme } from 'styled-components';
import Link from 'next/link';
const makeSiteUrl = (root, href) =>
root === '/' || root === '/pure' || href.substring(0, 4) === 'http'
? href
: `${root}${href}`;
const SiteLink = ({ href, children, theme, name, ...props }) => (
{children}
);
export default withTheme(SiteLink);
================================================
FILE: packages/site/components/layout/grid.js
================================================
import styled from 'styled-components';
import { verticalGradient, MAIN, MAIN_FADE, WHITE, cols, media } from '~/styles/vars';
export const Container = styled.div`
display: grid;
grid-template-columns: ${cols(1)} ${cols(16)} ${cols(56)} 1fr;
grid-template-rows: 75px auto;
grid-template-areas:
"left-margin header header header"
"left-margin content-nav content right-margin"
"left-margin content-nav footer right-margin";
grid-row-gap: 65px;
min-height: 100vh;
${media.large`
grid-template-columns: ${cols(1)} ${cols(12)} 1fr 0;
grid-row-gap: ${cols(3)};
`}
${media.medium`
grid-template-columns: 5px 1fr;
grid-template-areas:
"left-margin header"
"left-margin content-nav"
"left-margin content"
"left-margin footer";
grid-row-gap: ${cols(2)};
`}
`;
export const LeftMargin = styled.div`
background: ${verticalGradient(MAIN_FADE, MAIN)};
grid-area: left-margin;
`;
export const RightMargin = styled.div`
background: ${WHITE};
grid-area: right-margin;
`;
export const HeaderArea = styled.div`
grid-area: header;
`;
export const ContentArea = styled.article`
grid-area: content;
padding: 0 ${cols(3)} ${cols(3)} ${cols(1)};
${media.medium`
padding: 0 ${cols(1)} ${cols(1)};
`}
`;
export const ContentNavArea = styled.div`
grid-area: content-nav;
padding: 0 ${cols(2)} ${cols(3)};
${media.medium`
padding: 0 ${cols(1)};
margin: ${cols(1)} 0 ${cols(2)};
`}
`;
================================================
FILE: packages/site/components/template.js
================================================
import PopmotionLogo from '~/components/icons/Logo';
import Link from 'next/link';
import Head from 'next/head';
import reset from '~/styles/reset';
import { Fragment, useRef, useEffect, useState } from 'react';
import styled, { createGlobalStyle } from 'styled-components';
import { animate } from 'popmotion';
const Global = createGlobalStyle`
${reset}
`;
const tagline = 'The animator’s JavaScript toolbox.'.split('');
function TaglineCharacter({ character, index }) {
const ref = useRef(null);
useEffect(() => {
const controls = animate({
from: 0,
to: 0,
velocity: -500,
stiffness: 120,
elapsed: -index * 20,
onUpdate: (y) =>
(ref.current.style.transform = `translateY(${y}px) translateZ(0)`),
});
return () => controls.stop();
}, []);
return (
{character}
);
}
export default function ({ children, tableOfContents }) {
return (
Popmotion: The animator's JavaScript toolbox
{tagline.map((character, i) => (
))}
Supports keyframes, spring and{' '}
inertia animations on numbers,{' '}
colors, and complex strings.
Simple, composable functions, portable to any JS environment.
The library behind the library, it powers the animations in{' '}
Framer Motion.
Written in TypeScript and enjoys over 95% test coverage.
The animate function is less than 5kb, and every
utility function is individually importable.
{children}
);
}
function USP({ title, children }) {
return (
{title}
{children}
);
}
function Tick() {
return (
);
}
const generateSection = (list, filter) => (
{list.map((item) => generateLink(item, filter))}
);
const generateLink = ({ title, id, children }, filter) => {
if (!filter || title.toLowerCase().includes(filter.toLowerCase())) {
return (
{title}
{Boolean(children && children.length) &&
generateSection(children, filter)}
);
} else {
return (
Boolean(children && children.length) && generateSection(children, filter)
);
}
};
const TOC = ({ contents }) => {
const [filter, setFilter] = useState('');
return (
);
};
const Container = styled.div`
.usp-container {
margin-top: 80px;
margin-bottom: 100px;
}
.usp {
position: relative;
font-size: 18px;
padding-left: 30px;
margin-bottom: 25px;
line-height: 1.5;
.title {
display: block;
font-weight: bold;
font-size: 24px;
margin-bottom: 10px;
}
svg {
position: absolute;
left: 0px;
top: 6px;
}
}
.toc {
padding: 18px 20px;
border-radius: 10px;
margin-top: 40px;
overflow-y: auto;
font-size: 24px;
position: sticky;
top: 40px;
left: 40px;
background: var(--color-shade);
max-width: 300px;
max-height: 80vh;
input {
padding: 10px;
background: rgba(255, 255, 255, 0.6);
width: 100%;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
margin-bottom: 40px;
font-size: 18px;
}
li a {
display: inline-block;
margin-bottom: 10px;
}
ul {
margin-bottom: 10px;
}
nav > ul {
font-size: 24px;
> li > ul {
font-size: 22px;
margin-left: 10px;
> li > ul {
font-size: 18px;
margin-left: 10px;
> li > ul {
font-size: 14px;
margin-left: 10px;
}
}
}
}
}
header {
padding: 40px;
display: flex;
justify-content: space-between;
path {
fill: var(--color-popmotion) !important;
}
.button {
height: 39px;
overflow: visible;
background-color: var(--color-shade);
border-radius: 20px;
padding: 12px 20px;
color: var(--color-black);
}
}
footer {
margin: 100px 40px 0 40px;
border-top: 1px solid var(--color-shade);
padding: 40px;
nav {
max-width: 660px;
margin: auto;
display: flex;
justify-content: space-between;
}
li,
p {
margin-bottom: 10px;
font-size: 16px;
}
}
header {
margin-bottom: 100px;
}
.tagline {
font-size: 28px;
letter-spacing: -0.6px;
display: block;
}
.frame {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
border: 10px solid var(--color-popmotion);
pointer-events: none;
z-index: 2;
}
article {
display: grid;
grid-template-columns: 1fr [a] 250px [b] 60px [c] 650px [d] 1fr;
.toc {
grid-column: a / b;
}
.content-container {
grid-column: c / d;
}
}
.cg {
display: grid;
grid-template-columns: 40px [lo-start] 1fr [lo-end] 40px [c-start] 650px [c-end] 1fr [ro-end] 40px [end];
> * {
grid-column: c-start / c-end;
}
}
@media screen and (max-width: 960px) {
article {
display: block;
.content-container,
.toc {
width: 100%;
max-width: 650px;
margin-left: auto;
margin-right: auto;
}
.toc {
max-height: 200px;
box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2);
}
}
.cg {
display: block;
width: 100%;
max-width: 650px;
margin-left: auto;
margin-right: auto;
}
}
@media screen and (max-width: 740px) {
.frame {
border: 5px solid var(--color-popmotion);
}
header {
margin-bottom: 0;
}
.cg {
}
footer nav {
flex-direction: row;
justify-content: stretch;
flex-wrap: wrap;
ul {
margin-bottom: 20px;
}
}
}
@media screen and (max-width: 670px) {
article {
display: block;
.content-container,
.toc {
width: calc(100vw - 40px);
margin-left: 20px;
margin-right: 20px;
}
.toc {
top: 20px;
}
}
}
@media screen and (max-width: 600px) {
header {
svg {
width: 110px;
}
.button {
font-size: 14px;
}
}
.cg,
footer,
header {
padding: 20px !important;
}
.usp-container {
margin-top: 40px;
}
.usp {
font-size: 14px;
margin-bottom: 20px;
.title {
font-size: 18px;
margin-bottom: 10px;
}
}
.tagline {
font-size: 24px;
letter-spacing: -0.4px;
}
article {
margin-top: 80px;
}
}
@media screen and (max-width: 400px) {
.tagline {
font-size: 18px;
letter-spacing: -0.2px;
}
}
`;
================================================
FILE: packages/site/data/authors.json
================================================
{
"mattperry": {
"name": "Matt Perry",
"avatar": "/images/mattperry.jpg"
}
}
================================================
FILE: packages/site/data/category-names.json
================================================
{
"action": "Action",
"animation": "Animation",
"compositors": "Compositors",
"input": "Input",
"how-to": "How to...",
"stylefire": "Styler",
"basics": "Basics",
"advanced": "Advanced",
"plugins": "Plugins",
"projects": "Projects",
"reactions": "Reactions",
"dom": "DOM",
"react": "React",
"react-native": "React Native",
"stylers": "Stylers",
"vanilla": "Vanilla",
"vue": "Vue",
"functions": "Utilities"
}
================================================
FILE: packages/site/data/route-paths.json
================================================
{
"api": "/api",
"learn": "/learn/get-started",
"blog": "/blog",
"examples": "/examples"
}
================================================
FILE: packages/site/data/section-names.json
================================================
{
"api": "API",
"learn": "Learn",
"blog": "Blog",
"support": "Support us",
"examples": "Examples"
}
================================================
FILE: packages/site/data/settings.json
================================================
{
"siteName": "Popmotion",
"githubUrl": "https://github.com/popmotion/popmotion",
"stackOverflowUrl": "https://stackoverflow.com/questions/tagged/popmotion",
"twitterUrl": "https://twitter.com/popmotionjs",
"twitterUsername": "popmotionjs"
}
================================================
FILE: packages/site/data/site-names.json
================================================
{
"popcorn": "popcorn",
"popmotion": "pure",
"popmotion-pose": "pose",
"stylefire": "stylefire"
}
================================================
FILE: packages/site/docs/popcorn/index.md
================================================
# Popcorn
Popcorn is the Lodash of animation.
It contains utility functions to help animation and UI developers manipulate values over time and space.
## Features
- **Powerful interpolation:** [The powerful `interpolate`](/api/interpolate) function can map a range of numbers onto another range of numbers, colors and complex strings.
- **Linear RGB mixing:** Hex and RGB values are mixed as linear RGB, which avoids the usual brightness dips in many animation libraries. [Learn more](https://www.youtube.com/watch?v=LKnqECcg6Gw)
- **Tiny:** Import only what you need with ES6 imports.
## Install
### Package managers
Popcorn is currently available as `@popmotion/popcorn` on npm.
```bash
npm install @popmotion/popcorn
```
```bash
yarn add @popmotion/popcorn
```
### File download
Download or link to the [latest version of Popcorn](https://unpkg.com/@popmotion/popcorn/dist/popcorn.min.js). All functions will be available on the `popcorn` global variable (e.g. `popcorn.velocityPerSecond`).
================================================
FILE: packages/site/docs/stylefire/index.md
================================================
## Features
* **Render batching:** Prevents layout thrashing and unnecessary renders by batching all UI changes once per frame.
* **Individual transform props:** Set and animate `transform` props independently from one another.
* **Easy SVG transforms:** Replaces the complex SVG transform model with the user-friendly CSS equivalent.
* **Path drawing:** Set `path` length and offset as a percentage, simplifying the 'drawing' effect.
* **Scrolling:** `scrollTop` and `scrollLeft` support for the viewport and HTML elements.
* **Vendor prefixes:** Tests and detects rules for prefixed support.
* **Tiny:** At 3.5kb, it's the perfect fit for any animation library or website.
## Install
```bash
npm install stylefire
```
================================================
FILE: packages/site/package.json
================================================
{
"name": "popmotion-site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "npm run build-content && next dev",
"build": "npm run build-content && next build && next export",
"start": "next start",
"build-content": "node scripts/generate-content.js",
"s3-upload": "aws s3 cp public s3://popmotion.io --recursive",
"invalidate-cloudfront": "aws configure set preview.cloudfront true && aws cloudfront create-invalidation --distribution-id E31YV8NBB5M5N4 --paths /*",
"publish-site": "npm run build && npm run s3-upload && npm run invalidate-cloudfront"
},
"dependencies": {
"file-loader": "^1.1.11",
"front-matter": "^3.0.2",
"fs": "0.0.1-security",
"live-on-stage": "^2.0.19",
"marksy": "^8.0.0",
"next": "9.1.3",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"prismjs": "^1.6.0",
"raw-loader": "^0.5.1",
"react": "16.11.0",
"react-dom": "16.11.0",
"react-intersection": "^1.0.5",
"react-live": "^1.7.0",
"react-pose": "^4.0.10",
"react-pose-text": "^3.1.0",
"react-syntax-highlighter": "^5.8.0",
"styled-components": "^4.4.1",
"three": "^0.110.0"
},
"devDependencies": {
"babel-plugin-root-import": "^6.4.1",
"babel-plugin-styled-components": "^1.10.6",
"s3": "^4.4.0"
}
}
================================================
FILE: packages/site/pages/_document.js
================================================
import React from 'react';
import Document, { Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
import Analytics from '~/templates/global/Analytics';
export default class PageTemplate extends Document {
render() {
const stylesheet = new ServerStyleSheet();
const main = stylesheet.collectStyles();
const styleTags = stylesheet.getStyleElement();
return (
{styleTags}
{main}
);
}
}
================================================
FILE: packages/site/pages/_error.js
================================================
import GlobalTemplate from '~/templates/global/Template';
import ContentPage from '~/templates/global-new/ContentPage';
import { Section, PageHeader } from '~/templates/global-new/styled';
import Link from 'next/link';
export default () => (
);
================================================
FILE: packages/site/pages/api.js
================================================
import { Fragment } from 'react';
import GlobalTemplate from '~/templates/global/Template';
import ContentPage from '~/templates/global-new/ContentPage';
import { Section, PageHeader } from '~/templates/global-new/styled';
import MenuPage from '~/templates/content/MenuPage';
export default () => (
);
================================================
FILE: packages/site/pages/blog.js
================================================
import { Fragment } from 'react';
import GlobalTemplate from '~/templates/global/Template';
import ContentPage from '~/templates/global-new/ContentPage';
import { Section, PageHeader } from '~/templates/global-new/styled';
import BlogList from '~/templates/blog';
export default () => (
);
================================================
FILE: packages/site/pages/index.js
================================================
import marksy from 'marksy';
import Homepage from '~/components/template';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { createElement } from 'react';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
blockquote: Blockquote,
}
});
const Page = ({ section }) => (
{content.tree}
);
const content = convertMarkdown(`
## Quick start
\`\`\`bash
npm install popmotion
\`\`\`
\`\`\`javascript
import { animate } from "popmotion"
animate({
from: 0,
to: 100,
onUpdate: latest => console.log(latest)
})
\`\`\`
## Animation
### animate
\`animate\` performs a keyframes or spring animation.
\`\`\`javascript
import { animate } from "popmotion"
animate({
from: 0,
to: 100,
onUpdate: latest => console.log(latest)
})
\`\`\`
It can animate numbers:
\`\`\`javascript
animate({ from: 0, to: 100 })
\`\`\`
Or strings of the same type:
\`\`\`javascript
animate({ from: "0px", to: "100px" })
animate({ from: "#fff", to: "#000" })
\`\`\`
The strings can be pretty complex, for instance box shadows or SVG path definitions. The only limitation is that the numbers and colors contained within must be in the same order:
\`\`\`javascript
animate({
from: "0px 0px 0px rgba(0, 0, 0, 0)",
to: "10px 10px 0px rgba(0, 0, 0, 0.2)"
})
\`\`\`
The type of animation performed will be automatically detected from the provided options, or can be chosen manually by defining \`type\` as \`"keyframes"\`, \`"spring"\` or \`"decay"\`.
#### Options
These options can be set for **all animations**:
##### from
An initial value to start the animation from.
Defaults to \`0\`
\`\`\`javascript
animate({
from: "linear-gradient(#e66465, #9198e5)",
to: "linear-gradient(#9198e5, #e66465)"
})
\`\`\`
##### elapsed
Sets an initial elapsed time, in milliseconds. Set to a negative value for a delay.
\`\`\`javascript
animate({
to: 100,
elapsed: -300
})
\`\`\`
##### repeat
The number of times to repeat the animation. Set to \`Infinity\` to repeat forever.
\`\`\`javascript
animate({
to: 100,
repeat: 2
})
\`\`\`
##### repeatDelay
The duration, in milliseconds, to wait before repeating the animation.
\`\`\`javascript
animate({
to: 100,
repeat: 2,
repeatDelay: 200
})
\`\`\`
##### repeatType
Either \`"loop"\`, \`"mirror"\` or \`"reverse"\`. Defaults to \`"loop"\`.
- \`"loop"\`: Repeats the animation from \`0\`.
- \`"mirror":\` Swaps the \`from\`/\`to\` values alternately.
- \`"reverse":\` Reverses the animation alternately.
\`\`\`javascript
animate({
to: 100,
repeat: 2,
repeatType: "reverse"
})
\`\`\`
##### driver
By default, the animation will be driven by a \`requestAnimationFrame\` loop. \`driver\` can specify a different source.
A \`Driver\` is a function that accepts the animations \`update\` function. This is a function that can be called with a time delta from the previous frame. The \`Driver\` must return a function that will be called when the animation is stopped.
\`\`\`javascript
const xrDriver = session => update => {
let latestRequestId = 0
let prevTimestamp = performance.now()
const step = timestamp => {
const delta = timestamp - prevTimestamp
prevTimestamp = timestamp
update(delta)
latestRequestId = session.requestAnimationFrame(step)
}
let latestRequestId = session.requestAnimationFrame(step)
return () => session.cancelRequestAnimationFrame(latestRequestId)
}
animate({
to: 100,
driver: xrDriver(xrSession)
})
\`\`\`
##### type
\`animate\` will automatically detect the type of animation to use based on the options provided. But a specific type can be chosen manually by defining \`type\` as \`"keyframes"\`, \`"spring"\` or \`"decay"\`.
\`\`\`jsx
animate({
to: 100,
type: "spring"
})
\`\`\`
#### Lifecycle events
The following lifecycle events are available for **all animations**:
##### onUpdate
This is called every frame the animation fires with the latest computed value.
\`\`\`javascript
animate({
to: 100,
onUpdate: latest => console.log(latest)
})
\`\`\`
##### onPlay
This is called when the animation starts. Currently this automatically when \`animate\` is called.
\`\`\`javascript
animate({
to: 100,
onPlay: () => {}
})
\`\`\`
##### onComplete
This is called when the animation successfully completes.
\`\`\`javascript
animate({
to: 100,
onComplete:() => {}
})
\`\`\`
##### onRepeat
This is called when an animation repeats.
\`\`\`javascript
animate({
to: 100,
repeat: 2,
onRepeat: () => {}
})
\`\`\`
##### onStop
This is called when the animation is stopped by the \`stop\` control.
\`\`\`javascript
const animation = animate({
to: 100,
onStop: () => {}
})
animation.stop()
\`\`\`
#### Keyframes options
A keyframes animation is the default animation type and it can be defined either with a \`from\` and \`to\` option:
\`\`\`javascript
animate({ from: 0, to: 100 })
\`\`\`
Or as a series of keyframes provided to the \`to\` option:
\`\`\`javascript
animate({ to: [0, 100, 200] })
\`\`\`
##### to
A single value to animate to, or an array of values to animate through.
\`\`\`javascript
animate({
to: ["#0ff", "#f00", "#0f0"]
})
\`\`\`
If \`to\` is an array, any defined \`from\` will be ignored.
##### duration
This defines the duration of the animation, in milliseconds.
\`\`\`javascript
animate({
to: 100,
duration: 300
})
\`\`\`
##### ease
This is an easing function, or array of functions, to use when easing between each keyframe.
\`\`\`javascript
import { animate, linear, easeInOut } from "popmotion"
animate({
to: 100,
ease: linear
})
animate({
to: ["#fff", "#000", "#f00"],
ease: [linear, easeInOut]
})
\`\`\`
If set as any array, the length of this array must be one shorter than the number of values being animated between.
##### offset
This is an array of values between \`0\` and \`1\` that defines at which point throughout the animation each keyframe should be reached.
This array should be the same length as the number of defined keyframes.
\`\`\`javascript
animate({
to: ["#fff", "#000", "#f00"],
offset: [0, 0.2, 1]
})
\`\`\`
#### Spring options
Springs are great for creating natural-feeling interfaces and dynamic interruptable animations.
A spring animation will be used if any of the \`stiffness\`, \`damping\` or \`mass\` options are detected.
**Note:** A spring simulation is inherently numerical so if it's given a color, array or object, it runs the animation from \`0\` to \`100\` and interpolates that to the given values. This strategy is likely to be tweaked before the official release so animations made this way may change in feel.
##### to
A single value to animate to.
\`\`\`javascript
animate({
to: 100,
type: "spring"
})
\`\`\`
If \`to\` is an array, any defined \`from\` will be ignored.
##### stiffness
This defines the stiffness of the spring. A higher stiffness will result in a snappier animation.
Defaults to \`100\`
\`\`\`javascript
animate({
to: 100,
stiffness: 1000
})
\`\`\`
##### damping
This is the opposing force to \`stiffness\`. As you reduce this value, relative to \`stiffness\`, the spring will become bouncier and the animation will last longer. Likewise, higher relative values will have less bounciness and result in shorter animations.
Defaults to \`10\`
\`\`\`javascript
animate({
to: 100,
damping: 50
})
\`\`\`
##### mass
This is the mass of the animating object. Heavier objects will take longer to speed up and slow down.
Defaults to \`1\`.
\`\`\`javascript
animate({
to: 100,
mass: 2
})
\`\`\`
##### velocity
The initial velocity, in units per second, of the animation.
\`\`\`javascript
animate({
to: 100,
velocity: 1000
})
\`\`\`
##### duration
The duration of the spring, in milliseconds.
Will be overridden by \`stiffness\`, \`mass\` or \`damping\`.
\`\`\`javascript
animate({
to: 100,
duration: 1000
})
\`\`\`
##### bounce
The bounciness of the spring, as a value between \`0\` and \`1\`, where \`0\` is no bounce.
Will be overridden by \`stiffness\`, \`mass\` or \`damping\`.
\`\`\`javascript
animate({
to: 100,
bounce: 0.2
})
\`\`\`
##### restDelta
The distance from the animation target at which the animation can be considered complete. When both \`restDelta\` and \`restSpeed\` are met, the animation completes.
\`\`\`javascript
animate({
to: 100,
restDelta: 0.5
})
\`\`\`
##### restSpeed
The absolute velocity, in units per second, below which the animation can be considered complete. When both \`restDelta\` and \`restSpeed\` are met, the animation completes. Defaults to \`10\`.
\`\`\`javascript
animate({
to: 100,
restSpeed: 5
})
\`\`\`
#### Playback controls
\`animate\` returns \`PlaybackControls\`, which can be used to control the playback of the animation.
Currently this only includes a \`stop\` method, but may expand with more.
##### stop
Stops the animation.
\`\`\`javascript
const playback = animate({ from: 0, to: 100 })
playback.stop()
\`\`\`
### inertia
The \`inertia\` animation is used to gradually decelerate a number. Think smartphone scroll momentum.
#### Options
In addition to \`animate\`'s \`from\`, \`onUpdate\` and \`onComplete\` options, \`inertia\` also supports the following:
##### velocity
The initial velocity, in units per second, of the animation.
\`\`\`javascript
inertia({
from: 0,
velocity: 100
})
\`\`\`
##### power
A constant with which to calculate a target value. Higher power = further target.
Defaults to \`0.8\`.
\`\`\`javascript
inertia({
from: 0,
power: 0.3
})
\`\`\`
##### timeConstant
Adjusting the time constant will change the duration of the deceleration, thereby affecting its feel.
Defaults to \`350\`.
\`\`\`javascript
inertia({
from: 0,
velocity: 100,
timeConstant: 400
})
\`\`\`
##### modifyTarget
A function that receives the calculated target and returns a new one. Useful for snapping the target to a grid.
\`\`\`javascript
const roundToNearest = target => v => Math.ceil(v / target) * target
inertia({
from: 0,
velocity: 100,
modifyTarget: roundToNearest(100)
})
\`\`\`
##### min
The minimum value at which the animation will switch from gradual deceleration and use a spring animation to snap to this point.
\`\`\`javascript
inertia({
from: 50,
velocity: -100,
min: 0
})
\`\`\`
##### max
The maximum value at which the animation will switch from gradual deceleration and use a spring animation to snap to this point.
\`\`\`javascript
inertia({
from: 50,
velocity: 100,
max: 100
})
\`\`\`
##### bounceStiffness
This defines the stiffness of the spring when the animation hits either \`min\` or \`max\`. A higher stiffness will result in a snappier animation.
Defaults to \`500\`
\`\`\`javascript
inertia({
from: 0,
velocity: 100,
max: 50,
bounceStiffness: 1000
})
\`\`\`
##### bounceDamping
This is the opposing force to \`bounceStiffness\`. As you reduce this value, relative to \`bounceStiffness\`, the spring will become bouncier and the animation will last longer. Likewise, higher relative values will have less bounciness and result in shorter animations.
Defaults to \`10\`
\`\`\`javascript
inertia({
from: 0,
velocity: 100,
max: 50,
bounceDamping: 300
})
\`\`\`
##### restDelta
The distance from the animation target at which the animation can be considered complete.
\`\`\`javascript
inertia({
from: 0,
velocity: 100,
restDelta: 0.5
})
\`\`\`
### Iterators
Powering \`animate\` and \`inertia\` are the \`keyframes\`, \`spring\`, and \`decay\` [iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterator_protocol).
\`\`\`javascript
import { keyframes, spring, decay } from "popmotion";
\`\`\`
Iterators give you the ability to run an animation with a high degree of control. For example, [Framer](https://framer.com) uses the \`spring\` iterator to draw its animation editor visualiser by running it synchronously.
Each can be initialised with the matching options above (\`decay\` with a subset of \`inertia\`'s options, excluding the \`bounce-\` options):
\`\`\`javascript
const animation = spring({
from: 0,
to: 100,
stiffness: 200
})
\`\`\`
With the returned iterator, you can resolve the animation at a specific timestamp with its \`next\` method.
\`\`\`javascript
// Resolve the animation at 200ms
const { value, done } = animation.next(200)
\`\`\`
## Easing
Popmotion includes a number of in-built easing functions, as well as factory functions to make entirely new ones.
### Functions
Each easing function can be imported like so:
\`\`\`javascript
import { linear } from "popmotion"
\`\`\`
Each function accepts a progress value between \`0\` and \`1\`, and returns a new one:
\`\`\`javascript
const progress = 0.5
const easedProgress = easeInOut(progress)
\`\`\`
- \`linear\`
- \`easeIn\`
- \`easeInOut\`
- \`easeOut\`
- \`circIn\`
- \`circInOut\`
- \`circOut\`
- \`backIn\`
- \`backInOut\`
- \`backOut\`
- \`anticipate\`
- \`bounceIn\`
- \`bounceInOut\`
- \`bounceOut\`
### Factories
#### cubicBezier
\`\`\`javascript
import { cubicBezier } from "popmotion"
const easing = cubicBezier(0, .42, 0, 1)
\`\`\`
New cubic bezier definitions can be created in the [Framer](https://framer.com) animation editor and copy/pasted directly into this function.
#### steps
\`steps\` returns an easing function that will convert the animation into a discrete series of steps.
\`\`\`javascript
import { steps } from "popmotion"
const easing = steps(5)
\`\`\`
It optionally accepts a second parameter, either \`"start"\` or \`"end"\` (default)that decides whether the steps are aligned with the start or end of the animation.
\`\`\`javascript
steps(5, "start")
\`\`\`
#### mirrorEasing
Mirrors an existing easing function.
#### reverseEasing
Reverses an existing easing function. For instance, providing it \`easeIn\` would return an \`easeOut\`.
\`\`\`javascript
import { reverseEasing, linear } from "popmotion"
const reversed = reverseEasing(linear)
reversed(1) // 0
reversed(0.5) // 0.5
reversed(0) // 1
\`\`\`
#### createExpoIn
Creates an easing function based on the exponent of the provided \`power\`. The higher the \`power\`, the stronger the easing.
\`\`\`javascript
import { createExpoIn } from "popmotion"
const expoIn = createExpoIn(4)
\`\`\`
The returned easing function is an ease in, which means it starts slow and finished fast. \`mirrorEasing\` and \`reverseEasing\` can be used to create ease in out, and ease out variations:
\`\`\`javascript
const expoIn = createExpoIn(4)
const expoOut = mirrorEasing(easeIn)
const expoInOut = reverseEasing(easeIn)
\`\`\`
#### createBackIn
Creates an easing function with an overshoot. It accepts a \`power\` value, the higher the \`power\` the stronger the overshoot.
\`\`\`javascript
import { createBackIn } from "popmotion"
const backIn = createBackIn(4)
\`\`\`
The returned easing function is an ease in, which means the overshoot happens at the start of the animation. \`mirrorEasing\` and \`reverseEasing\` can be used to create ease in out, and ease out variations:
\`\`\`javascript
const backIn = createBackIn(4)
const backOut = mirrorEasing(easeIn)
const backInOut = reverseEasing(easeIn)
\`\`\`
#### createAnticipate
Creates an easing that pulls back a little before animating out with an overshoot. The stronger the \`power\` the bigger the overshoot.
\`\`\`javascript
import { createAnticipate } from "popmotion"
const anticipate = createAnticipate(4)
\`\`\`
## Utils
#### angle
Returns an angle between two points, in degrees.
\`\`\`javascript
import { angle } from "popmotion"
angle(
{ x: 0, y: 0 },
{ x: 45, y: 100 }
)
\`\`\`
#### attract
\`\`\`javascript
import { attract } from "popmotion"
attract(5, 10, 12)
\`\`\`
#### attractExpo
\`\`\`javascript
import { attractExpo } from "popmotion"
attractExpo(5, 10, 12)
\`\`\`
#### clamp
Clamp a value to within the given range.
\`\`\`javascript
import { clamp } from "popmotion"
const min = 50
const max = 100
clamp(min, max, 150) // 100
\`\`\`
#### degreesToRadians
Converts degrees to radians.
\`\`\`javascript
import { degreesToRadians } from "popmotion"
degreesToRadians(45) // 0.785...
\`\`\`
#### distance
Returns the distance between two numbers, two 2D points, or two 3D points.
\`\`\`javascript
import { distance } from "popmotion"
distance(10, 50)
distance({ x: 0, y: 0 }, { x: 45, y: 100 })
distance({ x: 0, y: 0, z: 100 }, { x: 45, y: 100, z: 0 })
\`\`\`
#### interpolate
Creates a function that will interpolate from an linear series of numbers, to a non-linear series of numbers, strings of the same numerical format, colours, or arrays/objects of those.
\`\`\`javascript
import { interpolate } from "popmotion"
const mapXToOpacity = interpolate(
[-100, 0, 100],
[0, 1, 0]
)
mapXToOpacity(-50) // 0.5
const mapProgressToValues = interpolate(
[0, 1],
[
{ x: 0, color: "#fff" },
{ x: 100, color: "#000" }
]
)
mapProgressToValues(0.5) // { x: 50, color: "#888" }
const rescale = interpolate(
[0, 1],
[100, 200],
{ clamp: false }
)
rescale(2) // 300
\`\`\`
#### Options
\`interpolate\` accepts an optional third argument, an object of options.
- \`clamp\`: Clamps values to within given range. Defaults to \`true\`.
- \`ease\`: An \`Easing\` function, or array of easing functions, to ease the interpolation of each segment.
- \`mixer\`: A function that, when provided a \`from\` and \`to\` value, will return a new function that accepts a progress value between \`0\` and \`1\` to mix between those two values. For integration with libraries like Flubber.
#### isPoint
Returns \`true\` if the provided argument is a 2D point.
\`\`\`javascript
import { isPoint } from "popmotion"
isPoint({ x: 0 }) // false
isPoint({ x: 0, y: 0 }) // true
\`\`\`
#### isPoint3D
Returns \`true\` if the provided argument is a 3D point.
\`\`\`javascript
import { isPoint3D } from "popmotion"
isPoint3D({ x: 0 }) // false
isPoint3D({ x: 0, y: 0 }) // false
isPoint3D({ x: 0, y: 0, z: 0 }) // true
\`\`\`
#### mix
Will mix between two values, given \`progress\` as a third argument.
\`\`\`javascript
import { mix } from "popmotion"
mix(0, 100, 0.5) // 50
mix(0, 100, 2) // 200
\`\`\`
#### mixColor
Returns a function that, when provided a \`progress\` value, will mix between two colors. Accepts hex, rgba and hsla colors.
\`\`\`javascript
import { mixColor } from "popmotion"
mixColor("#000", "#fff")(0.5) // "rgba(125, 125, 125, 1)"
\`\`\`
#### mixComplex
Returns a function that, when provided a \`progress\` value, will mix between two strings with the same order of numbers and colors.
\`\`\`javascript
import { mixComplex } from "popmotion"
mixComplex("100px #fff", "0px #000")(0.5) // "50px rgba(125, 125, 125, 1)"
\`\`\`
#### pointFromVector
Given a point, angle in degrees, and distance, will return a new point.
\`\`\`javascript
import { pointFromVector } from "popmotion"
const point = { x: 0, y: 0 }
const angle = 45
const distance = 100
pointFromVector(point, angle, distance)
\`\`\`
#### progress
Given a min and a max range, and a value, will return the \`progress\` of the value within the range as normalised to a \`0\`-\`1\` range.
\`\`\`javascript
import { progress } from "popmotion"
const min = 100
const max = 200
progress(min, max, 150) // 0.5
\`\`\`
#### radiansToDegrees
Converts radians to degrees.
\`\`\`javascript
import { radiansToDegrees } from "popmotion"
radiansToDegrees(0.785) // 45
\`\`\`
#### snap
Creates a function that will snap numbers to the nearest in a provided array or to a regular interval.
\`\`\`javascript
import { snap } from "popmotion"
// Snap to regular intervals
const snapTo = snap(45);
snapTo(1); // 0
snapTo(40); // 45
snapTo(50); // 45
snapTo(80); // 90
// Snap to values in an array
const snapTo = snap([-100, -50, 100, 200]);
snapTo(-200); // -100
snapTo(-76); // -100
snapTo(-74); // -50
\`\`\`
#### toDecimal
Rounds a number to a specific decimal place.
\`\`\`javascript
import { toDecimal } from "popmotion"
toDecimal(3.3333); // 3.33
toDecimal(6.6666, 1); // 6.67
\`\`\`
#### velocityPerFrame
\`\`\`javascript
import { velocityPerFrame } from "popmotion"
velocityPerFrame(50, 16.7); // 0.835
\`\`\`
#### velocityPerSecond
\`\`\`javascript
import { velocityPerSecond } from "popmotion"
velocityPerSecond(1, 16.7); // 59.880...
\`\`\`
#### wrap
\`\`\`javascript
import { wrap } from "popmotion"
wrap(0, 1, 0.5); // 0.5
wrap(0, 1, 1.5); // 0.5
\`\`\`
`);
export default Page;
================================================
FILE: packages/site/pages/popcorn/index.js
================================================
import PopcornHomepage from '~/templates/Popcorn';
export default () => ;
================================================
FILE: packages/site/pages/pose/api.js
================================================
import { Fragment } from 'react';
import GlobalTemplate from '~/templates/global/Template';
import ContentPage from '~/templates/global-new/ContentPage';
import { Section, PageHeader } from '~/templates/global-new/styled';
import MenuPage from '~/templates/content/MenuPage';
export default () => (
);
================================================
FILE: packages/site/pages/pose/examples.js
================================================
import ContentTemplate from '~/templates/content/Template';
import { Centered } from '~/templates/global/grid';
import styled from 'styled-components';
const Choose = styled(Centered.withComponent('h2'))`
color: #333;
margin: 0 auto;
display: block;
text-align: center;
font-size: 36px;
font-weight: bold;
border: 2px dashed #333;
margin-top: 50px;
padding: 40px;
border-radius: 20px;
`;
export default () => (
Choose an example from the menu
);
================================================
FILE: packages/site/pages/pose/index.js
================================================
import PoseHomepage from '~/templates/Pose';
export default () => ;
================================================
FILE: packages/site/pages/pure/api/action.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Action
Action is a simplified Rx-inspired reactive stream focused on animation.
**Every Popmotion animation and input is an action.**
When an action is started, it returns a simple interface that includes **at least** a \`stop\` method.
## Import
\`\`\`javascript
import { action } from 'popmotion';
\`\`\`
## Usage
### Definition
The \`action\` factory takes one argument, an \`init\` function.
This is a function that is provided an object of \`update\`, \`complete\`, and \`error\` functions.
Usage of these functions is optional. Your action may call all or just some of them:
\`\`\`javascript
action(({ update, complete, error }) => {
update(1);
});
\`\`\`
### Initialisation
\`action\` returns a \`start\` method. This also accepts an object of \`update\`, \`complete\`, and \`error\` functions.
When called, the \`init\` function is provided these functions, and a **new instance** of the action is created.
Calling \`start\` multiple times will create multiple, separate instances of the action.
For example:
\`\`\`javascript
const foo = action(({ update }) => {
let i = 0;
setInterval(() => update(i++), 50);
});
foo.start({
update: (v) => console.log(v)
}); // 0, 1, 2...
\`\`\`
If \`start\` is passed **only a function**, that is assigned to the \`update\` function:
\`\`\`javascript
foo.start((v) => console.log(v)); // 0, 1, 2...
\`\`\`
We can also listen for the \`complete\` event like this:
\`\`\`javascript
const foo = action(({ update, complete }) => {
let i = 0;
setInterval(() => {
update(i++);
if (i === 10) complete();
}, 50);
});
foo.start({
update: (v) => console.log(v), // ...8, 9, 10
complete: () => console.log('complete!')
});
\`\`\`
### Interface
The \`init\` function can **optionally** return an API.
For instance, we might use this to stop a timer:
\`\`\`javascript
const foo = action(({ update }) => {
const interval = setInterval(() => update('ping!'), 100);
return {
stop: () => clearInterval(interval)
};
});
const bar = foo.start(console.log);
setTimeout(() => bar.stop(), 1000);
\`\`\`
Any method returned by the action \`init\` function will be exposed when an action instance is created.
### Modification
\`action\` is **chainable**, which means it offers methods that can alter the behaviour of the base action. Currently, these are \`while\` and \`pipe\` (see [Methods](#methods)).
When an action is chained, a **new action** is returned. For instance:
\`\`\`javascript
const foo = action(({ update }) => {
let i = 0;
setInterval(() => update(i++), 50);
});
const lessThanTen = (v) => v < 10;
const log = (v) => console.log(v);
foo.start(log); // ...8, 9, 10, 11...
foo.while(lessThanTen).start(log); // ...8, 9
\`\`\`
## Methods
### \`pipe\`
\`\`\`typescript
pipe(...funcs: (v: any) => any)
\`\`\`
Returns a **new** action that passes the output of the original action's \`update\` through the provided functions, from left to right.
#### Example
\`\`\`javascript
const init = ({ update }) => update(10);
const double = (v) => v * 2;
const px = (v) => v + 'px';
action(init)
.pipe(double, px)
.start((v) => console.log(v)); // '20px'
\`\`\`
### \`start\`
\`\`\`typescript
start(update: (v: any) => void)
start({
complete? () => void,
error?: (err: any) => void,
update?: (v: any) => void
})
start(reaction)
\`\`\`
Starts the action by running its initiation function, and returning its API.
Every interface returned by a \`start\` call, **regardless of the API returned from the observable**, will return at least a \`stop\` function.
It can be provided either an \`update\` function, or an object with \`update\`, \`complete\` and \`error\` functions.
#### Example
\`\`\`javascript
// Doesn't return an API
const foo = action(({ update }) => update(1)).start();
foo.stop();
// Returns a custom API
const bar = action(({ update }) => {
let i = 0;
setInterval(() => update(i), 100);
return {
setOutput: (v) => i = v
};
}).start();
bar.setOutput(2);
setTimeout(() => bar.stop(), 1000);
\`\`\`
### \`while\`
\`\`\`typescript
while(predicate: (v: any) => boolean)
\`\`\`
Returns a new action, that will continue to run **while** the updated values match the provided predicate.
When the predicate function returns \`false\`, the action will \`complete\`.
### Example
\`\`\`javascript
let latest = 0;
const init = ({ update }) => {
let i = latest;
setInterval(() => update(i++), 50);
};
action(init)
.while((v) => v < 10)
.start({
update: (v) => latest = v,
complete: () => console.log(latest) // 9
});
\`\`\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/calc.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Calculators
Popmotion provides a series of simple functions useful in UI calculations.
## Import
\`\`\`javascript
import { calc } from 'popmotion';
\`\`\`
## Methods
### \`angle\`
Calculate the angle between two 2D points, in degrees.
\`type Point = { x: number, y: number }\`
\`angle(a: Point, b: Point)\`
\`\`\`javascript
const a = { x: 0, y: 0 };
const b = { x: 1, y: 1 };
calc.angle(a, b); // 45
\`\`\`
### \`degreesToRadians\`
Convert degrees to radians.
\`degreesToRadians(degrees: number)\`
\`\`\`javascript
calc.degreesToRadians(45); // 0.7853981633974483
\`\`\`
### \`dilate\`
Dilate the difference between two values.
\`dilate(a: number, b: number, dilation: number)\`
\`\`\`javascript
calc.dilate(0, 80, .5); // 40
calc.dilate(100, 200, 2); // 300
\`\`\`
### \`distance\`
Calculate the distance between 1D, 2D or 3D points.
\`type Point = { x: number, y: number, z?: number }\`
\`distance(a: number | Point, b: number | Point)\`
\`\`\`javascript
calc.distance(-100, 100); // 200
calc.distance({ x: 0, y: 0 }, { x: 1, y: 1}); // 1.4142135623730951
calc.distance({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 10 }); // 10
\`\`\`
### \`getProgressFromValue\`
Calculate a progress (0 - 1) from a value and range.
\`getProgressFromValue(min: number, max: number, value: number)\`
\`\`\`javascript
calc.getProgressFromValue(0, 100, 50); // 0.5
calc.getProgressFromValue(100, 200, 50); // -0.5
\`\`\`
### \`getValueFromProgress\`
Calculate a value from a progress (0 - 1) and range.
\`getValueFromProgress(min: number, max: number, value: number)\`
\`\`\`javascript
calc.getValueFromProgress(100, 200, 0.5); // 150
calc.getValueFromProgress(100, 200, -1); // 0
\`\`\`
### \`pointFromAngleAndDistance\`
Given an origin point, angle in degrees and distance, returns a new point.
\`pointFromAngleAndDistance(origin: Point, angle: number, distance: number)\`
\`\`\`javascript
calc.pointFromAngleAndDistance({ x: 0, y: 0 }, 45, 100);
/*
{
x: 70.71067811865476,
y: 70.71067811865474
}
*/
\`\`\`
### \`radiansToDegrees\`
Convert radians to degrees.
\`radiansToDegrees(radians: number)\`
\`\`\`javascript
calc.radiansToDegrees(0.7853981633974483); // 45
\`\`\`
### \`smooth\`
Framerate-independent value smoothing.
\`smooth(newValue: number, oldValue: number, frameDuration: number, smoothing: number)\`
### \`speedPerFrame\`
Convert speed per second into speed per frame.
\`speedPerFrame(speedPerSecond: number, frameDuration: number)\`
### \`speedPerSecond\`
Convert speed per frame into speed per second.
\`speedPerSecond(speedPerFrame: number, frameDuration: number)\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/chain.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Chain
Chain a sequence of actions, move to the next when the current one completes.
## Import
\`\`\`javascript
import { chain } from 'popmotion';
\`\`\`
## Usage
\`\`\`javascript
chain(
tween({ to: 300 }),
spring({ from: 300, to: 0 })
).start({
update: (v) => console.log(v),
complete: () => console.log('All actions complete')
})
\`\`\`
## Methods
### Action methods
\`chain()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`chain().start()\` returns:
- \`stop(): void\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/composite.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Composite
Control a named map of actions, and output to the same structure.
## Import
\`\`\`javascript
import { composite } from 'popmotion';
\`\`\`
## Usage
\`\`\`javascript
composite({
x: tween({ from: 60, to: 400 }),
y: physics({ from: 60, velocity: 300 })
}).start(({ x, y }) => {});
\`\`\`
\`composite\` outputs at most once per frame.
## Methods
### Action methods
\`composite()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`composite().start()\` returns:
- \`stop(): void\`
**Note:** If all actions return the same API, for instance all composed actions are \`tween\`s, the \`composite\` subscription will also return a version of that API that controls all child actions.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/crossfade.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Crossfade
Fade between two numerical actions.
## Import
\`\`\`javascript
import { crossfade } from 'popmotion';
\`\`\`
## Usage
Example: blend from one tween to another:
\`\`\`javascript
const blendTweens = crossfade(
tween({ from: 0, to: 500, elapsed: 200 }),
tween({ from: 500, to: 0 })
).start((v) => console.log(v));
tween({ duration: 100 }).start(blendTweens.setBalance);
\`\`\`
## Methods
### Action methods
\`crossfade()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`crossfade().start()\` returns:
- \`setBalance(): this\`: Sets the balance of blended output from the first action (\`0\`) to the second (\`1\`).
- \`stop(): void\`
## Example
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/css.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
[Moved to the Stylefire docs](/stylefire/api/html).`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/decay.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Decay
\`decay\` returns an [action](/api/action) that exponentially decelerates a number and velocity to an automatically generated target value. This target can be modified by the user.
This animation is particularly useful for implementing momentum scrolling.
## Import
\`\`\`javascript
import { decay } from 'popmotion';
\`\`\`
## Usage
\`\`\`javascript
decay({ velocity: 200, from: 50 })
.start((v) => console.log(v));
\`\`\`
## Props
- \`velocity: number = 0\`: Initial velocity to decelerate from.
- \`from: number = 0\`: Number to apply movement to.
- \`power: number = 0.8\`: A constant with which to calculate a target value. Higher power = further target. \`0.8\` should be okay.
- \`timeConstant: number = 350\`: Adjusting the time constant will change the duration of the deceleration, thereby affecting its feel.
- \`restDelta: number = 0.5\`: Automatically completes the action when the calculated value is this far away from the target.
- \`modifyTarget: (v: number) => number\`: A function that receives the calculated target and returns a new one. Useful for snapping the target to a grid, for example.
## Methods
### Action methods
\`decay()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`decay().start()\` returns:
- \`stop(): void\`
## Example
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/delay.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Delay
Fires \`complete\` after the defined interval.
## Import
\`\`\`javascript
import { delay } from 'popmotion';
\`\`\`
## Usage
\`\`\`javascript
delay(100).start({
complete: () => console.log('complete!')
});
\`\`\`
Useful for delaying actions in a \`chain\`.
\`\`\`javascript
chain(
delay(100),
tween({ to: 400, duration: 500 })
);
\`\`\`
### Action methods
\`delay()\` returns:
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
### Subscription methods
\`delay().start()\` returns:
- \`stop(): void\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/easing.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Easing
Easing functions make tweened motion look more natural by emulating the changes in velocity experienced by objects in the real world.
They work by accepting a progress value from \`0\` to \`1\`, and returning a new one.
Popmotion comes with a number of preset easing functions, and provides methods to create new easing functions.
## Import
\`\`\`javascript
import { easing } from 'popmotion';
\`\`\`
## Example
\`\`\`javascript
import { tween, easing } from 'popmotion';
tween({
ease: easing.easeOut
}).start();
\`\`\`
## Presets
Popmotion comes with the following preset easing functions:
- \`cubicBezier\`
- \`linear\`
- \`easeIn\`, \`easeOut\`, \`easeInOut\`
- \`circIn\`, \`circOut\`, \`circInOut\`
- \`backIn\`, \`backOut\`, \`backInOut\`
- \`anticipate\`
Try them out by editing this live example:
\`\`\`marksy
{\`
const ball = document.querySelector('#b .ball');
const ballStyler = styler(ball);
tween({
to: 300,
duration: 300,
ease: easing.linear
}).start(ballStyler.set('x'));
\`}
\`\`\`
## Easing creation
Popmotion provides the following functions to create your own easing functions:
### \`cubicBezier\`
Creates cubic bezier curve easing function.
\`\`\`javascript
const { cubicBezier } = easing;
const longTail = cubicBezier(0, .42, 0, 1);
\`\`\`
### \`createReversedEasing\`
Reverses the provided easing function.
\`\`\`javascript
const { anticipate, createReversedEasing } = easing;
const anticipateOut = createReversedEasing(anticipate);
\`\`\`
### \`createMirroredEasing\`
Mirrors the provided easing function.
\`\`\`javascript
const { anticipate, createMirroredEasing } = easing;
const anticipateInAndOut = createMirroredEasing(anticipate);
\`\`\`
### \`createExpoIn\`
Creates an easing function based on the exponent function \`progress ** exponent\`. \`easeIn\` is \`createExpoIn(2)\`.
\`\`\`javascript
const { createExpoIn } = easing;
const strongerEaseIn = createExpoIn(3);
\`\`\`
### \`createBackIn\`
Creates an easing function with an overshoot. \`backIn\` is \`createBackIn(1.525)\`.
\`\`\`javascript
const { createBackIn, createReversedEasing } = easing;
const strongerBackOut = createReversedEasing(createBackIn(3));
\`\`\`
### \`createAnticipateEasing\`
Creates an easing function with a small anticipate and ease out. \`anticipate\` is \`createAnticipateEasing(1.525)\`.
\`\`\`javascript
const { createAnticipateEasing } = easing;
const strongerAnticipate = createAnticipateEasing(3);
\`\`\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/every-frame.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Every Frame
\`everyFrame\` fires once per frame, and provides \`update\` with the duration of time since it started.
## Import
\`\`\`javascript
import { everyFrame } from 'popmotion';
\`\`\`
## Usage
\`\`\`javascript
everyFrame()
.start((timeSinceStart) => console.log(timeSinceStart));
\`\`\`
## Methods
### Action methods
\`everyFrame()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`everyFrame().start()\` returns:
- \`stop(): void\`
## Example
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/framesync.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Framesync
A tiny frame scheduler for performantly batching reads and renders.
Segregating actions that read and write to the DOM will avoid [layout thrashing](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing).
Popmotion batches updates on the \`frameUpdate\` step, and Stylefire batches renders on the \`frameRender\` step.
## Install
**Framesync is installed as part of Popmotion.**
To use as a standalone library, install with:
\`\`\`bash
npm install framesync --save
\`\`\`
## Usage
The Framesync render loop executes four sequential steps, once per frame.
- \`frameStart\`
- \`frameUpdate\`
- \`frameRender\`
- \`frameEnd\`
Developers can set any function to run at any of these steps using the \`on\` and \`cancel\` callbacks:
- \`onFrameStart\`, \`cancelOnFrameStart\`
- \`onFrameUpdate\`, \`cancelOnFrameUpdate\`
- \`onFrameRender\`, \`cancelOnFrameRender\`
- \`onFrameEnd\`, \`cancelOnFrameEnd\`
Framesync also exports some time-measurement methods:
- \`currentTime\`: The current time as measured by the host platform's most accurate \`now\` function.
- \`currentFrameTime\`: The time the current \`requestAnimationFrame\` was initiated.
- \`timeSinceLastFrame\`: The duration between the previous frame and the current \`currentFrameTime\`
### Example
\`\`\`javascript
import {
timeSinceLastFrame,
onFrameStart,
cancelFrameStart
} from 'framesync';
function logTimeSinceLastFrame() {
console.log(timeSinceLastFrame());
onFrameStart(logTimeSinceLastFrame);
}
onFrameStart(logTimeSinceLastFrame);
function stopLogging() {
cancelOnFrameStart(logTimeSinceLastFrame);
}
setTimeout(stopLogging, 5000);
\`\`\``);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/keyframes.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Keyframes
Keyframes accepts an array of \`values\` and will animate between each in sequence.
Timing is defined with a combination of \`duration\`, \`easings\` and \`times\` properties (see [Methods](#methods))
It returns a [\`tween\`](/api/tween), which allows the use of \`ease\`, \`loop\`, \`flip\` and \`yoyo\` props, as well as tween methods like \`pause\` and \`resume\`.
## Import
\`\`\`javascript
import { keyframes } from 'popmotion';
\`\`\`
## Usage
\`keyframes\` accepts numbers:
\`\`\`javascript
keyframes({
values: [0, 100, 200],
duration: 1000,
times: [0, 0.2, 1],
easings: [ease.linear, ease.cubicBezier(.17,.67,.83,.67)]
}).start((v) => console.log(v));
\`\`\`
Colors:
\`\`\`javascript
keyframes({
values: ['#f00', '#e533a1', 'rgba(0, 0, 0, 0)']
})
\`\`\`
Objects:
\`\`\`javascript
keyframes({
values: [
{
x: 100,
background: '#f00'
},
{
x: 200,
background: '#000'
}
]
});
\`\`\`
And arrays:
\`\`\`javascript
keyframes({
values: [
[0, 100, 300],
[100, 45, 0]
]
})
\`\`\`
## Props
- \`values: number[]\`: An array of numbers to animate between.
- \`duration?: number = 300\`: Total duration of animation, in milliseconds.
- \`easings?: Easing | Easing[]\`: An array of easing functions for each generated tween, or a single easing function applied to all tweens. This array should be \`values.length - 1\`. Defaults to \`easeOut\`.
- \`times?: number[]\`: An array of numbers between \`0\` and \`1\`, representing \`0\` to \`duration\`, that represent at which point each number should be hit. Defaults to an array of evenly-spread durations will be calculated.
- \`elapsed?: number = 0\`: Duration of animation already elapsed, in milliseconds.
- \`ease?: Easing | Vector[Easing] = easeOut\`: A function, given a progress between \`0\` and \`1\`, that returns a new progress value. Used to affect the rate of playback across the duration of the animation.
- \`loop?: number = 0\`: Number of times to loop animation on \`complete\`.
- \`flip?: number = 0\`: Number of times to flip animation on \`complete\`.
- \`yoyo?: number = 0\`: Number of times to reverse tween on \`complete\`.
### Tween props
As \`keyframes\` returns a [\`tween\`](/api/tween), the following properties can also be provided:
- \`loop: number = 0\`: Number of times to loop animation on \`complete\`.
- \`flip: number = 0\`: Number of times to flip animation on \`complete\`.
- \`yoyo: number = 0\`: Number of times to reverse tween on \`complete\`.
## Methods
### Action methods
\`keyframes()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`keyframes().start()\` returns:
- \`getElapsed(): number\`: Returns time elapsed in milliseconds.
- \`getProgress(): number\`: Returns animation progress as a value of \`0\`-\`1\`.
- \`seek(progress: number): this\`: Seeks animation to this position as a value of \`0\`-\`1\`.
- \`pause(): this\`
- \`resume(): this\`
- \`reverse(): this\`: Reverses the direction of playback.
- \`stop(): void\`
## Example
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/listen.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Listen
\`listen\` creates DOM event listeners as an action stream.
## Import
\`\`\`javascript
import { listen } from 'popmotion';
\`\`\`
## Usage
To listen to an event, provide a DOM element and an event name to \`listen\`:
\`\`\`javascript
listen(document, 'mousemove')
.start((e) => console.log(e));
\`\`\`
### Multiple events
Multiple events can be subscribed to by providing a space-delimited string:
\`\`\`javascript
listen(document, 'touchstart touchend')
\`\`\`
### Chainable actions
The primary benefit of using \`listen\` is passing each event through the chainable actions like \`filter\`. For instance, here's an event listener that only fires when two or more touches are detected:
\`\`\`javascript
const onMultitouch = listen(document, 'touchstart')
.filter(({ touches }) => touches.length > 1);
onMultitouch.start((e) => ...);
\`\`\`
### Options
\`listen\` optionally accepts a third argument of options:
\`\`\`typescript
type EventOpts = boolean | {
capture?: boolean;
passive?: boolean;
once?: boolean;
};
listen(element: Element, eventNames: string, opts?: EventOpts): Action
\`\`\`
## Methods
### Action methods
\`listen()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`listen().start()\` returns:
- \`stop(): void\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/merge.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Merge
Combine multiple actions into one output.
## Import
\`\`\`javascript
import { merge } from 'popmotion';
\`\`\`
## Usage
\`\`\`javascript
merge(
tween(),
action(({ update }) => update(1)),
physics({ velocity: 1000 })
).start((v) => console.log(v));
\`\`\`
## Methods
### Action methods
\`merge()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`merge().start()\` returns:
- \`stop(): void\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/multicast.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Multicast
The multicast reaction provides a reaction that many other reactions can \`subscribe\` to.
It also helps manage actions: if a \`multicast\` reaction is passed to another \`action\`, the first \`action\` will automatically \`stop\`.
## Import
\`\`\`javascript
import { multicast } from 'popmotion';
\`\`\`
## Usage
### Subscription
Provide a reactions to \`mulitcast.subscribe()\`:
\`\`\`javascript
const foo = multicast();
foo.subscribe((v) => console.log('first subscriber', v));
foo.subscribe((v) => console.log('second subscriber', v));
\`\`\`
When the multicast reaction is \`update\`d, all listeners will fire:
\`\`\`javascript
foo.update(5);
// first subscriber, 5
// second subscriber, 5
\`\`\`
### Automatically stop previous action
Passing the multicast reaction to a new action will stop the previous one:
\`\`\`javascript
tween().start(foo);
spring().start(foo); // This will stop \`tween\`
\`\`\`
### Chain methods
\`multicast\` can be chained in the same way as [actions](/api/action).
\`\`\`javascript
const double = (v) => v * 2;
const px = (v) => v + 'px';
const foo = multicast().pipe(double, px);
foo.update(5); // 10px
\`\`\`
### Unsubscribe
\`subscribe\` returns an \`unsubscribe\` method:
\`\`\`javascript
const foo = multicast();
const sub = foo.subcribe(console.log);
sub.unsubscribe();
\`\`\`
## Methods
### Multicast methods
\`multicast()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new reaction that will run \`update\` values through this sequence of functions.
- \`subscribe(update | { update, complete, error })\`: Returns a subscription.
- \`stop()\`: Stops current parent action.
- \`while((v: any) => boolean)\`: Returns a new reaction that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`multicast().subscribe()\` returns:
- \`unsubscribe()\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/multitouch.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Multitouch
Tracks multitouch input and outputs a list of active touches, plus scale and rotation delta between the first two touch points.
For single-point input, generally [pointer](/api/pointer) is more appropriate as it provides a simple, cross-platform interface.
## Import
\`\`\`javascript
import { multitouch } from 'popmotion';
\`\`\`
## Usage
\`\`\`typescript
multitouch({
preventDefault?: boolean = true,
scale?: number,
rotate?: number
})
\`\`\`
\`\`\`javascript
multitouch().start(({ touches, scale, rotate }) => {
touches.forEach(({ x, y }) => console.log(x, y))
});
\`\`\`
\`multitouch\` provides:
- \`touches: { x: number, y: number }[]\`: An array of \`x\`/\`y\` coordinates representing each active finger.
- \`scale: number\`: The distance between the first two fingers since \`start\`, represented as a multiplier of the original distance. \`scale\` starts from \`1.0\`, or the initially provided \`scale\`.
- \`rotate: number\`: The angle rotation of the first two fingers as a delta of the original rotation. \`rotate\` starts from \`0.0\`, or the initially provided \`rotate\`.
### Commonly-used properties
If you often use, for instance, \`rotate\`, you can easily create a new action that returns only that value:
\`\`\`javascript
const touchRotation = (initialRotate = 0) => multitouch({ rotate: initialRotate })
.pipe(({ rotate }) => rotate);
touchRotation(45).start((rotate) => console.log(rotate));
\`\`\`
## Props
- \`preventDefault?: boolean = true\`
- \`scale?: number = 1.0\`
- \`rotate?: number = 0.0\`
## Methods
### Action methods
\`multitouch()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`multitouch().start()\` returns:
- \`stop(): void\`
## Example
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/parallel.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Parallel
Control an n-dimensional array of actions in parallel, and output as an array.
## Import
\`\`\`javascript
import { parallel } from 'popmotion';
\`\`\`
## Usage
\`\`\`javascript
parallel(
tween({ from: 40, to: 50 }),
spring({ to: 500 })
).start(([ tweenOutput, springOutput ]) => {});
\`\`\`
\`parallel\` outputs max once per frame.
## Methods
### Action methods
\`parallel()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`parallel().start()\` returns:
- \`stop(): void\`
**Note:** If all actions return the same API, for instance all composed actions are \`tween\`s, the \`parallel\` subscription will also return a version of that API that controls all child actions.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/physics.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Physics
Integrated simulation of velocity, acceleration, friction and springs.
Because the simulation is integrated, we can expose \`set\` methods that can change the simulation while it is still running.
This is unlike the differential equations in [decay](/api/decay) and [spring](/api/spring), which can't be changed while in motion (although both offer higher-accuracy simulations which lead to smoother animations).
## Import
\`\`\`javascript
import { physics } from 'popmotion';
\`\`\`
## Usage
To simulate velocity, we just need to provide a \`velocity\` property:
\`\`\`javascript
physics({ velocity: 1000 })
.start((v) => console.log(v));
\`\`\`
To slow down over a duration of time, we can provide a \`friction\` prop between \`0\` (no friction) and \`1\` (dead stop):
\`\`\`javascript
physics({ velocity: 1000, friction: 0.8 })
.start((v) => console.log(v));
\`\`\`
Finally, to simulate a spring we need to add \`to\` and \`springStrength\` properties:
\`\`\`javascript
physics({
velocity: 1000,
friction: 0.8,
to: 400,
springStrength: 500
}).start((v) => console.log(v));
\`\`\`
We can also provide many properties as \`Vector\` types, which are maps or arrays of numbers:
\`\`\`javascript
physics({
from: 100,
to: { x: 30, y: 100, z: 20 },
springStrength: 500
})
\`\`\`
## Props
- \`acceleration: number | Vector = 0\`: Increase \`velocity\` by this amount every second.
- \`restSpeed: number = 0.001\`: When absolute speed drops below this value, \`complete\` is fired.
- \`friction: number | Vector = 0\`: Amount of friction to apply per frame, from \`0\` to \`1\`.
- \`from: number | Vector = 0\`: Start simulation from this number.
- \`springStrength: number | Vector = 0\`: If set with \`to\`, will spring towards target with this strength.
- \`to: number | Vector = 0\`: If set with \`springStrength\`, will gradually "spring" towards this value.
- \`velocity: number | Vector = 0\`: Velocity in units per second.
## Methods
### Action methods
\`physics()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`physics().start()\` returns:
- \`set(current: number): this\`
- \`setAcceleration(acceleration: number): this\`
- \`setFriction(friction: number): this\`
- \`setSpringStrength(strength: number): this\`
- \`setSpringTarget(target: number): this\`
- \`setVelocity(velocity: number): this\`
- \`stop(): void\`
## Example
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/pointer.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Pointer
Outputs the screen position of a single mouse or touch point.
## Import
\`\`\`javascript
import { pointer } from 'popmotion';
\`\`\`
## Usage
### Absolute position
\`\`\`javascript
pointer()
.start(({ x, y }) => console.log(x, y));
\`\`\`
### Relative position
Provide initial \`x\` and \`y\` properties to output pointer movement **applied to this initial point**.
\`\`\`javascript
pointer({ x: 200, y: 175 })
.start(({ x, y }) => console.log(x, y));
\`\`\`
Reactions are provided the following pointer data:
- \`x\`, \`y\`: Alias of \`clientX\` / \`clientY\`, or
- \`clientX\`, \`clientY\`: Position relative to the viewport.
- \`pageX\`, \`pageY\`: Position relative to the document.
To apply the change in pointer movement to, for instance, a slider, you can use the [\`deltaOffset\`](/api/delta-pointer) action.
## Props
- \`preventDefault: boolean = true\`
- \`x?: number\`: If defined, apply pointer \`y\` movement to this number.
- \`y?: number\`: If defined, apply pointer \`y\` movement to this number.
## Methods
### Action methods
\`pointer()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`pointer().start()\` returns:
- \`stop(): void\`
## Example
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/schedule.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Schedule
\`schedule\` can use one action to control the output of another.
For instance, by default \`pointer\` outputs only when the pointer updates.
With \`schedule\`, you could compose it with \`everyFrame\` to output the latest \`pointer\` value every frame.
## Import
\`\`\`javascript
import { schedule } from 'popmotion';
\`\`\`
## Usage
\`\`\`typescript
schedule(scheduler: Action, subject: Action): Action
\`\`\`
\`\`\`javascript
// \`pointer\` will output at most once every frame
schedule(
everyFrame(),
pointer()
).start(({ x, y }) => {});
\`\`\`
## Methods
### Action methods
\`schedule()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`schedule().start()\` returns:
- \`stop(): void\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/scroll.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
[Moved to the Stylefire docs](/stylefire/api/viewport).`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/spring.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Spring
A spring animation based on \`stiffness\`, \`damping\` and \`mass\`.
This simulation offers smoother motion and a greater variety of spring-feels than the basic spring integration found in [physics](/api/physics).
It's based on the differential equations governing a [damped harmonic oscillator](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), the same as those underlying Apple's \`CASpringAnimation\`.
## Import
\`\`\`javascript
import { spring } from 'popmotion';
\`\`\`
## Usage
Animate numbers:
\`\`\`javascript
spring({ to: 500, stiffness: 200, damping: 20 })
.start((v) => console.log(v))
\`\`\`
Or optionally provide vectors:
\`\`\`javascript
spring({
to: [500, 100],
stiffness: 300,
damping: [20, 10]
})
\`\`\`
\`\`\`javascript
spring({
to: { x: 200, y: 300, z: 25 }
})
\`\`\`
## Props
- \`from: number | Vector = 0.0\`: Value to start from.
- \`to: number | Vector = 0.0\`: Value to spring to.
- \`stiffness: number | Vector = 100\`: Spring stiffness.
- \`damping: number | Vector = 10\`: Strength of opposing force.
- \`mass: number | Vector = 1.0\`: Mass of the moving object.
- \`velocity: number | Vector = 0.0\`: Initial velocity of spring.
- \`restDelta: number = 0.01\`: End animation if distance to \`to\` is below this value **and** \`restSpeed\` is \`true\`.
- \`restSpeed: number = 0.01\`: End animation if speed drops below this value **and** \`restDelta\` is \`true\`.
## Methods
### Action methods
\`spring()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the action and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`spring().start()\` returns:
- \`stop(): void\`
## Example
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/stagger.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Stagger
Stagger the execution of a series of actions.
## Import
\`\`\`javascript
import { stagger } from 'popmotion';
\`\`\`
## Usage
\`\`\`typescript
stagger(actions: Action[], interval: number | (i: number) => number): Action
\`\`\`
\`stagger\` accepts two arguments, an array of actions and an \`interval\`:
\`\`\`javascript
stagger([
tween(),
spring()
], 100)
\`\`\`
When started, it outputs the values as an array. Actions that haven't yet started will output \`undefined\`, and you can define a default.
\`\`\`javascript
stagger([
tween(),
spring()
], 100).start((values) => values.forEach((v = 0, i) => {
console.log(v);
}))
\`\`\`
## Methods
### Action methods
\`stagger()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the tween and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`stagger().start()\` returns:
- \`stop(): void\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/stylefire.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
[Moved to the Stylefire docs](/stylefire/api/styler).`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/svg.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
[Moved to the Stylefire docs](/stylefire/api/svg).`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/timeline.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Timeline
Timeline is used to quickly script complex sequences of animation, split across independent tracks.
It returns a [\`tween\`](/api/tween), which allows the use of \`loop\`, \`flip\` and \`yoyo\` props, as well as tween methods like \`pause\` and \`resume\`.
## Import
\`\`\`javascript
import { timeline } from 'popmotion';
\`\`\`
## Usage
### Create a sequence of animations
\`timeline\` accepts an array of playhead instructions.
A playhead instruction can either be an:
- Animation
- Absolute or relative timestamp
- Array of animations
#### Animation
Each animation is defined as an object. This looks a lot like a simplified \`tween\`, with \`from\`, \`to\`, \`duration\` and \`ease\` properties:
\`\`\`javascript
timeline([
{ track: 'ballX', from: 0, to: 300, duration: 1000 }
])
\`\`\`
There's an extra, non-optional property called \`track\`. No two animations should overlap on the same \`track\`, and \`timeline\` will output every track to the reaction given to \`start\`:
\`\`\`javascript
timeline([
{ track: 'ballX', from: 0, to: 300, duration: 1000 }
]).start((v) => console.log(v.ballX))
\`\`\`
If we provide a second animation, it will (by default) play **after** the first:
\`\`\`javascript
timeline([
{ track: 'ballX', from: 0, to: 300, duration: 1000 },
{ track: 'ballY', from: 0, to: 300 }
])
\`\`\`
In this example animation, the second animation will start after \`1000\` milliseconds, as that's when the first ends (as defined by \`duration\`).
#### Timestamps
We can, however, move the playhead from that default position. If we provide a number as the next instruction, the playhead will move to that position.
In this example, the second animation will start after \`500\` milliseconds:
\`\`\`javascript
timeline([
{ track: 'ballX', from: 0, to: 300, duration: 1000 },
500,
{ track: 'ballY', from: 0, to: 300 }
])
\`\`\`
If we instead provide a string, we can move the playhead **relative** to the current timestamp with either \`'-'\` or \`'+'\` instructions. For instance, this time the second animation will start after \`800\` milliseconds:
\`\`\`javascript
timeline([
{ track: 'ballX', from: 0, to: 300, duration: 1000 },
'-200',
{ track: 'ballY', from: 0, to: 300 }
])
\`\`\`
#### Parallel and stagger
Animations can be played in parallel, from the same point in time, by providing them in an array.
In this example, both animations provided after the first animation will play after \`1000\`ms:
\`\`\`javascript
timeline([
{ track: 'ballX', from: 0, to: 300, duration: 1000 },
[
{ track: 'ballX', to: 0 },
{ track: 'ballY', from: 0, to: 300 }
]
])
\`\`\`
If we provide a number as the last item in the array, \`timeline\` will stagger over all the other items in the array with this delay:
\`\`\`javascript
timeline([
{ track: 'ballX', from: 0, to: 300, duration: 1000 },
[
{ track: 'ballX', to: 0 },
{ track: 'ballY', from: 0, to: 300 },
50
]
])
\`\`\`
#### Colors and multiprops
\`timeline\` can animate colors:
\`\`\`javascript
{ track: 'ballBackgroundColor', from: '#f00', to: '#fff' }
\`\`\`
Objects:
\`\`\`javascript
{ track: 'ball', from: 0, to: { x: 300, y: 300 } }
\`\`\`
And n-dimensional arrays:
\`\`\`javascript
{ track: 'foo', from: [300, 500], to: [0, 0] }
\`\`\`
### Types
\`\`\`typescript
type Value = number | string | (number | string)[] | { [key: string]: number | string };
type AnimationDefinition = {
track?: string,
from?: Value = 0,
to?: Value = 1,
ease?: EasingFunction = easeOut,
duration?: number = 300
};
type Instruction = string | number | AnimationDefinition | AnimationDefinition[];
timeline(instructions: Instruction, props: Props): Action
\`\`\`
## Props
These can be passed as the second argument to \`timeline\` and are used to define the behaviour of the master playhead.
- \`duration?: number\`: Total duration of animation, in milliseconds. By default, this is calculated by the instructions provided to \`timeline\`, but if manually overridden will rescale the whole animation.
- \`elapsed?: number = 0\`: Duration of animation already elapsed, in milliseconds.
- \`ease?: Easing | Vector[Easing] = linear\`: A function, given a progress between \`0\` and \`1\`, that returns a new progress value. Used to affect the rate of playback across the duration of the animation.
- \`loop?: number = 0\`: Number of times to loop animation on \`complete\`.
- \`flip?: number = 0\`: Number of times to flip animation on \`complete\`.
- \`yoyo?: number = 0\`: Number of times to reverse tween on \`complete\`.
## Methods
### Action methods
\`timeline()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the tween and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`timeline().start()\` returns:
- \`getElapsed(): number\`: Returns time elapsed in milliseconds.
- \`getProgress(): number\`: Returns animation progress as a value of \`0\`-\`1\`.
- \`seek(progress: number): this\`: Seeks animation to this position as a value of \`0\`-\`1\`.
- \`pause(): this\`
- \`resume(): this\`
- \`reverse(): this\`: Reverses the direction of playback.
- \`stop(): void\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/transformers.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Transformers
Transformers are used to take a value, transform it in some way, and then return it. Because all transformers have the same signature, they can be easily composed.
## Import
\`\`\`javascript
import { transform } from "popmotion";
\`\`\`
## Usage
As pure functions, transformers can be used in any situation where you want to change one value into another.
However, they're particularly useful with the [action](/api/action) \`pipe\` method. For instance, here we can easily compose some functions that will ensure that the \`tween\`'s output will always be a valid RGB value:
\`\`\`javascript
import { tween, transform } from "popmotion";
const { pipe, clamp } = transform;
tween({ to: 255 })
.pipe(clamp(0, 255), Math.round)
.start(v => console.log(v));
\`\`\`
## Preset transformers
### \`appendUnit\`
Returns a function that, when given a value, returns that value appended with the provided \`unit\`.
\`appendUnit(unit: string[])\`
\`\`\`javascript
const convertToPx = appendUnit("px");
convertToPx(5); // '5px'
\`\`\`
### \`applyOffset\`
Takes the offset of the provided value from \`from\`, and applies it to \`to\`.
\`applyOffset(from: number, to: number)\`
\`applyOffset(to: number)\`
\`\`\`javascript
// With two arguments
applyOffset(0, 10)(20); // 30
// With one argument
const offsetFromFirst = applyOffset(10);
offsetFromFirst(20); // 10
offsetFromFirst(21); // 11
\`\`\`
### \`bezier\`
Returns a function that, provided a progress value from \`0\` to \`1\`, will return a resolved number from the provided bezier array.
Can resolve either 3 or 4 bezier points. For more points, the [original implementation](https://github.com/hughsk/bezier) can be used.
\`bezier(...points: number[])\`
\`\`\`javascript
const resolveBezier = bezier(0, 1, 2, 3);
resolveBezier(0); // 0
resolveBezier(0.5); // 1.5
resolveBezier(1); // 3
\`\`\`
### \`blendColor\`
Given two colors, returns a function that takes a progress value (0 - 1) and returns a correctly blended color.
Watch [Computer Color is Broken](https://www.youtube.com/watch?v=LKnqECcg6Gw) for more information.
\`blendColor(colorA , colorB )\`
\`\`\`javascript
const blendRedToBlue = blendColor("#f00", "#00f");
blendRedToBlue(0.5); // Returns blended object with rgba properties
\`\`\`
### \`clamp\`
Returns a function that restricts given values to within the provided range.
\`clamp(min: number, max: number)\`
\`\`\`javascript
const rgbRange = clamp(0, 255);
rgbRange(256); // 255
\`\`\`
### \`conditional\`
Conditionally applies a transformer if \`check\` returns \`true\`.
\`\`\`javascript
const constrainWithSpring = conditional(v => v < 0, nonlinearSpring(50, 0));
\`\`\`
### \`interpolate\`
Returns a function that, when passed a value, interpolates from the \`inputRange\` to the \`outputRange\`.
An optional array of easing functions can be passed as the third argument, otherwise linear interpolation will be used by default.
Provided values outside the given ranges will be clamped to the output range limits.
**Note:** The \`inputRange\` must be in linear order. ie \`[100, 200, 300]\` and \`[100, 0]\` are valid, whereas \`[100, 50, 200]\` is not.
\`interpolate(inputRange: number[], outputRange: number[], ease: Easing[])\`
\`\`\`javascript
const invert = interpolate([0, 100], [100, 0]);
invert(75); // 25
const foo = interpolate([0, 50, 100], [0, 0.5, 0]);
foo(75); // 0.25
\`\`\`
### \`pipe\`
Used to compose other transformers, from left to right. The first argument passed to the returned function will be the value and any subsequent arguments will be passed to all functions unaltered.
\`pipe(...funcs:(v: any) => any[])\`
\`\`\`javascript
const rgbType = pipe(clamp(0, 255), Math.round);
rgbType(12.25); // 12
\`\`\`
### \`smooth\`
Will smooth a value over time.
\`smooth(strength: number)\`
**Note:** As \`smooth\` maintains an internal state, it must be initialised individually for every numerical value you wish to smooth.
### \`snap\`
Given a number or an array of two or more numbers, returns a function that will snap a given value to the nearest multiple or to the nearest number in the array.
\`snap(positions: number[])\`
\`\`\`javascript
const snapToIntervals = snap(45);
snapToIntervals(89); // 90
const snapToArbitraryDegrees = snap([0, 90, 270, 360]);
snapToArbitraryDegrees(75); // 90
\`\`\`
### \`steps\`
Given a number of steps and a range, returns a function that will fix a given value to the specific number of discrete steps within that range.
\`steps(steps: number, min: number, max: number)\`
\`\`\`javascript
const threeStep = steps(3, 0.4);
threeStep(0.1); // 0
threeStep(0.4); // 0.5
threeStep(0.9); // 1
\`\`\`
### \`linearSpring\`
Creates a spring that, given an elasticity (a value less than \`1\`) and an origin, will treat the provided value as a displacement.
\`linearSpring(elasticity: number, origin: number)\`
### \`nonlinearSpring\`
Creates a spring that has a non-linear effect on the displacement - the greater the displacement, the greater effect on the provided value.
\`nonlinearSpring(elasticity: number, origin: number)\`
### \`generateStaticSpring\`
A function that can generate new static springs like \`linearSpring\` and \`nonlinearSpring\`.
This function is actually used to create those functions:
\`\`\`javascript
const linearSpring = generateStaticSpring();
const nonlinearSpring = generateStaticSpring(Math.sqrt);
\`\`\`
\`\`\`
generateStaticSpring(alterDisplacement: (displacement: number) => number)
=> (elasticity: number, origin: number)
\`\`\`
### \`transformMap\`
Accepts an object of named transformers that expects \`v\` of the same structure. Applies those transformers to the corresponding property in \`v\` and outputs.
\`transformMap(map: { [key: string]: (v: any) => any })\`
\`\`\`javascript
const foo = transformMap({
x: v => v + "px",
y: v => v + "%"
});
foo({ x: 5, y: 10 }); // { x: '5px', y: '10%' }
\`\`\`
### \`wrap\`
Wraps a number around.
\`wrap(min: number, max: number)\`
\`\`\`javascript
physics({ velocity: 1000 })
.pipe(wrap(100, 400))
.start(v => console.log(v));
\`\`\`
### Value type transformers
Transformers that can convert from numbers and objects into value types like px or hsla can be found in the [\`style-value-types\` package](/api/value-types).
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/tween.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Tween
Animate between two values over a set duration of time.
The behaviour and feel of the animation can be affected by providing a different [easing](/api/easing) function.
## Import
\`\`\`javascript
import { tween } from 'popmotion';
\`\`\`
## Usage
By default, \`tween\` will tween from \`0\` to \`1\` over \`300\` milliseconds, with \`easeOut\` easing.
\`\`\`javascript
tween()
.start((v) => console.log(v));
\`\`\`
Pass props to adjust the character of the tween:
\`\`\`javascript
tween({ to: 200, duration: 500 })
.start((v) => console.log(v));
\`\`\`
\`from\`, \`to\` and \`ease\` can also be defined as \`Vectors\`, which are either a map or array of numbers:
\`\`\`javascript
tween({
from: { x: 124, y: 200 },
to: 0, // Both x and y will tween to 0
ease: { x: easing.easeOut, y: easing.easeIn }
})
\`\`\`
\`\`\`javascript
tween({
from: [0, 5, 20],
to: [100, 200, 300],
ease: easing.linear
})
\`\`\`
\`tween\` also supports colors:
\`\`\`javascript
tween({ from: '#000', to: 'rgba(255, 0, 0, 0.5)' })
\`\`\`
Colors can also be provided to arrays or objects:
\`\`\`javascript
tween({
from: {
x: 0,
background: 'hsla(125, 100, 50, 1)'
},
to: {
x: 100,
background: 'hsla(20, 100, 60, 1)'
}
})
\`\`\`
## Props
- \`from?: number | Vector[number] = 0\`: Start value of animation.
- \`to?: number | Vector[number] = 1\`: End value of animation.
- \`duration?: number = 300\`: Total duration of animation, in milliseconds.
- \`elapsed?: number = 0\`: Duration of animation already elapsed, in milliseconds.
- \`ease?: Easing | Vector[Easing] = easeOut\`: A function, given a progress between \`0\` and \`1\`, that returns a new progress value. Used to affect the rate of playback across the duration of the animation.
- \`loop?: number = 0\`: Number of times to loop animation on \`complete\`.
- \`flip?: number = 0\`: Number of times to flip animation on \`complete\`.
- \`yoyo?: number = 0\`: Number of times to reverse tween on \`complete\`.
## Methods
### Action methods
\`tween()\` returns:
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new action that will run \`update\` values through this sequence of functions.
- \`start(update | { update, complete })\`: Starts the tween and returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new action that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`tween().start()\` returns:
- \`getElapsed(): number\`: Returns time elapsed in milliseconds.
- \`getProgress(): number\`: Returns animation progress as a value of \`0\`-\`1\`.
- \`seek(progress: number): this\`: Seeks animation to this position as a value of \`0\`-\`1\`.
- \`pause(): this\`
- \`resume(): this\`
- \`reverse(): this\`: Reverses the direction of playback.
- \`stop(): void\`
## Example
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/value-types.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Value Types
To help convert numerical values into commonly-used special value types, like \`px\` or \`hex\`, we provide an optional module called \`style-value-types\`:
\`\`\`bash
npm install style-value-types --save
\`\`\`
Each value type has three functions:
- \`test\`: Returns \`true\` if the provided value is of that type.
- \`parse\`: Returns the value in a format suitable for animation. Either a \`number\` or \`{ [key: string]: number }\`.
- \`transform\`: The reverse of \`parse\`. Accepts a \`number\` or map of named numbers and converts that into the value type.
## Import
From Popmotion:
\`\`\`javascript
import { valueTypes } from 'popmotion';
const { color } = valueTypes;
\`\`\`
Or, either to save bytes and import \`valueTypes\` separately, or to use as a stand-alone library:
\`\`\`javascript
import { color } from 'style-value-types';
\`\`\`
## Example
\`\`\`javascript
// Test
color.test('#fff'); // true
color.test(0); // false
// Parse
color.parse('rgba(255, 255, 255, 0)');
// { red: 255, green: 255, blue: 255, alpha: 0 }
// Transform
color.transform({ hue: 200, saturation: 100, lightness: 50, alpha: 0.5 });
// 'hsla(200, 100%, 50%, 0.5)'
\`\`\`
## Included value types
- \`alpha\`: \`Number\` between \`0\` and \`1\`
- \`complex\`: \`String\` containing arbitrary sequence of numbers mixed with other characters. See below.
- \`color\`: \`String\` of either \`hex\`, \`hsla\` or \`rgba\` type
- \`degrees\`: \`String\` ending in \`deg\`
- \`hex\`: \`String\` beginning with \`#\` and followed by 3 or 6-digit hex code
- \`hsla\`: \`String\` with valid \`hsla\` property
- \`percent\`: \`String\` ending in \`%\`
- \`px\`: \`String\` ending in \`px\`
- \`scale\`: \`Number\` with a \`default\` of \`1\` instead of \`0\`
- \`rgbUnit\`: Integer between \`1\` and \`255\`
- \`rgba\`: String in \`rgba(rgbUnit, rgbUnit, rgbUnit, alpha)\` format
## Complex type
The \`complex\` value type is slightly different to the others. Instead of a \`transform\` method, it has a \`createTransformer\` method which returns the \`transform\` method:
\`\`\`javascript
const svgPath = 'M150 0 L75 200';
const transform = complex.createTransformer(svgPath);
\`\`\`
The returned \`transform\` function is unique to the string given to it. When this function is provided an object of the same format as returned by \`complex.parse()\` (in this example \`complex.parse(svgPath)\`), it will use the original string as a template.
Example:
\`\`\`javascript
transform({
'0': 300,
'1': 0,
'2': 100,
'3': 200
}); // Returns 'M300 0 L100 200'
\`\`\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/api/value.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Value
A [multicast reaction](/api/multicast) that tracks the state of a number and allows velocity queries.
## Import
\`\`\`javascript
import { value } from 'popmotion';
\`\`\`
## Usage
\`\`\`typescript
value(initialValue, onUpdate);
\`\`\`
\`\`\`javascript
import { tween, value } from 'popmotion';
import styler from 'stylefire';
const div = styler(document.querySelector('div'));
const divX = value(0, div.set('x'));
tween({ to: 500 }).start(divX);
setTimeout(() => console.log(() => {
physics({
velocity: divX.getVelocity()
}).start(divX); // This will automatically \`stop\` the tween
}), 150);
\`\`\`
\`value\` can also handle objects and arrays:
\`\`\`javascript
const foo = value({ x: 0, y: 0 });
const bar = value([0, 0]);
foo.getVelocity(); // { x: 0, y: 0 }
bar.getVelocity(); // [0, 0]
\`\`\`
As a multicast reaction, you can optionally \`subscribe\` with multiple other reactions rather than providing an initial subscription:
\`\`\`javascript
const foo = value(0);
foo.subscribe(() => console.log('first reaction'));
foo.subscribe(() => console.log('second reaction'));
\`\`\`
## Methods
### Reaction methods
\`value()\` returns:
- \`get(): number\`: Returns the current value state.
- \`getVelocity: number\`: Returns the current value velocity.
- \`filter((v: any) => boolean)\`: Returns a new action that filters out values when the provided function returns \`false\`.
- \`pipe(...funcs: Array<(v) => v)\`: Returns a new reaction that will run \`update\` values through this sequence of functions.
- \`stop()\`: Stops parent action.
- \`subscribe(update | { update, complete })\`: Returns a subscription.
- \`while((v: any) => boolean)\`: Returns a new reaction that will \`complete\` when the provided function returns \`false\`.
### Subscription methods
\`value().subscribe()\` returns:
- \`unsubscribe(): void\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/blog/20170703-choosing-a-default-easing-for-popmotion.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Choosing the correct default easing for Popmotion
Choosing default values for a library is difficult. Should you be the benefactor/victim of a wider audience:
- These defaults will be used, often unwittingly, by your consumers.
- You can change them only at the risk of breaking other people's stuff.
In a motion engine, these choices manifest themselves in a visual and visceral way. A duration here, a spring tightness there.
This means there's responsibility to choose values intelligently, so that the little patches of the web that use your library benefit from more responsive motion.
As the userbase of Popmotion is relatively small, I've always felt comfortable shirking the shackles of API stability until I finally felt like it had arrived at a the Good Place. Apologies to anyone who's lived that.
Luckily, over the years of wildly oscillating GitHub activity graphs,
there has been one (one) decision that I felt like I got right at the start: Making the \`tween\` default easing property \`easeOut\`.

## A brief history of timing
Like many greying front end devs, my first exposure to UI animation was [Greensock's](https://greensock.com) then-ActionScript library, TweenMax. For a platform so disposed to garish show-off loading screen animations, Flash animations were disgusting to hand code unless you were using TweenMax.
And that's largely what we used it for: showy animations. The Flash days were as over-the-top and as divorced from the user's interests as the Amiga demoscene. All you wanted to do was play your cracked version of Worms but first you had to sit through the seizure-inducing cock-prancing of some git in Norwich. At least Flash intros occasionally had a skip button.

Greensock, in its sunrise years, had the right intentions. It did, and still to this day, has an \`easeOut\` as default. That's because the author understands, despite our hedonism and heathenism, that animation in UI design **should** be as a result of a user's action. \`easeOut\` is snappy, and as we'll see later, "snappy" is how we should respond to user actions.
Flash died, and the community collectively denied any involvement.

Then it was all about jQuery. jQuery was incredible for literally **everything**, which in the new world of composable asynchronous micropackages is probably why we again collectively deny any involvement, and why I'm talking about it in the past tense even while it's standing **right there**, watching us.
For me, and maybe for you, the coolest thing about jQuery was \`animate\`. It was amazing to easily get something resembling movement in humble HTML. Even if it defaulted to \`swing\` easing, a tepid, muddy \`easeInOut\` of homeopathic strength.
That easing was coupled with a default duration of \`400\`ms. Then that was folded into our neanderthal, performance-ignorant tweening of the CPU-choking, layout-breaking \`width\` and \`height\` properties. Lethargic animation ruled the internet.

## The problem with \`easeInOut\`
To arrive at an actual point, the problem with \`easeInOut\` is that it **feels** incredibly slow. When we [watch videos about the principles of animation](https://vimeo.com/93206523), one of the things we're taught is that objects in motion don't start in motion and they don't stop dead.
They start slow, and end slow.
So that sounds about right, we think, and then we stop thinking, and use \`easeInOut\` by choice, or a framework like jQuery makes it default, and then everyone uses it, by default, and everything feels sluggish, by default. Why?
## Why
Our eyes and brain are hardwired to detect motion. We've evolved to find that motion distracting. Motion indicates that the present situation has changed, and we need to react if we'd prefer to survive. We **need** motion to be distracting.
When you're scrolling through a website, and a space opens for an advert in the middle of your article, and everything jumps around, and then after a couple of seconds your crap, overpriced "4G" connection finally loads the banner ad, which animates on loop, until you scroll past it, when it then needlessly closes, god I hate Motherboard, your text jumps back up, you think of the poor bastard forced to make it, their hopes, their dreams, their broken dreams - all of those things are distracting.
That motion, the things causing it: They're not imparting useful information, they're not even trying to kill you. So it's annoying.
It's all shit, the user isn't involved in any of this, he's happy about the broken dreams. And yet, \`easeInOut\` is entirely appropriate, it is the least damaging easing to use for uninstigated animation, it is the right choice.
What does it say about an easing that **this** is a good use case for it?
## It's sometimes okay, I guess
A **very** tiny amount of uninstigated motion is fine in a UI, as long as it correctly indicates that **the situation has changed, and the user needs to react**. A small "new message" dot, probably. Or a notification panel, possibly.
For this kind of animation, an \`easeInOut\` or \`anticipate\` is very appropriate because it gives the user a little bit of time to acclimatise to the movement, and it stops quite gently, naturally. It adheres to the principles of animation, these dots and panels now feel a little more real, your UI a little more alive.
It's certainly better than things just flicking around the page without the user understanding what's changed.
## But usually it very much isn't
The **vast majority** of animation in UI is, or should be, user instigated.
For these animations, these interactions, the user already knows that an action has taken place. **They** started the motion, and the situation has changed, and the UI needs to react. The "in" of an \`easeIn\` or \`easeInOut\` feels sluggish in this instance because for god's sake hurry up, get on with it.
The energy and momentum of \`easeInOut\` starts in the virtual reality and stays there. The energy and momentum \`easeOut\`, conversely, starts in the user's reality and ends in the interface's.
It takes the user's energy, directly from their finger, through the wire or screen, straight into the button, the tooltip, the whatever, and bursts with the maximum energy at that initial moment of contact.
\`easeInOut\` is barely bothered to get out of bed, like a stupid stroppy teenager who probably doesn't even have a finger. Probably.
A user will feel that difference, even if it's subconscious, even if I have to pretend to myself that deep down they **know**, just to justify the hours of my life I've spent writing (and now writing about) animations.
This is why Greensock comes with \`easeOut\` by default. It's why [Anime.js](http://animejs.com/) uses a zinger \`elasticOut\`. And it's why Popmotion uses \`easeOut\` too.
Whereas, CSS transitions default to the \`swing\`-alike \`ease\`, which is exactly as bland as it sounds. Worse, the native Web Animations API uses \`linear\` easing, which is, frankly, a fucking crime.

## Feel it for yourself
Here's \`easeInOut\`.
\`\`\`marksy
{\`
const ball = document.querySelector('#a .ball');
const ballRenderer = styler(ball);
tween({
to: 300,
duration: 500,
ease: easing.easeInOut
}).start((x) => ballRenderer.set('x', x));
\`}
\`\`\`
Buuuuuuurrrrr. END ME.
Conversely, here's an \`easeOut\` with the Popmotion default of \`300\`ms.
\`\`\`marksy
{\`
const ball = document.querySelector('#b .ball');
const ballRenderer = styler(ball);
tween({
to: 300,
ease: easing.easeOut
}).start((x) => ballRenderer.set('x', x));
\`}
\`\`\`
I. Am. Blushing.
Notice that when you press start on these examples, you're pressing the button yet it's the ball that's moving. Even with this physical disconnect, the first example feels like you're **telling** the ball to move, and then it moves of its own accord. Whereas the second example, you're **making** the ball move, it's using **your** energy.
It makes a difference! Users can tell.
It does. It makes a difference.It does.Userscantellitmakesadifferencepleasemakeadifferenceuserscantell
---
[Get started with Popmotion](/learn/get-started)
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/blog/20170704-manually-set-scroll-while-ios-momentum-scroll-bounces.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# How to manually set scrollTop during an iOS momentum bounce
Here's one to file under [the mobile is web is awful](/blog/20170710-mobile-web-is-awful-and-were-all-to-blame).
I've been writing a Slack-esque chat that lazy-loads new messages as you scroll up. The adventure of reliably anchoring scroll position as these new messages are added to the DOM is probably a blog post in itself, but there's a specific issue on iOS that I'd like to share solution to.
## The problem
Here's the code to restore a previous scroll position, (which has to be recalculated to accommodate the newly-loaded messages):
\`\`\`javascript
node.scrollTop = node.scrollHeight - prevScrollTop;
\`\`\`
Works great in every browser, except of course the modern-day IE:

As you can see, the scroll position stays at the top, almost as if we never manually reset the scroll position at all.
The problem, as I began to understand, was that if you try and manually set a node's (and probably the viewport's) scroll position **while iOS is doing it's out-of-bounds bouncy thing**, that scroll position will be immediately overridden by the rest of the bounce animation.
## The fix
To fix, we simply disable momentum scrolling on that element, restore scroll position, and then re-enable momentum scrolling:
\`\`\`javascript
node.style['-webkit-overflow-scrolling'] = 'auto';
node.scrollTop = node.scrollHeight - prevScrollTop;
node.style['-webkit-overflow-scrolling'] = 'touch';
\`\`\`
Here's how that looks:

Not as graceful as I'd prefer, but looking at the timestamps the user is still left on the correct message. It also avoids funny residual momentum effects, so I presume this technique can also be used to kill scroll momentum when a pointer moves from one overflowing node to another.
---
Popmotion is a 9kb JavaScript motion engine, and it'll give you way less bother than mobile Safari. Create awesome interactions with tweens, physics and input tracking.
[Excite your users with Popmotion](/learn/get-started) and/or [excite us with 140 characters](https://twitter.com/popmotionjs)
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/blog/20170710-mobile-web-is-awful-and-were-all-to-blame.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# The mobile web is awful, and everyone's to blame
At work today, we were discussing the future direction of the website and app. A concern was raised that, without a clear mandate, the website would "continue to be a great desktop version, and a much poorer version of the app."
As the front end lead, that stung. I felt like the "much" was undeserved.
I took the point. It's worth imagining that the use-case of the app and website may well be different.
However, the comment came as a response to a mini-moan that it'd taken me a couple hours to get an input field to stick to the bottom of the page and still be tappable in mobile Safari. A simple task made difficult.
It was an indictment that I haven't yet changed perceptions about the capabilities of the mobile web. Maybe those perceptions don't deserve to be changed.
I've long held that the web experience **can** be just as performant and polished as native. It's only for a lack of time and ambition that it isn't. That belief is the reason I wrote Popmotion in the first place.
So I began to think, it's usually easy and quite enjoyable to make a great desktop experience. Yet developing for mobile is a constant headache.
With some mobile Geekbench scores approaching laptops, the technical argument is diminishing. So why?
I realised that everyone is to blame. Browser vendors, Business People, every member of the Google AMP team, you reading this, me writing this: Everyone.
## Browsers Know Best
Implementing simple design features like overlays or bottom navigation on mobile browsers invariably involves trawling through blogs and forums to find that special hack that worked for that one guy that one day in June.
As every developer knows, no good hack goes unpunished. Testing cross-browser serves a specific cocktail; a nostalgic blend of heart palpitations, dream-like confusion and a palpable sense of impending disappointment. It takes you right back to '99.
It usually comes down to the same problem. Browsers Know Best. In their infinite wisdom, browser vendors implement novel or specific behaviour, ostensibly to aid performance and accessibility.
It's a noble aim but developing for browsers is already quite chaotic enough, thankyouverymuch. The absolute last thing the internet needs is more platform-specific behaviour. Let's take a look at some examples.
### Safari
A search for "Safari is the new IE" brings up enough results **refuting** that statement that there is, at the very least, a **vibe**.
It also wouldn't be surprising that a team of self-isolated Apple engineers working out of their space bunker would be susceptible to Browsers Know Best (BKB).
They've historically dragged their moonboots over implementing new performance-enhancing standards and APIs (like Service Workers), and what progress they do make is diminished by a lethargic release schedule intimately coupled with iOS and macOS.
Which is fine on desktop because virtually nobody gives a shit about desktop Safari. On mobile, however, the demographics are different, and this holds up progress.
### Bottom navigation
The behaviour that inspired this 2000-word rant.
Now we live in the future, the outrageous size of phones means they can't be safely touched in their upper halves. Reflecting this shift, design guidelines are increasingly recommending putting app navigation at the bottom of the screen.
Except, the greedy gits at Apple have colonised the bottom 40-ish pixels and used it as invisible magic space. Touch this, and the app navigation reappears.
In exchange for this genius piece of UX, you can't put a nav there, or an action button, or a link or or a chat box or whatever.
No, because we're dirty web peasants, we have to put our nav at the top, where no one can ever reach it. Unless, of course, we resort to the now-canonical hack detailed on [this god-given Eventbrite blog post](https://www.eventbrite.com/engineering/mobile-safari-why/), rightfully titled "Mobile Safari (Whyyyy?)". BKB.
Or what do \`100%\` and \`100vh\` mean in a viewport that constantly changes and never seems to report the right size? BKB.
Or why is it so difficult to lock page scroll when displaying an overlay, or prevent the page from scrolling to the top when you **do** manage to lock it, or or or. BKB.
### Get over here
Chrome isn't blameless, either.
In Chrome 56, the Chrome team set \`touch\` events to \`{ passive: true }\` by default. They [excuse themselves in this blog post](https://developers.google.com/web/updates/2017/01/scrolling-intervention).
\`passive: true\` allows a developer to reassure the browser that the page will not be calling \`e.preventDefault()\`, which would block page scroll. This allows the browser to respond immediately to scroll, which at risk of singing its praises, I don't remember ever being an issue in Safari. Is this an Android thing?
Either way, it's broken every instance of a draggable DOM element I've implemented, and every virtual DOM event-handler abstraction like those found in React. Thanks, Chrome team.
### A decade of this
It's been ten years of this shit.
Take \`hover\` events. A hover event **obviously** has no place firing on a touch device. Originally though, large parts of the web weren't written as an input-agnostic place. As a result, key functionality was hidden behind hover events.
To unbreak the web on mobile, browsers starting emulating \`hover\` on the very \`mousedown\`-looking \`touchstart\`.
So, we end up with very weird interactions, like having to double-tap to open menus that rely on hover. Or CSS hover styles enabling on taps. Or sticky hover states when you're merely trying to scroll the page. Or or or...
If Apple had the bravery to break Flash websites, where was this bravery with hover interactions? Just break them, stick to a standard, the web will fix itself.
Now, ten years later, we're still stuck with this. Scoping \`:hover\` effects to special capability classes and yet another esoteric behavior that's difficult to grok and embarrassing to explain to newcomers. "Well you see, in 2007..."
BKB.
## Business has feelings too
Whether the interests of business and user are polarised or harmonised is a debate much older than software.
As a wide-eyed, UX-minded front end dev, I'm predisposed to thinking the latter. I think most people **believe** they are.
Interests are hard to quantify. It's easy, then, for a business person to frame a stat in a way that makes interests look aligned.
In my own experience it's been an obvious, and incidentally statistical truth, that app users are stickier and more engaged with the product than mobile web users.
Somehow it then leads that it's in the user's interests to be sticky and engaged, which then leads into all manner of poor choices for the mobile web user's experience.
### Reddit
For example. I have installed two Reddit apps - Alien Blue and the official app. I usually use the desktop web version. Periodically, I'll be mid-surf and it'll be easier to hit the address bar, type "sta" and spent the next five minutes browsing the preloaded Star Trek subreddit.
My point is, as a user, **I'm engaged**. So why is it that I'm occasionally given the ol' mobile fuck you:

The USPs are mind-bending.
- **50% faster:** By what metric?
- **Infinite scrolling:** Infinite scrolling isn't difficult to implement, so why isn't it? Either the devs are lazy and/or incompetent, or much more likely the Business People needed another bullet point to whack on the pop-up and bag themselves a cheeky conversion. So there was a specific choice to make the web version worse.
- **Autoplay gifs:** BKB.
### Facebook
I had the Facebook app, once. Then, because an analyst got a whiff that Messenger had higher engagement on the stand-alone app, the Business People decided to force users to download that. Once that was downloaded, the analyst got a whiff that users were stickier when push notifications were enabled. So the Business People forced users to turn those on. So I deleted it and used the web instead.
The mobile messages worked perfectly. I was quite happy not to have Facebook on my phone - despite lacking a messenger, it's half a gig. The mobile site loaded near-instantly with just a couple hundred of progressively downloaded kb.
Sniff.
**Sniff.**

Now, when we click the "new messages" icon, we're treated to a splash screen telling us to download Messenger (or we were, there's presently a bug so it does literally nothing). This is purposeful degradation of a perfectly capable website and it is totally anti-user.
It is wild. I am wild.
## AMP
A lot has been made of the ill-comings of AMP (Accelerated Mobile Pages) recently, so I won't go too deep. My favourite is this [mic drop by The Register](https://www.theregister.co.uk/2017/05/19/open_source_insider_google_amp_bad_bad_bad/).
The complaints are usually the same; it's proprietary, Google is using you for your content, homogeneous, opaque, maintaining two views. Etc.
IMO the biggest problem is the flagrant abuse of power. Google has a special AMP section at the top of their search results, where AMP results get a nice picture. It's labeled "Top stories", but realistically it's just a carousel of sell-outs. Not that you can blame them.
Mobile browser usage is, on some sites, far higher than desktop. Yet can you imagine if Google created a special standard for desktop sites and showcased those at the top of their results? There'd be outrage. It'd be a clear power play. But this is just the shitty mobile web, so who cares, right?
Now, there's no denying that AMP pages are faster than the majority of the net and this definitely has some value for the user. AMP isn't magic though, and if a normal site is fast enough to be featured, why not include those, too?
We can't complain, though. AMP is our fault, yours and mine.
## Mostly you
I'll take a bit of this blame. You can take the rest.
Ultimately we're in this position because the mobile web is slow. We develop sites on our fancy Macbook Pros and fiber connections and then serve those sites to low-end Android phones on Edge networks.
When we're not being mindful of our user, this is our fault.
Or when we include expansive libraries like Lodash or Moment in their entirety, this is our fault.
Or when we serve a site without server-side rendering, this is our fault.
Or when we don't split our bundles, this is our fault.
Or or or.
## Do we even deserve redemption
John Gruber of [Daring Fireball](https://daringfireball.net) has been banging on recently about how JavaScript was a mistake, that the web would load in under a second if it didn't exist.
Well, maybe. It's an interesting thought experiment. Practically, JS does make things slower in the majority of cases. A little bit of power.
With care, and \`fetch\`, and code sharding, it can theoretically also make things quicker.
It can also be the case that a server-side rendered website with a JS payload of 100kb can belt down the network and render **quicker** than a bloated 500mb app can load into memory. There is opportunity here.
The address bar is an app launcher of power user speeds, and browsers can preload pages while the user is still thinking. No context-switching, or sloshing about with Apple's horrific app search, no need to delete, to arrange.
There's opportunity here, too.
## Proceed with caution
Browsers Still Know Best, and keep releasing breaking changes that we have to learn to hack around. Business interests will always inexplicably collide with user interests. Google will always want control.
And you'll always be lazy. Me, too, a bit. Mostly you.
Let's be optimistic though. JavaScript, for all Gruber's delicious tears, isn't going anywhere. Nor should it. It's important that there's a democratic and open platform, and in the emerging world of closed devices the **web is it**.
It's essential this platform remains open, and continues to become more and more capable.
We're getting WebAssembly, so it'll get faster. WebGL is gaining adoption and we'll see improved AR and VR support, so it'll become more capable.
Already, we can use tools like Webpack and [bundlesize](https://github.com/siddharthkp/bundlesize) to code split and maintain size budgets.
Tools like [Next.js](https://github.com/zeit/next.js/) makes SSR trivial to implement.
Hopefully, [Popmotion](/learn/get-started) can help create delightful, high-quality interactions.
We just need to have the opportunity to create the time and space to dedicate to polish and performance, rather blindly belting along the Business People's feature treadmill. Then next time there won't be a "much" in sight.
---
Popmotion is a 9kb JavaScript motion engine. Create awesome interactions with tweens, physics and input tracking.
[Make the mobile web marginally better](/learn/get-started) or [follow us on Twitter](https://twitter.com/popmotionjs)
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/blog/20170803-coding-style-dont-be-a-dick.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# When it comes to adhering to a project's coding style, follow the golden rule.
I just finished reading Ben Nadel's blog post, titled [After 3 Months Of JavaScript Linting, It's Pretty Much All Pain And No Gain](https://www.bennadel.com/blog/3312-after-3-months-of-javascript-linting-it-s-pretty-much-all-pain-and-no-gain.htm). It makes the argument that linting for maintaining a consistent style is burdensome.
I couldn't disagree more. In fact, I wouldn't disagree more even if the post was titled "2 plus 2 actually equals 5", a 500 word rebuttal of a hard, rudimentary, objective fact.
For both solo, pet projects, and sprawling codebases with multiple contributors, linting for maintaining style is essential.
Popmotion itself is just under 3,000 lines of code, I'm the primary contributor, and linting is useful even to keep just my messy self in check.
If anything, I'd argue it doesn't go far enough. The number of times I've argued with myself about whether I should declare as a \`function\` or an arrow function \`const\`, in different situations, and decided **different things**... I would to deprecate that from my life.
At work, five of us sit on about 100,000 lines of code. To you, this might be relatively big, or relatively small. It's all relative. Here, linting is one of many tools (types, tests) that have helped keeps us sane, for reasons we'll explore.
> [...] the goal of consistency is a complete charade.
A charade? Well it isn't a charade in the sense that linting does a great job of achieving that goal. To use Ben's own examples, indentation, quotes etc are easily kept consistent. So it's a slightly self-defeating argument.
One example given is \`let\` vs \`const\`. I don't understand how this is stealth-bundled in amongst actual stylistic choices. There is a right way and a wrong way to use these, they have semantic and practical meaning and a linter helps here, too.
But I suppose his question is, why would you want to? The following quotes are, with varying quantities of melodrama, essentially saying **because I don't want to**.
> [...] my approach to code formatting is the best. If it weren't, I wouldn't use it.
And a doozy:
> When I'm asked to change my style, I am - quite literally - asked to deny a fundamental Truth of my being.
Me, I know best, you do it your way, I do it my way. But my way's the best way.
This attitude simply isn't very team-like. One of the nice things about code consistency is simply the human interaction that leads you to your linting settings. It is team bonding. Not as bonding as oversharing at the pub, but certainly bonding, learning how to compromise and sharing ownership.
When we originally set up linting at work, I insisted on semicolons. The team didn't feel strongly either way, fine, semicolons. The team liked two space indentations. Despite my preference for four, I compromised on two. I now like two. And safely refactoring Popmotion to also use two was a simple as changing - yes - my linting config.
On a more practical note, this discussion settles the argument early. You don't "ugh" when you inevitably stumble across four-space indentations and you don't get passive-aggressive PRs back-and-forthing over indentation.
The thought hurts my brain a little. Are there seriously projects out there where lines are written by Bobbie Two-Spaces, interleaved with heathen lines written by Four-Space Jimmy?
If we remember to consider that our own stylistic **preferences** are **not** a "fundamental Truth of [our] being", we'll immediately arrive at a more collaborative, less combative mindset and thus workplace.
It's fine. Nobody is dying today.
The sober point comes a little later:
> [...] code is so much more than indentation and quotation choices.
It is true that code is an expression, even an artistic one. If the one thing we can all agree on is that indentation and quotation choices are essentially petty, then we won't feel so precious about them and start to trust that our character will come through in the actual solutions we choose and invent.
We're not selling out through compromise.
It doesn't make things hard, it doesn't make things painful. It is a collaborative effort and an act of imposed self-discipline. It's the act of letting go of ego, a little, and acting as a team. Because we're not all fucking "peacocks". We're just assholes, trying to do something cool.
---
Popmotion is a 9kb JavaScript motion engine, and way better than this daft nonsense. Create awesome interactions with tweens, physics and input tracking.
[Make things lovely](/learn/get-started) and/or [send us your stupid abuse on Twitter](https://twitter.com/popmotionjs)
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/blog/20171210-popmotion-8-upgrade-guide.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Popmotion 8 upgrade guide
This guide will explain the breaking changes in Popmotion 8 and how to upgrade your project from version 7.
## \`onUpdate\` and \`onComplete\`
The biggest change, from a practical point of view, is the way animations are initialised.
Previously, a \`tween\` might have been defined like this:
\`\`\`javascript
tween({
from: 0,
to: 1,
onUpdate: (v) => console.log(v)
}).start();
\`\`\`
Now, that \`onUpdate\` is instead provided to \`start\`:
\`\`\`javascript
tween({ from: 0, to: 1 })
.start((v) => console.log(v));
// This is equivalent:
tween({ from: 0, to: 1 })
.start({
update: (v) => console.log(v)
});
\`\`\`
Likewise, \`onComplete\` also provided to \`start\`:
\`\`\`javascript
tween({
from: 0,
to: 1
}).start({
update: (v) => console.log(v),
complete: () => console.log('complete!')
})
\`\`\`
## Immutable
Each animation, once defined, is immutable. Once \`start\` is called, a **new instance** of that animation is created and executed. Which means you can define an animation once:
\`\`\`javascript
const foo = tween({ from: 0, to: 1 });
\`\`\`
And use that definition to power multiple animations:
\`\`\`javascript
foo.start((v) => console.log('first animation', v));
foo.start((v) => console.log('second animation', v));
\`\`\`
## Playback controls
This means that \`stop\`, and other playback controls, aren't returned by \`tween\`, they're returned from \`start\`:
\`\`\`javascript
const myTween = foo.start(console.log);
myTween.stop();
\`\`\`
## \`transform\` property
Previously, there was a \`transform\` property available to actions. It accepted a pure function that accepted a value and returned a new one.
In Popmotion, we refer to these as [transformers](/api/transformers). As the API has evolved, its skewed towards using these pure functions to define functionality. Documents increasingly included references to the \`pipe\` transformer, which composes a new transformer from multiple others.
In Popmotion 8, \`pipe\` has been promoted to an action method. When used, it returns **a new version of that action** that pipes all \`update\` values through the functions given to \`pipe\`:
\`\`\`javascript
const double = (v) => v * 2;
const px = (v) => v + 'px';
const foo = tween({ from: 0, to: 1 });
const bar = foo.pipe(double, px);
foo.start(console.log); // Ends with 1
bar.start(console.log); // Ends with '2px'
\`\`\`
## Chainable
\`pipe\` isn't the only chainable functions available to actions. There's also \`while\` and \`filter\`, and an API to add new functions will be available in the near future.
Here's a \`listen\` action, which is our [DOM event listener action](/api/listen), that only fires when it detects more than two \`touches\`:
\`\`\`javascript
listen(document, 'touchstart')
.filter(({ touches }) => touches.length > 1)
.start(({ touches }) => {
console.log(touches.length); // more than 1
});
\`\`\`
## \`value.set\`
\`value\` is now a **reaction**. Which means you can still pass one to an action like this:
\`\`\`javascript
const foo = value(0, console.log);
spring({ to: 300 }).start(foo);
\`\`\`
As \`start\` accepts reactions either as a function or an object with an \`update\` function, \`value.set()\` becomes \`value.update()\`.
## \`getVelocity\`
Previously, every action has a \`getVelocity\` method. In Popmotion 8 I'm attempting to reduce statefulness and move to reactive streams.
\`value\` is now the only stateful function to offer a \`getVelocity\` function. It's a special type of reaction that maintains state.
\`\`\`javascript
const ball = document.querySelector('#ball');
const ballX = value(0, styler(ball).set('x'));
spring({ to: 400, stiffness: 500 }).start(ballX);
setTimeout(() => console.log(ballX.getVelocity()), 200);
\`\`\`
## Every animation can handle color
The \`colorTween\` animation has been replaced by the capability for every animation to handle colors. Even \`spring\` and \`physics\`!
So this:
\`\`\`javascript
colorTween({ from: '#fff', to: '#000' })
\`\`\`
Is now this:
\`\`\`javascript
tween({ from: '#fff', to: '#000' })
\`\`\`
## Every animation can handle arrays and objects
In Popmotion 7, animation of arrays and objects was achieved via the \`parallel\` and \`composite\` actions respectively.
These functions still exist in order to compose different animations. But when the animations are the same, this syntax has been greatly simplified:
\`\`\`javascript
physics({
from: 0,
velocity: {
x: 100,
y: 200
}
}).start(console.log) // Receives { x, y }
\`\`\`
\`value\` can now also be an n-dimensional array or object too, and \`value.getVelocity\` will return velocities in the defined format.
## \`stagger\`
As the \`update\` method is bound to an animation only when \`start\` is called, \`stagger\` now accepts functions that can **optionally** return a started action. So this:
\`\`\`javascript
stagger([
tween({ onUpdate: (v) => console.log('1st', v) }),
tween({ onUpdate: (v) => console.log('2nd', v) })
], 50).start();
\`\`\`
Becomes:
\`\`\`javascript
stagger([
() => tween().start((v) => console.log('1st', v)),
() => tween().start((v) => console.log('2nd', v))
], 50).start();
\`\`\`
This also means that \`stagger\` can iterate over any function, not just animations.
## \`touch\` is now \`multitouch\`
The \`touch\` action has been renamed \`multitouch\`.
Previously, it provided \`onUpdate\` with an array of touches. Now, \`touches\` is a property that sits alongside \`scale\` and \`rotate\` properties:
\`\`\`javascript
multitouch().start(({ touches, scale, rotate }) => {});
\`\`\`
## And the rest
- \`physics\` \`spring\` property is now \`springStrength\`.
- \`physics\` \`autoStopSpeed\` property is now \`restSpeed\`.
- \`spring\` \`restDisplacement\` property is now \`restDelta\`.
- \`flow\` alias for \`pipe\` has been removed.
- \`add\`, \`subtract\`, \`divide\`, \`multiply\`, \`conditional\`, \`alpha\`, \`percent\`, \`degrees\`, \`px\`, \`rgbUnit\`, \`rgba\`, \`hex\`, \`color\`, \`hsla\` transformers have been removed. Value type transformers like \`hex\` can still be accessed from the [valueTypes](/api/value-types).
- Render loop function must be imported separately from [Framesync](https://github.com/Popmotion/framesync)
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/blog/20171211-introducing-popmotion-8.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Introducing Popmotion 8
Popmotion 8 is a functional, reactive animation library.
It's the result of over three months work, the result of wanting to add a humble \`pipe\` method and instead tumbling down the rabbit hole.
It introduces new animations: \`decay\`, \`everyFrame\`, \`keyframes\` and \`timeline\`. It makes input dragging a breeze with our revamped \`pointer\` and \`multitouch\` actions.
Crucially, there's a new streamlined, reactive API.
This new API reflects Popmotion's gradual shift towards functional programming and adopts it as a core part of the design philosophy.
The result is a small, flexible and composable library that I hope you'll find fun to use. Let's take a look at what's new.
## Animations
### \`decay\`
\`decay\` models a form of exponential deceleration to create motion like momentum scrolling on smartphones.
Just provide it \`velocity\` and \`from\` properties:
\`\`\`javascript
decay({
velocity: 1000,
from: 0
}).start((v) => ...)
\`\`\`
It also exposes a \`modifyTarget\` option that provides a functional API for adding features like snap-to-grid.
### \`everyFrame\`
\`everyFrame\`, as you might imagine, fires once per frame. It provides the amount of time since it started:
\`\`\`javascript
everyFrame().start((timeSinceStart) => ...);
\`\`\`
It's the fundamental driver of every Popmotion animation. We've exposed it because we reckon this kind low-level action will be useful in animations, like porting After Effects expressions:
### \`keyframes\`
\`keyframes\` transitions through a number of states over a set duration of time.
Its API is inspired by Apple's \`CAKeyframeAnimation\`, which makes it trivial to resize the overall animation:
\`\`\`javascript
keyframes({
values: [
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 }
],
times: [0, 0.3, 1],
duration: 1000
})
\`\`\`
### \`timeline\`
\`timeline\` is used to orchestrate more complicated patterns of tweens.
It supports absolute and relative timestamps, as well as parallel and staggered motion. The output action has all the same playback methods as \`tween\`, making it fully scrubbable.
In most animation libraries, the timeline function is a bit of a black box that we chuck setters or selectors into.
As Popmotion is a reactive library, we label each segment with a \`track\` property, and that then the latest state gets output as an object with those labels:
\`\`\`javascript
timeline([
{ track: 'shade', from: 0, to: 1 },
'-100',
{
track: 'modal',
from: { y: -100, opacity: 0 },
to: { y: 0, opacity: 1 }
}
]).start(({ shade, modal }) => ...)
\`\`\`
This means we can pass the output of timeline through the chainable methods \`pipe\`, \`while\` and \`filter\` (more on those later).
As timelines become more complicated, maintaining the link between labels and setters can become increasingly difficult, but the trade-off is a timeline that is immutable, composable, pure, and testable.
We're also experimenting with functions that can take a timeline definition and automatically generate the output reaction.
## Input
### \`pointer\`
The \`pointer\` action has been in Popmotion since before it was called Popmotion. But, its API has always been confused making simple tasks like dragging more convoluted than they needed to be.
In Popmotion 7, \`pointer\` output its absolute position and it was up to a second action, \`trackOffset\`, to get the **movement** of \`pointer\` relative to another point (say, a DOM element).
Popmotion 8 scraps the \`trackOffset\` action entirely. Now, \`pointer\` will output its absolute position when used like this:
\`\`\`javascript
pointer().start(({ x, y }) => ...)
\`\`\`
(Also notice how we no longer need to provide \`pointer\` with a \`MouseEvent\` or \`TouchEvent\`.)
To output that pointer's movement **applied to another point**, we simply need to provide that point as our initial argument:
\`\`\`javascript
pointer({ x: 0, y: 0 }).start(({ x, y }) => ...)
\`\`\`
### \`multitouch\`
Thanks to the efforts of [Mars](https://twitter.com/marsi), Popmotion has had multitouch support since version 6.
There are two major changes coming in 8. The first and most apparent is we've changed the name of the action from \`touches\` to \`multitouch\` to highlight the second change:
It no longer outputs just an array of touches. \`touches\` is joined by \`scale\` and \`rotate\` properties:
\`\`\`javascript
multitouch().start(({ touches, scale, rotate }) => ...)
\`\`\`
Like \`pointer\`, \`multitouch\` accepts \`scale\` and \`rotate\` arguments and, if defined, will output the **change** in those properties as applied to the given values.
## Reactive API
At the heart of all these new features is a change in the core building block of all animations, the **action**.
The \`action\` function is used to create streams of reactive values. Think of it as an animation-focused, tiny alternative to Rx Observables.
It looks like this:
\`\`\`javascript
action(({ update, complete, error }) => {})
\`\`\`
For a practical example of how \`action\` works, let's define an function called \`just\`. It'll return an action that, when started, will fire \`update\` with the provided value and then \`complete\`:
\`\`\`javascript
const just = (v) => action(({ update, complete }) => {
update(v);
complete();
});
just(2).start(console.log); // 2
\`\`\`
Every time we \`start\` an action, its initialisation function runs anew, creating a new instance of the action. Because all animations are actions, we can define an animation once:
\`\`\`javascript
const moveRight = tween({ to: 300 });
\`\`\`
And use it multiple times:
\`\`\`javascript
moveRight.start(console.log); // 0 - 300
moveRight.start(console.log); // 0 - 300
\`\`\`
Actions offer a number of chainable methods (currently \`filter\`, \`pipe\` and \`while\`, with an API for adding more on the way). Each returns a brand new version of the action with the added functionality:
\`\`\`javascript
const double = v => v * 2;
const px = v => v + 'px';
const justTwo = just(2);
justTwo.start(console.log); // 2
justTwo
.pipe(double, px)
.start(console.log); // '4px'
\`\`\`
In the last 6 months Popmotion has spun out [Framesync](/api/framesync) and [Stylefire](/stylefire) as standalone libraries.
It's helped me take greater care and consideration over where to draw the lines between the role and responsibilities of various parts of the library and enables people to use the isolated functionality in their own code or libraries.
I can imagine a near-future where actions are spun out as a tiny reactive library, where people can start dabbling with reactive programming outside of animation without the full payload of something the size of Rx.
## And the rest...
### File size and individual imports
Popmotion has always tried to respect your bytes. One of the reasons I wrote it in the first place was a dissatisfaction with the size of existing libraries in comparison to the benefits they provided.
Popmotion 8 is a little bigger than 7 (11.5kb vs 10kb). Though, as such a radical rewrite with so many new features, I think there are efficiencies to be made over the coming months.
In the meantime, everything in Popmotion is **now available as an individual import**. Which means, if you only want to use (for instance) \`spring\`, you can import and use that for roughly 2kb.
### Colours and multi-prop animations
In Popmotion 7, we exposed the ability to animate colours with \`colorTween\`. Multi-property animations could be composed with the \`composite\` and \`parallel\` compositors.
This has all been streamlined in 8. Every animation can now animate colors, objects, arrays (and objects and arrays of colors!).
It's as simple as this:
\`\`\`javascript
tween({
from: { x: 0, color: '#f00' },
to: { x: 100, color: '#fff' }
}).start(({ x, color }) => ...)
\`\`\`
## Conclusion
That's most of what's new in Popmotion 8. Existing users should check out our [upgrade guide](/blog/20171210-popmotion-8-upgrade-guide) to handle breaking changes.
After three years of development I'm **finally** happy with the API. I think the reactive model works incredibly well for neatly and declaratively handling streams of values and fits perfectly with the functional approach I was already moving towards.
With these solid foundations in place, the next logical step feels like exploring a way of describing the properties of UI elements and having motion and interactions derive naturally from that. Like a physically-based rendering for motion.
Once more down the rabbit hole.
---
[Get started with Popmotion](/learn/get-started)
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/blog/20180104-when-ios-throttles-requestanimationframe.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# When iOS throttles requestAnimationFrame to 30fps
> **TL;DR:** iOS throttles \`requestAnimationFrame\` to 30fps in cross-origin iframes and low power mode.
Yesterday I was looking at the examples on the [Popmotion homepage](https://popmotion.io) with my iPhone. They looked odd, clearly suffering a sluggish stutter, the cruel affliction of (**gasp**) 30 frames per second.
Here's an artificially-throttled example:
I'd seen this illness once before. If you're looking at the example above on an iOS browser, check out this unthrottled example:
Looks the same, right? To other readers, this second example runs at a silky 60fps. Animation as the gods intended.
I found that it was only affecting animations running on CodePen. The reason was initially unclear.
I assumed that CodePen itself was doing **something** weird in the background that caused the stuttering. They helpfully pointed me to the Debug View, which runs the pen without the iframe.
It worked. 60fps on iOS. It wasn't CodePen after all.
My first discovery:
## iOS throttles \`requestAnimationFrame\` in iframes
Specifically, it turns out, **cross-origin iframes**. Which is every CodePen embed, even the ones hosted on CodePen's own site (as the live panel is served on a subdomain).
As detailed in this [WebKit changelog](https://trac.webkit.org/changeset/215070/webkit), the throttling is cleared once a user interacts with the iframe.
Try it for yourself. If you're on iOS, take a look at the tween animation again:
This time, tap anywhere within the frame and watch as the 30fps eye-thrashing is replaced by the soft caress of 60fps.
### But... why?
I haven't found a stated reason for this behaviour. But that doesn't stop semi-educated speculation.
Iframes are commonly used for advertising. Adverts are fairly liberal with your CPU cycles. So I imagine the throttling is an attempt to prevent adverts from eating your battery.
The performance cap is cleared once the user interacts with the iframe and indicates that the advert (or other embedded content) isn't unwelcome.
An alternative solution might be to throttle iframes that lie outside the current viewport (maybe pause execution entirely), and unthrottle when they enter. This current solution feels heavy-handed.
### The remaining mystery
Back to the present, this didn't explain why the examples on the homepage were stuttering. They're part of the page itself, no iframes.
As the myriad of known and unknown potential causes flashed before my eyes, a familiar feeling of nausea set in.
Standing on the precipice of a rabbit hole of unknown depth, I began the ritual.
Opened my MacBook. Chrome. Performance tab. Throttled CPU... 60fps.
Checked my partner's phone, same OS, same make and model... 60fps.
Googled for bugs with iOS 11.2.1 and 30fps rAF... Nothing. What the fuuuuuu...
I stared at my phone, dumbfounded. Maybe I needed to reset my phone. Maybe it was because my phone was the wrong colour. Maybe I simply needed to be kinder to Siri.
Luckily, I saw it, and I knew I'd found the culprit. The nausea lifted. Relief settled:

Low power mode. Though part of me wouldn't believe it until I toggled it on and off and saw the difference with my own eyes, it made sense.
My second discovery:
## iOS throttles \`requestAnimationFrame\` in low power mode
As detailed in this [WebKit bug](https://bugs.webkit.org/show_bug.cgi?id=168837), iOS throttles \`requestAnimationFrame\` to 30fps whilst low power mode is active.
I have mixed feelings about this.
First, there's no doubt that it's very clever. It is, in the short-term, a boon for users.
In the long-term, I'm not so sure. I've already covered in detail why the [mobile web is a dreadful platform](https://popmotion.io/blog/20170710-mobile-web-is-awful-and-were-all-to-blame/) to develop for.
This is yet another unpredictable tributary feeding into a much larger river of unpredictable nonsense that we have to put up with.
The salmon will get tired. It's a self-perpetuating cycle. Horizons, ambitions and standards will drop. Further emboldening mobile browser vendors to push for clever solutions to improve user experience and degrade the developer one.
On iOS, the app developers themselves are given the responsibility to [respond to low power mode](https://developer.apple.com/library/content/documentation/Performance/Conceptual/EnergyGuide-iOS/LowPowerMode.html), whereas WebKit still hasn't implemented the [Battery Status API](https://developer.mozilla.org/en-US/docs/Web/API/Battery_Status_API). It's a familiar story, and a shame.
A clever, battery-saving shame.
---
If you still have any energy left in your tired little salmon fins, make the mobile web an app-quality kinda place with [Popmotion](https://popmotion.io). Alternatively, throw your opinions in my stupid salmon face [on Twitter](https://twitter.com/popmotionjs)
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/blog/20180521-pose-2-migration-guide.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# What's new in Pose 2.0?
Pose and React Pose 2.0 are out now!
They introduce a simple new API for \`transition\`, so there's now no requirement to import Popmotion separately for custom transitions.
\`transition\` can also now accept a named map of values, meaning no more having to check \`key\` to return different animations.
As we all know all-too-well, major version increases also mean one thing: All your shit's broke. Just kidding. Only some of your shit's broke.
Let's take a look.
## Transition definitions
Until now, creating custom transitions involved importing Popmotion animations and manually passing along Pose-generated values like \`from\`, \`to\` and \`velocity\`:
\`\`\`javascript
import { tween, easing } from 'popmotion';
const config = {
visible: {
opacity: 0,
transition: (props) => tween({
...props,
duration: 1000,
ease: easing.linear
})
}
};
\`\`\`
Now, we can simply define \`transition\` as an object of animation props:
\`\`\`javascript
const config = {
visible: {
opacity: 0,
transition: { duration: 1000, ease: 'linear' }
}
};
\`\`\`
Notice that we don't need to import easing functions separately, as Pose provides a collection of named easings. We can even automatically define a cubic bezier easing by providing an array:
\`\`\`javascript
ease: [.01, .64, .99, .56]
\`\`\`
By default, a transition defined like this will be a [tween](/api/tween). But Pose supports Popmotion's [spring](/api/spring), [keyframes](/api/keyframes), [physics](/api/physics) and [decay](/api/decay) animations too.
We can switch to one of these by setting \`transition.type\`:
\`\`\`javascript
const config = {
dragEnd: {
x: 0,
transition: { type: 'spring', stiffness: 1000 }
}
}
\`\`\`
Or dynamically set them as a function:
\`\`\`javascript
const config = {
dragEnd: {
x: 0,
transition: ({ velocity }) => velocity < 0
? { type: 'spring', stiffness: 1000 }
: { type: 'physics', to: -1000 }
}
}
\`\`\`
There's also some new properties supported by all animations: \`round\`, \`min\`, \`max\` and \`delay\`. Take a look at the [custom transition tutorial for full details](/pose/learn/custom-transitions).
Of course, for animations that aren't yet possible with the new transition API, these functions can still return Popmotion animations just as before.
## Named transition maps
Previously, to return different animations for different properties, we had to check the provided \`key\` property:
\`\`\`javascript
const config = {
hidden: {
x: -100,
opacity: 0,
transition: ({ key, ...props }) => (key === 'x')
? spring({ ...props, stiffness: 1000 })
: tween({ ...props, duration: 100 })
}
}
\`\`\`
Instead, we can now **optionally** define \`transition\` as a named map, with a \`default\` prop for a catch-all transition:
\`\`\`javascript
const config = {
hidden: {
x: -100,
opacity: 0,
transition: {
x: { type: 'spring', stiffness: 1000 },
default: { duration: 100 }
}
}
}
\`\`\`
These named transitions can also be functions that return dynamic transition definitions or Popmotion animations.
Transition definitions and named transition maps will both be coming to React Native Pose in the near future.
## Migration guide
Of course, with a major version change there's always a breaking change or two. We've started flagging these in advance with deprecation warnings, where possible, so hopefully the impact will be minimal.
Here's what's changed:
### \`transitionProps\` is now \`props\`
In a bit of bikeshedding that should, nonetheless, make the API a little easier to explain, \`transitionProps\` is now \`props\`.
When they were first introduced, \`transitionProps\` were used to dynamically generate \`transition\`s. Now, they can be used to dynamically set any pose prop, hence the change in terminology:
\`\`\`javascript
const config = {
open: {
x: ({ x }) => x
},
props: { x: 0 }
};
// Pose
const poser = pose(config);
// React Pose
const Posed = posed.div(config);
() =>
\`\`\`
This has also led to the renaming of \`setTransitionProps\` to \`setProps\` for the vanilla Pose users in the audience.
### FLIP is now opt-in
Previously, Pose would detect if you were animating a layout property like \`width\` or \`top\` and automatically convert that to a FLIP animation.
Although FLIP animations are more performant, if not handled with care they can cause unexpected visual artifacts. We also don't currently have a good strategy for animating to \`0\` (which makes the "invert" stage of FLIP decidedly tricky - what's the inverse of 0?)
This was one of the most commonly-occurring points of confusion with the API, so I've made it opt-in with \`flip: true\`:
\`\`\`javascript
const config = {
open: {
width: '100vw',
height: '100vh',
flip: true
},
close: {
width: '100px',
height: '100px',
flip: true
}
}
\`\`\`
The good news is that this shouldn't break any of your code. If left unchanged, Pose will simply animate the layout property, **unless**, as above, you're animating between two different unit types which Pose doesn't currently support (PRs welcome!)
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/index.js
================================================
import PopmotionHomepage from '~/templates/Popmotion';
export default () => ;
================================================
FILE: packages/site/pages/pure/learn/action-reaction.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Actions and reactions
The \`tween\` function returns what's known in Popmotion as an **action**.
Popmotion is a reactive library, and actions are functions that **create** streams of values. These actions output to **reactions**.
**In Popmotion, every animation and input is an action**. In this quick tutorial, we'll gain a better understanding of actions and reactions by writing our own.
## Import
\`\`\`javascript
import { action } from 'popmotion';
\`\`\`
## Creating an action
The \`action\` function accepts a single argument. It's an initialisation function that will be executed **each time the returned action is started** via its \`start\` method.
This means that we can define one action, and start it multiple times, leading to multiple instances of that action.
The \`init\` function is provided an object of three functions: \`update\`, \`complete\`, and \`error\`:
\`\`\`javascript
action(({ update, complete, error }) => {})
\`\`\`
Let's define a function called \`just\`. It'll return an action that, when started, will fire \`update\` with the provided value and then \`complete\`:
\`\`\`javascript
const just = (v) => action(({ update, complete }) => {
update(v);
complete();
});
\`\`\`
Now, when the action returned from \`just\` is \`start\`ed, it'll emit the provided value:
\`\`\`javascript
just(1).start(console.log); // 1
\`\`\`
\`console.log\` is being used as a **reaction**. It will fire whenever the new action instance calls \`update\` with a new value.
We also defined \`just\` to fire \`complete\` once it's finished. Instead of a function, we can provide an object of \`update\` and \`complete\` functions as our reaction:
\`\`\`javascript
just(1).start({
update: console.log,
complete: () => console.log('complete!')
});
\`\`\`
When \`start\` runs, the initialisation function is run anew, and a **new instance** of the active action is returned:
\`\`\`javascript
const justOne = just(1);
justOne.start(console.log); // 1
justOne.start(console.log); // 1
\`\`\`
As all Popmotion animations are actions, we can define an animation once and use it multiple times:
\`\`\`javascript
const mySpring = spring({ to: 500 });
mySpring.start({
update: console.log,
complete: () => console.log('complete!')
});
mySpring.start({
update: (v) => console.log('second spring: ' + v),
complete: () => console.log('second spring also complete!')
});
\`\`\`
## Chainable modifiers
All actions, as well as special reactions like [multicast](/api/multicast) and [value](/api/value), are **chainable**.
They offer methods that **return a new instance of the action or reaction** with altered behaviour.
Currently, there are three chainable methods: \`filter\`, \`pipe\` and \`while\`.
An API for developers to add more is on the roadmap.
Let's take a look at an example of each:
### \`filter\`
\`filter\` accepts a single function. This function is passed the latest value emitted from \`update\`.
The value is only passed down the chain if the function provided to \`filter\` returns \`true\`.
\`\`\`javascript
action(({ update }) => {
update(1);
update(2);
update(1);
}).filter((v) => v === 1)
.start(log); // 1, 1
\`\`\`
### \`pipe\`
\`pipe\` accepts a series of functions.
Each function is provided the latest value emitted from \`update\`, and returns a new value that is passed down the chain:
\`\`\`javascript
const double = (v) => v * 2;
const px = (v) => v + 'px';
const one = just(1);
const twoPx = one.pipe(double, px);
one.start(console.log); // 1
twoPx.start(console.log); // '2px'
\`\`\`
### \`while\`
\`while\` accepts a single function. This function is passed every value from \`update\` and fires \`complete\` if the function returns \`false\`:
\`\`\`javascript
just(1)
.while((v) => v === 2);
.start(console.log); // never fires, as while returned false
\`\`\`
### Combining
Let's combine \`pipe\` and \`while\` to make a [pointer](/api/pointer) that outputs its \`x\` position as percentage of the current viewport, and automatically stops itself if the pointer is more than 75% across the screen:
\`\`\`javascript
const pickX = ({ x }) => x;
const viewportWidth = window.innerWidth;
const percentageOfViewport = (v) => v / viewportWidth * 100;
const asPercent = (v) => v + '%';
pointer()
.pipe(pickX, percentageOfViewport) // The output of this
.while((v) => v < 75) // Gets passed to this
.pipe(asPercent) // Before being passed to this
\`\`\`
## Stopping actions
Every action returns a \`stop\` method:
\`\`\`javascript
const foo = tween().start(console.log);
foo.stop();
\`\`\`
But how does the code defined in your \`init\` function know its been stopped?
Each init function can **optionally** return an API. This can include a \`stop\` function:
\`\`\`javascript
const oneEverySecond = action(({ update }) => {
const updateOne = () => update(1);
const interval = setInterval(updateOne, 1000);
return {
stop: () => clearInterval(interval)
};
});
const foo = oneEverySecond.start();
setTimeout(() => foo.stop(), 3000); // 1, 1, 1
\`\`\`
## Custom API
Your action can return a custom API, too:
\`\`\`javascript
const valueEverySecond = action(({ update }) => {
let outputValue = 1;
const updateOne = () => update(outputValue);
const interval = setInterval(updateOne, 1000);
return {
stop: () => clearInterval(interval),
setOutputValue: (v) => outputValue = v
};
});
const foo = valueEverySecond.start();
foo.setOutputValue(2); // 2, 2...
\`\`\`
## Conclusion
By chaining actions we can create new actions with different behaviour.
This flexibility is available on all animations and inputs, and later tutorials will touch on these capabilities.
In the next tutorial, we'll learn how to implement pointer tracking with two input actions, [Pointer and Listen](/learn/input-tracking).
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/circular-motion.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Circular motion
HTML and SVG elements are positioned by \`x\` and \`y\` coordinates. This type of positioning is known as **cartesian coordinates**, and animating these is great for moving elements in straight lines (the majority use-case).
However, to move elements in a circular fashion, it's much easier to animate position using **polar coordinates**: Position as defined by \`angle\` and \`radius\`.
We can then convert these polar coordinates into \`x\` and \`y\` to produce movement like this:
In this quick tutorial, we'll create circular motion converting polar to cartesian coordinates using functional composition.
As a bonus step, we'll then explain how we can rotate the element to face its direction of travel.
You can [fork this CodePen](https://codepen.io/popmotion/pen/qpRROg?editors=0010) to follow along.
## Position
We can convert \`radius\` and \`angle\` into \`x\` and \`y\` using \`cos\` and \`sin\` functions:
\`\`\`javascript
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
\`\`\`
By expressing this as a pure function, we'll be able to provide it to any animation's \`pipe\` method:
\`\`\`javascript
const polarToCartesian = ({ angle, radius }) => ({
x: radius * Math.cos(angle),
y: radius * Math.sin(angle)
});
\`\`\`
With this function we can write a simple animation that:
- 1) Changes \`radius\` at a constant velocity of 5 radians a second.
- 2) Pipes the animation output through \`polarToCartesian\` to convert into \`x\` and \`y\`.
- 3) Styles the div by using \`boxStyler.set\`:
\`\`\`javascript
physics({
from: { angle: 0, radius: 150 },
velocity: { angle: 5, radius: 0 }
}).pipe(polarToCartesian)
.start(boxStyler.set);
\`\`\`
Now our box moves in a circular motion.
In this animation we're keeping \`radius\` at a constant value by setting \`velocity\` to \`0\`. By using the \`composite\` function, we can combine two different animations to animate \`radius\` and \`angle\` in different ways:
\`\`\`javascript
composite({
angle: physics({ velocity: 5 }),
radius: tween({
from: 0,
to: 150,
yoyo: Infinity,
ease: easing.easeInOut,
duration: 2000
})
}).pipe(polarToCartesian)
.start(boxStyler.set);
\`\`\`
## Direction
It looks a little awkward to have a square stay upright as it moves in a circular motion.
We can calculate and inject a \`rotate\` property based on the current \`angle\` to ensure the square is facing the direction of travel.
First, let's make our \`polarToCartesian\` function more composable by allowing it to pass through any properties that it doesn't consume using spread props:
\`\`\`javascript
const polarToCartesian = ({ angle, radius, ...props }) => ({
x: radius * Math.cos(angle),
y: radius * Math.sin(angle),
...props
});
\`\`\`
Next, we need to make a function that simply takes \`angle\` and returns the angle perpendicular to it as a new property, \`rotate\`.
\`cos\` and \`sin\` functions accept radians, whereas CSS and SVG \`rotate\` properties are defined in degrees. We can use Popmotion's [\`radiansToDegrees\` calculator](/api/calc) to convert \`angle\` into degrees, and then simply rotate it by \`90\`:
\`\`\`javascript
const rotatePerpendicular = (props) => {
const { angle } = props;
return {
rotate: radiansToDegrees(angle) + 90,
...props
};
};
\`\`\`
Applying this new function is as easy as modifying \`pipe\` to read:
\`\`\`javascript
.pipe(rotatePerpendicular, polarToCartesian)
\`\`\`
## Conclusion
Circular motion is much easier to reason about in polar coordinates, and mapping these to cartesian is simple with \`pipe\`.
We can animate \`angle\` and \`radius\` with separate animations by using the \`composite\` composition function.
And finally, we can make the element rotate along with the direction of travel by converting \`angle\` into degrees and then adding an extra \`90\` degrees.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/constrain-motion.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Constrain motion
When creating UIs, there's plenty of instances when we want to constrain motion to within a specific range.
For instance, if we're making an input slider, we want to constrain its sliding motion to within the slider's visible boundaries.
Otherwise, the illusion of a coherent piece of UI is broken, and the input is useless:
In this tutorial, we'll look at a functional approach to solving this problem. We'll handle out-of-range motion with 1) a hard clamp, and 2) a static spring.
You can follow along by forking [this CodePen](https://codepen.io/popmotion/pen/KZzWdz?editors=0010).
## Clamp
The easiest way to restrict motion to within a range is by clamping it. Clamping is simply the process of restricting a number within a range.
JavaScript includes two commonly-used clamping functions, \`Math.min\` and \`Math.max\`.
We can change the dragging action from:
\`\`\`javascript
pointerX(handleX.get())
.start(handleX)
\`\`\`
To:
\`\`\`javascript
pointerX(handleX.get())
.pipe(v => Math.max(0, v))
.start(handleX)
\`\`\`
By using \`Math.max\` to clamp the lower range to \`0\`, you'll notice you can't drag the handle beyond the left boundary:
We could add the right boundary like this:
\`\`\`javascript
pointerX(handleX.get())
.pipe(
v => Math.max(0, v),
v => Math.min(250, v)
)
.start(handleX)
\`\`\`
Popmotion provides a clearer way of expressing this via the \`clamp\` [transformer](/api/transformers). This function accepts a range and returns a function that will clamp any provided number to within that range:
\`\`\`javascript
clamp(0, 250)(-50) // 0
\`\`\`
So our \`pointerX\` function becomes:
\`\`\`javascript
pointerX(handleX.get())
.pipe(clamp(0, 250))
.start(handleX)
\`\`\`
We now have a functional input slider! But we don't have a **delightful** one.
Think of iOS, when you scroll a view beyond its boundaries. It doesn't stop dead, it tugs back. It's a visceral and satisfying experience.
We can replicate this experience with Popmotion using static springs.
## Static springs
Popmotion has so many springs you could use it as a mattress. It's not a point of pride, it can be confusing. But, different springs are useful in different situations:
- [Spring](/api/spring) is a highly-accurate simulation, but immutable. Most appropriate for **spring animations**.
- [Physics](/api/physics) is a lightweight integrated simulation that can change over time. Most appropriate for **spring interactions**.
Of the two, \`physics\`, in theory, could be used to restrain motion to a range. But it's overkill. You'd have to set conditional statements to start and stop animations, grab the velocity from \`handleX\`, set this and that. It'd be an imperative soup.
Instead, Popmotion provides two transformers that can be used in a purely functional manner: \`linearSpring\` and \`nonlinearSpring\`.
They both share an API. Provide a \`strength\` and a \`target\`, and they'll return a function. This function, when given a numerical value, will return a new number "pulled" towards the target.
\`\`\`javascript
linearSpring(0.25, 0)(4) // 1
\`\`\`
\`linearSpring\` applies a **constant force**, whereas \`nonlinearSpring\` applies a **greater force the further the given number is away from \`target\`**.
In tandem with the \`conditional\` transformer, we can apply these springs only when the number output by \`pointerX\` exceeds the defined boundaries:
\`\`\`javascript
conditional(
v => v < 0, // If less than 0
linearSpring(0.25, 0) // Apply spring
)
\`\`\`
Let's compose a new function using \`pipe\`, \`conditional\` and \`linearSpring\` that will restrict a range of motion using springs:
\`\`\`javascript
const springRange = (from, to, strength) => pipe(
conditional(
v => v < from,
linearSpring(strength, from)
),
conditional(
v => v > to,
linearSpring(strength, to)
)
);
\`\`\`
This can be passed to our \`pipe\`:
\`\`\`javascript
pointerX(handleX.get())
.pipe(springRange(0, 200, 0.2))
.start(handleX)
\`\`\`
To create spring-restricted motion that feels like this:
Try replacing \`linearSpring\` with \`nonlinearSpring\` and adjust the \`strength\` to see how that changes the behaviour of the handle.
### Spring back
You'll notice that if you release the handle outside the slider's boundaries, it just sits still. This is at odds with the perceived spring that binds the handle to the slider.
For this, we can start a \`spring\` animation on the mouseup/touchend event.
Currently, we just call \`() => handleX.stop()\` which ungracefully stops any action driving \`handleX\`, which in this example is \`pointerX\`.
Let's replace this reaction with:
\`\`\`javascript
() => {
const x = handleX.get()
if (x < 0 || x > 250) {
// Start spring animation
} else {
handleX.stop();
}
}
\`\`\`
Now the function will only stop motion abruptly if the handle is **within** the slider's range.
To handle cases when the handle is **outside** the range, replace the commented line with this:
\`\`\`javascript
spring({
from: x,
to: x < 0 ? 0 : 250,
velocity: handleX.getVelocity(),
stiffness: 900,
damping: 30
}).start(handleX);
\`\`\`
You can play around with the settings of both the static spring and the \`spring\` animation until you find something that feels responsive, satisfying and in-keeping with your brand.
## Conclusion
We can declaratively implement motion constraints using functional composition.
Clamping is the most basic approach, but static springs can yield more satisfying results when paired with user input.
\`pipe\` and \`conditional\` allow you the freedom to devise and compose your own strategies for imposing motion constraints.
A couple ideas for next steps:
- Replace the \`stop\` call in our mouseup event with a \`decay\` animation that allows the user to throw the handle. It could include a little bump animation when it hits a slider limit.
- Generate your own static springs using the \`generateStaticSpring\` transformer.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/get-started.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Get started
Popmotion is a functional, reactive JavaScript motion library.
It allows developers to create animations and interactions from **actions**.
Actions are streams of values that can be started and stopped, like tweens, physics and pointer input.
Actions are unopinionated, so those values can be used to create animations with CSS, SVG, React, Three.js... any API that accepts a number as an input.
In this simple introductory guide we're going to install Popmotion and use it to animate an element using the \`tween\` animation and DOM \`styler\`.
## Installation
You can install Popmotion directly from npm:
\`\`\`bash
npm install popmotion --save
\`\`\`
Alternatively, you can also **download pre-bundled files** or **fork CodePen playgrounds**. Full installation options are available on the [Install Popmotion](/learn/install) page.
## Import
In Popmotion, everything can be imported from the main library:
\`\`\`javascript
import { tween } from 'popmotion';
\`\`\`
Or, if you're only using a small subset of the library you can respect your user's bytes by importing everything individually:
\`\`\`javascript
import tween from 'popmotion/animations/tween';
\`\`\`
## The "Hello World" tween
A \`tween\` is a function that changes one number to another, over a set duration of time. If you're familiar with CSS transitions, that is also a form of tweening.
By default, a \`tween\` will change \`0\` to \`1\` over \`300\` milliseconds:
\`\`\`marksy
{\`
const counter = document.querySelector('#a .counter');
const updateCounter = (v) => counter.innerHTML = v;
tween().start(updateCounter);
\`}
\`\`\`
**All examples in the green-bordered boxes are editable**. Try editing the above example by writing
\`\`\`javascript
{ to: 300, duration: 500 }
\`\`\`
as the argument to the \`tween\` function. The counter will now count up to \`300\` over the course of half a second.
## Animate!
As web developers, the DOM is our most common target for animations.
So, let's use the exact same animation from before to output to an element's \`translateX\` property.
For this, we can use the \`styler\` function:
\`\`\`javascript
import { tween, styler } from 'popmotion';
\`\`\`
\`styler\` accepts a single \`Element\` and returns a get/set interface for HTML and SVG styles that's optimised for animation.
\`\`\`javascript
const ball = styler(document.querySelector('.ball'))
\`\`\`
When \`set\` is called, it schedules a render [on the next frame](/api/framesync). All renders are batched to prevent layout thrashing.
\`set\`, if called with just a property name, returns a setter function. So we can swap \`updateCounter\` from the previous example with \`styler(element).set('x')\` to animate the element:
\`\`\`marksy
{\`
const ball = document.querySelector('#css .ball');
tween({ to: 300, duration: 500 })
.start(styler(ball).set('x'));
\`}
\`\`\`
And that's it! Your first animation.
## Multi-property animations
All animations included with Popmotion can animate:
- Numbers
- Colors
- Objects of numbers and colors
- Arrays of numbers and colors
For instance, by replacing \`300\` in the previous example with \`{ x: 300, scale: 2 }\` the action will animate and output \`x\` and \`scale\` values:
\`\`\`marksy
{\`
const ball = document.querySelector('#object .ball');
const ballStyler = styler(ball);
tween({
from: { x: 0, scale: 1 },
to: { x: 300, scale: 2 },
ease: easing.easeInOut,
flip: Infinity,
duration: 1000
}).start(ballStyler.set);
\`}
\`\`\`
## Next
This tutorial has covered just the basics for the \`tween\` animation. You can find more details in the full [tween API docs](/api/tween).
Popmotion uses a simple reactive model. Every animation, like \`tween\`, and every input is an **action**.
And for every action, there is (naturally) a **reaction**.
In the next tutorial, we'll briefly look at [actions and reactions](/learn/action-reaction).
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/input-smoothing.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Input smoothing
You can easily add input smoothing to pointer tracking using some light functional composition.
Drag this ball to see pointer tracking without smoothing:
\`\`\`marksy
{\`
const ball = document.querySelector('#a .ball');
const ballStyler = styler(ball);
const ballXY = value({ x: 0, y: 0 }, ballStyler.set);
let pointerTracker;
const startTracking = () => {
pointerTracker = pointer(ballXY.get())
.start(ballXY);
};
const stopTracking = () => {
if (pointerTracker) pointerTracker.stop();
};
listen(ball, 'mousedown touchstart').start(startTracking);
listen(document, 'mouseup touchend').start(stopTracking);
\`}
\`\`\`
And now with smoothing applied:
\`\`\`marksy
{\`
const ball = document.querySelector('#b .ball');
const ballStyler = styler(ball);
const ballXY = value({ x: 0, y: 0 }, ballStyler.set);
const { transformMap, smooth } = transform;
let pointerTracker;
const startTracking = () => {
pointerTracker = pointer(ballXY.get())
.pipe(transformMap({
x: smooth(200),
y: smooth(200)
}))
.start(ballXY);
};
const stopTracking = () => {
if (pointerTracker) pointerTracker.stop();
};
listen(ball, 'mousedown touchstart').start(startTracking);
listen(document, 'mouseup touchend').start(stopTracking);
\`}
\`\`\`
## Add smoothing
We can use the \`smooth\` [transformer](/api/transformers#smooth) to apply smoothing to any numerical value.
\`smooth\` works by passing a smoothing amount, in milliseconds. This is the duration over which movement will be averaged. So, high numbers equal greater smoothing.
\`\`\`javascript
smooth(200)
\`\`\`
**Importantly**, a new instance of \`smooth\` must be created for each value we want to smooth. This is because the returned function refers to an internal average value. If we pass it multiple values from different sources, it can't work!
### Multi-axis smoothing
So, our \`pointer\` might originally look like this:
\`\`\`javascript
pointer({ x: 0, y: 0 })
.start(({ x, y }) => /* unsmoothed output */)
\`\`\`
To apply smoothing on both \`x\` and \`y\`, we can use the \`transformMap\` transformer along with \`smooth\`:
\`\`\`javascript
import { pointer, transform } from 'popmotion';
const { transformMap, smooth } = transform;
pointer({ x: 0, y: 0 })
.pipe(transformMap({
x: smooth(200),
y: smooth(200)
}))
.start(({ x, y }) => /* smoothed! */)
\`\`\`
### Single-axis smoothing
If we're just tracking a single axis, we don't even need \`transformMap\`:
\`\`\`javascript
pointer({ x: 0, y: 0 })
.pipe(
({ x }) => x,
smooth(200)
).start((x) => /* smoothed x axis */)
\`\`\`
## Conclusion
By using \`smooth\`, we can apply smoothing to not just \`pointer\` output, but that of any action.
More examples of \`pipe\` and functional composition can be found in the [value pipelines](/api/value-pipelines) tutorial.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/input-tracking.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Pointer tracking
Pointer tracking is an integral part of user interfaces.
Drag & drop, scrolling galleries, and measuring touch scroll speed are some obvious use cases.
In this quick tutorial, we'll first see how to convert DOM events into streams of values with the \`listen\` action.
Then, we'll look at how to track mouse and touch movement with the \`pointer\` action and then use that pointer's movement to drag a DOM element.
## The \`listen\` action
Popmotion provides the [\`listen\` action](/api/listen) to add event listeners to the DOM.
\`\`\`javascript
import { listen } from 'popmotion';
\`\`\`
It accepts event names as a space-delimited string, meaning you can use a single \`listen\` call to listen to multiple events:
\`\`\`javascript
listen(document, 'mousedown touchstart')
.start((e) => console.log(e));
\`\`\`
Just like the good-old days of jQuery!
As \`listen\` is an [action](/api/action), it offers all the same chainable methods.
For example, here's how you could make a \`touchmove\` listener that only fires when there's more than two \`touches\`:
\`\`\`javascript
listen(document, 'touchmove')
.filter(({ touches }) => touches.length >= 2)
.start((e) => /* This event has more than 2 touches! */);
\`\`\`
## The \`pointer\` action
The \`pointer\` action provides a generic interface for interacting with **single point** mouse and touch inputs (for multitouch, Popmotion offers the [\`multitouch\` action](/api/multitouch)).
\`\`\`javascript
import { pointer } from 'popmotion';
\`\`\`
By default, \`pointer\` outputs the pointer's \`clientX\` and \`clientY\` properties as \`x\` and \`y\`.
\`\`\`javascript
let pointerTracker;
listen(document, 'mousedown touchstart').start(() => {
pointerTracker = pointer()
.start(({ x, y }) => console.log(x, y));
});
listen(document, 'mouseup touchend').start(() => {
if (pointerTracker) pointerTracker.stop();
});
\`\`\`
## Dragging
The majority of time we actually want to use this movement data to drag or scroll.
Let's use the \`styler\` function again to create a DOM element interface to provide the positional data to.
Look at \`startTracking\` function and try to drag the ball:
\`\`\`marksy
{\`
const ball = document.querySelector('#a .ball');
const ballStyler = styler(ball);
let pointerTracker;
const startTracking = () => {
pointerTracker = pointer()
.start(ballStyler.set);
};
const stopTracking = () => {
if (pointerTracker) pointerTracker.stop();
};
listen(ball, 'mousedown touchstart').start(startTracking);
listen(document, 'mouseup touchend').start(stopTracking);
\`}
\`\`\`
**Oh dear.** The ball moves, but it jumps to a weird location (maybe one that's off-screen entirely!)
The reason for this is simple. \`pointer\` is outputting the \`{ x, y }\` position of the pointer **relative to the viewport**.
The **ball's** \`{ x, y }\` transform begins at \`0, 0\`. So if we apply the pointer's absolute position directly to the ball, it won't move where we want it to.
Instead, we want to apply the **movement** of the \`pointer\` to the ball's initial position.
To do this, we can provide \`pointer\` with an initial \`{ x, y }\` point, and it will output the movement of its \`{ x, y }\` **relative to that point**:
\`\`\`javascript
pointer({ x: 0, y: 0 })
\`\`\`
Dragging becomes trivial:
\`\`\`marksy
{\`
const ball = document.querySelector('#b .ball');
const ballStyler = styler(ball);
let pointerTracker;
const startTracking = () => {
pointerTracker = pointer({
x: ballStyler.get('x'),
y: ballStyler.get('y')
}).start(ballStyler.set);
};
const stopTracking = () => {
if (pointerTracker) pointerTracker.stop();
};
listen(ball, 'mousedown touchstart').start(startTracking);
listen(document, 'mouseup touchend').start(stopTracking);
\`}
\`\`\`
## Single-axis dragging
Limiting dragging to a single axis is a matter of using just the \`x\` or \`y\` output from \`pointer\`.
We could do this via the reaction:
\`\`\`javascript
pointer().start(({ x }) => ballStyler.set('x', x));
\`\`\`
But the more reusable way is to **compose a new action** using \`pointer\`'s \`pipe\` method. We can provide it a simple picker function that selects \`x\` from \`pointer\`'s output and returns it:
\`\`\`javascript
const xPointer = (initialX) => pointer({ x: initialX })
.pipe(({ x }) => x);
\`\`\`
Now we can use our newly composed \`xPointer\` like so:
\`\`\`javascript
xPointer(ballStyler.get('x'))
.start(ballStyler.set('x'));
\`\`\`
\`\`\`marksy
{\`
const ball = document.querySelector('#c .ball');
const ballStyler = styler(ball);
const xPointer = (initialX) => pointer({ x: initialX })
.pipe(({ x }) => x);
let pointerTracker;
const startTracking = () => {
pointerTracker = xPointer(ballStyler.get('x'))
.start(ballStyler.set('x'));
};
const stopTracking = () => {
if (pointerTracker) pointerTracker.stop();
};
listen(ball, 'mousedown touchstart').start(startTracking);
listen(document, 'mouseup touchend').start(stopTracking);
\`}
\`\`\`
## Conclusion
\`listen\` can convert DOM events into reactive streams of values. Useful for leveraging \`pipe\` and other chainable methods, and composing \`listen\` with other actions.
\`pointer\` can output values either absolutely, or, if we provide initial coordinates, by applying its delta relatively.
Finally, we can compose new actions to produce reusable bits of code with new behaviour, like single-axis dragging.
## Next
Now that we've got dragging working, in the [next tutorial](/learn/velocity-and-physics) we will learn how to inspect the \`velocity\` of the dragged object and apply that to \`decay\`, \`physics\` and \`spring\` actions to create natural-feeling interactions.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/install.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Install Popmotion
## Package managers
### npm
\`\`\`bash
npm install popmotion --save
\`\`\`
### yarn
\`\`\`bash
yarn add popmotion
\`\`\`
## File include
**Note:** The Popmotion documentation uses the \`import\` syntax for importing individual modules.
**If you use one of the following installation methods, Popmotion will be available on the global \`popmotion\` variable.**
So, when you see in the docs \`import { tween } from 'popmotion'\`, you will use \`const { tween } = popmotion\` instead.
### Download
You can download the latest version of Popmotion at https://unpkg.com/popmotion/dist/popmotion.global.min.js
### \`script\` include:
Or include it directly in your HTML with this \`script\` tag:
\`\`\`html
\`\`\`
### CodePen
You can fork the [Popmotion playground on CodePen](https://codepen.io/popmotion/pen/zPjXWa?editors=0010), which is set up with the latest version of Popmotion.
You an also add Popmotion to any existing CodePen project by clicking on Settings > JavaScript and then pasting \`https://unpkg.com/popmotion/dist/popmotion.global.min.js\` into the "Add External JavaScript" field.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/morph-svg.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Morph SVG
Popmotion animations can be used to morph between any two SVG \`path\` shapes.
SVG-morphing libraries [Flubber](https://github.com/veltman/flubber) and [Polymorph](https://github.com/notoriousb1t/polymorph) both functions that accept a value between 0 and 1 to blend between two shapes. This works great with Popmotion's functional API, and animating the morph is as simple as this:
\`\`\`javascript
tween()
.pipe(morph)
.start(styler(pathElement).set('d'));
\`\`\`
In this quick tutorial we'll define this \`morph\` function with both Flubber and Polymorph and animate it with a \`tween\`.
## Set up
First, we need an SVG with a \`path\` element to render into.
\`\`\`html
\`\`\`
As we're going to animate the \`path\`'s verbosely-named \`d\` attribute, which defines the shape of the path, we can use Popmotion's [\`styler\` function](/api/styler) to create an optimised setter:
\`\`\`javascript
const path = document.getElementById('#target');
const setPathAttr = styler(path).set('d');
\`\`\`
Now we simply need a path definition string to provide to \`setPathAttr\`.
## Flubber
[Flubber](https://github.com/veltman/flubber) is a fully-featured morphing library that yields very accurate and pleasing results. The drawback being its filesize: A whopping 53kb!
Once you've installed and imported Flubber according to the docs, you can make the \`morph\` function using \`flubber.interpolate\`.
This function accepts two strings, one of each path you want to morph between:
\`\`\`javascript
const star = "M103.04 162.52L39.36 196l12.16-70.9L0 74.86 71.2 64.5 103.04 0l31.85 64.52 71.2 10.35-51.57 50.22L166.7 196z";
const circle = "M86,171.5 C38.7796539,171.5 0.5,133.220346 0.5,86 C0.5,38.7796539 38.7796539,0.5 86,0.5 C133.220346,0.5 171.5,38.7796539 171.5,86 C171.5,133.220346 133.220346,171.5 86,171.5 Z";
const morph = flubber.interpolate(circle, star);
\`\`\`
By default, the blending of the two shapes is pretty rough. We can pass \`interpolate\` a third options argument \`{ maxSegmentLength: 2 }\` to output more accurate shapes, though this might have performance impacts with more complex blends.
The generated \`morph\` function accepts a number between \`0\` and \`1\` and outputs a path definition string generated from a blend of \`circle\` and \`star\`.
So to animate our target \`path\`, we simply need to write an animation that outputs a number between \`0\` and \`1\` and provide \`morph\` to its \`pipe\` method so its output is the new path:
\`\`\`javascript
tween()
.pipe(morph)
.start(setPathAttr);
\`\`\`
## Polymorph
[Polymorph](https://github.com/notoriousb1t/polymorph) is a much lighter library clocking in at just 6kb! For complex shapes it works just as well as Flubber, though results may vary for simpler shapes.
Setup works a little differently as it takes \`path\` definitions from existing elements, so we'll need to add those:
\`\`\`html
\`\`\`
We simply provide the element ids to \`polymorph.interpolate\`, along with an optional \`precision\` setting:
\`\`\`javascript
const morph = polymorph.interpolate(['#Star', '#Oval'], { precision: 4 });
tween()
.pipe(morph)
.start(setPathAttr);
\`\`\`
## Next steps
Popmotion, Flubber and Polymorph's functional approach gives you the freedom to combine whichever libraries suit your project.
In this article, we've shown how simple it is to animate between shapes using \`tween\`. You could play around with other animations like \`pointer\`, which would make the blending scrubbable:
\`\`\`javascript
pointer()
.pipe(
({ x }) => x,
interpolate([0, window.innerWidth], [0, 1])
)
.start()
\`\`\`
One caveat, however, is neither library accepts numbers outside of \`0\` and \`1\`, which entails the following:
- \`spring\` needs to be overdamped by choosing a \`damping\` property high enough to prevent overshoot.
- Easing functions that generate overshoot, like \`backOut\`, will lead to stunted animations.
You can of course \`pipe\` animations through the \`clamp\` [transformer](/api/transformers) for safety, but ideally you'll want to create animations that don't exceed these limits.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/rounded-values.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Rounded values
Popmotion's emphasis on functional composition means actions don't offer explicit support via properties like \`rounded: true\`.
Instead, we can round the output of any [action](/api/action) by providing JavaScript's native \`Math.round\` to \`pipe\`. For instance:
## Round a single value
If we have a \`tween\` that outputs a single value, we can round it like this:
\`\`\`javascript
tween({ to: 1000 }).pipe(Math.round)
\`\`\`
\`\`\`marksy
{\`
const counter = document.querySelector('#a .counter');
const updateCounter = (v) => counter.innerHTML = v;
tween({ to: 1000, duration: 2000 })
.pipe(Math.round)
.start(updateCounter);
\`}
\`\`\`
Every animation is an action, so this applies to animations like \`physics\` too:
\`\`\`javascript
physics({ velocity: 100 }).pipe(Math.round)
\`\`\`
\`\`\`marksy
{\`
const counter = document.querySelector('#b .counter');
const updateCounter = (v) => counter.innerHTML = v;
physics({ velocity: 100 })
.pipe(Math.round)
.start(updateCounter);
\`}
\`\`\`
## Round a complex value
If we're animating an object, we can apply rounding to specific values using the \`transformMap\` [transformer](/api/transformers#transformmap):
\`\`\`javascript
import { tween, transform } from 'popmotion';
const { transformMap } = transform;
tween({
to: { x: 100, y: 100 }
}).pipe(transformMap({
x: Math.round
}));
\`\`\`
More examples of \`pipe\` and functional composition can be found in the [value pipelines](/api/value-pipelines) tutorial.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/spring-loaded-characters-remaining.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Spring-loaded "characters remaining" counter
Forms are, by nature, dreary. From a user's perspective, there's nothing fun about them: Non-zero friction, mental effort, the cold exchange of info for goods.
It doesn't have to be this way! By adding thoughtful little touches, we can soften the negative form experience and maybe even make something a little bit delightful.
In this tutorial we're going to take a traditionally mundane part of form, the remaining character counter, and increase its functionality by adding a little playfulness.
We're going to attach a spring that fires on every keypress that goes over the character count limit, drawing attention to the counter. We're also going to slowly change the counter color to red as we approach the limit.
Have a play by typing in this box:
To begin, you can use [this CodePen template](https://codepen.io/popmotion/pen/XzyypY?editors=0010) to follow along.
## The counter
Our first job is to get the counter to actually count down as a user enters characters.
The input field's \`maxlength\` is set to \`10\`. We can read this with JavaScript:
\`\`\`javascript
const charLimit = parseInt(input.getAttribute('maxlength'));
\`\`\`
Now, let's create a function that takes a string and updates the character counter with the remaining number of characters, which will be calculated by measuring the string and subtracting that from the \`charLimit\`:
\`\`\`javascript
function updateRemainingCharsCounter(val) {
counter.innerHTML = charLimit - val.length;
}
\`\`\`
We can test that this function works by, on the following line, writing:
\`\`\`javascript
updateRemainingCharsCounter('test');
\`\`\`
\`'test'\` is four characters long, so our counter displays \`6\`.
We want this function to fire on every \`keyup\` event, as this event carries the \`input\` field's latest value.
We're going to use the [\`listen\` action](/api/listen) to bind the event. \`listen\` converts DOM events into reactive streams. As an action, we can use \`pipe\` to pick the latest value out of the event before passing it on to \`updateRemainingCharsCounter\`:
\`\`\`javascript
listen(input, 'keyup')
.pipe(e => e.target.value)
.start(updateRemainingCharsCounter);
\`\`\`
Now when you type, the character counter updates!
We have a functional counter, but not a delightful one. Let's attach a \`spring\`.
## The spring
We're going to use the spring to increase the counter's \`scale\` property.
This isn't just going to look playful. By rapidly enlarging the counter, it'll draw the user's attention. You could use a little shake, or another effect. It's the movement itself that will distract the user to make sure they understand that there's no space for new characters.
Unlike a simple \`tween\`, spring physics can take into account a pre-existing velocity. This will make the animation interactive and playful: I haven't seen many people resist hammering away at the keyboard once they realise rapid keypresses builds momentum!
### Rendering the counter's \`scale\` prop
First, we need to import the [\`value\`](/api/value) and [\`styler\`](/api/styler) functions.
\`value\` will help us track and measure the velocity of \`scale\`, and \`styler\` will enable us to render it performantly.
\`\`\`javascript
const { listen, value, styler } = window.popmotion;
\`\`\`
We make our styler by simply passing the \`counter\`'s DOM node to \`styler\`:
\`\`\`javascript
const counterStyler = styler(counter);
\`\`\`
And we can initialise the \`counterScale\` value by passing it an initial value (\`1\`), and create a setter function with \`counterStyler.set\`:
\`\`\`javascript
const counterScale = value(1, counterStyler.set('scale'));
\`\`\`
Now, whenever \`counterScale\` updates, the \`counter\` DOM node will be updated too.
### Listening to \`keydown\`
We also need to listen for a new event, \`keydown\`.
This event the moment the user presses down on the key, which is the moment they're imparting their physical energy into the UI.
It feels very responsive - try putting the following code under a \`keyup\` event instead and you'll immediately notice how disconnected the animation feels from your physical actions.
We'll use \`listen\` again, this time chained with a different method, \`filter\`.
\`filter\`, as the name implies, filters out values that don't meet the provided criteria. In this case, we want to create an event listener that only fires when the number of entered characters is the same as the \`chatLimit\`:
\`\`\`javascript
listen(input, 'keydown')
.filter(e => e.target.value.length === charLimit)
.start(fireSpring);
\`\`\`
### The \`spring\` function
Now, we're ready to add our \`spring\`.
\`\`\`javascript
const { listen, value, styler, spring } = window.popmotion;
\`\`\`
Before this event listener, create a new function called \`fireSpring\` that'll start a new \`spring\` animation:
\`\`\`javascript
function fireSpring() {
spring({
// Start the animation from the current scale:
from: counterScale.get()
// We want the spring to rest on 1
to: 1,
// We set the initial velocity to whichever the smallest is:
// a) counterScale's current velocity, or
// b) an arbitrary minimum. You can experiment.
velocity: Math.max(counterScale.getVelocity(), 100),
// This ratio of stiffness to damping gives a nice, tight spring. Experiment!
stiffness: 700,
damping: 80
}).start(counterScale);
}
\`\`\`
By tweaking the properties of \`spring\`, you can make springs with wildly different feelings. Some can be playful, some can be terse. Try to find one appropriate for your brand or website.
There's one final modification to make. Currently, the spring says "Hey! You've reached the character count!" in a loud and abrupt way. By slowly changing the color of the counter we can also quietly inform the user that they're **approaching** the limit.
## The warning color
We're going to compose a very simple [value pipeline](/learn/value-pipelines) function that will convert our remaining character count into a color.
We can use three of Popmotion's [transformers](/api/transformers) to achieve this: \`pipe\`, \`blendColor\`, and \`interpolate\`.
We'll use \`pipe\` to make a new function. This new function will accept a character count and map that to a value between \`0\` and \`1\`. That new number is then used to blend between the \`counter\`'s text color and red:
Import:
\`\`\`javascript
const { listen, value, styler, spring, transform } = window.popmotion;
const { blendColor, interpolate, pipe } = transform;
\`\`\`
And then, after we define \`charLimit\` and \`counterStyler\`, create our new function:
\`\`\`javascript
const convertCountToColor = pipe(
// The input range starts at half the charLimit and ends at the
// charLimit itself. This means the color will start changing, in this
// instance, when the counter hits 5
interpolate([charLimit * 0.5, charLimit], [0, 1]),
blendColor(counterStyler.get('color'), '#f00')
);
\`\`\`
Now we just need to amend our \`updateRemainingCharsCounter\` function to set \`counterStyler\`'s \`'color'\` property with the output of this function:
\`\`\`javascript
function updateRemainingCharsCounter(val) {
// Measure char count
const charCount = val.length;
// Set remaining chars
counter.innerHTML = charLimit - charCount;
// Set counter color
counterStyler.set('color', convertCharCountToColor(charCount));
}
\`\`\`
Now when you type, the counter will begin to change color as your reach the character limit.
## Further optimisations
That's all for this tutorial, but there's plenty of ways in which we can go on to improve this form field counter:
- Visual \`focus\` state - maybe only show the character remaining count while the input has focus.
- Allow extra characters to be entered, and allow the "characters remaining" counter to run into the negatives.
- Not firing the spring on backspace.
- Only show the counter if JavaScript has loaded.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/value-pipelines.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Composable value pipelines
Popmotion provides simple utility functions that can take a value and return it transformed. These are, unsurprisingly, called **[transformers](/api/transformers)**.
\`\`\`javascript
import { transform } from 'popmotion';
// Or save your user's bytes!
import * as transform from 'popmotion/transformers';
\`\`\`
These functions can perform a wide range of tasks. Something as simple as appending a unit:
\`\`\`javascript
const { appendUnit } = transform;
const px = appendUnit('px');
px(5); // '5px'
\`\`\`
Make an infinite looping sequence:
\`\`\`javascript
wrap(0, 100)(150); // 50
\`\`\`
Or converting a value from one range to another:
\`\`\`javascript
interpolate([0,1], [-500, 500])(0.5); // 0
\`\`\`
## Curry
As you can see, many of these transformers are curried. So we can make a function:
\`\`\`javascript
const restrictNormalised = clamp(0, 1);
\`\`\`
And then reuse it elsewhere:
\`\`\`javascript
restrictNormalised(5); // 1
\`\`\`
## Compose
Because these functions are very simple and all carry the same signature, we can compose them. Popmotion provides a special transformer to do just that.
\`\`\`javascript
const { pipe } = transform;
\`\`\`
\`pipe\` is named as such because it takes a list of functions and returns a new function that will run these functions from **left to right**. Essentially, creating a value pipeline.
As our functions are descriptively named, and as many of them are curried, our pipelines become very easy to read. They become less imperative, and more declarative.
For instance, let's compose a function that will always return a valid RGB value. RGB values are simply integers between \`0\` and \`255\`.
\`\`\`javascript
const rgbUnit = pipe(
clamp(0, 255),
Math.round
);
\`\`\`
What's cool about this example is that it shows that **any** function that 1) takes a number and 2) returns a number, can be composed. \`Math.round\` does exactly that, so we can compose it.
You don't have to write this function yourself, because that exact code is already included as a Popmotion transformer, and used in the \`rgba\` [value type](/api/value-types).
The \`rgba\` transformer is **itself** composed. Here's the exact code:
\`\`\`javascript
const rgba = pipe(
transformMap({
red: rgbUnit,
green: rgbUnit,
blue: rgbUnit,
alpha
}),
rgbaTemplate
);
\`\`\`
This is an example of composing functions **which were themselves composed**. This is a very clear way of expressing and reusing our logic.
## Applying transformers to animations
Every Popmotion [action](/api/action) and [reaction](/api/reaction) has a native \`pipe\` function.
Providing a list of functions to \`pipe\` will return a new instance of the action or reaction, and whenever its \`update\` function is called, the value will be passed through these functions before being emitted.
Consider this tween:
\`\`\`javascript
tween({ from: 0, to: 255 });
\`\`\`
We can use our \`rgbUnit\` transformer from before to ensure that whenever this tween is called, it **always** returns a valid RGB unit:
\`\`\`javascript
tween({ from: 0, to: 255 })
.pipe(rgbUnit)
.start(console.log);
\`\`\`
This is a versatile approach to adding functionality to any animation. For instance, many animation libraries offer an option to create stepped tweens, but with this kind of composition we can easily bring that same functionality to \`physics\` (or any other animation).
Here, we can easily create something that spins at a constant velocity, outputting an angle value that snaps to \`45\` degree intervals:
\`\`\`javascript
physics({ velocity: 100 })
.pipe(
snap(45),
appendUnit('deg')
);
\`\`\`
Etc.
## Conclusion
These pure, simple functions are easily composed and reused. They can be used on their own, or with any action (not just tweens), making them extremely versatile.
We believe this functional approach gives developers the greatest flexibility and predictability.
We've covered just some of the many transformers here, but more are documented in our [API docs](/api/transformers). As they're pure functions, not specific to Popmotion, you can easily have fun creating your own.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/pure/learn/velocity-and-physics.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/components';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video
}
});
const content = convertMarkdown(`
# Physics and velocity
A core feature of Popmotion is the ability for some animations ([decay](/api/decay), [spring](/api/spring) and [physics](/api/physics)) to take a \`velocity\` parameter.
\`velocity\` changes each of these animations in a way that feels natural and visceral.
By passing the velocity of an existing animation to another, we can create natural-feeling transitions. And if that existing animation is \`pointer\`, it allows the user to directly affect animations with their own force leading to natural and playful interactions.
## Inspect velocity
But how do we get \`velocity\`?
Popmotion provides a special type of reaction called \`value\`.
\`\`\`javascript
import { value } from 'popmotion';
\`\`\`
\`value\` sits between your action and another reaction (for instance a style setter), and can be queried with \`get\` and \`getVelocity\`:
\`\`\`javascript
const myValue = value(0, console.log);
tween().start(myValue);
setTimeout(() => myValue.getVelocity(), 100)
\`\`\`
The returned \`velocity\` is measured in **units per second**. Why? Although 60fps is the current common framerate, VR devices support 90+ fps and the iPad Pro delivers a silky 120 frames per second!
To future-proof our code, we decouple velocity from the device framerate, otherwise our animations would run at 1.5x or even 2x the speed on these faster displays.
## Using \`value\`
\`value\` is provided two arguments: A value, and a function to call when this value updates:
\`\`\`javascript
const foo = value(0, console.log);
\`\`\`
As \`value\` is a reaction, it has an \`update\` method. We can call it to update the value:
\`\`\`javascript
foo.update(5);
\`\`\`
Usually though, we provide the \`value\` directly to an action:
\`\`\`javascript
tween({ to: 5 }).start(foo);
\`\`\`
Like our animations, \`value\` can accept objects and arrays:
\`\`\`javascript
const xy = value({ x: 0, y: 0 }, console.log);
const foo = tween({
to: { x: 100, y: 200 }
}).start(xy);
setTimeout(() => xy.getVelocity(), 100); // Returns as object
\`\`\`
Now we know enough about \`value\` to get the velocity of our user's pointer.
## Get \`pointer\` velocity
Using the example from the [previous tutorial](/learn/input-tracking), let's first make a \`value\` that updates \`ballStyler\`'s \`x\` and \`y\` properties:
\`\`\`javascript
const ballXY = value({ x: 0, y: 0 }, ballStyler.set);
\`\`\`
Now we can replace our \`startTracking\` function with this:
\`\`\`javascript
const startTracking = () => {
pointer(ballXY.get()).start(ballXY);
};
\`\`\`
As an added benefit of using \`value\`, a \`value\` **can't be subscribed to more than one action at a time**.
This means that we can stop using \`pointerTracker\` to maintain a reference to our active \`pointer\`.
Instead, we can either use \`ballXY.stop()\`, which will stop the action it's currently subscribed to. Or, we simply provide it to a different action, which is what we'll do in the following examples.
For now, amend \`stopTracking\` so it queries \`ballXY\`'s current velocity:
\`\`\`javascript
const stopTracking = () => {
const velocity = ballXY.getVelocity();
};
\`\`\`
## Using \`velocity\`
Three Popmotion animations accept a \`velocity\` property: \`decay\`, \`physics\` and \`spring\`.
Let's modify \`stopTracking\` three times, once for each, to see what they each do with \`velocity\`.
### \`decay\`
[\`decay\`](/api/decay) exponentially decreases a velocity over time. It's a form of the algorithm used in smartphone momentum scrolling, making it a natural-feeling way of slowing something down.
Based on the initial properties and \`velocity\`, it'll automatically calculate a \`target\` to animate to.
Using it is as easy as passing our newly-calculated \`velocity\` to \`decay\`:
\`\`\`javascript
decay({ velocity }).start(ballXY);
\`\`\`
\`\`\`marksy
{\`
const ball = document.querySelector('#a .ball');
const ballStyler = styler(ball);
const ballXY = value({ x: 0, y: 0 }, ballStyler.set);
function startTracking() {
pointer(ballXY.get())
.start(ballXY);
}
function stopTracking() {
decay({
from: ballXY.get(),
velocity: ballXY.getVelocity()
}).start(ballXY);
}
listen(ball, 'mousedown touchstart').start(startTracking);
listen(document, 'mouseup touchend').start(stopTracking);
\`}
\`\`\`
\`decay\` also accepts a \`modifyTarget\` function, which is provided the calculated target and returns a new one.
This can be used, for instance, to snap the target to a grid:
\`\`\`javascript
decay({
from: ballX.get(),
velocity,
modifyTarget: (target) => Math.ceil(target / 100) * 100
})
\`\`\`
### \`spring\`
[\`spring\`](/api/spring) is a spring simulation using \`mass\`, \`velocity\`, \`stiffness\` and \`damping\`. It can be used to simulate a wide variety of spring-feels.
Springs are great for interaction designers because they're expressive. For instance, you could design a spring that has a \`stiffness\` and \`damping\` property, but the \`mass\` property is based on the relative size of the element moving.
\`spring\` has defaults for all properties but you'll likely want to adjust at least \`stiffness\` and \`damping\`:
\`\`\`javascript
spring({
from: ballXY.get(),
velocity,
stiffness: 300,
damping: 10
}).start(ballXY);
\`\`\`
\`\`\`marksy
{\`
const ball = document.querySelector('#b .ball');
const ballStyler = styler(ball);
const ballXY = value({ x: 0, y: 0 }, ballStyler.set);
function startTracking() {
pointer(ballXY.get())
.start(ballXY);
}
function stopTracking() {
spring({
from: ballXY.get(),
velocity: ballXY.getVelocity(),
stiffness: 100,
damping: 10
}).start(ballXY);
}
listen(ball, 'mousedown touchstart').start(startTracking);
listen(document, 'mouseup touchend').start(stopTracking);
\`}
\`\`\`
### \`physics\`
The \`physics\` animation is the swiss army knife of velocity-based animations.
It offers \`friction\`, \`to\` and \`springStrength\` properties, so it can theoretically be used to create motion similar to \`decay\` and \`spring\`.
However, \`decay\` and \`spring\` animations are **differential equations** that resolve for a given \`elapsedTime\`. In practical terms, this means that if you want to change the animation they're creating, you need create a new animation with the new properties.
These equations are incredibly accurate, offering the smoothest motion and in the near future, will allow these animations to be scrubbable the same way \`tween\` is.
Instead, \`physics\` is an **integrated simulation**. This means that, once the simulation has started, its properties **can be modified** because \`physics\` uses **its current state** to calculate its next
For instance, we can tether a \`physics\` spring between the ball and the pointer:
\`\`\`javascript
const springTo = physics({
velocity: ballXY.getVelocity(),
friction: 0.6,
springStrength: 400,
to: ballXY.get(),
restSpeed: false
}).start(ballXY);
pointer(ballXY.get())
.start((v) => springTo.setSpringTarget(v));
\`\`\`
\`\`\`marksy
{\`
const ball = document.querySelector('#c .ball');
const ballStyler = styler(ball);
const ballXY = value({ x: 0, y: 0 }, ballStyler.set);
let activeAction;
let pointerTracker;
function startTracking() {
activeAction = physics({
velocity: ballXY.getVelocity(),
friction: 0.6,
springStrength: 400,
to: ballXY.get(),
restSpeed: false
}).start(ballXY);
pointerTracker = pointer(ballXY.get())
.start((v) => activeAction.setSpringTarget(v));
}
function stopTracking() {
if (activeAction) activeAction.stop();
if (pointerTracker) pointerTracker.stop();
spring({
velocity: ballXY.getVelocity(),
from: ballXY.get(),
stiffness: 300,
damping: 10
}).start(ballXY);
}
listen(ball, 'mousedown touchstart').start(startTracking);
listen(document, 'mouseup touchend').start(stopTracking);
\`}
\`\`\`
## Conclusion
\`velocity\` is a key part of creating natural interactions with Popmotion.
Be sure to check out the full docs of [value](/api/value), [decay](/api/decay), [spring](/api/spring) and [physics](/api/physics), as there's much more to each than we've been able to cover in this introductory tutorial.
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/stylefire/api/create-styler-factory.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/jsx';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
import CodeSandbox from '~/components/examples/CodeSandbox';
import TOC from '~/templates/content/TableOfContents';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video,
TOC: () => ,
CodeSandbox
}
});
const content = convertMarkdown(`# createStylerFactory
By default, \`styler\` outputs stylers for HTML and SVG elements. It's possible to create a \`styler\` for any JavaScript API that might benefit from render batching or cached state.
For this, we can use the \`createStylerFactory\` function.
## Usage
### Import
\`\`\`javascript
import { createStylerFactory } from 'stylefire';
\`\`\`
### Create a styler factory
\`createStylerFactory\` accepts an object of properties:
#### onRead
This method is fired when the styler's \`get\` is called and no property with this name is present in the state cache (or \`useCache\` is set to \`false\`).
\`\`\`typescript
(key: string, props: {}): any
\`\`\`
#### onRender
This method is fired on the render step, or if the styler's \`render\` method is called manually.
\`\`\`typescript
(state: {}, props: {}, changedValues: string[]): void
\`\`\`
#### useCache
Set to \`false\` if you always want \`onRead\` to fire when a user calls \`get\`.
### Using the styler factory
\`createStylerFactory\` returns a styler factory:
\`\`\`javascript
const myStylerFactory = createStylerFactory({ onRead, onRender });
\`\`\`
This is a function that accepts one argument, an object of props. These props can be anything - they are for you to use in \`onRead\` and \`onRender\` methods.
Here's an incredibly simple example of a styler that simply reads and writes any object provided to it:
\`\`\`javascript
import { createStylerFactory } from 'stylefire';
const exampleStyler = createStylerFactory({
onRead: (key, props) => props[key],
onRender: (state, props, changedValues) => {
changedValues.forEach(key => (props[key] = state[key]));
}
});
const myExampleStyler = exampleStyler({ red: 255, green: 255, blue: 255 });
myExampleStyler.set('green', 0);
\`\`\``);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/stylefire/api/html.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/jsx';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
import CodeSandbox from '~/components/examples/CodeSandbox';
import TOC from '~/templates/content/TableOfContents';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video,
TOC: () => ,
CodeSandbox
}
});
const content = convertMarkdown(`# HTML styler
When [\`styler\`](/stylefire/stylefire) is provided a \`HTMLElement\`, it returns a styler capable of setting CSS and scroll props.
\`\`\`javascript
const divStyler = styler(document.getElementByTag('div'))
divStyler.set({ x: 0, scale: 1.2 })
\`\`\`
## Config
A configuration argument can be **optionally** passed to \`styler\` as the second argument:
\`\`\`javascript
const divStyler = styler(div, {});
\`\`\`
### enableHardwareAcceleration
If set to \`false\`, Stylefire won't optimise animations with the GPU.
This can result in higher image quality when scaling up elements.
## Props
### Property alias
The following alias' can be optionally used for setting CSS props:
* \`x\`: \`translateX\`
* \`y\`: \`translateY\`
* \`z\`: \`translateZ\`
* \`originX\`: \`transformOriginX\`
* \`originY\`: \`transformOriginY\`
* \`originZ\`: \`transformOriginZ\`
If set as a number, \`originX\` and \`originY\` are set as a progress value between \`0\` and \`1\`. \`originZ\` is set in pixels.
### Transform property order
The \`transform\` property can be set directly, but in most cases it's simpler to animate \`rotate\`, \`translate\` and \`scale\` as individual props.
In accordance with the [CSS Transforms Level 2 spec](https://drafts.csswg.org/css-transforms-2/#individual-transforms), if set individually these properties will be set in the following order:
\`translate\`, \`scale\`, \`rotate\`
A different order can be specified by setting \`transform\` to a function. It'll be provided two values, the individually-set props and the Stylefire-generated. \`transform\`.
\`\`\`javascript
styler.set({
transform: ({ x, y }, generated) => 'translateX' + x * y + 'px)'
})
\`\`\`
### CSS variables
[CSS variables](https://css-tricks.com/difference-between-types-of-css-variables/#article-header-id-1) can be set and animated just like any other property:
\`\`\`javascript
const htmlStyler = css(document.documentElement);
htmlRenderer.set('--bg-color', '#000');
\`\`\`
### Supported props
**All CSS properties are supported**, in addition to these scroll properties:
* \`scrollTop\`
* \`scrollLeft\`
If \`window\` is passed to styler, these are the **only** two supported props.
### Prop types
For convenience and safety, many props are mapped to [value types](https://github.com/Popmotion/popmotion/tree/master/packages/style-value-types) for safety and convenience.
#### Color props
* \`color\`: \`color\`
* \`backgroundColor\`: \`color\`
* \`outlineColor\`: \`color\`
* \`fill\`: \`color\`
* \`stroke\`: \`color\`
#### Border props
- \`borderColor\`: \`color\`
- \`borderTopColor\`: \`color\`
- \`borderRightColor\`: \`color\`
- \`borderBottomColor\`: \`color\`
- \`borderLeftColor\`: \`color\`
- \`borderWidth\`: \`px\`
- \`borderTopWidth\`: \`px\`
- \`borderRightWidth\`: \`px\`
- \`borderBottomWidth\`: \`px\`
- \`borderLeftWidth\`: \`px\`
- \`borderRadius\`: \`px\`
- \`borderTopLeftRadius\`: \`px\`
- \`borderTopRightRadius\`: \`px\`
- \`borderBottomRightRadius\`: \`px\`
- \`borderBottomLeftRadius\`: \`px\`
#### Positioning
* \`width\`: \`px\`
- \`maxWidth\`: \`px\`
* \`height\`: \`px\`
- \`maxHeight\`: \`px\`
* \`top\`: \`px\`
* \`left\`: \`px\`
* \`bottom\`: \`px\`
* \`right\`: \`px\`
#### Transform
* \`rotate\`: \`degrees\`
* \`rotateX\`: \`degrees\`
* \`rotateY\`: \`degrees\`
* \`rotateZ\`: \`degrees\`
* \`scale\`: \`scale\`
* \`scaleX\`: \`scale\`
* \`scaleY\`: \`scale\`
* \`scaleZ\`: \`scale\`
* \`skewX\`: \`degrees\`
* \`skewY\`: \`degrees\`
* \`translateX\`: \`px\`
* \`translateY\`: \`px\`
* \`translateZ\`: \`px\`
* \`perspective\`: \`px\`
* \`opacity\`: \`alpha\`
#### Spacing
- \`padding\`: \`px\`
- \`paddingTop\`: \`px\`
- \`paddingRight\`: \`px\`
- \`paddingBottom\`: \`px\`
- \`paddingLeft\`: \`px\`
- \`margin\`: \`px\`
- \`marginTop\`: \`px\`
- \`marginRight\`: \`px\`
- \`marginBottom\`: \`px\`
- \`marginLeft\`: \`px\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/stylefire/api/styler.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/jsx';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
import CodeSandbox from '~/components/examples/CodeSandbox';
import TOC from '~/templates/content/TableOfContents';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video,
TOC: () => ,
CodeSandbox
}
});
const content = convertMarkdown(`# Styler
Stylers are performant style setters for HTML and SVG elements, optimised to work with animation libraries.
They batch updates to a single step on the [Framesync](/api/framesync) render loop, preventing layout thrashing and unnecessary renders.
They also allow \`transform\` properties to be set and animated independently.
The \`styler\` factory function is used to create a new styler for a single element or \`window\`.
## Usage
### Import
\`\`\`javascript
import styler from 'stylefire';
\`\`\`
### Create a styler
\`\`\`javascript
const div = document.querySelector('div');
const divStyler = styler(div);
\`\`\`
### Set style properties
The \`set\` method is used to schedule an update on the next render step.
\`\`\`javascript
divStyler.set({ x: 100 });
\`\`\`
Stylers understand default property types for many [CSS](/stylefire/api/html) and [SVG](/stylefire/api/svg) props. So even though we just set \`x\` as \`100\`, Stylefire will output \`transform: translateX(100px)\`.
### Forced render
Sometimes we need to force a render outside of the render loop. For instance, if we want to set some properties and then immediately measure the state of the element.
We can do so with the \`render\` method:
\`\`\`javascript
divStyler.set({ width: 'auto' });
divStyler.render();
\`\`\`
### Get style property
The \`get\` method can be used to read individual properties:
\`\`\`javascript
const div = document.querySelector('path');
const pathStyler = styler(path);
pathStyler.get('pathLength');
\`\`\`
**Note:** Due to the considerable filesize overhead in reading CSS \`transform\` properties, Stylefire will return the default value for any transform properties unless they've already been \`set\`.
## Methods
### set
Sets one or multiple properties, and schedules a render for the next available render step.
\`\`\`typescript
set(props: {}): this
set(key: string, prop: any): this
\`\`\`
### get
Returns the value of the provided key.
\`\`\`typescript
get(key: string): any
\`\`\`
### render
Immediately render, without waiting for the next frame.
\`\`\`typescript
render(): this
\`\`\``);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/stylefire/api/svg.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/jsx';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
import CodeSandbox from '~/components/examples/CodeSandbox';
import TOC from '~/templates/content/TableOfContents';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video,
TOC: () => ,
CodeSandbox
}
});
const content = convertMarkdown(`# SVG styler
The SVG styler replaces the confusing SVG transformation model with the CSS model and provides a simple API for \`path\` drawing.
\`\`\`javascript
const pathStyler = styler(document.getElementByTag('path'))
pathStyler.set({ pathLength: 50 })
\`\`\`
## Path drawing
Line drawing the process of using an SVG \`path\` element and its \`stroke-dasharray\` and \`stoke-dashoffset\` properties to emulate a pen drawing a line. [This blog post](https://css-tricks.com/svg-line-animation-works/) explains the effect in more detail.
Stylefire supports both\`'stoke-dasharray'\` and \`stroke-dashorigin\` properties, but also provides:
* \`pathLength\`
* \`pathSpacing\`
* \`pathOffset\`
These are all set as a **progress of the total path length, from \`0\` to \`1\`**, which is automatically measured by Stylefire.
So you can define an animation from \`0\` to \`1\`:
\`\`\`javascript
import { tween } from 'popmotion';
import styler from 'stylefire';
const path = document.querySelector('path');
const pathStyler = styler(polygon);
tween({ to: 1 }).start(
v => pathStyler.set('pathLength', v)
);
\`\`\`
In this example you can change the real \`path\` shape and length without having to update the animation.
## Props
### Supported props
All SVG attributes are supported. \`x\` and \`y\` attributes can be accessed via \`attrX\` and \`attrY\`.
The following transform props are supported:
* \`rotate\`: \`degrees\`
* \`rotateX\`: \`degrees\`
* \`rotateY\`: \`degrees\`
* \`scale\`: \`scale\`
* \`scaleX\`: \`scale\`
* \`scaleY\`: \`scale\`
* \`skewX\`: \`degrees\`
* \`skewY\`: \`degrees\`
* \`translateX\`: \`px\`
* \`translateY\`: \`px\`
* \`translateZ\`: \`px\`
* \`perspective\`: \`px\`
## Prop types
For convenience and safety, many props are mapped to [value types](https://github.com/Popmotion/popmotion/tree/master/packages/style-value-types) for safety and convenience.
* \`fill\`: \`color\`
* \`stroke\`: \`color\`
* \`x\`/\`y\`: \`px\`
* \`scale\`: \`scale\`
* \`scaleX\`: \`scale\`
* \`scaleY\`: \`scale\`
* \`opacity\`: \`alpha\`
* \`fillOpacity\`: \`alpha\`
* \`strokeOpacity\`: \`alpha\`
`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/stylefire/api/viewport.js
================================================
import { createElement } from 'react';
import marksy from 'marksy/jsx';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
import CodeSandbox from '~/components/examples/CodeSandbox';
import TOC from '~/templates/content/TableOfContents';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video,
TOC: () => ,
CodeSandbox
}
});
const content = convertMarkdown(`# Viewport styler
When \`styler\` is provided \`window\`, it returns a styler capable of scrolling the viewport.
\`\`\`javascript
const viewportStyler = styler(window);
viewportStyler.set('scrollTop', 20);
\`\`\`
It supports \`scrollTop\` and \`scrollLeft\` props.`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
================================================
FILE: packages/site/pages/stylefire/api.js
================================================
import { Fragment } from 'react';
import GlobalTemplate from '~/templates/global/Template';
import ContentPage from '~/templates/global-new/ContentPage';
import { Section, PageHeader } from '~/templates/global-new/styled';
import MenuPage from '~/templates/content/MenuPage';
export default () => (
);
================================================
FILE: packages/site/pages/stylefire/index.js
================================================
import StylefireHomepage from '~/templates/Stylefire';
export default () => ;
================================================
FILE: packages/site/pages/support.js
================================================
import { Fragment } from 'react';
import GlobalTemplate from '~/templates/global/Template';
import ContentPage from '~/templates/global-new/ContentPage';
import { Section, PageHeader } from '~/templates/global-new/styled';
import { A, P, H2, Li, Ul } from '~/templates/global/styled';
export default () => (
Support
Popmotion makes animations libraries that are open source and free to
use. It isn't sustainable without financial backing, which is why
we've turned to the community for funding.
Recurring pledges
There's a range of support tiers suited to individuals and
organisations alike.
You can pledge any amount, but for $10 a month individuals will
receive a special permissive, commercial license for premium React
components, like{' '}
React Pose Text.
For $100 or more a month, companies can get this license for their
entire team, as well as sponsoring the project publicly on our
website.
Finally, contributions of any amount are welcomed, and all will get
your name on our{' '}
official backers list
.
);
================================================
FILE: packages/site/public/manifest.json
================================================
{
"theme_color": "#ED2754"
}
================================================
FILE: packages/site/scripts/build-next-config.js
================================================
const fs = require('fs');
const path = require('path');
const outputPath = path.join(__dirname, '../next.config.js');
const fileTemplate = routes => `
module.exports = {
distDir: 'build',
exportTrailingSlash: true,
exportPathMap: function () {
return ${routes};
},
webpack: (config, props) => {
config.module.rules.push({
test: /\.md$/,
loader: 'emit-file-loader',
options: {
name: 'dist/[path][name].[ext]',
},
},
{
test: /\.md$/,
loader: 'raw-loader',
})
return config
}
};
`;
const generateRouteDefinitions = data => {
// TODO: Automatically generate api/blog etc
let routes = `
'/': { page: '/' },
'/support': { page: '/support' },
'/api': { page: '/api' },
'/blog': { page: '/blog' },
'/pose': { page: '/pose' },
'/pure': { page: '/pure' },
'/popcorn': { page: '/popcorn' },
'/pose/api': { page: '/pose/api' },
'/pose/examples': { page: '/pose/examples' },
'/page-not-found': { page: '/_error' },
'/stylefire': { page: '/stylefire' },
'/stylefire/api': { page: '/stylefire/api' },
`;
Object.keys(data).forEach(siteId => {
const siteData = data[siteId];
Object.keys(siteData).forEach(sectionId => {
const sectionData = siteData[sectionId];
const pageIds = Object.keys(sectionData);
pageIds.forEach(pageId => {
const route = `/${
siteId === 'pure' ? '' : siteId + '/'
}${sectionId}/${pageId}`;
routes += `
'${route}': {
page: '${route}'
},
`;
});
});
});
return `{${routes}}`;
};
module.exports = function(data) {
const routes = generateRouteDefinitions(data);
fs.writeFileSync(outputPath, fileTemplate(routes));
};
================================================
FILE: packages/site/scripts/convert-date-format.js
================================================
/*
Simple script to convert YYYYMMDD format into eg 28 Jan 2017 format
*/
const months = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
module.exports = function (yyyymmdd) {
const year = yyyymmdd.substring(2, 4);
const month = parseInt(yyyymmdd.substring(4, 6));
const day = parseInt(yyyymmdd.substring(6, 8));
return `${day} ${months[month]} ${year}`;
};
================================================
FILE: packages/site/scripts/filename-operations.js
================================================
const DS_STORE = '.DS_Store';
module.exports = {
filterFiles: (filename) => (filename.indexOf('.') === -1),
filterSystemFiles: (filename) => (filename !== DS_STORE)
};
================================================
FILE: packages/site/scripts/generate-content-page.js
================================================
const escapeBackticks = (string) => string.replace(/`/g, '\\`');
module.exports = (
body,
{
category,
id,
title,
description,
published,
siteName,
section,
next,
author,
},
isHomepage = false
) =>
isHomepage
? `
import marksy from 'marksy';
import Homepage from '~/components/template';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { createElement } from 'react';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
blockquote: Blockquote,
}
});
const Page = ({ section }) => (
{content.tree}
);
const content = convertMarkdown(\`${escapeBackticks(body)}\`);
export default Page;
`
: `
import { createElement } from 'react';
import marksy from 'marksy/jsx';
import { A, H1, H2, H3, H4, H5, P, Li, Ol, Ul, Hr, Code, Blockquote, ArticleHeader, Video } from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
import ContentTemplate from '~/templates/content/Template';
import Example from '~/components/examples/Example';
import CodePen from '~/components/examples/CodePen';
import CodeSandbox from '~/components/examples/CodeSandbox';
import TOC from '~/templates/content/TableOfContents';
const removeEmpty = filename => filename !== '';
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ol: Ol,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote,
},
components: {
Example,
CodePen,
Video,
TOC: () => ,
CodeSandbox
}
});
const content = convertMarkdown(\`${escapeBackticks(body)}\`);
const Page = ({ section }) => (
{content.tree}
);
export default Page;
`;
================================================
FILE: packages/site/scripts/generate-content.js
================================================
const fs = require('fs');
const path = require('path');
const frontMatter = require('front-matter');
const generatePage = require('./generate-content-page');
const generateMenus = require('./generate-menus');
const convertDateFormat = require('./convert-date-format');
const { filterFiles, filterSystemFiles } = require('./filename-operations');
const buildNextConfig = require('./build-next-config');
const packagesPath = path.join(__dirname, '../../');
const contentMetadataOutputPath = path.join(__dirname, '../data/content.json');
const siteNameMap = JSON.parse(
fs.readFileSync(path.join(__dirname, '../data/site-names.json'), {
encoding: 'utf-8',
})
);
const siteMetadata = {};
function generateContent({
rootDir,
subDir = '',
firstLevelDir = '',
siteName,
packageName,
}) {
packageName = packageName || rootDir;
siteName = siteName || siteNameMap[rootDir];
if (!siteName) return;
if (!siteMetadata[siteName]) siteMetadata[siteName] = {};
const readPath = path.join(__dirname, `../../${packageName}/docs/${subDir}`);
const outputPath = path.join(
__dirname,
`../pages/${siteName === 'pure' ? '' : siteName}/${firstLevelDir}`
);
// Create directory if none exists
if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath);
const dirList = fs.readdirSync(readPath).filter(filterSystemFiles);
dirList.forEach((filename) => {
// If directory, generate content for that directory
if (filename.indexOf('.') === -1) {
generateContent({
rootDir,
subDir: `${subDir}/${filename}/`,
firstLevelDir: firstLevelDir === '' ? filename : firstLevelDir,
siteName,
packageName,
});
// If file, generate content
} else {
const [id] = filename.split('.');
const file = fs.readFileSync(`${readPath}/${filename}`, {
encoding: 'utf-8',
});
const { attributes, body } = frontMatter(file);
const {
title,
description,
category,
published,
author,
next,
draft,
} = attributes;
const outputId = id === 'README' ? category : id;
if (!siteMetadata[siteName][firstLevelDir])
siteMetadata[siteName][firstLevelDir] = {};
const metadata = {
id: outputId,
title,
description,
category,
date: published,
author,
draft,
published: published ? convertDateFormat(`${published}`) : '',
section: firstLevelDir,
siteName: firstLevelDir === 'blog' ? 'popmotion' : siteName,
next,
};
fs.writeFileSync(
`${outputPath}/${outputId}.js`,
generatePage(body.replace(new RegExp('.md', 'g'), ''), metadata)
);
siteMetadata[siteName][firstLevelDir][outputId] = metadata;
}
});
}
const topLevel = fs.readdirSync(packagesPath).filter(filterFiles);
topLevel.forEach((rootDir) => generateContent({ rootDir }));
// Convert readme to homepage
const readme = fs
.readFileSync(path.join(__dirname, '../../../README.md'))
.toString('utf8')
.split('')[1];
fs.writeFileSync(
path.join(__dirname, '../pages/index.js'),
generatePage(
readme,
{
id: 'index',
title: 'Popmotion: The JavaScript animation toolbox',
description: '',
siteName: 'popmotion',
},
true
)
);
fs.writeFile(contentMetadataOutputPath, JSON.stringify(siteMetadata), (err) => {
const msg = !err ? 'Site content created' : err;
console.log(msg);
});
generateMenus(siteMetadata);
buildNextConfig(siteMetadata);
================================================
FILE: packages/site/scripts/generate-menus.js
================================================
const fs = require('fs');
const path = require('path');
const menuOutputPath = path.join(__dirname, '../data/menus.json');
const categoryNames = JSON.parse(
fs.readFileSync(path.join(__dirname, '../data/category-names.json'), 'utf8')
);
module.exports = function(siteMetadata) {
const menus = {};
// Iterate over sites
Object.keys(siteMetadata).forEach(siteName => {
const siteMenu = {};
// Iterate over sections
Object.keys(siteMetadata[siteName]).forEach(sectionKey => {
const menu = [];
const categories = {};
const sectionMetadata = siteMetadata[siteName][sectionKey];
// Iterate over posts
Object.keys(sectionMetadata).forEach(postKey => {
const { id, title, category } = sectionMetadata[postKey];
// This post has a category
if (category) {
// If category data doesn't exist
if (!categories[category]) {
categories[category] = {
id: category,
title: categoryNames[category],
posts: []
};
menu.push(categories[category]);
}
if (id !== category) {
categories[category].posts.push({ id, title });
}
// Or stand-alone post
} else {
menu.push({ id, title });
}
});
// Sort posts - adapted/butchered from https://blog.theodorejb.me/linked-list-sorting/
menu.sort((a, b) => (a.id < b.id ? -1 : 1)).forEach(menuItem => {
if (menuItem.posts) {
const unsortedList = [];
const sortedList = [];
const map = new Map();
let currentPost = null;
menuItem.posts.forEach((post, i) => {
const { next } = sectionMetadata[post.id];
if (!next || !sectionMetadata[next]) {
unsortedList.push(post);
} else {
const nextIndex = menuItem.posts.findIndex(
({ id }) => id === next
);
const isFirstPost =
menuItem.posts.find(({ id }) => {
const thisPost = sectionMetadata[id];
return post.id === thisPost.next;
}) === undefined;
if (isFirstPost) currentPost = post;
if (nextIndex > -1) {
map.set(post.id, nextIndex);
} else {
throw new Error(`${post.id} incorrectly linked to ${next}`);
}
}
});
const numPosts = menuItem.posts.length;
const numUnsorted = unsortedList.length;
const numToSort = numPosts - numUnsorted;
if (numToSort && currentPost) {
sortedList.push(currentPost);
const removeIndex = unsortedList.findIndex(
({ id }) => id === currentPost.id
);
if (removeIndex >= 0) {
unsortedList.splice(removeIndex, 1);
}
while (sortedList.length < numToSort) {
const nextPost = menuItem.posts[map.get(currentPost.id)];
sortedList.push(nextPost);
currentPost = nextPost;
}
menuItem.posts = [...sortedList, ...unsortedList];
}
}
});
siteMenu[sectionKey] = menu;
if (sectionKey === 'blog') menu.reverse();
});
menus[siteName] = siteMenu;
});
fs.writeFile(menuOutputPath, JSON.stringify(menus), err => {
const msg = !err ? 'Menus created' : err;
});
};
================================================
FILE: packages/site/styles/fonts.js
================================================
const fontWeight = (weight) => `font-weight: ${weight};`;
export const bodyFont = `
font-family: GT Walsheim,Neue Helvetica W02,Helvetica Neue,Helvetica,Arial,sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
${fontWeight(400)}
`;
export const codeFont = `
font-family: "IBM Plex Mono", "MonaLisa", "Dank Mono",
"Source Sans Pro", Courier, Monaco, monospace !important;
${fontWeight(400)}
`;
export const fontSize = (size) => `
font-size: ${size}px;
`;
export const fontBold = `font-weight: 700;`;
export const lineHeight = (height) => `line-height: ${height}px;`;
export const font = {
body: bodyFont,
bold: fontBold,
};
================================================
FILE: packages/site/styles/nprogress.js
================================================
import { color } from '~/styles/vars';
export default `
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: ${color.black};
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px ${color.black}, 0 0 5px ${color.black};
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
`;
================================================
FILE: packages/site/styles/reset.js
================================================
import { bodyFont, codeFont, fontSize } from './fonts';
import { ACTION, color, cols, ACTION_BURN } from './vars';
import { prismTheme } from '~/styles/syntax-highlighting';
export default `
body {
--color-popmotion: #FA196C;
--color-black: #2D2D2F;
--color-shade: #F7F7F7;
}
@font-face {
font-family: "GT Walsheim";
src: url("/fonts/GT-Walsheim-Regular.woff2")
format("woff2"),
url("/fonts/GT-Walsheim-Regular.woff")
format("woff");
}
@font-face {
font-family: "GT Walsheim Bold";
src: url("/fonts/GT-Walsheim-Bold.woff2")
format("woff2"),
url("/fonts/GT-Walsheim-Bold.woff")
format("woff");
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-style: inherit;
color: ${color.black};
${bodyFont}
line-height: 1;
vertical-align: baseline;
word-wrap: break-word;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-font-smoothing: antialiased;
}
br {
line-height: 0;
}
textarea,
input {
-webkit-appearance: none;
border-radius: 0;
}
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset;
}
/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Change the default font family in all browsers (opinionated).
* 2. Prevent adjustments of font size after orientation changes in IE and iOS.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
overflow-x: hidden;
}
/* HTML5 display definitions
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
* 2. Add the correct display in IE.
*/
article,
aside,
details, /* 1 */
figcaption,
figure,
footer,
header,
main, /* 2 */
menu,
nav,
section,
summary { /* 1 */
display: block;
}
/**
* Add the correct display in IE 9-.
*/
audio,
canvas,
progress,
video {
display: inline-block;
}
ul, ol {
list-style-type: none;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/* Links
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
color: #09f;
text-decoration: none;
&:hover {
color: #09f;
}
}
/**
* Remove the outline on focused links when they are also active or hovered
* in all browsers (opinionated).
*/
a:active,
a:hover {
outline-width: 0;
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the bottom border in Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of bolder by the next rule in Safari 6.
*/
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
strong {
font-weight: bolder;
}
/**
* Prevent sub and sup elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd em font sizing in all browsers.
*/
code,
kbd,
pre,
samp,
code span,
pre span {
${codeFont}
-webkit-font-smoothing: initial;
tab-size: 2;
}
p code,
li code {
background: ${color.superLightGrey};
padding: 3px 5px;
border: 1px solid ${color.lightGrey};
}
@media (max-width: ${cols(50)}) {
pre, pre code, pre span, code span {
${fontSize(14)}
line-height: 18px;
}
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
${prismTheme}
/* Forms
========================================================================== */
/**
* 1. Change font properties to inherit in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font: inherit; /* 1 */
margin: 0; /* 2 */
}
/**
* Restore the font weight unset by the previous rule.
*/
optgroup {
font-weight: bold;
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/* Reset button and button-style input default styles */
input[type="submit"],
input[type="reset"],
input[type="button"],
button {
background: none;
border: 0;
color: inherit;
cursor: default;
font: inherit;
line-height: normal;
overflow: visible;
padding: 0;
-webkit-appearance: none; /* for input */
-webkit-user-select: none; /* for button */
-moz-user-select: none;
-ms-user-select: none;
}
input::-moz-focus-inner,
button::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native audio and video
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: none; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from fieldset elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* fieldset elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on OS X.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Correct the text style of placeholders in Chrome, Edge, and Safari.
*/
::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to inherit in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: none; /* 1 */
font: inherit; /* 2 */
}
`;
================================================
FILE: packages/site/styles/syntax-highlighting.js
================================================
import { BLACK, BRAND, ENTITY, ACTION, cols } from './vars';
const createTheme = ({ fontSize = 14, lineHeight = 20, width = '100%' }) => ({
hljs: {
display: 'block',
overflowX: 'auto',
padding: cols(1),
color: BLACK,
fontSize: `${fontSize}px`,
lineHeight: `${lineHeight}px`,
width,
},
'hljs-comment': {
opacity: 0.5,
},
'hljs-keyword': {
color: ENTITY,
},
'hljs-name': {
color: ACTION,
},
'hljs-number': {
color: BRAND,
},
'hljs-params': {
color: ACTION,
},
'hljs-string': {
color: BRAND,
},
'hljs-attribute': {
color: BRAND,
},
'hljs-function': {
color: ACTION,
},
});
export const prismTheme = `
.token {
color: ${BLACK};
&.string {
color: ${BRAND};
}
&.keyword {
color: ${ENTITY};
}
&.comment {
opacity: 0.5;
}
&.number {
color: ${BRAND};
}
&.function {
color: ${ACTION};
}
}
`;
export const codeTheme = createTheme({});
export const codeThemeLarge = createTheme({
fontSize: 14,
lineHeight: 20,
width: '85%',
});
================================================
FILE: packages/site/styles/themes.js
================================================
import menus from '~/data/menus.json';
import content from '~/data/content.json';
import PopmotionLogo from '~/components/icons/Logo';
import PoseLogo from '~/components/icons/PoseLogo';
import PureLogo from '~/components/icons/PopmotionPure';
import StylefireLogo from '~/components/icons/StylefireLogo';
import FramesyncLogo from '~/components/icons/FramesyncLogo';
import { color } from './vars';
import PopcornLogo from '../components/icons/PopcornLogo';
const generateTheme = (name, props) => ({
data: {
menus: menus[name] || menus.pure, // Hack while blog is being published under popmotion
content: content[name] || content.pure,
rootUrl: name === 'popmotion' ? '/' : `/${name}`,
},
...props,
});
const themeSettings = {
framerMotion: {
id: 'framer-motion',
name: 'Framer Motion',
tagline: 'A truly simple React animation and gesture library.',
url: 'https://framer.com/motion',
color: {
base: '#f08',
baseShadow: '#c0c',
twist: '#d0e',
action: '#09f',
actionHighlight: '#09f',
},
Logo: () => (
),
},
popmotion: {
id: 'popmotion',
name: 'Popmotion',
url: '/',
tagline: 'Simple libraries for delightful interfaces',
headerNavLinks: [
{
href: '/blog',
label: 'Blog',
id: 'blog',
},
],
Logo: PopmotionLogo,
homepageLogoSize: {
width: 283,
height: 64,
},
headerLogoSize: {
width: 124,
height: 29,
},
footerLogoSize: {
width: 215,
height: 43,
},
color: {
base: '#FF1C68',
baseShadow: '#810066',
twist: '#960076',
action: '#049CD4',
actionHighlight: '#252942',
},
// Deprecated colors
actionColor: color.blue,
brandColor: color.brand,
mastheadBackground: `linear-gradient(${color.brand}, ${color.brandBurn})`,
shareImage: 'https://popmotion.io/images/twitter-card.png',
},
pose: {
id: 'pose',
name: 'Pose',
url: '/pose',
tagline: 'Declarative motion system for React, React Native, and Vue',
sections: ['api', 'learn', 'examples'],
headerNavLinks: [
{
href: '/pose/api',
label: 'API',
id: 'api',
},
{
href: '/pose/learn/get-started',
label: 'Learn',
id: 'learn',
},
{
href: '/pose/examples',
label: 'Examples',
id: 'examples',
},
],
Logo: PoseLogo,
homepageLogoSize: {
width: 175,
height: 53,
},
headerLogoSize: {
width: 127,
height: 40,
},
footerLogoSize: {
width: 193,
height: 70,
},
mastheadBackground: `linear-gradient(${color.pink}, ${color.purple})`,
color: {
base: '#ED00BB',
baseShadow: '#5B0089',
twist: '#A100F6',
action: '#0866C2',
},
// Deprecated colors
actionColor: color.blue,
brandColor: color.purple,
shareImage: 'https://popmotion.io/images/pose-twitter-card.png',
},
pure: {
id: 'pure',
name: 'Popmotion Pure',
url: '/pure',
tagline: 'A functional, flexible JavaScript motion library',
sections: ['api', 'learn'],
headerNavLinks: [
{
href: '/api',
label: 'API',
id: 'api',
},
{
href: '/learn/get-started',
label: 'Learn',
id: 'learn',
},
],
Logo: PureLogo,
homepageLogoSize: {
width: 140,
height: 65,
},
headerLogoSize: {
width: 106,
height: 50,
},
footerLogoSize: {
width: 150,
height: 70,
},
color: {
base: '#FF1C68',
baseShadow: '#810066',
twist: '#960076',
action: '#0866C2',
},
// Deprecated colors
actionColor: color.blue,
brandColor: color.brand,
mastheadBackground: `linear-gradient(${color.brand}, ${color.brandBurn})`,
shareImage: 'https://popmotion.io/images/twitter-card.png',
},
stylefire: {
id: 'stylefire',
name: 'Stylefire',
url: '/stylefire',
tagline: 'Simple HTML & SVG styler, optimised for animation',
sections: ['api'],
headerNavLinks: [
{
href: '/stylefire/api',
label: 'API',
id: 'api',
},
],
Logo: StylefireLogo,
homepageLogoSize: {
width: 204,
height: 54,
},
headerLogoSize: {
width: 130,
height: 36,
},
footerLogoSize: {
width: 188,
height: 55,
},
mastheadBackground: `linear-gradient(${color.orange}, ${color.orangeBurn})`,
color: {
base: '#FFE400',
baseShadow: '#CB4300',
twist: '#FF6A25',
action: '#0866C2',
},
// Deprecated colors
actionColor: color.orangeBurn,
brandColor: color.orange,
shareImage: 'https://popmotion.io/images/pose-twitter-card.png',
},
popcorn: {
id: 'popcorn',
name: 'Popcorn',
url: '/popcorn',
tagline: 'Utility functions for animation and interaction designers.',
sections: ['api'],
headerNavLinks: [
{
href: '/popcorn',
label: 'API',
id: 'api',
},
],
Logo: PopcornLogo,
homepageLogoSize: {
width: 190,
height: 45,
},
headerLogoSize: {
width: 140,
height: 33,
},
footerLogoSize: {
width: 210,
height: 50,
},
color: {
base: '#95FF13',
baseShadow: '#00CF93',
twist: '#00CF93',
action: '#06A69D',
},
shareImage: 'https://popmotion.io/images/twitter-card-popcorn.png',
},
framesync: {
id: 'framesync',
name: 'Framesync',
url: '/api/framesync',
tagline: 'Unity-inspired render loop for browsers',
headerNavLinks: [],
Logo: FramesyncLogo,
homepageLogoSize: {
width: 190,
height: 40,
},
headerLogoSize: {
width: 127,
height: 40,
},
footerLogoSize: {
width: 240,
height: 50,
},
mastheadBackground: `linear-gradient(${color.pink}, ${color.purple})`,
color: {
base: '#00CDE5',
baseShadow: '#012A52',
twist: '#0866C2',
action: '#0866C2',
},
// Deprecated colors
actionColor: color.blue,
brandColor: color.purple,
shareImage: 'https://popmotion.io/images/pose-twitter-card.png',
},
};
export default Object.keys(themeSettings).reduce((compiledThemes, name) => {
compiledThemes[name] = generateTheme(name, themeSettings[name]);
return compiledThemes;
}, {});
================================================
FILE: packages/site/styles/vars.js
================================================
import { css } from 'styled-components';
export const verticalGradient = (from, to, start = 0, end = 100) =>
`linear-gradient(to bottom, ${from} ${start}%, ${to} ${end}%)`;
// Deprecated - migrate to single `color` export
export const WHITE = '#fff';
export const BLACK = '#3d454e';
export const SUPER_LIGHT_GREY = '#FAFAFA';
export const LIGHT_GREY = '#f2f2f2';
export const PINK = '#FF1C68';
export const PINK_BURN = '#DB0068';
export const BLUE = '#09f';
export const BLUE_BURN = '#064FB5';
export const GREEN = '#14D790';
const PURPLE = '#9B65DE';
export const BRAND = PINK;
export const BRAND_BURN = PINK_BURN;
export const BRAND_GRADIENT = verticalGradient(PINK, PINK_BURN);
export const ACTION = BLUE;
export const ACTION_BURN = BLUE_BURN;
export const ACTION_GRADIENT = verticalGradient(BLUE, BLUE_BURN);
export const ENTITY = PURPLE;
export const color = {
black: '#252942',
blue: '#09f',
green: '#14D790',
white: '#fff',
pink: '#FF00C8',
purple: '#A100F6',
brand: '#FF1C68',
brandBurn: '#960076',
orange: '#F30',
orangeBurn: '#FF8213',
yellow: '#FFE42B',
grey: '#5E606C',
lightGrey: '#ECECEC',
superLightGrey: '#fafafa',
};
export const font = {
body: `
font-family: 'PT Sans', sans-serif;
font-weight: 400;
`,
code: `
font-family: 'Inconsolata', monospace;
font-weight: 400;
`,
bold: `font-weight: 700;`,
};
export const SKEW = '-5.7deg';
export const UNSKEW = '5.7deg';
const COL_WIDTH = 15;
export const cols = (num) => `${num * COL_WIDTH}px`;
const breakpoints = {
large: cols(72),
medium: cols(50),
small: cols(26),
};
export const media = Object.keys(breakpoints).reduce((acc, label) => {
acc[label] = (...args) => css`
@media (max-width: ${breakpoints[label]}) {
${css(...args)};
}
`;
return acc;
}, {});
================================================
FILE: packages/site/templates/Popcorn/index.js
================================================
import ContentTemplate from '~/templates/content/Template';
import Footer from '~/templates/global-new/Footer';
import { createElement } from 'react';
import settings from '~/data/settings.json';
import docs from '~/docs/popcorn/index.md';
import marksy from 'marksy/components';
import SiteLink from '~/components/layout/SiteLink';
import styled from 'styled-components';
import { CTA } from '../Popmotion/Masthead/styled';
import {
A,
H1,
H2,
H3,
H4,
H5,
P,
Li,
Ol,
Ul,
Hr,
Code,
Blockquote,
ArticleHeader,
Video
} from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
const Container = styled.article`
padding-top: 30px;
`;
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote
}
});
const { tree, toc } = convertMarkdown(docs);
export default () => (
{tree}
);
================================================
FILE: packages/site/templates/Popmotion/FinalCTA/index.js
================================================
import { Container, ContentContainer } from './styled';
import { BlurbText } from '~/templates/Popmotion/USPs/styled';
import { CTA } from '../Masthead/styled';
import Link from 'next/link';
export default () => (
Pick and choose any part of Popmotion by importing modules individually.
Or take it all for 11.7kb!
Get started
);
================================================
FILE: packages/site/templates/Popmotion/FinalCTA/styled.js
================================================
import styled from 'styled-components';
import { Centered } from '~/templates/global/grid';
import { cols, LIGHT_GREY } from '~/styles/vars';
export const Container = styled.section`
padding-top: ${cols(4)};
border-top: ${LIGHT_GREY} 1px solid;
border-bottom: none;
`;
export const ContentContainer = styled(Centered)`
display: flex;
flex-direction: column;
margin: 0 auto;
`;
================================================
FILE: packages/site/templates/Popmotion/Header/index.js
================================================
import SplitText from 'react-pose-text';
import styled from 'styled-components';
import { ActionLink } from '~/templates/global-new/styled';
import { color, media } from '~/styles/vars';
const Container = styled.div`
height: 320px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
flex-direction: column;
${media.medium`height: 180px;`} ${media.small`height: 140px;`};
`;
const HeaderText = styled.h2`
margin-bottom: 30px;
${media.medium`margin-bottom: 15px`};
div {
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.12);
color: ${color.white};
font-size: 24px;
font-weight: 700;
${media.medium`
font-size: 22px;`};
${media.small`
font-size: 18px;`};
}
`;
const charPoses = {
hidden: { opacity: 0, y: 10 },
visible: {
opacity: 1,
y: 0,
delay: ({ charIndex }) => charIndex * 10
}
};
export default () => (
A functional, flexible JavaScript animation library
Get started
);
================================================
FILE: packages/site/templates/Popmotion/Masthead/index.js
================================================
import { withTheme } from 'styled-components';
import SiteLink from '~/components/layout/SiteLink';
import {
Container,
MastheadContainer,
Title,
Logo,
LogoContainer,
LogoText,
Blurb,
CTA
} from './styled';
import Link from 'next/link';
const Masthead = ({ children, theme, getStarted = true }) => (
{children}
{theme.name}
{theme.tagline}
{getStarted ? (
Quick start
) : null}
);
export default withTheme(Masthead);
================================================
FILE: packages/site/templates/Popmotion/Masthead/styled.js
================================================
import styled, { css } from 'styled-components';
import {
WHITE,
BLACK,
ACTION_GRADIENT,
ACTION_BURN,
LIGHT_GREY,
UNSKEW,
SKEW,
cols,
media,
color
} from '~/styles/vars';
import { fontSize, fontBold } from '~/styles/fonts';
export const Container = styled.section`
background: ${({ theme }) => theme.mastheadBackground};
z-index: 0;
svg path {
fill: ${WHITE};
}
li a:hover {
color: ${WHITE};
}
`;
export const MastheadContainer = styled.section`
display: flex;
flex-direction: column;
flex-align: center;
justify-content: center;
text-align: center;
margin: 0 ${cols(4)};
padding-bottom: ${cols(4)};
position: relative;
z-index: 1;
${media.medium`
margin: 0 ${cols(2)};
padding-bottom: ${cols(2)};
`};
`;
export const Title = styled.h1`
display: block;
overflow: hidden;
margin-bottom: ${cols(1)};
`;
export const LogoContainer = styled.div`
svg {
${({ width, height }) => `
width: ${width}px!important; // eugh
height: ${height}px!important;
`};
}
${media.medium`
svg {
${({ width, height }) => `
width: ${width * 0.75}px!important;
height: ${height * 0.75}px!important;
`}
}
`};
${media.small`
svg {
${({ width, height }) => `
width: ${width * 0.6}px!important;
height: ${height * 0.6}px!important;
`}
}
`};
`;
export const LogoText = styled.div`
width: 0px;
height: 0px;
text-indent: -9999px;
`;
export const Blurb = styled.h2`
${fontSize(24)}
${fontBold}
margin-bottom: ${cols(3)};
color: ${WHITE};
${media.medium`
${fontSize(18)}
margin-bottom: ${cols(2)};
`}
`;
export const CTA = styled.div`
background: ${WHITE};
margin: 0 auto;
display: flex;
transform: skewX(${SKEW});
transition: transform 100ms cubic-bezier(.17,.67,.34,1.54);
box-shadow: 0 2px 0 0 black;
justify-content: stretch;
&:hover {
transform: skewX(${SKEW}) scale(1.1);
}
a, button {
${fontSize(24)}
${fontBold}
color: ${BLACK};
cursor: pointer;
text-decoration: none;
padding: ${cols(1)} ${cols(2)};
transform: skewX(${UNSKEW});
text-align: center;
display: block;
width: 100%;
}
${media.medium`
a {
${fontSize(18)}
}
`}
${props =>
props.brandFill &&
css`
background: ${props.theme.actionColor};
a {
color: ${color.white};
}
`};
`;
================================================
FILE: packages/site/templates/Popmotion/USPs/Example.js
================================================
import {
ExampleContainer,
ExampleHeader,
Description,
CenteredContent
} from './styled';
import Link from 'next/link';
export default ({ title, children, link, description }) => (
{link ? (
{title}
) : title}
{description}
{children}
);
================================================
FILE: packages/site/templates/Popmotion/USPs/ExampleSection.js
================================================
import { SectionContainer, SectionHeader } from './styled';
export default ({ title, children }) => (
{title && {title}}
{children}
);
================================================
FILE: packages/site/templates/Popmotion/USPs/index.js
================================================
import { Container, Blurb } from './styled';
export default () => (
These are the legacy docs for Popmotion 8.
);
================================================
FILE: packages/site/templates/Popmotion/USPs/styled.js
================================================
import styled from 'styled-components';
import { cols, media } from '~/styles/vars';
import { Centered } from '~/templates/global/grid';
import { fontSize, lineHeight, fontBold } from '~/styles/fonts';
const MAX_WIDTH = cols(60);
export const Container = styled.section`
display: flex;
align-items: center;
flex-direction: column;
`;
const BlurbContainer = styled(Centered)`
display: flex;
align-items: center;
padding: ${cols(1)};
margin-bottom: ${cols(4)};
width: 100%;
span {
background: #eee;
flex: 1;
height: 1px;
}
${media.medium`
margin-bottom: ${cols(2)};
`};
${media.small`
display: block;
span {
display: none;
}
`};
`;
export const BlurbText = styled.p`
${fontSize(24)}
${lineHeight(32)}
max-width: ${cols(32)};
text-align: center;
padding-left: ${cols(2)};
padding-right: ${cols(2)};
${media.medium`
${fontSize(18)}
${lineHeight(26)}
padding-left: ${cols(1)};
padding-right: ${cols(1)};
`}
`;
export const Blurb = ({ children }) => (
{children}
);
export const SectionContainer = styled.section`
width: 100%;
margin-bottom: ${cols(2)};
overflow-x: hidden;
`;
export const SectionHeader = styled.h2`
${fontSize(48)} ${fontBold}
text-align: center;
margin-bottom: ${cols(2)};
${media.medium`
${fontSize(36)}
`} ${media.small`
${fontSize(28)}
`};
`;
export const ExampleContainer = styled.div`
margin-bottom: ${cols(2)};
`;
export const ExampleHeader = styled.h3`
${fontSize(28)}
${lineHeight(28)}
${fontBold}
margin-bottom: ${cols(1)};
display: block;
width: 100%;
a {
${fontBold}
}
${media.medium`
${fontSize(22)}
${lineHeight(22)}
`}
${media.small`
${fontSize(18)}
${lineHeight(18)}
`}
`;
export const Description = styled.p`
${fontSize(18)} ${lineHeight(28)}
width: 50%;
${media.medium`
width: 100%;
`} ${media.small`
${fontSize(14)}
${lineHeight(22)}
width: 100%;
`};
`;
export const LiveContainer = styled.div``;
export const CenteredContent = styled.div`
max-width: ${MAX_WIDTH};
margin: 0 auto;
display: flex;
flex-direction: column;
${media.large`
margin: 0 ${cols(1)}
`};
`;
================================================
FILE: packages/site/templates/Popmotion/index.js
================================================
import Homepage from '~/templates/homepage';
import Header from './Header';
import Footer from '~/templates/global-new/Footer';
export default () => (
);
================================================
FILE: packages/site/templates/Pose/Header/index.js
================================================
import SplitText from 'react-pose-text';
import styled from 'styled-components';
import { ActionLink } from '~/templates/global-new/styled';
import { color, media } from '~/styles/vars';
const Container = styled.div`
height: 320px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
flex-direction: column;
${media.medium`height: 180px;`} ${media.small`height: 140px;`};
`;
const HeaderText = styled.h2`
margin-bottom: 30px;
${media.medium`margin-bottom: 15px`};
div {
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.12);
color: ${color.white};
font-size: 24px;
font-weight: 700;
${media.medium`
font-size: 22px;`};
${media.small`
font-size: 18px;`};
}
`;
const charPoses = {
hidden: { opacity: 0, y: 10 },
visible: {
opacity: 1,
y: 0,
delay: ({ charIndex }) => charIndex * 10
}
};
export default () => (
A truly simple animation library for React, React Native, and Vue
Get started
);
================================================
FILE: packages/site/templates/Pose/USPs/AnimateAnything.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import {
Carousel,
Item,
AlignCenter
} from '~/templates/Popmotion/LiveExamples/styled';
import { styler, value, listen, pointer, decay, transform } from 'popmotion';
import posed from 'react-pose';
import styled from 'styled-components';
import { color } from '~/styles/vars';
const props = {
rest: {
x: 0,
y: 0,
background: 'rgba(161, 0, 246, 0)',
boxShadow: '0px 0px 0px rgba(0, 0, 0, 0)',
transition: { duration: 700 }
},
popped: {
x: -10,
y: -10,
background: 'rgba(161, 0, 246, 1)',
boxShadow: '10px 10px 20px rgba(161, 0, 246, 0.2)',
transition: { duration: 700 }
}
};
const Box = styled(posed.div(props))`
width: 100px;
height: 100px;
`;
const code = `popped: {
x: -10,
y: -10,
background: 'rgba(161, 0, 246, 1)',
boxShadow: '10px 10px 20px rgba(161, 0, 246, 0.2)',
transition: { duration: 700 }
}`;
class Example extends React.Component {
state = { isVisible: false };
componentDidMount() {
this.interval = setInterval(this.toggleVisibility, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
toggleVisibility = () => this.setState({ isVisible: !this.state.isVisible });
render() {
return ;
}
}
export default () => (
);
================================================
FILE: packages/site/templates/Pose/USPs/ChildrenExample.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import { AlignCenter } from '~/templates/Popmotion/LiveExamples/styled';
import {
styler,
spring,
value,
listen,
tween,
pointer,
decay,
transform
} from 'popmotion';
import posed from 'react-pose';
import styled from 'styled-components';
import { color } from '~/styles/vars';
import { eachValue } from 'popmotion-pose';
const Container = styled.div`
overflow: hidden;
`;
const sidepanelProps = {
closed: {
x: '-100%'
},
open: {
x: '0%',
delayChildren: 100,
staggerChildren: 60
}
};
const itemProps = {
closed: {
y: 20,
opacity: 0
},
open: {
y: 0,
opacity: 1
}
};
const Sidepanel = styled(posed.ul(sidepanelProps))`
background: ${color.blue};
width: 300px;
padding: 20px;
`;
const Item = styled(posed.li(itemProps))`
width: 100%;
border-radius: 5px;
height: 35px;
background: ${color.white};
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
`;
const code = {
react: `const Parent = posed.ul(config)
const Child = posed.li(childConfig)
({ items }) => (
{items.map(item => )}
)
`,
vue: `const Component = {
components: {
Parent: posed.ul(config),
Child: posed.li(childConfig)
},
template: \`
\`
}`
};
class Example extends React.Component {
state = { isVisible: false };
componentDidMount() {
this.interval = setInterval(this.toggleVisibility, 2000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
toggleVisibility = () => this.setState({ isVisible: !this.state.isVisible });
render() {
return (
);
}
}
export default props => (
);
================================================
FILE: packages/site/templates/Pose/USPs/CustomExample.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import {
Carousel,
Item,
AlignCenter
} from '~/templates/Popmotion/LiveExamples/styled';
import {
styler,
spring,
value,
listen,
tween,
pointer,
decay,
transform
} from 'popmotion';
import posed from 'react-pose';
import styled from 'styled-components';
import { color } from '~/styles/vars';
const props = {
rest: {
scale: 1,
backgroundColor: color.green
},
alert: {
scale: 1.3,
transition: {
scale: props => spring({ ...props, stiffness: 200, damping: 0 }),
backgroundColor: tween
}
}
};
const Box = styled(posed.div(props))`
width: 100px;
height: 100px;
background: ${color.green};
border-radius: 50%;
transform: scaleX(0);
transform-origin: 50%;
`;
const code = `const Circle = posed.div({
attention: {
scale: 1.3,
transition: {
type: 'spring',
stiffness: 200,
damping: 0
}
}
})`;
class Example extends React.Component {
state = { isVisible: false };
componentDidMount() {
this.interval = setInterval(this.toggleVisibility, 2000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
toggleVisibility = () => this.setState({ isVisible: !this.state.isVisible });
render() {
return ;
}
}
export default () => (
);
================================================
FILE: packages/site/templates/Pose/USPs/DeclarativeExample.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import {
Carousel,
Item,
AlignCenter
} from '~/templates/Popmotion/LiveExamples/styled';
import { styler, value, listen, pointer, decay, transform } from 'popmotion';
import posed from 'react-pose';
import styled from 'styled-components';
import { color } from '~/styles/vars';
const props = {
open: { scale: 1 },
closed: { scale: 0 }
};
const Box = styled(posed.div(props))`
width: 100px;
height: 100px;
background: ${color.blue};
border-radius: 50%;
transform: scaleX(0);
transform-origin: 50%;
`;
const code = {
react: `({ isOpen }) =>
`,
vue: `
`
};
class Example extends React.Component {
state = { isVisible: false };
componentDidMount() {
this.interval = setInterval(this.toggleVisibility, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
toggleVisibility = () => this.setState({ isVisible: !this.state.isVisible });
render() {
return ;
}
}
export default props => (
);
================================================
FILE: packages/site/templates/Pose/USPs/DraggableExample.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import {
Carousel,
Item,
AlignCenter
} from '~/templates/Popmotion/LiveExamples/styled';
import {
styler,
value,
listen,
pointer,
decay,
spring,
transform
} from 'popmotion';
import posed from 'react-pose';
import styled from 'styled-components';
import { color } from '~/styles/vars';
const props = {
hoverable: true,
draggable: 'x',
dragBounds: { left: '-100%', right: '100%' },
init: { scale: 1 },
hover: { scale: 1.2 },
drag: { scale: 1.1 }
};
const Box = styled(posed.div(props))`
width: 100px;
height: 100px;
background: ${color.brand};
transform: scaleX(0);
transform-origin: 50%;
color: white;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
`;
const code = `const Box = posed.div({
hoverable: true,
draggable: 'x',
dragBounds: { left: '-100%', right: '100%' },
init: { scale: 1 },
hover: { scale: 1.2 },
drag: { scale: 1.1 }
})`;
class Example extends React.Component {
render() {
return Drag;
}
}
export default () => (
);
================================================
FILE: packages/site/templates/Pose/USPs/FlipExample.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import {
Carousel,
Item,
AlignCenter
} from '~/templates/Popmotion/LiveExamples/styled';
import { tween, value, listen, pointer, decay, transform } from 'popmotion';
import posed from 'react-pose';
import styled from 'styled-components';
import pose from 'popmotion-pose';
import { color } from '~/styles/vars';
const props = {
left: { height: '50px', left: '-50px' },
right: { height: '100px', left: '50px' }
};
const code = `// Vanilla & React Pose only
await poser.set('childOut')
await poser.flip(() => {
poser.clearChildren()
parentElement.removeChild(parent.firstChild)
parentElement.appendChild(newChild)
poser.addChild(newChild, childProps)
})
poser.set('childIn')`;
const Modal = styled.div`
background: ${color.green};
width: 300px;
padding: 20px;
overflow: hidden;
> div {
background: white;
height: 40px;
margin-bottom: 10px;
opacity: 0;
border-radius: 5px;
&:last-child {
margin-bottom: 0;
}
}
`;
const modalProps = {
itemsOut: {
staggerChildren: 50
}
};
const modalItemProps = {
initialPose: 'itemsOut',
flip: {
transition: tween
},
itemsOut: {
x: -50,
opacity: 0,
transition: tween
},
itemsIn: {
x: 0,
opacity: 1,
transition: tween
}
};
class Example extends React.Component {
a = [0, 1, 2, 3];
b = [4, 5, 6];
state = {
list: this.a
};
listRefs = new Set();
setContainerRef = ref => {
if (ref) {
this.ref = ref;
} else if (this.modalPoser) {
this.modalPoser.destroy();
}
};
setItemRef = ref => {
if (ref) {
this.listRefs.add(ref);
} else if (this.modalPoser) {
// remove
}
};
componentDidMount() {
this.modalPoser = pose(this.ref, modalProps);
this.listRefs.forEach(el => this.modalPoser.addChild(el, modalItemProps));
this.listRefs.clear();
this.modalPoser.set('itemsIn');
this.interval = setInterval(() => {
this.modalPoser.set('itemsOut').then(() => {
this.modalPoser.clearChildren();
this.modalPoser.measure();
this.setState({
list: this.state.list === this.a ? this.b : this.a
});
});
}, 5000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
componentDidUpdate() {
this.listRefs.forEach(el => this.modalPoser.addChild(el, modalItemProps));
this.listRefs.clear();
this.modalPoser.flip().then(() => this.modalPoser.set('itemsIn'));
}
render() {
return (
{this.state.list.map(i => (
))}
);
}
}
export default () => (
);
================================================
FILE: packages/site/templates/Pose/USPs/PassiveExample.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import {
Carousel,
Item,
AlignCenter
} from '~/templates/Popmotion/LiveExamples/styled';
import { styler, value, listen, pointer, decay, transform } from 'popmotion';
import posed from 'react-pose';
import styled from 'styled-components';
import { color } from '~/styles/vars';
const { interpolate } = transform;
const props = {
draggable: 'x',
passive: {
opacity: ['x', interpolate([-200, -100, 100, 200], [0, 1, 1, 0])]
}
};
const Box = styled(posed.div(props))`
width: 100px;
height: 100px;
background: ${color.blue};
border-radius: 50%;
transform: scaleX(0);
transform-origin: 50%;
color: white;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
`;
const code = `// React, Vue & vanilla use Popmotion's functional pipelines
passive: {
opacity: ['x', interpolate(
[-200, -100, 100, 200],
[0, 1, 1, 0]
)]
}
// React Native uses React Animated's interpolate function
passive: {
opacity: ['x', {
inputRange: [-200, -100, 100, 200],
outputRange: [0, 1, 1, 0]
}]
}`;
class Example extends React.Component {
state = { isVisible: false };
componentDidMount() {
this.interval = setInterval(this.toggleVisibility, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
toggleVisibility = () => this.setState({ isVisible: !this.state.isVisible });
render() {
return Drag;
}
}
export default () => (
);
================================================
FILE: packages/site/templates/Pose/USPs/PluginsExample.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import { AlignCenter } from '~/templates/Popmotion/LiveExamples/styled';
import posed, { PoseGroup } from 'react-pose';
import styled from 'styled-components';
import { color } from '~/styles/vars';
import trackVisibility from '~/templates/Popmotion/LiveExamples/track-visibility';
import SplitText from 'react-pose-text';
const code = `const charPoses = {
hidden: { y: 10, opacity: 0 },
visible: {
y: 0,
opacity: 1,
delay: ({ charIndex }) => charIndex * 50
}
};
({ isVisible }) => (
React Pose Text
)`;
const charPoses = {
hidden: { y: 10, opacity: 0 },
visible: {
y: 0,
opacity: 1,
delay: ({ charIndex }) => charIndex * 50
}
};
class Example extends React.Component {
render() {
return (
React Pose Text
);
}
}
export default trackVisibility(({ isVisible }) => (
));
================================================
FILE: packages/site/templates/Pose/USPs/ReactExample.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import { AlignCenter } from '~/templates/Popmotion/LiveExamples/styled';
import {
styler,
spring,
value,
listen,
tween,
pointer,
decay,
transform
} from 'popmotion';
import posed, { PoseGroup } from 'react-pose';
import styled from 'styled-components';
import { color } from '~/styles/vars';
const Container = styled.div`
overflow: hidden;
`;
const sidepanelProps = {};
const itemProps = {
closed: {
y: 20,
opacity: 0
},
open: {
y: 0,
opacity: 1
}
};
const Sidepanel = styled(posed.ul(sidepanelProps))`
width: 300px;
padding: 20px;
`;
const Item = styled(posed.li())`
width: 100%;
border-radius: 5px;
height: 35px;
background: ${color.white};
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
&[data-key='0'] {
background: ${color.green};
}
&[data-key='1'] {
background: ${color.brand};
}
&[data-key='2'] {
background: ${color.blue};
}
&[data-key='3'] {
background: ${color.purple};
}
`;
const code = `// PoseGroup currently React DOM only
const Item = posed.li()
const List = ({ items }) => (
)`;
function shuffle(array) {
var currentIndex = array.length,
temporaryValue,
randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
class Example extends React.Component {
state = { items: [0, 1, 2, 3] };
componentDidMount() {
this.interval = setInterval(this.toggleVisibility, 2000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
toggleVisibility = () => this.setState({ items: shuffle(this.state.items) });
render() {
return (
{this.state.items.map(item => )}
);
}
}
export default () => (
);
================================================
FILE: packages/site/templates/Pose/USPs/ZeroConfigExample.js
================================================
import Template from '~/templates/Popmotion/LiveExamples/Template';
import {
Carousel,
Item,
AlignCenter
} from '~/templates/Popmotion/LiveExamples/styled';
import { styler, value, listen, pointer, decay, transform } from 'popmotion';
import posed from 'react-pose';
import styled from 'styled-components';
import { color } from '~/styles/vars';
const props = {
left: { x: '-100%' },
right: { x: '100%' }
};
const Box = styled(posed.div(props))`
width: 100px;
height: 100px;
background: ${color.brand};
transform: translateX(-100%);
`;
const code = {
react: `const Box = posed.div({
left: { x: -100 },
right: { x: 100 }
})
const Component = ({ position }) =>
`,
vue: `const Component = {
components: {
Box: posed.div({
left: { x: -100 },
right: { x: 100 }
})
},
template: \`\`
}`
};
class Example extends React.Component {
state = { isVisible: true };
componentDidMount() {
this.interval = setInterval(this.toggleVisibility, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
toggleVisibility = () => this.setState({ isVisible: !this.state.isVisible });
render() {
return ;
}
}
export default props => (
);
================================================
FILE: packages/site/templates/Pose/USPs/index.js
================================================
import styled from 'styled-components';
import ZeroConfigExample from './ZeroConfigExample';
import DeclarativeExample from './DeclarativeExample';
import DraggableExample from './DraggableExample';
import PassiveExample from './PassiveExample';
import ReactExample from './ReactExample';
import { Strong } from '~/templates/global/styled';
import CustomExample from './CustomExample';
import ChildrenExample from './ChildrenExample';
import FlipExample from './FlipExample';
import PluginsExample from './PluginsExample';
import Link from 'next/link';
import { CTA } from '~/templates/Popmotion/Masthead/styled';
import AnimateAnythingExample from './AnimateAnything';
import { ActionLink } from '~/templates/global-new/styled';
const MoreExamples = styled.div`
margin: 20px auto 50px;
`;
export default class Examples extends React.Component {
state = { framework: 'react' };
setFramework = (framework) => () => this.setState({ framework });
setFrameworkTo = {
vue: this.setFramework('vue'),
react: this.setFramework('react'),
};
render() {
return (
These are legacy docs for Pose. Please upgrade to{' '}
Framer Motion.
Get started
See all examples
);
}
}
================================================
FILE: packages/site/templates/Pose/index.js
================================================
import Homepage from '~/templates/homepage';
import Header from './Header';
import Footer from '~/templates/global-new/Footer';
import styled from 'styled-components';
const Notice = styled.section`
width: 100%;
max-width: 900px;
margin: 20px auto 50px;
background: #f4f4f4;
border-left: 4px solid #ff1c68;
padding: 20px;
h2 {
font-weight: 800;
margin-bottom: 20px;
font-size: 24px;
}
`;
export default () => (
⚠️ Notice
React Pose for web has been deprecated by{' '}
Framer Motion.
);
================================================
FILE: packages/site/templates/Stylefire/index.js
================================================
import Homepage from '~/templates/homepage';
import Footer from '~/templates/global-new/Footer';
import { createElement } from 'react';
import settings from '~/data/settings.json';
import docs from '~/docs/stylefire/index.md';
import marksy from 'marksy/components';
import SiteLink from '~/components/layout/SiteLink';
import styled from 'styled-components';
import { CTA } from '../Popmotion/Masthead/styled';
import {
A,
H1,
H2,
H3,
H4,
H5,
P,
Li,
Ol,
Ul,
Hr,
Code,
Blockquote,
ArticleHeader,
Video
} from '~/templates/global/styled';
import { Img } from '~/templates/content/styled';
const Container = styled.article`
padding-top: 30px;
`;
const convertMarkdown = marksy({
createElement,
elements: {
a: A,
h1: ArticleHeader,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
p: P,
code: Code,
li: Li,
ul: Ul,
hr: Hr,
img: Img,
blockquote: Blockquote
}
});
const { tree, toc } = convertMarkdown(docs);
export default () => (
null}
>
{tree}
Read API docs
);
================================================
FILE: packages/site/templates/blog/components/BlogItem/index.js
================================================
import Link from 'next/link';
import { Container, Title, Description, Date } from './styled';
export default ({ id, title, description, published }) => (
{title}
{published}
{description}
);
================================================
FILE: packages/site/templates/blog/components/BlogItem/styled.js
================================================
import styled from 'styled-components';
import { font, media, color } from '~/styles/vars';
import { P } from '~/templates/global-new/styled';
export const Container = styled.li`
margin-bottom: 40px;
${({ isSection }) =>
isSection &&
`
border-bottom: 1px solid ${color.lightGrey};
`} a:visited {
opacity: 0.75;
}
a h2,
a h3 {
${({ theme }) => `color: ${theme.color.action}`};
}
`;
export const Title = styled.h2`
font-size: 24px;
${font.bold};
letter-spacing: -0.1px;
display: inline;
margin-bottom: 10px;
margin-right: 6px;
${({ theme }) => `color: ${color.black}`};
${media.medium`
font-size: 20px;
`};
a {
${({ theme }) => `color: ${theme.color.action}`};
${font.bold};
}
`;
export const Subtitle = styled(Title.withComponent('h3'))`
font-size: 18px;
${media.medium`
font-size: 14px;
`};
a {
${font.bold};
}
`;
export const Date = styled.span`
opacity: 0.6;
${media.medium`
display: block;
margin-top: 4px;
font-size: 14px;
`};
`;
export const Description = styled(P)`
margin-top: 5px;
`;
export const TitleContainer = styled.div`
margin-bottom: 40px;
`;
================================================
FILE: packages/site/templates/blog/index.js
================================================
import { Fragment } from 'react';
import BlogItem from './components/BlogItem';
import { withTheme } from 'styled-components';
const BlogList = ({ theme, numItems }) => {
const content = theme.data.content.blog;
const menu = theme.data.menus.blog;
const filteredMenu = menu.filter(({ id }) => content[id].draft !== true);
const menuSubsection = filteredMenu.slice(0, numItems);
return (
{menuSubsection.map(({ id, title }) => (
))}
);
};
export default withTheme(BlogList);
================================================
FILE: packages/site/templates/content/ContentCTA/index.js
================================================
import React from 'react';
import { Container, Content, P, Support } from './styled';
import Link from 'next/link';
export default ({ children }) => (
Hey! Did you find this article useful? Popmotion relies on contributions
from the community to operate.
Support us
);
================================================
FILE: packages/site/templates/content/ContentCTA/styled.js
================================================
import styled from 'styled-components';
import { color } from '~/styles/vars';
import { Centered } from '~/templates/global/grid';
import { P as Paragraph } from '~/templates/global/styled';
export const Container = styled(Centered)`
background-image: linear-gradient(-180deg, #fff51c 0%, #e88003 100%);
padding: 5px;
margin-top: 80px;
`;
export const Content = styled.div`
background: ${color.white};
padding: 20px;
`;
export const P = styled(Paragraph)`
margin: 0;
`;
export const Support = styled.div`
margin-top: 20px;
a {
font-size: 22px;
font-weight: bold;
}
`;
================================================
FILE: packages/site/templates/content/ContentNav.js
================================================
import React from 'react';
import SiteLink from '~/components/layout/SiteLink';
import styled, { css, withTheme } from 'styled-components';
import { Centered } from '~/templates/global/grid';
import DropDownArrow from '~/components/icons/DropDownArrow';
import { fontSize, fontBold, lineHeight } from '~/styles/fonts';
import { ENTITY, cols, media } from '~/styles/vars';
import sectionNames from '~/data/section-names.json';
export const ContentNavArea = styled.div`
width: ${cols(14)};
margin-top: ${cols(1)};
z-index: 2;
${media.large`
position: relative;
width: auto;
border: 1px solid ${ENTITY};
margin-top: 0;
margin-bottom: ${cols(2)};
`};
`;
const CategoryContainer = styled.li`
margin-bottom: ${cols(2)};
`;
const selectable = ({ isSelected }) =>
isSelected &&
css`
a {
${fontBold}
color: ${ENTITY};
}
`;
const CategoryTitle = styled.h2`
${fontSize(18)}
margin-bottom: ${cols(1)};
${selectable}
a {
text-decoration: none;
&:hover {
color: #0866C2;
}
}
`;
const MenuItem = styled.li`
${fontSize(14)} ${lineHeight(18)}
margin-bottom: 5px;
margin-left: ${cols(1)};
${selectable} a {
text-decoration: none;
&:hover {
color: #0866c2;
}
}
`;
const MenuToggle = styled.div`
position: relative;
${fontSize(18)}
${fontBold}
border-bottom: 1px solid ${({ theme }) => theme.color.base};
width: 100%;
padding-bottom: ${cols(1)};
margin-bottom: ${cols(2)};
${media.large`
cursor: pointer;
position: relative;
border: none;
${fontSize(18)}
padding: ${cols(1)};
margin-bottom: 0;
`}
`;
const Menu = styled.ul`
${media.large`
display: ${({ isOpen }) => (isOpen ? 'block' : 'none')};
padding: ${cols(1)};
`};
`;
const DropDownMenuIcon = styled(DropDownArrow)`
position: absolute;
right: ${cols(1)};
top: 50%;
margin-top: -5px;
${({ isOpen }) => isOpen && 'transform: rotate(180deg);'} display: none;
${media.large`display: block;`};
`;
const Item = ({ id, title, contentId, section, draft }) => (
);
const Category = ({ id, title, contentId, content, section, posts }) => (
{content[id] ? (
{title}
) : (
title
)}
{posts ? (
{posts.map(
post =>
!content[post.id].draft && (
)
)}
) : null}
);
class ContentNav extends React.PureComponent {
render() {
const { section, id, theme, isOpen, toggleMenu } = this.props;
const menu = theme.data.menus[section];
const content = theme.data.content[section];
return (
{sectionNames[section]}
);
}
}
export default withTheme(ContentNav);
================================================
FILE: packages/site/templates/content/MenuPage/index.js
================================================
import { Fragment } from 'react';
import SiteLink from '~/components/layout/SiteLink';
import {
Container,
Title,
Description,
Date,
Subtitle,
TitleContainer
} from '../../blog/components/BlogItem/styled';
import { withTheme } from 'styled-components';
const MenuItem = ({ id, title, content, section }) => (
{title}
{content[id].description}
);
const MenuSection = ({ id, title, content, section, posts }) => (
{content[id] ? (
{title}
{content[id].description}
) : (
{title}
)}
{posts ? (
) : null}
);
const BlogList = ({ theme, section }) => {
const { generateSiteUrl } = theme.data;
const content = theme.data.content[section];
const menu = theme.data.menus[section];
return (
{menu.map(({ id, title, posts }) => (
))}
);
};
export default withTheme(BlogList);
================================================
FILE: packages/site/templates/content/PostDetails/index.js
================================================
import styled from 'styled-components';
import { color, media } from '~/styles/vars';
import { Centered } from '~/templates/global/grid';
const Container = styled(Centered)`
display: flex;
align-items: center;
margin-bottom: 15px;
span {
color: ${color.grey};
font-size: 14px;
${media.small`font-size: 12px;`};
}
> * {
margin-right: 10px;
}
`;
const avatarSize = '30px';
const Avatar = styled.div`
width: ${avatarSize};
height: ${avatarSize};
border-radius: 50%;
overflow: hidden;
img {
width: ${avatarSize};
height: ${avatarSize};
}
`;
const Name = styled.span``;
export default ({ published, name, avatar }) => (
{avatar && (
)}
{name && {name}}
{name && published && {`|`}}
{published && {published}}
);
================================================
FILE: packages/site/templates/content/TableOfContents/index.js
================================================
import { Container, List, Item, Link, Header } from './styled';
const generateSection = (list, maxLevel) => (
{list.map((item) => generateLink(item, maxLevel))}
);
const generateLink = ({ title, id, children, level }, maxLevel) => {
return maxLevel !== undefined && level > maxLevel ? null : (
-
{title}
{children && generateSection(children, maxLevel)}
);
};
export default ({ toc, maxLevel }) => (
{generateSection(toc[0].children, maxLevel)}
);
================================================
FILE: packages/site/templates/content/TableOfContents/styled.js
================================================
import styled from 'styled-components';
import { Centered } from '~/templates/global/grid';
import { color, cols, font } from '~/styles/vars';
export const Container = styled(Centered.withComponent('nav'))`
margin-top: 40px;
margin-bottom: 20px;
padding: 20px;
background: ${color.superLightGrey};
`;
export const Header = styled.h2`
border-bottom: 1px solid var(--color-popmotion);
padding: 0 0 10px;
margin-bottom: 20px;
${font.bold};
`;
export const List = styled.ul``;
export const Item = styled.li`
li {
padding-left: ${cols(1)};
}
margin-top: 10px;
margin-bottom: 10px;
`;
export const Link = styled.a``;
================================================
FILE: packages/site/templates/content/Template.js
================================================
import {
Container,
ContentArea,
NextLink,
NextLinkContainer,
NextLinkSmall,
EmptyCol
} from './styled';
import { Content } from '~/templates/global/grid';
import ContentNav from './ContentNav';
import GlobalTemplate from '~/templates/global/Template';
import ContentPage from '~/templates/global-new/ContentPage';
import PostDetails from './PostDetails';
import themes from '~/styles/themes';
import authorData from '~/data/authors.json';
export default class Template extends React.PureComponent {
state = {
isMenuOpen: false
};
toggleMenu = () => {
const { isMenuOpen } = this.state;
this.setState({ isMenuOpen: !isMenuOpen });
};
render() {
const {
children,
title,
id,
description,
section,
published,
author,
theme,
next
} = this.props;
const { isMenuOpen } = this.state;
return (
{section !== 'blog' ? (
) : (
)}
{published || authorData[author] ? (
) : null}
{children}
{next && themes[theme].data.content[section][next] ? (
Next
{themes[theme].data.content[section][next].title}
) : null}
);
}
}
================================================
FILE: packages/site/templates/content/styled.js
================================================
import styled from 'styled-components';
import { fontSize, fontBold } from '~/styles/fonts';
import { cols, BLACK, LIGHT_GREY, BRAND, media } from '~/styles/vars';
import { htmlUnencode } from '~/utils/string';
import SiteLink from '~/components/layout/SiteLink';
import { Centered } from '~/templates/global/grid';
import posed from 'react-pose';
export const Container = styled.div`
position: relative;
margin: 75px 0;
display: flex;
justify-content: space-between;
${media.large`display: block;`};
`;
export const ContentArea = styled(
posed.div({
flip: {
transition: { type: 'spring', stiffness: 1000, damping: 35 }
}
})
)`
width: calc(100% - (100vw - ${cols(48)}) / 2);
${media.large`width: 100%;`};
`;
const ImgFrame = styled.span`
max-width: ${cols(43)};
border-bottom: 1px solid ${LIGHT_GREY};
display: block;
padding: ${cols(1)} 0 ${cols(2)};
`;
const Image = styled.img`
margin: 0 auto;
display: block;
max-width: 90vw;
`;
const Caption = styled.span`
${fontSize(14)}
color: ${BLACK};
display: block;
text-align: center;
margin-top: ${cols(1)};
`;
export const Img = ({ className, alt, ...props }) => (
{alt ? {htmlUnencode(alt)} : null}
);
export const NextLinkContainer = styled(Centered)`
border-top: 1px ${BRAND} solid;
margin-top: ${cols(2)};
padding-top: ${cols(2)};
padding-bottom: ${cols(2)};
display: flex;
justify-content: flex-end;
a {
display: flex;
flex-direction: column;
align-items: flex-end;
font-size: 28px;
${fontBold};
span {
font-size: 18px;
font-weight: normal;
}
}
`;
export const NextLink = styled(SiteLink)`
display: block;
`;
export const NextLinkSmall = styled.span``;
export const EmptyCol = styled.div`
width: 150px;
`;
================================================
FILE: packages/site/templates/global/Analytics.js
================================================
const analyticsCode = `
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-3215563-7', 'auto');
ga('send', 'pageview');
`;
export default () => (
);
================================================
FILE: packages/site/templates/global/Footer.js
================================================
import styled from 'styled-components';
import Logo from '~/components/icons/Logo';
import { fontSize, fontBold } from '~/styles/fonts';
import { cols, color, media } from '~/styles/vars';
import themes from '~/styles/themes';
import { CenteredContent } from '../Popmotion/USPs/styled';
import SocialLinks from '~/templates/global/SocialLinks';
const YEAR = new Date().getFullYear();
const Container = styled.div`
padding-top: 90px;
padding-bottom: 90px;
background: linear-gradient(#262e34, #101416);
margin-top: ${cols(4)};
p,
h2 {
${fontBold};
color: ${color.white};
text-align: center;
}
`;
const Header = styled.header`
display: flex;
align-items: center;
span {
background: #3f4d56;
flex: 1;
height: 1px;
}
${media.small`
display: block;
span {
display: none;
}
`};
`;
const HeaderText = styled.h2`
font-size: 24px;
flex-shrink: 0;
padding-left: ${cols(1)};
padding-right: ${cols(1)};
`;
const LibraryList = styled.ul`
display: flex;
flex-wrap: wrap;
margin-top: ${cols(4)};
margin-bottom: ${cols(2)};
`;
const LibraryContainer = styled.li`
flex: 50% 0 0;
overflow: hidden;
margin-bottom: ${cols(4)};
${media.medium`
flex: 100% 0 0;
`} p {
max-width: 50%;
text-align: center;
}
a {
display: flex;
flex-direction: column;
align-items: center;
}
`;
const LogoContainer = styled.div`
height: 80px;
display: flex;
align-items: center;
`;
const Copyright = styled.p`
font-size: 14px;
`;
const MadeInfo = styled.p`
font-size: 24px;
margin-bottom: 10px;
`;
const SocialLinksContainer = styled.div`
margin: ${cols(2)} auto 0;
path {
fill: white;
}
`;
const Library = ({ library }) => {
const { title, url, tagline, Logo, footerLogoSize } = library;
return (
{tagline}
);
};
export default () => (
Libraries to move the web
{Object.keys(themes)
.filter(theme => theme !== 'popmotion')
.map(key => )}
{`Made in London with 🌯`}
{`© 2014-2018 Matt Perry`}
);
================================================
FILE: packages/site/templates/global/Header.js
================================================
import Link from 'next/link';
import styled, { withTheme } from 'styled-components';
import SectionNav from './SectionNav';
import SocialLinks from '~/templates/global/SocialLinks';
import Logo from '~/components/icons/Logo';
import Icon from '~/components/icons/PopmotionIcon';
import settings from '~/data/settings.json';
import { cols, media } from '~/styles/vars';
import { Centered } from '~/templates/global/grid';
const HeaderContainer = styled.nav`
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
padding: 25px ${cols(2)};
margin-bottom: ${cols(4)};
height: 91px;
background: ${props =>
props.isHomepage
? 'linear-gradient(rgba(0,0,0,0.2), rgba(0,0,0,0))'
: 'none'};
${media.large`
margin-bottom: ${cols(2)};
height: 87px;
`} ${media.small`
padding: ${cols(1)};
height: auto;
flex-direction: column;
align-items: center;
`};
`;
const NavArea = styled.nav`
display: flex;
align-items: center;
${props => !props.isHomepage && 'position: absolute;'} top: 50%;
transform: translateY(-50%);
`;
const LogoArea = styled(NavArea)`
left: ${cols(2)};
${media.large`
position: static;
transform: none;
margin-right: ${cols(2)};
`} ${props => props.isHomepage && `display: none;`};
${media.small`
display: block;
position: static;
margin: 0;
margin-bottom: 5px;
`};
`;
const SectionNavArea = styled(Centered)`
width: 100%;
${media.large`margin-left: ${cols(1)};`} ${props =>
props.isHomepage &&
`
margin: 0;
`};
${media.small`
margin: 0;
display: flex;
justify-content: center;
`};
`;
const SocialArea = styled(NavArea)`
right: ${cols(2)};
${media.medium`
display: none;
`};
`;
const Header = ({ section, isHomepage, theme }) => (
{!isHomepage ? (
) : null}
);
export default withTheme(Header);
================================================
FILE: packages/site/templates/global/SectionNav.js
================================================
import styled, { withTheme } from 'styled-components';
import SiteLink from '~/components/layout/SiteLink';
import { fontSize, fontBold } from '~/styles/fonts';
import { ACTION, ENTITY, WHITE, BLACK, cols, media, SKEW } from '~/styles/vars';
import sectionNames from '~/data/section-names.json';
import routes from '~/data/route-paths.json';
const MenuItem = styled.li`
${fontSize(18)} display: inline;
padding-bottom: 4px;
margin-right: ${cols(2)};
position: relative;
&:last-child {
margin-right: 0;
}
${({ isSelected }) =>
isSelected &&
`
&:after {
content: '';
display: block;
background: ${ENTITY};
position: absolute;
bottom: -3px;
right: 0;
left: 0;
height: 4px;
transform: skewX(${SKEW});
}
`} ${media.large`
${fontSize(16)}
margin-right: ${cols(1)};
`} a {
color: ${props => (props.isHomepage ? WHITE : BLACK)};
text-decoration: none;
${fontBold};
}
`;
const SectionNav = ({ section, theme, isHomepage }) => (
{theme.sections.map(name => (
))}
);
export default withTheme(SectionNav);
================================================
FILE: packages/site/templates/global/SocialLinks.js
================================================
import React from 'react';
import styled from 'styled-components';
import GitHub from '~/components/icons/GitHub';
import Twitter from '~/components/icons/Twitter';
import settings from '~/data/settings.json';
import { cols, media } from '~/styles/vars';
const IconLink = styled.a`
margin-left: ${cols(1)};
`;
const TwitterLink = styled(IconLink)`
margin-left: none;
transform: translateY(2px);
`;
const GitHubIcon = styled(GitHub)``;
const TwitterIcon = styled(Twitter)``;
export default () => (
);
================================================
FILE: packages/site/templates/global/Template.js
================================================
import { Fragment } from 'react';
import { createGlobalStyle, ThemeProvider } from 'styled-components';
import NProgress from 'nprogress';
import Head from 'next/head';
import Router from 'next/router';
import reset from '~/styles/reset';
import nprogressStyles from '~/styles/nprogress';
import { BRAND } from '~/styles/vars';
import themes from '~/styles/themes';
import settings from '~/data/settings.json';
import * as popmotion from 'popmotion';
if (typeof window !== 'undefined') {
console.log(
'Hey explorer! You can play around with Popmotion right from your console, by using window.popmotion.'
);
window.popmotion = popmotion;
}
Router.onRouteChangeStart = () => NProgress.start();
Router.onRouteChangeComplete = () => {
if (typeof window !== 'undefined' && typeof window.Prism !== 'undefined') {
window.Prism.highlightAll();
}
NProgress.done();
};
Router.onRouteChangeError = () => NProgress.done();
const Global = createGlobalStyle`
${reset}
${nprogressStyles}
`;
export default ({ children, title, theme, description, image }) => (
{title}
{children}
);
================================================
FILE: packages/site/templates/global/grid.js
================================================
import styled from 'styled-components';
import { cols, media, LIGHT_GREY } from '~/styles/vars';
export const Content = styled.article``;
export const Centered = styled.div`
max-width: ${cols(42)};
${media.large`
margin-left: 20px;
margin-right: 20px;
`};
${media.medium`
margin-left: ${cols(1)};
margin-right: ${cols(1)};
`};
${media.small`
margin-left: 5px;
margin-right: 5px;
`};
`;
export const MajorCentered = styled(Centered)`
width: 100%;
${media.large`
width: auto;
margin-left: 20px;
`};
${media.medium`
margin-left: ${cols(1)};
`};
${media.small`
margin-left: 5px;
margin-right: 5px;
`};
`;
export const ArticleHeader = styled(MajorCentered.withComponent('header'))`
padding-bottom: 10px;
border-bottom: 1px solid ${LIGHT_GREY};
margin-bottom: ${cols(2)};
`;
export const SectionContainer = styled.li``;
export const ItemContainer = styled(Centered.withComponent('li'))`
border-left: 1px solid ${LIGHT_GREY};
padding: ${cols(1)} ${cols(2)};
margin-bottom: ${cols(1)};
${media.medium`
border: 0;
padding: 0;
margin-bottom: ${cols(2)};
`}
`;
================================================
FILE: packages/site/templates/global/styled.js
================================================
import styled from 'styled-components';
import { fontSize, fontBold, lineHeight } from '~/styles/fonts';
import {
LIGHT_GREY,
WHITE,
cols,
color,
media,
GREEN,
PURPLE,
ENTITY,
} from '~/styles/vars';
import {
Centered,
MajorCentered,
ArticleHeader as ArticleHeaderPrimitive,
} from './grid';
import SyntaxHighlighter, {
registerLanguage,
} from 'react-syntax-highlighter/dist/light';
import js from 'react-syntax-highlighter/dist/languages/javascript';
import { codeThemeLarge } from '~/styles/syntax-highlighting';
registerLanguage('javascript', js);
export const Strong = styled.strong`
${fontBold};
`;
export const A = styled.a`
color: #0866c2;
text-decoration: none;
&:hover {
color: #064fb5;
}
`;
export const H1 = styled.h1`
${fontSize(48)}
${lineHeight(54)}
${fontBold}
text-align: left;
${media.medium`
${fontSize(36)}
${lineHeight(42)}
`}
${media.small`
${fontSize(28)}
${lineHeight(32)}
`}
`;
export const H2 = styled(Centered.withComponent('h2'))`
${fontSize(36)}
${fontBold}
margin-top: ${cols(4)};
margin-bottom: ${cols(2)};
border-bottom: 1px solid ${LIGHT_GREY};
padding-bottom: ${cols(1)};
${media.medium`
${fontSize(28)}
${lineHeight(32)}
margin-top: ${cols(2)};
margin-bottom: ${cols(1)};
`}
${media.small`
${fontSize(24)}
${lineHeight(28)}
`}
a {
${fontBold}
}
`;
export const H3 = styled(Centered.withComponent('h3'))`
${fontSize(24)}
${lineHeight(32)}
${fontBold}
margin-top: ${cols(2)};
margin-bottom: ${cols(1)};
${media.medium`
${fontSize(24)}
${lineHeight(30)}
`}
${media.small`
${fontSize(18)}
${lineHeight(28)}
`}
a {
${fontBold}
}
`;
export const H4 = styled(Centered.withComponent('h4'))`
${fontSize(20)}
margin-top: 2.2rem;
margin-bottom: 1.1rem;
font-weight: 600;
${media.medium`${fontSize(16)}`}
`;
export const H5 = styled(Centered.withComponent('h5'))`
${fontSize(18)}
margin-top: 2.2rem;
margin-bottom: 1.1rem;
font-weight: 600;
${media.medium`${fontSize(14)}`}
`;
export const P = styled(Centered.withComponent('p'))`
${fontSize(18)}
${lineHeight(26)}
line-height: 1.5;
margin-bottom: 1.1rem;
word-break: break-word;
${media.medium`
${fontSize(14)}
${lineHeight(22)}
`}
`;
export const Blockquote = styled(MajorCentered.withComponent('blockquote'))`
border-left: 1px solid ${color.lightGrey};
background: ${color.superLightGrey};
padding: ${cols(1)};
margin-bottom: ${cols(2)};
p {
margin: 0;
}
`;
export const CodeTag = styled.code`
background: var(--color-shade);
padding: 2px 5px;
font-size: 18px;
border: none;
`;
export const CodeBlock = styled(MajorCentered)`
background: var(--color-shade);
border-left: 2px solid var(--color-popmotion);
margin-bottom: ${cols(3)};
font-size: 14px;
${media.medium`
margin-left: 0;
margin-right: 0;
pre {
padding-right: 0!important;
width: 100%!important;
}
`};
`;
export const Code = ({ language, children, code }) =>
children ? (
{children}
) : (
{code}
);
export const Ol = styled(Centered.withComponent('ol'))`
list-style-type: decimal;
padding-left: ${cols(2)};
max-width: ${cols(43)};
margin-bottom: 1.1rem;
`;
export const Ul = styled(Centered.withComponent('ul'))`
list-style-type: disc;
padding-left: ${cols(2)};
max-width: ${cols(43)};
margin-bottom: 1.1rem;
`;
export const Li = styled.li`
line-height: 1.7;
margin-bottom: 0.5rem;
${fontSize(18)} ${media.medium`${fontSize(14)}`};
`;
export const Hr = styled.hr`
border: none;
height: 1px;
background-color: ${LIGHT_GREY};
margin: ${cols(2)} 0;
`;
const Button = styled.button`
background: ${GREEN};
border-radius: 0;
padding: ${cols(1)} ${cols(2)};
cursor: pointer;
margin: 0 auto;
`;
const ButtonContent = styled.span`
color: ${WHITE};
${fontBold} display: block;
`;
export const ActionButton = ({ children, onClick }) => (
);
export const ArticleHeader = ({ children }) => (
{children}
);
export const Video = ({ src, height = 320 }) => (
);
================================================
FILE: packages/site/templates/global-new/ContentPage/index.js
================================================
import { Fragment } from 'react';
import Header from '../Header';
import Footer from '../Footer';
import { Container, ContentContainer } from '../styled';
export default ({ children, section }) => (
{children}
);
================================================
FILE: packages/site/templates/global-new/Footer/index.js
================================================
import PopmotionLogo from '~/components/icons/Logo';
import SocialLinks from '~/templates/global/SocialLinks';
import themes from '~/styles/themes';
import {
Container,
Section,
Header,
NavItem,
MadeIn,
Copyright
} from './styled';
import Link from 'next/link';
const openSource = [
themes.pose,
themes.pure,
themes.popcorn,
themes.stylefire,
themes.framesync
];
export default () => (
{themes.popmotion.headerNavLinks.map(({ href, label, isExternal }) => (
{label}
))}
{openSource.map(({ url, name }) => (
{name}
))}
{`Made in London with 🌯`}
{`© 2014-2019 Framer BV`}
);
================================================
FILE: packages/site/templates/global-new/Footer/styled.js
================================================
import styled from 'styled-components';
import Link from 'next/link';
import { color, font, media } from '~/styles/vars';
import { P } from '~/templates/global-new/styled';
export const Container = styled.div`
display: flex;
justify-content: flex-start;
padding-top: 75px;
width: 100%;
border-top: 1px solid ${color.lightGrey};
${media.small`
flex-wrap: wrap;
`};
`;
export const Section = styled.nav`
width: 200px;
flex: 200px 0 1;
&:last-child {
text-align: right;
margin-left: auto;
}
${media.small`
flex: 100% 0 0;
width: 100%;
margin-bottom: 30px;
&:last-child {
text-align: left;
}
`};
`;
export const Header = styled.h2`
${font.bold};
font-size: 18px;
margin-bottom: 10px;
`;
export const NavItem = styled.li`
margin-bottom: 7px;
`;
export const MadeIn = styled(P)`
margin-top: 7px;
`;
export const Copyright = styled(P)`
font-size: 14px;
margin-bottom: 10px;
`;
================================================
FILE: packages/site/templates/global-new/Header/index.js
================================================
import { Fragment } from 'react';
import Nav from '../Nav';
export default props => ;
================================================
FILE: packages/site/templates/global-new/Nav/index.js
================================================
import Link from 'next/link';
import { Container, Links, NavItem } from './styled';
import { withTheme } from 'styled-components';
const Nav = ({ theme, isWhite, section }) => (
{theme.headerNavLinks.map(({ href, isExternal, label, id }) => (
{label}
))}
);
export default withTheme(Nav);
================================================
FILE: packages/site/templates/global-new/Nav/styled.js
================================================
import styled from 'styled-components';
import { color, font, media, cols } from '~/styles/vars';
export const Container = styled.nav`
display: flex;
justify-content: space-between;
align-items: center;
${media.large`
margin: 0 20px;
`}
${media.medium`
flex-wrap: wrap;
> a,
> ul {
flex: 0 0 100%;
display: flex;
justify-content: center;
}
`} ${({ isWhite }) =>
isWhite &&
`
path {
fill: ${color.white};
}
a,
a:hover {
color: ${color.white};
font-weight: bold;
}
`};
${media.small`margin: 0;`}
`;
export const Links = styled.ul`
display: flex;
${media.medium`margin-top: 20px`};
li {
margin-left: 30px;
${media.medium`margin-left: 20px`};
&:first-child {
margin-left: 0;
}
}
`;
export const NavItem = styled.li`
${({ isSelected, theme }) =>
isSelected &&
`
position: relative;
a {
color: ${theme.color.base};
}
&:after {
content: '';
display: block;
position: absolute;
height: 3px;
background-color: ${theme.color.base};
left: 0;
right: 0;
bottom: -5px;
}
`};
`;
================================================
FILE: packages/site/templates/global-new/styled.js
================================================
import styled from 'styled-components';
import Link from 'next/link';
import { color, font, media } from '~/styles/vars';
export const Container = styled.div`
${({ theme }) => `
background-color: ${theme.color.base};
background-image: radial-gradient(120% 600px at 50% 200px, ${theme.color.base}, ${theme.color.twist} 120%);
min-height: 100vh;
`};
padding: 10px;
`;
export const ContentContainer = styled.div`
background: ${color.white};
padding: ${({ noHeader }) => (noHeader ? '75px' : '30px')} 30px;
${media.small`padding: ${({ noHeader }) =>
noHeader ? '10px' : '30px'} 10px;`};
${media.large`padding: ${({ noHeader }) =>
noHeader ? '30px' : '30px'} 10px;`};
a {
color: ${({ theme }) => theme.color.action};
&:hover {
color: ${({ theme }) => theme.color.actionHighlight};
}
}
`;
export const Section = styled.section`
margin: 75px auto;
max-width: 650px;
`;
export const PageHeader = styled.h1`
font-size: 36px;
color: ${color.black};
${font.bold};
letter-spacing: -1.1px;
text-align: center;
margin-bottom: 40px;
${media.medium`
font-size: 24px;
letter-spacing: -0.5px;
`};
`;
export const P = styled.p`
font-size: 18px;
line-height: 24px;
letter-spacing: -0.2px;
em {
font-weight: bold;
}
${media.medium`
font-size: 16px;
line-height: 22px;
`};
`;
export const ActionButton = styled.button`
${({ theme }) => `
background-image: linear-gradient(-180deg, ${theme.color.base} 0%, ${theme.color.twist} 100%);
box-shadow: 0 1px 0 0 ${theme.color.baseShadow}, 0 2px 3px 0 rgba(0,0,0,0.22);
text-shadow: 0 -1px 0 ${theme.color.baseShadow};
&:active {
box-shadow: 0 1px 1px 0 rgba(0,0,0,0.22);
transform: translateY(1px);
}
`};
border-radius: 300px;
color: ${color.white}!important;
font-size: 18px;
${font.body};
${font.bold};
letter-spacing: -0.2px;
cursor: pointer;
padding: 10px 30px;
${({ white, theme }) =>
white &&
`
background-image: linear-gradient(-180deg, #fff 0%, #eee 100%);
color: ${color.black}!important;
text-shadow: none;
`};
${({ cta }) =>
cta &&
`
font-size: 24px;
padding: 15px 30px;
${media.medium`font-size: 18px;`}
`};
`;
const ButtonAsLink = styled(ActionButton.withComponent('a'))`
display: inline-block;
`;
export const ActionLink = ({ href, ...props }) => (
);
================================================
FILE: packages/site/templates/homepage/index.js
================================================
import styled from 'styled-components';
import { color, media } from '~/styles/vars';
import Nav from '~/templates/global-new/Nav';
import GlobalTemplate from '~/templates/global/Template';
import { Container, ContentContainer } from '~/templates/global-new/styled';
const HeaderContainer = styled.header`
padding: 30px;
${media.large`padding: 20px;`};
${media.small`padding: 10px;`};
`;
export default ({ title, description, theme, Header, children }) => (
{children}
);
================================================
FILE: packages/site/utils/string.js
================================================
export function htmlUnencode(str) {
return str
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&');
}
================================================
FILE: packages/style-value-types/CHANGELOG.md
================================================
# Changelog
Style Value Types adheres to [Semantic Versioning](http://semver.org/).
## [5.1.2] 2022-08-15
### Update
- Updating `tslib` and `typescript`.
## [5.1.1] 2022-08-10
### Fixed
- Supporting `rgba` and `hsla` values without spaces around the alpha slash.
### Update
- Adding `types` to `exports` field.
## [5.1.0] 2021-11-24
### Update
- Updating `tslib`.
## [5.0.0] 2021-09-23
### Fixed
- Fixing `exports` and `module` in `package.json`. This will break (unsupported) direct file imports.
### [4.1.5] 2021-09-21
### Fixed
- Making complex value type gracefully accept numbers.
### [4.1.4] 2021-03-19
### Fixed
- Fixing `main` entry point.
### [4.1.3] 2021-03-19
### Fixed
- Fixing `main` entry point.
### [4.1.2] 2021-03-19
### Added
- Adding `exports` to `package.json`.
### Updated
- `tslib` to latest.
### [4.1.1] 2021-03-01
### Fixed
- Fixing es entry point.
### [4.1.0] 2021-03-01
### Fixed
- Unbundling ES code to facilitate code-splitting in Webpack.
## [4.0.3] 2020-02-22
### Fixed
- Fixing `hasOwnProperty` call on `null` for color test.
## [4.0.1] 2020-01-08
### Added
- Restoring support for RGBA/HSLA objects in rgba.parse/hsla.parse.
## [4.0.0] 2020-01-08
### Added
- Support for hex alpha.
- `filter` type.
- Improved handling of decimals with no preceding digit.
### Removed
- Support for RGBA/HSLA objects in rgba.parse/hsla.parse.
## [3.2.0] 2020-12-18
### Fixed
- Including `tslib` as a separate dependency.
## [3.1.9] 2020-07-23
### Fixed
- Fixed opacity with whitespace syntax colors.
## [3.1.8] 2020-07-23
### Fixed
- Fixed whitespace syntax colors.
- Fixed HSL(A) colors containing decimals.
## [3.1.7] 2019-11-14
### Fixed
- Updating to Typescript 3.7.
## [3.1.6] 2019-07-25
### Fixed
- Clamping color alpha to 0-1.
## [3.1.4] 2019-05-01
### Fixed
- Rejecting `complex` values in `color` tests.
## [3.1.3] 2019-04-29
### Fixed
- Dynamic type imports.
## [3.1.2] 2019-04-29
### Removed
- Hard `ValueType` typing from `complex`.
## [3.1.1] 2019-04-29
### Added
- `getAnimatableNone` to `complex` value type.
## [3.1.0] 2019-03-12
### Added
- `progressPercentage` value type.
## [3.0.7] 2018-08-30
### Fixed
- Preventing unit types from matching anything that contains that string ie px matching to `blur(20px)`.
## [3.0.6] 2018-08-16
### Fixed
- Detecting exponential values in complex.createTransformer. [#423](https://github.com/Popmotion/popmotion/issues/423)
## [3.0.5] 2018-08-14
### Fixed
- Preventing `complex` from matching `number`.
## [3.0.4] 2018-08-13
### Fixed
- Fixed a bug in the complex value type where single function-like values, like `grayscale(0%)` aren't recognised as complex value types.
## [3.0.3] 2018-06-28
### Fixed
- Fixing error in `tsconfig.json` that leaves code untranspiled.
## [3.0.2] 2018-06-27
### Fixed
- Improving color regex to pick 6-letter hex codes first.
## [3.0.1] 2018-06-27
### Fixed
- Fixing template algo.
## [3.0.0] 2018-06-22
### Removed
- Removed new `combo` in favour of a single unified `complex` value type that can handle mixed number and color strings.
## [2.0.1] 2018-06-21
### Fixed
- Now finding colors containing spaces within combo values.
## [2.0.0] 2018-06-20
### Added
- New units: `vh`, `vw`
- New space-delimited combo type
- Strengthened unit type tests
- Color types can accept already-parsed numbers
================================================
FILE: packages/style-value-types/LICENSE.md
================================================
The MIT License (MIT)
Copyright (c) 2018 Popmotion
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: packages/style-value-types/README.md
================================================
# Style Value Types
Parsers, transformers and tests for common style value types, eg: %, hex codes etc.
To help convert numerical values into commonly-used special value types, like `px` or `hex`, we provide an optional module called `style-value-types`:
```bash
npm install style-value-types --save
```
Each value type has three functions:
- `test`: Returns `true` if the provided value is of that type.
- `parse`: Returns the value in a format suitable for animation. Either a `number` or `{ [key: string]: number }`.
And one of:
- `transform`: The reverse of `parse`. Accepts a `number` or map of named numbers and converts that into the value type.
- `createTransformer`: Accepts a value and returns a `transform` based on that specific value.
## Import
```javascript
import { color } from 'style-value-types';
```
## Example
```javascript
// Test
color.test('#fff'); // true
color.test(0); // false
// Parse
color.parse('rgba(255, 255, 255, 0)');
// { red: 255, green: 255, blue: 255, alpha: 0 }
// Transform
color.transform({ hue: 200, saturation: 100, lightness: 50, alpha: 0.5 });
// 'hsla(200, 100%, 50%, 0.5)'
```
## Included value types
- `alpha`: `Number` between `0` and `1`
- `complex`: Handles space and comma delimited values, like CSS box-shadow: `'10px 10px inset #f00, 5px 5px 30px #fff'`, gradient or a path definition.
- `color`: `String` of either `hex`, `hsla` or `rgba` type
- `degrees`: `String` ending in `deg`
- `hex`: `String` beginning with `#` and followed by 3 or 6-digit hex code
- `hsla`: `String` with valid `hsla` property
- `percent`: `String` ending in `%`
- `px`: `String` ending in `px`
- `scale`: `Number` with a `default` of `1` instead of `0`
- `rgbUnit`: Integer between `1` and `255`
- `rgba`: String in `rgba(rgbUnit, rgbUnit, rgbUnit, alpha)` format
## complex
The `complex` value type is slightly different to the others. Instead of a `transform` method, it has a `createTransformer` method which returns the `transform` method:
```javascript
const svgPath = 'M150 0 L75 200';
const transform = complex.createTransformer(svgPath);
```
The returned `transform` function is unique to the string given to it. When this function is provided an object of the same format as returned by `complex.parse()` (in this example `complex.parse(svgPath)`), it will use the original string as a template.
================================================
FILE: packages/style-value-types/package.json
================================================
{
"name": "style-value-types",
"version": "5.1.2",
"description": "Parsers, transformers and tests for special value types, eg: %, hex codes etc.",
"main": "dist/valueTypes.cjs.js",
"types": "lib/index.d.ts",
"module": "dist/es/index.mjs",
"jsnext:main": "dist/es/index.mjs",
"exports": {
".": {
"types": "./lib/index.d.ts",
"import": "./dist/es/index.mjs",
"require": "./dist/valueTypes.cjs.js",
"default": "./dist/valueTypes.cjs.js"
},
"./package.json": "./package.json"
},
"files": [
"lib",
"dist"
],
"sideEffects": false,
"scripts": {
"build": "tsc -p . && rollup -c && yarn measure",
"watch": "rollup -c -w",
"lint": "tslint -c tslint.json 'src/**/*.{ts}'",
"test": "jest --maxWorkers=2",
"measure": "gzip -c dist/style-value-types.min.js | wc -c",
"prepublishOnly": "npm run test && npm run build"
},
"repository": {
"type": "git",
"url": "https://github.com/Popmotion/popmotion/tree/master/packages/style-value-types"
},
"keywords": [
"css",
"svg",
"hex",
"rgba",
"hsla"
],
"author": "Matt Perry",
"license": "MIT",
"bugs": {
"url": "https://github.com/Popmotion/popmotion/issues"
},
"homepage": "https://popmotion.io",
"devDependencies": {
"@types/jest": "^23.1.1",
"webpack": "^3.12.0"
},
"jest": {
"moduleFileExtensions": [
"ts",
"js"
],
"transform": {
"\\.(ts)$": "../../../node_modules/ts-jest/preprocessor.js"
},
"testRegex": "/_tests/.*\\.(ts|tsx|js)$",
"rootDir": "src",
"testURL": "http://localhost/"
},
"unpkg": "./dist/style-value-types.min.js",
"prettier": {
"parser": "typescript",
"singleQuote": true
},
"dependencies": {
"hey-listen": "^1.0.8",
"tslib": "2.4.0"
}
}
================================================
FILE: packages/style-value-types/rollup.config.js
================================================
import generateConfig from '../../rollup-generate-config';
import pkg from './package.json';
export default generateConfig(pkg, 'valueTypes');
================================================
FILE: packages/style-value-types/src/_tests/index.test.ts
================================================
import {
alpha,
hex,
rgba,
rgbUnit,
hsla,
color,
px,
degrees,
percent,
progressPercentage,
complex,
filter,
} from '../';
import { singleColorRegex, colorRegex, floatRegex } from '../utils';
const PATH = 'M150 0 L75 200 L225 200 Z';
const GREYSCALE = 'greyscale(100%)';
const PATH_VALUES = [150, 0, 75, 200, 225, 200];
const MIXED = '0px 0px 0px rgba(161, 0, 246, 0)';
describe('regex', () => {
it('should correctly identify values', () => {
expect(singleColorRegex.test('#fff000')).toBe(true);
expect(singleColorRegex.test('#fff000aa')).toBe(true);
expect(singleColorRegex.test('rgba(161, 0, 246, 0)')).toBe(true);
expect(singleColorRegex.test('rgba(161 0 246 / 0)')).toBe(true);
expect(singleColorRegex.test('rgba(161 0 246/0)')).toBe(true);
expect(
singleColorRegex.test('rgba(161 0 246 / 0) rgba(161 0 246 / 0)')
).toBe(false);
expect(singleColorRegex.test('#fff000 #fff000')).toBe(false);
expect(colorRegex.test('#fff000 #fff000aa')).toBe(true);
expect(colorRegex.test('rgba(161 0 246 / 0) rgba(161 0 246 / 0)')).toBe(
true
);
expect(
'rgba(161 0 246 / 0) rgba(161 0 246 / 0)'.match(colorRegex)?.length
).toEqual(2);
expect(colorRegex.test('rgba(161 0 246/0) rgba(161 0 246/0)')).toBe(true);
expect(
'rgba(161 0 246/0) rgba(161 0 246/0)'.match(colorRegex)?.length
).toEqual(2);
expect(
'a0.9b 1 2 -3 -4.0 -.5 -0.6 a.7b 800 10.0009'.match(floatRegex)
).toEqual([
'0.9',
'1',
'2',
'-3',
'-4.0',
'-.5',
'-0.6',
'.7',
'800',
'10.0009',
]);
expect('hsla(177, 37.4978%, 76.66804%, 1)'.match(floatRegex)).toEqual([
'177',
'37.4978',
'76.66804',
'1',
]);
});
});
describe('complex value type', () => {
it('test returns correctly', () => {
expect(complex.test(GREYSCALE)).toBe(true);
expect(complex.test(PATH)).toBe(true);
expect(complex.test(3)).toBe(false);
expect(complex.test('3')).toBe(false);
expect(complex.test('3px')).toBe(true);
expect(complex.test(MIXED)).toBe(true);
});
it('parse converts string to array', () => {
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,
]);
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,
]);
});
it('createTransformer returns a transformer function that correctly inserts values', () => {
const transform = complex.createTransformer(PATH);
expect(transform(PATH_VALUES)).toBe(PATH);
const transformMixedExpo = complex.createTransformer(
'0px 0px 0px rgba(161, 0, 246, 0)'
);
expect(
transformMixedExpo([
{
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)');
const transformSingleFunction = complex.createTransformer(GREYSCALE);
expect(transformSingleFunction([100])).toBe(GREYSCALE);
const transformSingleNumber = complex.createTransformer(2);
expect(transformSingleNumber([100])).toBe('100');
});
it('can create an animatable "none"', () => {
expect(complex.getAnimatableNone('100% 0px #fff')).toBe(
'0% 0px rgba(255, 255, 255, 1)'
);
});
});
const red = {
red: 255,
green: 0,
blue: 0,
alpha: 1,
};
const redOutOfRange = {
red: 300,
green: 0,
blue: 0,
alpha: 2,
};
const hslaTestColor = {
hue: 170,
saturation: 50,
lightness: 45,
alpha: 1,
};
const hslaOutOfRange = {
hue: 170,
saturation: 50,
lightness: 45,
alpha: 2,
};
describe('hex()', () => {
it('should correctly test for colors', () => {
expect(hex.test('#f00')).toEqual(true);
expect(hex.test('#f00a')).toEqual(true);
expect(hex.test('#f000aa')).toEqual(true);
expect(hex.test('#f000aa00')).toEqual(true);
expect(hex.test('#f00 0px')).toEqual(false);
expect(hex.test(red)).toEqual(false);
});
it('should split a hex value into the correct params', () => {
expect(hex.parse('#f00')).toEqual(red);
expect(hex.parse('#ff0000')).toEqual(red);
expect(hex.parse('#ffff00')).not.toEqual(red);
expect(hex.parse('#ff0000ff')).toEqual(red);
expect(hex.parse('#ff000000')).not.toEqual(red);
expect(hex.parse('#f00f')).toEqual(red);
expect(hex.parse('#f000')).not.toEqual(red);
});
it('should correctly combine a hex value', () => {
expect(hex.transform(red)).toBe('rgba(255, 0, 0, 1)');
});
});
describe('rgba()', () => {
it('should correctly test for colors', () => {
expect(rgba.test('rgba(255, 0, 0, 0.5)')).toEqual(true);
expect(rgba.test('rgba(255 0 0 / 0.5)')).toEqual(true);
expect(rgba.test('rgba(255, 0, 0, 0.5) 0px')).toEqual(false);
expect(rgba.test({ red: 255 })).toEqual(true);
expect(rgba.test({ hue: 255 })).toEqual(false);
});
it('should split an rgba value into the correct params', () => {
expect(rgba.parse('rgba(255, 0, 0, 0.5)')).toEqual({ ...red, alpha: 0.5 });
expect(rgba.parse('rgb(255,0,0)')).toEqual(red);
expect(rgba.parse('rgb(255,250,1)')).toEqual({
red: 255,
green: 250,
blue: 1,
alpha: 1,
});
expect(rgba.parse('rgb(255 0 0)')).toEqual(red);
expect(rgba.parse('rgba(161 0 246 / 0)')).toEqual({
red: 161,
green: 0,
blue: 246,
alpha: 0,
});
expect(rgba.parse(red)).toEqual(red);
});
it('should correctly combine rgba value', () => {
expect(rgba.transform(red)).toEqual('rgba(255, 0, 0, 1)');
expect(rgba.transform(redOutOfRange)).toEqual('rgba(255, 0, 0, 1)');
});
});
describe('hsla()', () => {
it('should correctly test for colors', () => {
expect(hsla.test('hsla(170, 50%, 45%, 1)')).toEqual(true);
expect(hsla.test('hsla(177, 37.4978%, 76.66804%, 1)')).toEqual(true);
expect(hsla.test('hsla(170 50% 45% / 1)')).toEqual(true);
expect(hsla.test('hsla(170 50% 45%/1)')).toEqual(true);
expect(hsla.test('hsla(177 37.4978% 76.66804% / 1)')).toEqual(true);
expect(hsla.test('hsla(170, 50%, 45%, 1) 0px')).toEqual(false);
});
it('should split an hsl value into the correct params', () => {
expect(hsla.parse('hsla(170, 50%, 45%, 1)')).toEqual(hslaTestColor);
expect(hsla.parse('hsl(170, 50%, 45%)')).toEqual(hslaTestColor);
expect(hsla.parse('hsla(170 50% 45% 1)')).toEqual(hslaTestColor);
expect(hsla.parse('hsl(170 50% 45%)')).toEqual(hslaTestColor);
expect(hsla.parse('hsla(177, 37.4978%, 76.66804%, 1)')).toEqual({
hue: 177,
saturation: 37.4978,
lightness: 76.66804,
alpha: 1,
});
expect(hsla.parse('hsla(177 37.4978% 76.66804% / 1)')).toEqual({
hue: 177,
saturation: 37.4978,
lightness: 76.66804,
alpha: 1,
});
expect(hsla.parse('hsla(177 37.4978% 76.66804% / 0.5)')).toEqual({
hue: 177,
saturation: 37.4978,
lightness: 76.66804,
alpha: 0.5,
});
expect(hsla.parse('hsla(177 37.4978% 76.66804%/0.5)')).toEqual({
hue: 177,
saturation: 37.4978,
lightness: 76.66804,
alpha: 0.5,
});
expect(hsla.parse(hslaTestColor)).toEqual(hslaTestColor);
});
it('should correctly combine hsla value', () => {
expect(hsla.transform(hslaTestColor)).toEqual('hsla(170, 50%, 45%, 1)');
expect(hsla.transform(hslaOutOfRange)).toEqual('hsla(170, 50%, 45%, 1)');
expect(
hsla.transform({
hue: 177,
saturation: 37.4978,
lightness: 76.66804,
alpha: 1,
})
).toEqual('hsla(177, 37.4978%, 76.66804%, 1)');
});
});
describe('color()', () => {
it('should split the color with the appropriate parser', () => {
expect(color.parse('rgba(255, 0, 0, 1)')).toEqual(red);
expect(color.parse('rgba(255, 0, 0, 0.8)')).toEqual({ ...red, alpha: 0.8 });
expect(color.parse('rgba(255, 0, 0, .8)')).toEqual({ ...red, alpha: 0.8 });
expect(color.parse('rgba(255, 0, 0,.8)')).toEqual({ ...red, alpha: 0.8 });
expect(color.parse('#f00')).toEqual(red);
expect(color.parse('#f00f')).toEqual(red);
expect(color.parse('hsla(170, 50%, 45%, 1)')).toEqual(hslaTestColor);
});
it('should correctly combine color value', () => {
expect(color.transform(red)).toEqual('rgba(255, 0, 0, 1)');
expect(color.transform('rgba(255, 0, 0, 1)')).toEqual('rgba(255, 0, 0, 1)');
expect(color.transform(hslaTestColor)).toEqual('hsla(170, 50%, 45%, 1)');
});
it('should correctly identify color', () => {
expect(color.test('#e66465')).toBe(true);
expect(color.test('#fff')).toBe(true);
expect(color.test('#fff000')).toBe(true);
expect(color.test('#fff000aa')).toBe(true);
expect(color.test('#fff 0px')).toBe(false);
expect(color.test('#f0f0f0')).toBe(true);
expect(color.test('rgb(233, 233, 1)')).toBe(true);
expect(color.test('rgb(0, 0, 0) 5px 5px 50px 0px')).toBe(false);
expect(color.test('rgba(255, 255, 0, 1)')).toBe(true);
expect(color.test('rgba(255,255,0,1)')).toBe(true);
expect(color.test('rgba(255,255, 0,1)')).toBe(true);
expect(color.test('hsl(0, 0%, 0%)')).toBe(true);
expect(color.test('hsl(0, 0%,0%)')).toBe(true);
expect(color.test('hsla(180, 360%, 360%, 0.5)')).toBe(true);
expect(color.test('hsla(180, 360%, 360%, 0.5) 0px')).toBe(false);
expect(color.test('greensock')).toBe(false);
expect(color.test('filter(190deg)')).toBe(false);
});
});
describe('unit transformers', () => {
it('should correctly identify units', () => {
expect(px.test(10)).toBe(false);
expect(px.test('10px')).toBe(true);
expect(px.test('blur(10px)')).toBe(false);
expect(percent.test('10px')).toBe(false);
expect(percent.test('10%')).toBe(true);
expect(percent.test('blur(10%)')).toBe(false);
});
it('should append the correct units', () => {
expect(px.transform(10)).toBe('10px');
expect(degrees.transform(360)).toBe('360deg');
expect(percent.transform(100)).toBe('100%');
expect(rgbUnit.transform(256)).toBe(255);
expect(rgbUnit.transform(24.5)).toBe(25);
expect(
rgba.transform({
red: 256,
green: 24.5,
blue: 0,
})
).toBe('rgba(255, 25, 0, 1)');
expect(
hsla.transform({
hue: 100,
saturation: 50,
lightness: 50,
alpha: 1,
})
).toBe('hsla(100, 50%, 50%, 1)');
expect(alpha.transform(2)).toBe(1);
expect(
color.transform({
red: 256,
green: 24.5,
blue: 0,
})
).toBe('rgba(255, 25, 0, 1)');
expect(
color.transform({
hue: 100,
saturation: 50,
lightness: 50,
alpha: 1,
})
).toBe('hsla(100, 50%, 50%, 1)');
});
});
describe('combination values', () => {
it('should test correctly', () => {
expect(complex.test('20px 20px 10px inset #fff')).toEqual(true);
expect(
complex.test('20px 20px 10px inset rgba(255, 255, 255, 1), 20px')
).toEqual(true);
expect(
complex.test('linear-gradient(0.25turn, #3f87a6, #ebf8e1, #f69d3c')
).toEqual(true);
});
it('should parse into an array', () => {
expect(complex.parse('0px 10px #fff')).toEqual([
{ red: 255, green: 255, blue: 255, alpha: 1 },
0,
10,
]);
expect(complex.parse('20px 20px 10px inset #fff')).toEqual([
{ red: 255, green: 255, blue: 255, alpha: 1 },
20,
20,
10,
]);
expect(
complex.parse('20px 20px 10px inset rgba(255, 255, 255, 1)')
).toEqual([{ red: 255, green: 255, blue: 255, alpha: 1 }, 20, 20, 10]);
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,
20,
20,
10,
]);
expect(complex.parse('linear-gradient(0.25turn, #fff)')).toEqual([
{
red: 255,
green: 255,
blue: 255,
alpha: 1,
},
0.25,
]);
expect(
complex.parse('linear-gradient(1deg, rgba(255, 255, 255, 1))')
).toEqual([
{
red: 255,
green: 255,
blue: 255,
alpha: 1,
},
1,
]);
expect(
complex.parse(
'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%)'
)
).toEqual([
{
red: 255,
green: 0,
blue: 0,
alpha: 0.8,
},
{
red: 255,
green: 0,
blue: 0,
alpha: 0,
},
217,
70.71,
]);
expect(
complex.parse('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,
]);
});
it('should create a transformer', () => {
const animatable = complex.parse(
'20px 20px 10px inset rgba(255, 255, 255, 1), 20px 20px 10px inset #fff'
);
const transformer = complex.createTransformer(
'20px 20px 10px inset #fff, 20px 20px 10px inset rgba(255, 255, 255, 1)'
);
expect(transformer(animatable)).toBe(
'20px 20px 10px inset rgba(255, 255, 255, 1), 20px 20px 10px inset rgba(255, 255, 255, 1)'
);
const gradient = complex.parse(
'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%)'
);
const gradientTransformer = complex.createTransformer(
'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%)'
);
expect(gradientTransformer(gradient)).toBe(
'linear-gradient(217deg, rgba(255, 0, 0, 0.8), rgba(255, 0, 0, 0) 70.71%)'
);
});
});
describe('progress value type', () => {
it('should test correctly', () => {
expect(progressPercentage.test('100%')).toBe(true);
expect(progressPercentage.test('100px')).toBe(false);
});
it('should parse correctly', () => {
expect(progressPercentage.parse('50%')).toBe(0.5);
expect(progressPercentage.parse('0%')).toBe(0);
expect(progressPercentage.parse('-200%')).toBe(-2);
});
it('should transform correctly', () => {
expect(progressPercentage.transform(0.1)).toBe('10%');
expect(progressPercentage.transform(1.5)).toBe('150%');
});
});
describe('filter', () => {
it('should create an animatableNone with correct default values', () => {
expect(
filter.getAnimatableNone(
'blur(10.5px) brightness(50.5%) contrast(50.5%) drop-shadow(10px 10px #fff) grayscale(50.5%) hue-rotate(90deg) invert(50%) opacity(0.5) saturate(50.5%) sepia(50.5%)'
)
).toEqual(
'blur(0px) brightness(100%) contrast(100%) drop-shadow(10px 10px #fff) grayscale(0%) hue-rotate(0deg) invert(0%) opacity(1) saturate(100%) sepia(0%)'
);
expect(
filter.getAnimatableNone(
'blur(5rem) brightness(0.5) contrast(0.5) drop-shadow(10px 10px #fff) grayscale(0.5) hue-rotate(1.75turn) invert(0.5) opacity(0.5) saturate(0.5) sepia(0.5)'
)
).toEqual(
'blur(0rem) brightness(1) contrast(1) drop-shadow(10px 10px #fff) grayscale(0) hue-rotate(0turn) invert(0) opacity(1) saturate(1) sepia(0)'
);
});
});
================================================
FILE: packages/style-value-types/src/color/hex.ts
================================================
import { RGBA, ValueType } from '../types';
import { rgba } from './rgba';
import { isColorString } from './utils';
function parseHex(v: string): RGBA {
let r = '';
let g = '';
let b = '';
let a = '';
// If we have 6 characters, ie #FF0000
if (v.length > 5) {
r = v.substr(1, 2);
g = v.substr(3, 2);
b = v.substr(5, 2);
a = v.substr(7, 2);
// Or we have 3 characters, ie #F00
} else {
r = v.substr(1, 1);
g = v.substr(2, 1);
b = v.substr(3, 1);
a = v.substr(4, 1);
r += r;
g += g;
b += b;
a += a;
}
return {
red: parseInt(r, 16),
green: parseInt(g, 16),
blue: parseInt(b, 16),
alpha: a ? parseInt(a, 16) / 255 : 1,
};
}
export const hex: ValueType = {
test: isColorString('#'),
parse: parseHex,
transform: rgba.transform,
};
================================================
FILE: packages/style-value-types/src/color/hsla.ts
================================================
import { alpha as alphaType } from '../numbers';
import { percent } from '../numbers/units';
import { HSLA, ValueType } from '../types';
import { sanitize } from '../utils';
import { isColorString, splitColor } from './utils';
export const hsla: ValueType = {
test: isColorString('hsl', 'hue'),
parse: splitColor('hue', 'saturation', 'lightness'),
transform: ({ hue, saturation, lightness, alpha = 1 }: HSLA) => {
return (
'hsla(' +
Math.round(hue) +
', ' +
percent.transform(sanitize(saturation)) +
', ' +
percent.transform(sanitize(lightness)) +
', ' +
sanitize(alphaType.transform(alpha)) +
')'
);
},
};
================================================
FILE: packages/style-value-types/src/color/index.ts
================================================
import { HSLA, RGBA, ValueType } from '../types';
import { isString } from '../utils';
import { hex } from './hex';
import { hsla } from './hsla';
import { rgba } from './rgba';
export const color: ValueType = {
test: (v: any) => rgba.test(v) || hex.test(v) || hsla.test(v),
parse: (v: any) => {
if (rgba.test(v)) {
return rgba.parse(v);
} else if (hsla.test(v)) {
return hsla.parse(v);
} else {
return hex.parse(v);
}
},
transform: (v: HSLA | RGBA | string) => {
return isString(v)
? v
: v.hasOwnProperty('red')
? rgba.transform(v)
: hsla.transform(v);
},
};
================================================
FILE: packages/style-value-types/src/color/rgba.ts
================================================
import { alpha as alphaType, number } from '../numbers';
import { RGBA, ValueType } from '../types';
import { clamp, sanitize } from '../utils';
import { isColorString, splitColor } from './utils';
const clampRgbUnit = clamp(0, 255);
export const rgbUnit: ValueType = {
...number,
transform: (v: number) => Math.round(clampRgbUnit(v)),
};
export const rgba: ValueType = {
test: isColorString('rgb', 'red'),
parse: splitColor('red', 'green', 'blue'),
transform: ({ red, green, blue, alpha = 1 }: RGBA) =>
'rgba(' +
rgbUnit.transform(red) +
', ' +
rgbUnit.transform(green) +
', ' +
rgbUnit.transform(blue) +
', ' +
sanitize(alphaType.transform(alpha)) +
')',
};
================================================
FILE: packages/style-value-types/src/color/utils.ts
================================================
import { Color } from '../types';
import { floatRegex, isString, singleColorRegex } from '../utils';
/**
* Returns true if the provided string is a color, ie rgba(0,0,0,0) or #000,
* but false if a number or multiple colors
*/
export const isColorString = (type: string, testProp?: string) => (v: any) => {
return Boolean(
(isString(v) && singleColorRegex.test(v) && v.startsWith(type)) ||
(testProp && Object.prototype.hasOwnProperty.call(v, testProp))
);
};
export const splitColor = (aName: string, bName: string, cName: string) => (
v: string | Color
) => {
if (!isString(v)) return v;
const [a, b, c, alpha] = v.match(floatRegex);
return {
[aName]: parseFloat(a),
[bName]: parseFloat(b),
[cName]: parseFloat(c),
alpha: alpha !== undefined ? parseFloat(alpha) : 1,
};
};
================================================
FILE: packages/style-value-types/src/complex/filter.ts
================================================
import { complex } from '.';
import { floatRegex } from '../utils';
/**
* Properties that should default to 1 or 100%
*/
const maxDefaults = new Set(['brightness', 'contrast', 'saturate', 'opacity']);
function applyDefaultFilter(v: string) {
let [name, value] = v.slice(0, -1).split('(');
if (name === 'drop-shadow') return v;
const [number] = value.match(floatRegex) || [];
if (!number) return v;
const unit = value.replace(number, '');
let defaultValue = maxDefaults.has(name) ? 1 : 0;
if (number !== value) defaultValue *= 100;
return name + '(' + defaultValue + unit + ')';
}
const functionRegex = /([a-z-]*)\(.*?\)/g;
export const filter = {
...complex,
getAnimatableNone: (v: string) => {
const functions = v.match(functionRegex);
return functions ? functions.map(applyDefaultFilter).join(' ') : v;
},
};
================================================
FILE: packages/style-value-types/src/complex/index.ts
================================================
import { color } from '../color';
import { number } from '../numbers';
import { Color } from '../types';
import { colorRegex, floatRegex, isString, sanitize } from '../utils';
const colorToken = '${c}';
const numberToken = '${n}';
function test(v: any) {
return (
isNaN(v) &&
isString(v) &&
(v.match(floatRegex)?.length ?? 0) + (v.match(colorRegex)?.length ?? 0) > 0
);
}
function analyse(v: string | number) {
if (typeof v === 'number') v = `${v}`;
const values: Array = [];
let numColors = 0;
const colors = v.match(colorRegex);
if (colors) {
numColors = colors.length;
// Strip colors from input so they're not picked up by number regex.
// There's a better way to combine these regex searches, but its beyond my regex skills
v = v.replace(colorRegex, colorToken);
values.push(...colors.map(color.parse));
}
const numbers = v.match(floatRegex);
if (numbers) {
v = v.replace(floatRegex, numberToken);
values.push(...numbers.map(number.parse));
}
return { values, numColors, tokenised: v };
}
function parse(v: string | number) {
return analyse(v).values;
}
function createTransformer(v: string | number) {
const { values, numColors, tokenised } = analyse(v);
const numValues = values.length;
return (v: Array) => {
let output = tokenised;
for (let i = 0; i < numValues; i++) {
output = output.replace(
i < numColors ? colorToken : numberToken,
i < numColors ? color.transform(v[i]) : sanitize(v[i] as number)
);
}
return output;
};
}
const convertNumbersToZero = (v: number | Color) =>
typeof v === 'number' ? 0 : v;
function getAnimatableNone(v: string | number) {
const parsed = parse(v);
const transformer = createTransformer(v);
return transformer(parsed.map(convertNumbersToZero));
}
export const complex = { test, parse, createTransformer, getAnimatableNone };
================================================
FILE: packages/style-value-types/src/index.ts
================================================
export * from './types';
export { number, scale, alpha } from './numbers';
export {
degrees,
percent,
progressPercentage,
px,
vw,
vh,
} from './numbers/units';
export { hsla } from './color/hsla';
export { rgba, rgbUnit } from './color/rgba';
export { hex } from './color/hex';
export { color } from './color';
export { complex } from './complex';
export { filter } from './complex/filter';
================================================
FILE: packages/style-value-types/src/numbers/index.ts
================================================
import { clamp } from '../utils';
import { ValueType } from '../types';
export const number: ValueType = {
test: (v) => typeof v === 'number',
parse: parseFloat,
transform: (v: number) => v,
};
export const alpha: ValueType = {
...number,
transform: clamp(0, 1),
};
export const scale: ValueType = {
...number,
default: 1,
};
================================================
FILE: packages/style-value-types/src/numbers/units.ts
================================================
import { ValueType } from '../types';
import { isString } from '../utils';
const createUnitType = (unit: string): ValueType => ({
test: (v: string) =>
isString(v) && v.endsWith(unit) && v.split(' ').length === 1,
parse: parseFloat,
transform: (v: number | string) => `${v}${unit}`,
});
export const degrees = createUnitType('deg');
export const percent = createUnitType('%');
export const px = createUnitType('px');
export const vh = createUnitType('vh');
export const vw = createUnitType('vw');
export const progressPercentage: ValueType = {
...percent,
parse: (v: string) => percent.parse(v) / 100,
transform: (v: number) => percent.transform(v * 100),
};
================================================
FILE: packages/style-value-types/src/types.ts
================================================
export type Transformer = (v: any) => any;
export type ValueType = {
test: (v: any) => boolean;
parse: (v: any) => any;
transform?: Transformer;
createTransformer?: (template: string) => Transformer;
default?: any;
getAnimatableNone?: (v: any) => any;
};
export type NumberMap = {
[key: string]: number;
};
export type RGBA = {
red: number;
green: number;
blue: number;
alpha?: number;
};
export type HSLA = {
hue: number;
saturation: number;
lightness: number;
alpha?: number;
};
export type Color = HSLA | RGBA;
================================================
FILE: packages/style-value-types/src/utils.ts
================================================
export const clamp = (min: number, max: number) => (v: number) =>
Math.max(Math.min(v, max), min);
// If this number is a decimal, make it just five decimal places
// to avoid exponents
export const sanitize = (v: number) => (v % 1 ? Number(v.toFixed(5)) : v);
export const floatRegex = /(-)?([\d]*\.?[\d])+/g;
export const colorRegex = /(#[0-9a-f]{6}|#[0-9a-f]{3}|#(?:[0-9a-f]{2}){2,4}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))/gi;
export const singleColorRegex = /^(#[0-9a-f]{3}|#(?:[0-9a-f]{2}){2,4}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))$/i;
export function isString(v: any): v is string {
return typeof v === 'string';
}
================================================
FILE: packages/style-value-types/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"declarationDir": "lib",
},
"include": ["src/**/*"]
}
================================================
FILE: packages/style-value-types/tslint.json
================================================
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"ban-types": [false, ["Function"]],
"quotemark": [true, "single", "avoid-escape", "avoid-template"],
"trailing-comma": [false],
"no-unused-variable": true,
"interface-name": false,
"interface-over-type-literal": false,
"max-line-length":false,
"curly": false,
"prefer-for-of": false,
"member-access": false,
"object-literal-sort-keys": false,
"ordered-imports": false,
"variable-name": false
},
"rulesDirectory": []
}
================================================
FILE: playground/animate.js
================================================
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { animate } from 'popmotion';
const Box = styled.div`
width: 100px;
height: 100px;
background-color: #09f;
border-radius: 10px;
`;
export function Keyframes() {
const ref = useRef();
const [opacity, setOpacity] = useState(0);
useEffect(() => {
const controls = animate({
type: 'spring',
from: 0,
to: 400,
duration: 300,
mass: 1,
velocity: 20,
dampingRatio: 1,
onUpdate: (v) => {
ref.current.style.transform = `translateX(${v}px) translateZ(0)`;
// if (v > 20) controls.stop();
},
});
return () => controls.stop();
// animate({
// from: 0,
// to: 300,
// type: 'spring',
// repeat: Infinity,
// repeatType: 'mirror',
// onUpdate: v => {
// ref.current.style.transform = `translateX(${v}px)`;
// }
// });
}, [opacity]);
return setOpacity(opacity ? 0 : 1)} />;
}
================================================
FILE: playground/index.js
================================================
import React from 'react';
import { storiesOf } from '@storybook/react';
import * as Animate from './animate';
// import * as Worklet from './worklet';
// storiesOf('worklet', module).add('tween', () => );
storiesOf('animate', module).add('keyframes', () => );
// .add('decay', () => )
// .add('spring', () => );
================================================
FILE: playground/worklet.js
================================================
import * as React from 'react';
import { useEffect, useRef } from 'react';
import styled from 'styled-components';
import { animate, workletReady } from 'popmotion';
CSS.animationWorklet.addModule('popmotion-worklet.js').then(workletReady);
const Box = styled.div`
width: 100px;
height: 100px;
background-color: #09f;
border-radius: 10px;
`;
export function Tween() {
const ref = useRef();
useEffect(() => {
animate(ref.current, { x: 100 }, { duration: 3000 });
}, []);
return ;
}
================================================
FILE: rollup-generate-config.js
================================================
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';
const makeExternalPredicate = (externalArr) => {
if (externalArr.length === 0) {
return () => false;
}
const pattern = new RegExp(`^(${externalArr.join('|')})($|/)`);
return (id) => pattern.test(id);
};
export default function (pkg, name = pkg.name) {
const deps = Object.keys(pkg.dependencies || {});
const peerDeps = Object.keys(pkg.peerDependencies || {});
const config = {
input: 'lib/index.js',
external: makeExternalPredicate(deps.concat(peerDeps)),
};
const umd = {
...config,
output: {
file: `dist/${name}.js`,
format: 'umd',
name,
exports: 'named',
globals: {
tslib: 'tslib',
'hey-listen': 'heyListen',
'style-value-types': 'valueTypes',
framesync: 'framesync',
popmotion: 'popmotion',
'pose-core': 'poseCore',
'@popmotion/easing': 'easing',
'@popmotion/popcorn': 'popcorn',
},
},
external: makeExternalPredicate(peerDeps),
plugins: [
resolve(),
replace({
'process.env.NODE_ENV': JSON.stringify('development'),
}),
],
};
const umdProd = {
...umd,
output: {
...umd.output,
file: pkg.unpkg || `dist/${name}.min.js`,
},
plugins: [
resolve(),
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
terser({ output: { comments: false } }),
],
};
const es = {
...config,
output: {
entryFileNames: '[name].mjs',
format: 'es',
preserveModules: true,
dir: 'dist/es',
},
plugins: [resolve()],
};
const cjs = {
...config,
output: {
file: `dist/${name}.cjs.js`,
format: 'cjs',
exports: 'named',
},
plugins: [resolve()],
};
return [umd, umdProd, es, cjs];
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "es6",
"moduleResolution": "node",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"noUnusedLocals": true,
"target": "ES6",
"watch": false,
"baseUrl": "./packages",
"importHelpers": true,
"paths": {
"framesync": [
"./framesync/lib"
],
"popmotion": [
"./popmotion/lib"
],
"style-value-types": [
"./style-value-types/lib"
]
},
"alwaysStrict": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"lib": [
"es5",
"es6",
"dom"
],
"skipLibCheck": true
},
"exclude": [
"**/*.test.ts"
]
}
================================================
FILE: tslint.json
================================================
{
"defaultSeverity": "error",
"extends": [
"tslint:latest",
"tslint-react",
"tslint-config-prettier",
"tslint-circular-dependencies"
],
"jsRules": {},
"rules": {
"ban-types": [
false,
[
"Function"
]
],
"quotemark": [
true,
"single",
"avoid-escape",
"avoid-template"
],
"trailing-comma": [
false
],
"forin": false,
"interface-name": false,
"interface-over-type-literal": false,
"max-line-length": false,
"curly": false,
"prefer-for-of": false,
"member-access": false,
"ordered-imports": false,
"object-literal-sort-keys": false,
"imports-after-export": false,
"no-instanceof-operator": false,
"initialize-statics-after-imports": false
},
"rulesDirectory": []
}
================================================
FILE: types/animations/decay.d.ts
================================================
import { Animator, DecayOptions } from './types';
export declare class DecayAnimator implements Animator {
options: DecayOptions;
amplitude: number;
target: number;
static needsInterpolation: boolean;
isComplete: boolean;
constructor(options: DecayOptions);
update(t: number): number;
updateOptions({
velocity,
from,
power,
timeConstant,
restDelta,
modifyTarget
}?: DecayOptions): void;
static uniqueOptionKeys: Set<
| 'velocity'
| 'to'
| 'restDelta'
| 'from'
| 'timeConstant'
| 'power'
| 'modifyTarget'
>;
}
================================================
FILE: types/animations/index.d.ts
================================================
import { Animatable, AnimationOptions, PlaybackControls } from './types';
export declare function animate({
from,
to,
autoplay,
driver,
elapsed,
repeat: repeatMax,
repeatType,
repeatDelay,
onPlay,
onComplete,
onRepeat,
onUpdate,
...options
}: AnimationOptions): PlaybackControls;
================================================
FILE: types/animations/inertia.d.ts
================================================
================================================
FILE: types/animations/keyframes.d.ts
================================================
import { KeyframeOptions, Animator } from './types';
import { Easing } from '../easing/types';
export declare function defaultEasing(values: any[], easing?: Easing): Easing[];
export declare function defaultOffset(values: any[]): number[];
export declare function convertOffsetToTimes(
offset: number[],
duration: number
): number[];
export declare class KeyframesAnimator
implements Animator {
options: KeyframeOptions;
isComplete: boolean;
static needsInterpolation: boolean;
interpolator: (t: number) => any;
constructor(options: KeyframeOptions);
update(t: number): any;
updateOptions({ from, to, ease, offset, duration }: KeyframeOptions): void;
static uniqueOptionKeys: Set<'to' | 'from' | 'ease' | 'duration' | 'offset'>;
}
================================================
FILE: types/animations/spring.d.ts
================================================
import { SpringOptions, Animator } from './types';
export declare class SpringAnimator implements Animator {
options: SpringOptions;
isComplete: boolean;
resolveSpring: (t: number) => number;
static needsInterpolation: boolean;
constructor(options: SpringOptions);
update(t: number): number | undefined;
updateOptions({
from,
to,
velocity,
stiffness,
damping,
mass,
restSpeed,
restDelta
}: SpringOptions): void;
static uniqueOptionKeys: Set<
| 'velocity'
| 'restSpeed'
| 'to'
| 'restDelta'
| 'from'
| 'damping'
| 'stiffness'
| 'mass'
>;
}
================================================
FILE: types/animations/types.d.ts
================================================
import { Easing } from '../easing/types';
/**
* The kinds of values that can currently be animated.
*/
export declare type Animatable = string | number;
/**
* Animators that can be resolved for time
*/
export interface Animator {
options: O;
update(t: number): V;
updateOptions(options: O): void;
isComplete: boolean;
}
export interface PlaybackControls {
play: () => void;
pause: () => void;
resume: () => void;
reverse: () => void;
seek: () => void;
stop: () => void;
}
/**
* An update function. It accepts a timestamp used to advance the animation.
*/
declare type Update = (timestamp: number) => void;
/**
* Drivers accept a update function and call it at an interval. This interval
* could be a synchronous loop, a setInterval, or tied to the device's framerate.
*/
export declare type Driver = (update: Update) => () => void;
/**
* Playback options common to all animations.
*/
export interface PlaybackOptions {
/**
* Whether to autoplay the animation when animate is called. If
* set to false, the animation must be started manually via the returned
* play method.
*/
autoplay?: boolean;
driver?: Driver;
elapsed?: number;
from?: V;
repeat?: number;
repeatType?: 'loop' | 'reverse';
repeatDelay?: number;
onUpdate?: (latest: V) => void;
onPlay?: () => void;
onComplete?: () => void;
onRepeat?: () => void;
}
export interface KeyframeOptions {
to: V | V[];
from?: V;
duration?: number;
ease?: Easing | Easing[];
offset?: number[];
}
export interface DecayOptions {
from?: T;
to?: T;
velocity?: number;
power?: number;
timeConstant?: number;
modifyTarget?: (target: number) => number;
restDelta?: number;
}
export interface SpringOptions {
from?: T;
to?: T;
velocity?: number;
stiffness?: number;
damping?: number;
mass?: number;
restSpeed?: number;
restDelta?: number;
}
export declare type AnimationOptions = PlaybackOptions<
V
> &
(DecayOptions | KeyframeOptions | SpringOptions);
export {};
================================================
FILE: types/animations/utils/detect-animation-from-options.d.ts
================================================
import { SpringAnimator } from '../spring';
import { KeyframesAnimator } from '../keyframes';
import { DecayAnimator } from '../decay';
interface Options {}
export declare function detectAnimationFromOptions(
config: T
):
| typeof SpringAnimator
| typeof KeyframesAnimator
| typeof DecayAnimator
| undefined;
export {};
================================================
FILE: types/easing/cubic-bezier.d.ts
================================================
export declare function cubicBezier(
mX1: number,
mY1: number,
mX2: number,
mY2: number
): import('./types').Easing;
================================================
FILE: types/easing/index.d.ts
================================================
import { Easing } from './types';
export declare const linear: Easing;
export declare const easeIn: Easing;
export declare const easeOut: Easing;
export declare const easeInOut: Easing;
export declare const circIn: Easing;
export declare const circOut: Easing;
export declare const circInOut: Easing;
export declare const backIn: Easing;
export declare const backOut: Easing;
export declare const backInOut: Easing;
export declare const anticipate: Easing;
export declare const bounceOut: (p: number) => number;
export declare const bounceIn: (p: number) => number;
export declare const bounceInOut: (p: number) => number;
================================================
FILE: types/easing/steps.d.ts
================================================
import { Easing } from './types';
export declare type Direction = 'start' | 'end';
export declare const steps: (steps: number, direction?: Direction) => Easing;
================================================
FILE: types/easing/types.d.ts
================================================
export declare type Easing = (v: number) => number;
export declare type EasingModifier = (easing: Easing) => Easing;
================================================
FILE: types/easing/utils.d.ts
================================================
import { Easing, EasingModifier } from './types';
export declare const reverseEasing: EasingModifier;
export declare const mirrorEasing: EasingModifier;
export declare const createExpoIn: (power: number) => Easing;
export declare const createBackIn: (power: number) => Easing;
export declare const createAnticipate: (power: number) => Easing;
================================================
FILE: types/global.d.ts
================================================
export {};
================================================
FILE: types/index.d.ts
================================================
export { animate } from './animations';
export { DecayAnimator } from './animations/decay';
export { SpringAnimator } from './animations/spring';
export { KeyframesAnimator } from './animations/keyframes';
export { angle } from './utils/angle';
export { applyOffset } from './utils/apply-offset';
export { attract, attractLinear, attractExpo } from './utils/attract';
export { clamp } from './utils/clamp';
export { degreesToRadians } from './utils/degrees-to-radians';
export { distance } from './utils/distance';
export { interpolate } from './utils/interpolate';
export { isPoint3D } from './utils/is-point-3d';
export { isPoint } from './utils/is-point';
export { mixColor } from './utils/mix-color';
export { mixComplex } from './utils/mix-complex';
export { mix } from './utils/mix';
export { pipe } from './utils/pipe';
export { pointFromVector } from './utils/point-from-vector';
export { progress } from './utils/progress';
export { radiansToDegrees } from './utils/radians-to-degrees';
export { smoothFrame } from './utils/smooth-frame';
export { smooth } from './utils/smooth';
export { snap } from './utils/snap';
export { toDecimal } from './utils/to-decimal';
export { velocityPerFrame } from './utils/velocity-per-frame';
export { velocityPerSecond } from './utils/velocity-per-second';
export { wrap } from './utils/wrap';
export {
linear,
easeIn,
easeInOut,
easeOut,
circIn,
circInOut,
circOut,
backIn,
backInOut,
backOut,
anticipate,
bounceIn,
bounceInOut,
bounceOut
} from './easing';
export { cubicBezier } from './easing/cubic-bezier';
export { steps } from './easing/steps';
export {
mirrorEasing,
reverseEasing,
createExpoIn,
createBackIn,
createAnticipate
} from './easing/utils';
================================================
FILE: types/types.d.ts
================================================
export declare type Point2D = {
x: number;
y: number;
};
export declare type Point3D = Point2D & {
z: number;
};
export declare type Point = Point2D | Point3D;
================================================
FILE: types/utils/angle.d.ts
================================================
import { Point } from '../types';
export declare const angle: (a: Point, b?: Point) => number;
================================================
FILE: types/utils/apply-offset.d.ts
================================================
/**
* Apply offset
* A function that, given a value, will get the offset from `from`
* and apply it to `to`
* @param {number} from
* @param {number} to
* @return {function}
*/
export declare const applyOffset: (
from: number,
to?: number | undefined
) => (v: number) => number | undefined;
================================================
FILE: types/utils/attract.d.ts
================================================
export declare const attract: (
alterDisplacement?: Function
) => (constant: number, origin: number, v: number) => number;
export declare const attractLinear: (
constant: number,
origin: number,
v: number
) => number;
export declare const attractExpo: (
constant: number,
origin: number,
v: number
) => number;
================================================
FILE: types/utils/clamp.d.ts
================================================
export declare const clamp: (min: number, max: number, v: number) => number;
================================================
FILE: types/utils/degrees-to-radians.d.ts
================================================
export declare const degreesToRadians: (degrees: number) => number;
================================================
FILE: types/utils/distance.d.ts
================================================
import { Point } from '../types';
declare type _Point = Point | number;
export declare const distance: (a: _Point, b?: _Point) => number;
export {};
================================================
FILE: types/utils/inc.d.ts
================================================
import { Point } from '../types';
export declare const zeroPoint: Point;
export declare const isNum: (v: any) => v is number;
================================================
FILE: types/utils/interpolate.d.ts
================================================
import { Easing } from '../easing/types';
declare type MixEasing = Easing | Easing[];
declare type InterpolateOptions = {
clamp?: boolean;
ease?: MixEasing;
mixer?: MixerFactory;
};
declare type Mix = (v: number) => T;
export declare type MixerFactory = (from: T, to: T) => Mix;
/**
* Create a function that maps from a numerical input array to a generic output array.
*
* Accepts:
* - Numbers
* - Colors (hex, hsl, hsla, rgb, rgba)
* - Complex (combinations of one or more numbers or strings)
*
* ```jsx
* const mixColor = interpolate([0, 1], ['#fff', '#000'])
*
* mixColor(0.5) // 'rgba(128, 128, 128, 1)'
* ```
*
* @public
*/
export declare function interpolate(
input: number[],
output: T[],
{ clamp: isClamp, ease, mixer }?: InterpolateOptions
): (v: number) => T;
export {};
================================================
FILE: types/utils/is-point-3d.d.ts
================================================
import { Point, Point3D } from '../types';
export declare const isPoint3D: (point: Point) => point is Point3D;
================================================
FILE: types/utils/is-point.d.ts
================================================
import { Point } from '../types';
export declare const isPoint: (point: Object) => point is Point;
================================================
FILE: types/utils/mix-color.d.ts
================================================
export declare const mixLinearColor: (
from: number,
to: number,
v: number
) => number;
export declare const mixColor: (
from:
| string
| import('../../../style-value-types/lib').HSLA
| import('../../../style-value-types/lib').RGBA,
to:
| string
| import('../../../style-value-types/lib').HSLA
| import('../../../style-value-types/lib').RGBA
) => (v: number) => any;
================================================
FILE: types/utils/mix-complex.d.ts
================================================
import { RGBA, HSLA } from 'style-value-types';
declare type MixComplex = (p: number) => string;
declare type BlendableObject = {
[key: string]: string | number | RGBA | HSLA;
};
export declare const mixArray: (
from: (string | number | HSLA | RGBA)[],
to: (string | number | HSLA | RGBA)[]
) => (v: number) => (string | number | HSLA | RGBA)[];
export declare const mixObject: (
origin: BlendableObject,
target: BlendableObject
) => (
v: number
) => {
[x: string]: string | number | HSLA | RGBA;
};
export declare const mixComplex: (origin: string, target: string) => MixComplex;
export {};
================================================
FILE: types/utils/mix.d.ts
================================================
export declare const mix: (
from: number,
to: number,
progress: number
) => number;
================================================
FILE: types/utils/pipe.d.ts
================================================
export declare const pipe: (...transformers: Function[]) => Function;
================================================
FILE: types/utils/point-from-vector.d.ts
================================================
import { Point2D } from '../types';
export declare const pointFromVector: (
origin: Point2D,
angle: number,
distance: number
) => {
x: number;
y: number;
};
================================================
FILE: types/utils/progress.d.ts
================================================
export declare const progress: (
from: number,
to: number,
value: number
) => number;
================================================
FILE: types/utils/radians-to-degrees.d.ts
================================================
export declare const radiansToDegrees: (radians: number) => number;
================================================
FILE: types/utils/smooth-frame.d.ts
================================================
export declare const smoothFrame: (
prevValue: number,
nextValue: number,
duration: number,
smoothing?: number
) => number;
================================================
FILE: types/utils/smooth.d.ts
================================================
export declare const smooth: (strength?: number) => (v: number) => number;
================================================
FILE: types/utils/snap.d.ts
================================================
export declare const snap: (
points: number | number[]
) => (v: number) => number | undefined;
================================================
FILE: types/utils/to-decimal.d.ts
================================================
export declare const toDecimal: (num: number, precision?: number) => number;
================================================
FILE: types/utils/velocity-per-frame.d.ts
================================================
export declare const velocityPerFrame: (
xps: number,
frameDuration: number
) => number;
================================================
FILE: types/utils/velocity-per-second.d.ts
================================================
export declare const velocityPerSecond: (
velocity: number,
frameDuration: number
) => number;
================================================
FILE: types/utils/wrap.d.ts
================================================
export declare const wrap: (min: number, max: number, v: number) => number;
================================================
FILE: types/worklet/custom-properties.d.ts
================================================
export declare function namespace(name: string): string;
export declare function registerCustomProperties(): void;
================================================
FILE: types/worklet/load-worklet.d.ts
================================================
export declare function whenWorkletReady(): Promise;
export declare function workletReady(): void;