Thanks to visit codestin.com
Credit goes to github.com

Skip to content

anxpara/projectrix

Repository files navigation

Projectrix

minimalist dom projection library in js/ts | alpha

$ npm install projectrix --save
Dom 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."

Summary

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

Usage examples

Animate target directly to subject using Anime.js

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

FLIP target between parents using Anime.js

// 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

API / Types / Documentation

Projection

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>;

getProjection(), ProjectionOptions, ProjectionResults

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;
};

measureSubject(), Measurement

/**
 * 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 */ };

setInlineStyles(), clearInlineStyles()

/**
 * 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;

Testing

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

Roadmap

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.

Limitations

  • 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
  • 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

Contribute

All contributions are greatly appreciated!

<3 anxpara

About

minimalist dom projection library | alpha

Resources

License

Stars

Watchers

Forks