Back to Lit

02

packages/lit-dev-content/site/tutorials/content/svg-templates/02.md

latest6.7 KB
Original Source

This section of the tutorial covers the basics of composition in SVG including how to:

  • Define reuseable structures with <defs>
  • Clone structures in <defs> with <use>
  • Modify cloned structures through attributes on <use>
  • Apply properties across multiple structures with <g> (group)

Learn

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.

ts
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.

ts
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>.

ts
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.

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

Apply

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

ts
export class RepeatPattern extends LitElement {
  @property({type: Number, attribute: "num-prints"}) numPrints = 7;
  @property({
    type: Number,
    attribute: "rotation-offset",
  }) rotationOffset = 0;
  ...
}
js
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 %}

ts
const createElement = (
  chars: string,
): SVGTemplateResult => svg`
  <text
    id="chars"
    dominant-basline="hanging"
    font-family="monospace"
    font-size="24px">
    ${chars}
  </text>`;
js
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.

ts
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 %}

ts
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>
    `);
  }
};
js
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 %}

ts
const createMotif = (numPrints: number, offset: number = 0): SVGTemplateResult => {
  ...
  return svg`<g transform="translate(50, 50)">${prints}</g>`;
}
js
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 %}

ts
@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>
    `;
  }
}
js
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.

Extra Credit

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!