packages/lit-dev-content/site/tutorials/content/svg-templates/02.md
This section of the tutorial covers the basics of composition in SVG including how to:
<defs><defs> with <use><use><g> (group)In pattern making, a motif
is an arrangement of two or more elements. This demo's motif will be a
series of text rotated around a point. The <defs> and <use>
elements will compose a motif from a single <text> element.
The <defs> element contains svg elements without rendering them. The
example below defines a <text> element inside a <defs> element.
Elements in <defs> can be referenced by an id.
const helloDefs = svg`
<defs>
<text id="chars">Hello defs!</text>
</defs>
`;
To reuse the <text> element previously defined in <defs>, set the
href property on a <use> element to #chars like the example below.
const helloDefs = svg`
<defs>
<text id="chars">Hello defs!</text>
</defs>
<use href="#chars"></use>
`;
A key feature that makes the combination of <defs> and <use> so
powerful is the fact that attributes and properties applied to <use>
do not affect the referenced element in <defs>. This allows
developers to compound properties like transform.
In the example below, the <text> element in <defs> has no
rotation. However, we can apply rotation to <use> without
affecting the layout of the <text> element in <defs>.
const helloDefs = svg`
<defs>
<text id="chars">Hello defs!</text>
</defs>
<use
href="#chars"
transform="rotate(180, 0,0)">
</use>
`;
Lastly, <g> is a special element that applies its properties to all
child elements. This is helpful when we need to distribute a property
to a number of elements.
In the example below, the <g> element will apply its transform
to every child element. In this case, the <use> element will both
rotate and translate.
const helloGroups = svg`
<defs>
<text id="chars">Hello defs!</text>
</defs>
<g transform="translate(50, 50)">
<use
href="#chars"
transform="rotate(${currRotation}, 0,0)">
</use>
</g>
`;
Add two new reactive properties to repeat-pattern: num-prints
and rotation-offset.
The num-prints attribute will affect how many prints are in a
motif. The rotation-offset attribute will provide an
initial rotation to the motif.
{% switchable-sample %}
export class RepeatPattern extends LitElement {
@property({type: Number, attribute: "num-prints"}) numPrints = 7;
@property({
type: Number,
attribute: "rotation-offset",
}) rotationOffset = 0;
...
}
export class RepeatPattern extends LitElement {
static properties = {
chars: {type: String},
numPrints: {type: Number, attribute: 'num-prints'},
rotationOffset: {
type: Number,
attribute: 'rotation-offset',
},
};
constructor() {
super();
this.chars = 'lit';
this.numPrints = 7;
this.rotationOffset = 0;
}
...
}
{% endswitchable-sample %}
Provide an id to the text element in createElement.
{% switchable-sample %}
const createElement = (
chars: string,
): SVGTemplateResult => svg`
<text
id="chars"
dominant-basline="hanging"
font-family="monospace"
font-size="24px">
${chars}
</text>`;
const createElement = (chars) => svg`
<text
id="chars"
dominant-basline="hanging"
font-family="monospace"
font-size="24px">
${chars}
</text>`;
{% endswitchable-sample %}
Call createElement inside <defs> in render.
render() {
return html`
<svg>
<defs>
${createElement(this.chars)}
</defs>
...
</svg>
`;
}
Create a function called createMotif that generates a series of rotated texts.
createMotif could take two arguments, numPrints which is the number of times
the element will be printed, and an optional offset property which is the initial
rotation offset.
createMotif should calculate the rotation given the numPrints to print, and
apply the rotation to a <use> element that references #chars.
{% switchable-sample %}
const createMotif = (
numPrints: number,
offset: number = 0
): SVGTemplateResult => {
const rotation = 360 / numPrints;
const prints = [];
let currRotation = offset;
for (let index = 0; index < numPrints; index++) {
currRotation += rotation;
prints.push(svg`
<use
href="#chars"
transform="rotate(${currRotation}, 0, 0)">
</use>
`);
}
};
const createMotif = (numPrints, offset = 0) => {
const rotation = 360 / numPrints;
const prints = [];
let currRotation = offset;
for (let index = 0; index < numPrints; index++) {
currRotation += rotation;
prints.push(svg`
<use
href="#chars"
transform="rotate(${currRotation}, 0, 0)">
</use>
`);
}
};
{% endswitchable-sample %}
Wrap the prints list in a <g> element. Move the group down
and to the right 50px to keep the entire motif in frame by setting
the transform property on <g> to translate(50, 50). This
applies the transform in <g> to all prints. Return this
group.
{% switchable-sample %}
const createMotif = (numPrints: number, offset: number = 0): SVGTemplateResult => {
...
return svg`<g transform="translate(50, 50)">${prints}</g>`;
}
const createMotif = (numPrints, offset = 0) => {
...
return svg`<g transform="translate(50, 50)">${prints}</g>`;
}
{% endswitchable-sample %}
Finally, call createMotif in the render function in repeat-pattern.
{% switchable-sample %}
@customElement('repeat-pattern')
export class RepeatPattern extends LitElement {
...
render() {
return html`
<svg height="100%" width="100%">
<defs>
${createElement(this.chars)}
</defs>
${createMotif(
this.numPrints,
this.rotationOffset,
)}
</svg>
`;
}
}
export class RepeatPattern extends LitElement {
...
render() {
return html`
<svg height="100%" width="100%">
<defs>
${createElement(this.chars)}
</defs>
${createMotif(
this.numPrints,
this.rotationOffset,
)}
</svg>
`;
}
}
{% endswitchable-sample %}
After completing this section, you'll be ready to learn how to use clip paths and compose graphics with groups.
Lit's reactive properties can also be set via attributes. Try changing the
chars, rotation-offset, and num-prints attributes on the
<repeat-pattern> HTML element in index.html!