$ npm install projectrix --saveDom Projection (noun): The calculation of an element's position, size, and transform on a web page in relation to an element elsewhere in the DOM hierarchy.
         Projection (noun): The set of styles that will make a target element align with a subject. "Animate the target to the subject's projection."
Dom projection has many uses, such as view transitions, FLIP animations, css puzzle games, and art. However, implementations of this tricky math technique are usually hidden behind apis that prescribe a specific use-case or technology.
Projectrix provides getProjection(), which returns the styles needed to align a target element with a subject, as well as the styles needed to align it with its original state. Use the projected styles however you want; if animation is your goal, the projections can be spread directly into Anime.js, Motion, or your preferred animation engine.
Also provided...
- measureSubject(): calculates a subject's position in case the subject and target cannot coexist (e.g. a FLIP animation where the subject is the target's past)
- setInlineStyles(): sets a target to match a projection
- clearInlineStyles(): clears the projection styles from setInlineStyles
See all demos here: https://tg.projectrix.dev/demos
import { getProjection, type PartialProjectionResults } from 'projectrix';
import { animate } from 'animejs';
function animateTargetToSubject(target: HTMLElement, subject: HTMLElement): void {
const { toSubject } = getProjection(subject, target) as PartialProjectionResults;
delete toSubject.borderStyle; // preserve target's border style
animate(target, {
...toSubject,
duration: 400,
ease: 'outQuad',
});
}animate.mp4
// don't combine projectrix's setInlineStyles with animejs' animate; use utils.set
import { measureSubject, getProjection, clearInlineStyles } from 'projectrix';
import { animate, utils } from 'animejs';
// the F.L.I.P. technique inverts a direct animation: measure the element's first position,
// apply a dom change to it, then play the animation from its first position to its last
function flipTargetToNextParent(target: HTMLElement, nextParent: HTMLElement): void {
const firstPosition = measureSubject(target);
nextParent.append(target);
// RAF waits for pending dom changes to be rendered
requestAnimationFrame(() => {
// set target to the projection of its first position
const { toSubject, toTargetOrigin } = getProjection(firstPosition, target);
utils.set(target, toSubject);
// animate target to its last position, i.e. its current origin
animate(target, {
...toTargetOrigin,
duration: 1000,
ease: 'outQuad',
// clear inline styles from the projection once they're redundant
onComplete: () => clearInlineStyles(target),
});
});
}flip.mp4
export type Projection = {
width: string; // 'Wpx'
height: string; // 'Hpx'
borderStyle: string; // '' | 'none' | 'solid' | 'dashed' | etc.
borderWidth: string; // 'Tpx Rpx Bpx Lpx'
borderRadius: string; // 'TLpx TRpx BRpx BLpx'
transformOrigin: string; // 'X% Y% Zpx'
/**
* contains exactly one of the following members, depending on the given transformType option:
* @member transform: string; // (default) `matrix3d(${matrix3d})`
* @member matrix3d: string;
* @member transformMat4: mat4; // row-major array from gl-matrix
*/
[TransformType: string]: string | mat4 | any; // any is only necessary to allow spreading into anime.js, motion one, etc.
};
export type PartialProjection = Partial<Projection>;export function getProjection(
subject: HTMLElement | Measurement, // the element or measurement that you plan to align the target to
target: HTMLElement, // the element that you plan to modify
options?: ProjectionOptions,
): ProjectionResults;
export type TransformType = 'transform' | 'matrix3d' | 'transformMat4';
export type BorderSource = 'subject' | 'target' | 'zero';
export type ProjectionOptions = {
transformType?: TransformType; // (default = 'transform')
// designates which element's border width, radius, and style to match.
// projected width and height are auto-adjusted if the target has content-box sizing.
// zero means override with 0px border width and radius
useBorder?: BorderSource; // (default = 'subject')
log?: boolean; // (default = false)
};
/**
* when a subject is projected onto a target, you get two Projections. 'toSubject' contains the set of styles that--when applied
* to the target element--will make the target align with the subject. the styles in 'toTargetOrigin' will make
* the target align with its original state
*/
export type ProjectionResults = {
toSubject: Projection;
toTargetOrigin: Projection;
transformType: TransformType; // the type of transform that both projections contain
subject: HTMLElement | Measurement;
target: HTMLElement;
};
export type PartialProjectionResults = {
toSubject: PartialProjection;
toTargetOrigin: PartialProjection;
transformType: TransformType;
subject: HTMLElement | Measurement;
target: HTMLElement;
};/**
* measures a subject for future projections. useful if the subject and target cannot coexist,
* such as a flip animation where the subject is the target's past
*/
export function measureSubject(subject: HTMLElement): Measurement;
export type Measurement = {
acr: ActualClientRect; // from https://github.com/anxpara/getActualClientRect
border: BorderMeasurement;
};
export type BorderMeasurement = { /* style, top, right, bottom, left, and radius properties */ };/**
* sets the inline style on the target for each style in the given partial projection.
* converts any matrix3d or transformMat4 value to a transform style
*/
export function setInlineStyles(target: HTMLElement, partialProjection: PartialProjection): void;
/**
* clears the inline style on the target for each style in the given partial projection.
* if no partial projection is given, assumes target's inline styles were set to a full projection.
* if the projection contains matrix3d or transformMat4, then the transform style is cleared
*/
export function clearInlineStyles(target: HTMLElement, partialProjection?: PartialProjection): void;All trial scenarios are defined in the /trialgrounds directory, and can be viewed at https://trialgrounds.projectrix.dev
- While the trials are animated by default on the site, the tests run by Playwright only consider the static projection results (toSubject and toTargetOrigin).
- The mobile safari configuration for Playwright has a decent number of false failures; any such trials are effectively disabled and tested manually.
- The Playwright screenshots in the repo are created on Windows, and Playwright might not use the right configurations if run from another environment.
$ cd trialgrounds
$ npm run link
$ npm run test
I chip away at this library here and there, and it's in a fairly stable state for my purposes. I will mark the library as beta once I complete the following:
- Make a proper landing page and docs site
- Set up a daily test runner Github action
Other plans:
- Projectrix, and more specifically getActualClientRect, have not been optimized yet. I will eventually optimize when the time is right.
- SVGs, canvases, and 3D libraries will be considered in the future.
"Does Projectrix support layout projection and animation like Framer Motion?"
Not currently, Projectrix does not care about the contents of the subject and target elements. I haven't decided if layout projection will be in-scope for this library, but that would be a long ways off.
- Projectrix will not attempt to match, emulate, or mitigate bugs in rendering engines
- Stackoverflow: -webkit-transform-style: preserve-3d not working
- some engines don't follow the preserve-3d used value specs, and still use preserve-3d even when combined with certain grouping properties:
- Chrome v123 / Blink -- contain: strict | content | paint, content-visibility: auto
- Firefox v124 / Gecko -- will-change: filter
- Safari v17.4 / Webkit -- will-change: filter | opacity
- (Properties not yet supported by particular browsers omitted from their respective lists)
- Projectrix is not an animation engine, and will not attempt to mitigate bugs in animation engines
- motiondivision/motionone#249
- some engines might animate perspective incorrectly in particular scenarios
- Targeting an element with an "internal" display value, or any value that causes the element to control its own size, will lead to undefined behavior, since the projected width and height will be ignored:
- display: inline | table | inline-table | table-row | table-column | table-cell | table-row-group | table-column-group | table-header-group | table-footer-group | ruby-base | ruby-text | ruby-base-container | ruby-text-container | run-in
All contributions are greatly appreciated!
- If you find a bug, please file an issue
- Feedback and help requests can be posted in the Projectrix Discord
<3 anxpara
