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>[]): voidParameters
| Name | Description |
|---|---|
element | A rendered SVG element. |
orders | The 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 positionTests
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 ofapplySvgTransforms, created bysvgRotateBy,svgRotateTo,svgFlipTo,svgScaleBy,svgTranslateByorsvgPlaceOn: keep it to change its values later withset.
Files it needs
src/svg-transform/apply-svg-transforms.ts imports, directly or not:
src/svg-transform/internal/create-matrix.tssrc/svg-transform/internal/get-order-state.tssrc/svg-transform/internal/get-owner-svg.tssrc/svg-transform/internal/get-screen-matrix.tssrc/svg-transform/internal/order-states.tssrc/svg-transform/svg-transform-order.ts
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