Skip to content

applySvgTransforms function ​

Applies orders to an SVG element, in order, each on what the previous ones give: one transform per order is added at the end of its transform list. Changing an order later with set updates its own transform only, from what the element shows without it.

ts
import { applySvgTransforms } from 'utils';

Signature ​

ts
export function applySvgTransforms(element: SVGGraphicsElement, orders: readonly SvgTransformOrder<never>[]): void

Parameters ​

NameDescription
elementA rendered SVG element.
ordersThe orders, such as svgRotateTo(0), svgPlaceOn(target), svgRotateBy(0).

Errors ​

  • Throws TypeError: When the element is not rendered or not inside an <svg>, or an order was not created by an order function.

Example ​

ts
const rotation = svgRotateBy(0);
applySvgTransforms(needle, [svgPlaceOn(hub, 'center', 'bottom'), rotation]);
rotation.set(45); // at each frame: 45° more than its first position

Tests ​

Line coverage: 100 %.

  • ✅ applies each order on what the previous ones give
  • ✅ keeps the transforms already in the list
  • ✅ updates only the changed order
  • ✅ updates an order followed by others, in the axes the element shows
  • ✅ keeps the values of an order changed before it is applied
  • ✅ throws a TypeError for an object that is not an order
  • ✅ throws a TypeError for an element outside an svg

Source ​

src/svg-transform/apply-svg-transforms.ts (on GitHub)

Uses ​

  • SvgTransformOrder: An order of applySvgTransforms, created by svgRotateBy, svgRotateTo, svgFlipTo, svgScaleBy, svgTranslateBy or svgPlaceOn: keep it to change its values later with set.

Files it needs ​

src/svg-transform/apply-svg-transforms.ts imports, directly or not:

Source code
ts
import { createMatrix, getOrderState, getOwnerSvg, getScreenMatrix } from './internal';
import type { SvgTransformOrder } from './svg-transform-order';

/**
 * Applies orders to an SVG element, in order, each on what the previous ones give: one transform per order
 * is added at the end of its `transform` list. Changing an order later with `set` updates its own transform
 * only, from what the element shows without it.
 *
 * @param element - A rendered SVG element.
 * @param orders - The orders, such as `svgRotateTo(0)`, `svgPlaceOn(target)`, `svgRotateBy(0)`.
 * @throws {TypeError} When the element is not rendered or not inside an `<svg>`, or an order was not created
 * by an order function.
 * @example
 * const rotation = svgRotateBy(0);
 * applySvgTransforms(needle, [svgPlaceOn(hub, 'center', 'bottom'), rotation]);
 * rotation.set(45); // at each frame: 45° more than its first position
 */
export function applySvgTransforms(element: SVGGraphicsElement, orders: readonly SvgTransformOrder<never>[]): void {
  const svg = getOwnerSvg(element);
  const steps = orders
    .map((order) => getOrderState(order))
    .map((state) => ({ state, transform: element.transform.baseVal.appendItem(svg.createSVGTransform()) }));

  for (const [index, { state, transform }] of steps.entries()) {
    const later = steps.slice(index + 1);
    state.update = (): void => {
      transform.setMatrix(createMatrix(element));
      let after = createMatrix(element);
      for (const next of later) {
        after = after.multiply(next.transform.matrix);
      }
      const screen = getScreenMatrix(element);
      const change = state.toChange(element, screen);
      transform.setMatrix(after.multiply(screen.inverse()).multiply(change).multiply(screen).multiply(after.inverse()));
    };
    state.update();
  }
}
Test file
ts
import { applySvgTransforms } from './apply-svg-transforms';
import { svgFlipTo } from './svg-flip-to';
import { svgPlaceOn } from './svg-place-on';
import { svgRotateBy } from './svg-rotate-by';
import { svgRotateTo } from './svg-rotate-to';
import type { SvgTransformOrder } from './svg-transform-order';
import { svgTranslateBy } from './svg-translate-by';
import { describeOnScreen, renderSvg } from './testing';

const SCENE = `
  <g transform="translate(60 60) rotate(30)"><rect id="symbol" transform="scale(-1 1)" width="20" height="10" /></g>
  <g transform="scale(2)"><circle id="target" cx="100" cy="100" r="5" /></g>
`;

describe(applySvgTransforms, () => {
  it('applies each order on what the previous ones give', () => {
    const byId = renderSvg(SCENE);
    applySvgTransforms(byId('symbol'), [
      svgFlipTo(false),
      svgRotateTo(0),
      svgPlaceOn(byId('target')),
      svgTranslateBy(0, -10),
    ]);
    expect(describeOnScreen(byId('symbol'))).toMatchObject({
      x: 190,
      y: 185,
      width: 20,
      height: 10,
      rotation: 0,
      isFlipped: false,
    });
  });

  it('keeps the transforms already in the list', () => {
    const byId = renderSvg(SCENE);
    applySvgTransforms(byId('symbol'), [svgRotateTo(0)]);
    expect(byId('symbol').transform.baseVal.numberOfItems).toBe(2);
  });

  it('updates only the changed order', () => {
    const byId = renderSvg(SCENE);
    const rotation = svgRotateBy(0);
    applySvgTransforms(byId('symbol'), [svgFlipTo(false), svgRotateTo(0), svgPlaceOn(byId('target')), rotation]);
    rotation.set(90);
    expect(describeOnScreen(byId('symbol'))).toMatchObject({ x: 195, y: 190, width: 10, height: 20, rotation: 90 });
  });

  it('updates an order followed by others, in the axes the element shows', () => {
    const byId = renderSvg('<rect id="train" x="100" y="100" width="10" height="10" />');
    const position = svgTranslateBy(0, 0);
    applySvgTransforms(byId('train'), [position, svgRotateBy(90)]);
    position.set(10, 0);
    expect(describeOnScreen(byId('train'))).toMatchObject({ x: 100, y: 110, rotation: 90 });
  });

  it('keeps the values of an order changed before it is applied', () => {
    const byId = renderSvg(SCENE);
    const rotation = svgRotateTo(0);
    rotation.set(90);
    applySvgTransforms(byId('symbol'), [rotation]);
    expect(describeOnScreen(byId('symbol'))).toMatchObject({ rotation: 90 });
  });

  it('throws a TypeError for an object that is not an order', () => {
    const byId = renderSvg(SCENE);
    const fake: SvgTransformOrder<[]> = { set: (): void => undefined };
    expect(() => {
      applySvgTransforms(byId('symbol'), [fake]);
    }).toThrow(TypeError);
  });

  it('throws a TypeError for an element outside an svg', () => {
    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    expect(() => {
      applySvgTransforms(rect, [svgTranslateBy(1, 1)]);
    }).toThrow(TypeError);
  });
});

Keywords: apply, svg, transforms, svg transforms, function

Generated from the JSDoc of the sources.