framer-controller
Version:
Control components and state in Framer X with reusable controllers.
335 lines (334 loc) • 13.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const anime = require("animejs");
const framer_1 = require("framer");
class ScrollController {
constructor() {
this._scrollPoint = {
x: framer_1.motionValue(0),
y: framer_1.motionValue(0),
};
this._direction = {
x: framer_1.motionValue('none'),
y: framer_1.motionValue('none'),
};
this._progress = {
x: framer_1.motionValue(0),
y: framer_1.motionValue(0),
};
this._tracked = new Map();
// Connect to a scroll component's props
this.connect = (props) => {
// Guard against connecting to a Frame that isn't a Scroll component
if (!props || !props.children) {
console.error('Error: Connect this ScrollController to the' +
' props of a Scroll component (using an Override).');
return;
}
const [component] = props.children;
const [content] = component.props.children;
// Guard against reconnecting to tthe same Scroll component
// if (component === this._scrollComponent) {
// return
// }
// Guard against connecting to a Scroll component without an attached Frame
if (!content) {
console.error('Error: Your Scroll component must be connected ' +
'to a content Frame.');
return;
}
// Save scroll component to the class
this._scrollComponent = component;
this._scrollSize = {
height: framer_1.motionValue(props.height),
width: framer_1.motionValue(props.width),
};
// Save scroll content to the class
this._content = content;
this._contentSize = {
height: content.props.height,
width: content.props.width,
};
// Set tthe controller's direction and progress
this.setControllerValues();
this.tracked.clear();
// Track all children
// this._tracked = this._content.props.children.reduce(
// (acc: { [key: string]: Tracked }, tracked: any) => {
// acc[tracked.props.id] = this.trackFrame(tracked.props)
// return acc
// },
// {}
// )
return this.overrides;
};
this.setControllerValues = () => {
const { x, y } = this._scrollPoint;
const { height: scrollHeight, width: scrollWidth } = this._scrollSize;
const { height: contentHeight, width: contentWidth } = this._contentSize;
// Direction:
// Which way is the scroll scrolling?
// closure
let direction = {
x: 0,
y: 0,
};
this._direction = {
x: framer_1.useTransform(x, (x) => {
const next = x < direction.x ? 'right' : x > direction.x ? 'left' : 'none';
direction.x = x;
return next;
}),
y: framer_1.useTransform(y, (y) => {
const next = y < direction.y ? 'down' : y > direction.y ? 'up' : 'none';
direction.y = y;
return next;
}),
};
// Declare controller's progress
this._progress = {
x: framer_1.useTransform(x, (x) => x / -(contentWidth - scrollWidth.get())),
y: framer_1.useTransform(y, (y) => y / -(contentHeight - scrollHeight.get())),
};
};
// Connect to / track a child of the scroll's content
this.trackFrame = (props, margin = { x: 0, y: 0 }) => {
const { id } = props;
if (this.tracked[id])
return;
const { x, y } = this._scrollPoint;
const { height: scrollHeight, width: scrollWidth } = this._scrollSize;
const { height: contentHeight, width: contentWidth } = this._contentSize;
const { centerX, centerY, height, width } = props;
const cx = parseFloat(centerX) / 100;
const cy = parseFloat(centerY) / 100;
let minX = 0, maxX = scrollHeight.get(), minY = 0, maxY = scrollWidth.get();
// calculate absolute position
const absolute = {
top: contentHeight * cy - height / 2,
bottom: contentHeight * cy + height / 2,
left: contentWidth * cx - width / 2,
right: contentWidth * cx + width / 2,
height: height,
width: height,
};
// offset:
// Where is the Frame now, relative to the scroll window?
const offset = {
top: framer_1.useTransform(y, (value) => absolute.top + value),
right: framer_1.useTransform(x, (value) => absolute.right + value),
bottom: framer_1.useTransform(y, (value) => absolute.bottom + value),
left: framer_1.useTransform(x, (value) => absolute.left + value),
};
// clip:
// Which edges of the tracked Frame are overlapping the edges of the scroll window?
const clip = {
x: framer_1.useTransform(x, (x) => {
x += margin.x;
const l = offset.left.get();
const r = offset.right.get();
return l > maxX
? 'right'
: r < minX
? 'left'
: isBetween(l, minX, maxX) && r > maxX
? 'clip-right'
: isBetween(r, minX, maxX) && l < minX
? 'clip-left'
: l < minX && r > maxX
? 'overflow'
: 'contain';
}),
y: framer_1.useTransform(y, (y) => {
y += margin.y;
const t = offset.top.get();
const b = offset.bottom.get();
return t > maxY
? 'below'
: b < minY
? 'above'
: isBetween(t, minY, maxY) && b > maxY
? 'clip-bottom'
: isBetween(b, minY, maxY) && t < minY
? 'clip-top'
: t < minY && b > maxY
? 'overflow'
: 'contain';
}),
};
// intersect:
// How much of the tracked Frame is within the scroll window?
const intersect = {
x: framer_1.useTransform(clip.x, (c) => {
const l = offset.left.get();
const r = offset.right.get();
const intersects = {
'clip-right': (maxX - l) / width,
'clip-left': r / width,
overflow: maxX / width,
contain: 1,
};
return intersects[c] || 0;
}),
y: framer_1.useTransform(clip.y, (c) => {
const t = offset.top.get();
const b = offset.bottom.get();
const intersects = {
'clip-bottom': (maxY - t) / height,
'clip-top': b / height,
overflow: maxY / height,
contain: 1,
};
return intersects[c] || 0;
}),
};
// progress:
// How far is the tracked frame from scrolling into view (0) to scrolling out (1)?
const progress = {
x: framer_1.useTransform(offset.left, (l) => {
return framer_1.transform(l, [maxX, -width], [0, 1]);
}),
y: framer_1.useTransform(offset.top, (t) => {
return framer_1.transform(t, [maxY, -height], [0, 1]);
}),
};
// travel:
// Is the tracked Frame below/right (-1), contained (0), or above/left (1)?
const travel = {
x: framer_1.useTransform(clip.x, (c) => {
const l = offset.left.get();
const r = offset.right.get();
const intersects = {
left: 1,
right: -1,
'clip-right': -1 + (maxX - l) / width,
'clip-left': 1 - r / width,
overflow: 0,
contain: 0,
};
return intersects[c];
}),
y: framer_1.useTransform(clip.y, (c) => {
const t = offset.top.get();
const b = offset.bottom.get();
const intersects = {
above: 1,
below: -1,
'clip-bottom': -1 + (maxY - t) / height,
'clip-top': 1 - b / height,
overflow: 0,
contain: 0,
};
return intersects[c];
}),
};
// Store everything to tracked
// under this Frame's id
this.tracked.set(id, {
absolute,
offset,
clip,
travel,
intersect,
progress,
});
return this.tracked.get(id);
};
// Scroll to a point
this.scrollToPoint = (point, options = {}) => {
const { x, y } = this._scrollPoint;
if (this.animation) {
this.animation.pause();
}
// Create the targets to animate
const targets = {
x: x.get(),
y: y.get(),
};
// Use animejs to animate target to new values,
// and update the motionValues when these change
this._animation = anime(Object.assign({ targets, x: -point.x, y: -point.y, change: () => {
x.set(targets.x);
y.set(targets.y);
}, begin: () => (this._isAnimating = true), complete: () => (this._isAnimating = false) }, options));
return this._animation;
};
// Scroll to a specific child Frame
this.scrollToFrame = (id, edge, offset = 0, options = {}) => {
const { x, y } = this._scrollPoint;
if (this.animation) {
this.animation.pause();
}
const targets = {
x: x.get(),
y: y.get(),
};
if (!Array.isArray(edge)) {
edge = [edge];
}
let anim = {
x: 0,
y: 0,
};
const tracked = this.tracked[id];
// Decide which edges to offset
const edgeX = edge.find((e) => e === 'left' || e === 'right');
const edgeY = edge.find((e) => e === 'top' || e === 'bottom');
// Set anim
if (edgeX) {
anim.x = -(tracked.absolute[edgeX] - offset);
}
if (edgeY) {
anim.y = -(tracked.absolute[edgeY] - offset);
}
// Use animejs to animate target to new values,
// and update the motionValues when these change
this._animation = anime(Object.assign({ targets }, anim, { change: () => {
x.set(targets.x);
y.set(targets.y);
}, begin: () => (this._isAnimating = true), complete: () => (this._isAnimating = false) }, options));
return this._animation;
};
}
get overrides() {
return {
contentOffsetX: this._scrollPoint.x,
contentOffsetY: this._scrollPoint.y,
height: this._scrollSize.height,
width: this._scrollSize.width,
};
}
get scrollComponent() {
return this._scrollComponent;
}
get content() {
return this._content;
}
get contentSize() {
return this._contentSize;
}
get scrollSize() {
return this._scrollSize;
}
get direction() {
return this._direction;
}
get progress() {
return this._progress;
}
get scrollPoint() {
return this._scrollPoint;
}
get tracked() {
return this._tracked;
}
get animation() {
return this._animation;
}
get isAnimating() {
return this._isAnimating;
}
}
exports.ScrollController = ScrollController;
const isBetween = (a, b, c) => a >= b && a <= c;
const isntBetween = (a, b, c) => a < b && a > c;