Angular2 ♥ SVG 

@subarroca

Angular Beers

What's SVG
Angular2 meets SVG
Four Examples:

  • Masked Stack

  • Gauge

  • Duotone image

  • Funnel

Animation

Resources

What's SVG

Scalable Vector Graphic

XML Markup

CSS styling

circle, rect, polygon, path

image, text, filter, mask

SMIL animations (getting deprecated)

CSS animations

Different positioning (viewbox)

Viewbox

160px (or em, ...)

200px (or em, ...)

392

314

xOrigin yOrigin width height

viewBox = "0 0 392 314"

BEWARE OF BROWSERS!!!

Angular 2 meets SVG

SVG components = HTML components

editable attrs must be used as attr.x

CSS per component

subcomponents are trickier and break linter

<g subComponent></g>

data-binding

events

angular2 team working on it

Masked stack

Using:

rect

CSS

mask

Tricky parts:

unique mask id

z-index

<rect>

<circle>

<polygon>

no z-index!!!

<svg
  attr.viewBox = "0 0 {{maskWidth}} {{maskHeight}}">
  <g
    #holder
    attr.clip-path = "url(#{{uniqueId}}-mask)">
    <rect
      x = "0"
      y = "0"
      [attr.width] = "maskWidth"
      [attr.height] = "maskHeight"
      [style.fill] = "bgColor"/>

    <rect
      x = "0"
      y = "0"
      [attr.width] = "maskWidth"
      [attr.height] = "maskHeight"
      [style.transform] = "position"
      [style.transition] = "'transform ' + animationSecs + 's'"
      [style.fill] = "frontColor"/>
  </g>


  <clipPath
    #mask
    id = "{{uniqueId}}-mask">
    <ng-content></ng-content>
  </clipPath>
</svg>
@Component({
  selector: 'ng2-kw-masked-stack',
  templateUrl: './masked-stack.component.html',
  styleUrls: ['./masked-stack.component.scss']
})
export class MaskedStackComponent implements OnInit {
  @Input() maskWidth: number = 100;
  @Input() maskHeight: number = 100;

  @Input() bgColor: string = 'transparent';
  @Input() frontColor: string = 'transparent';

  @Input() from: string = 'bottom'; // top, bottom, left, right
  @Input() goal: number = 100;

  @Input()
  set value(value: number) {
    this._value = value;

    Observable.timer(0)
      .first()
      .subscribe(() => this.valueLoaded = true);
  }

  _value: number;

  @Input() animationSecs: number = 0.5;


  // TODO: this sucks but I still haven't found a way to get component unique Id
  @Input() uniqueId: string;

  valueLoaded: boolean = false;



  constructor(
    private sanitizer: DomSanitizer
  ) { }

  ngOnInit() {
  }


  get position() {
    let str: string;

    switch (this.from) {
      case 'top':
        str = `translateY(${
          this.valueLoaded
            ? (-this.maskHeight * (1 - this._value / this.goal))
            : -this.maskHeight
          }px)`;
        break;

      case 'bottom':
        str = `translateY(${
          this.valueLoaded
            ? this.maskHeight * (1 - this._value / this.goal)
            : this.maskHeight
          }px)`;
        break;

      case 'left':
        str = `translateX(${
          this.valueLoaded
            ? -this.maskWidth * (1 - this._value / this.goal)
            : -this.maskWidth
          }px)`;
        break;

      case 'right':
        str = `translateX(${
          this.valueLoaded
            ? this.maskWidth * (1 - this._value / this.goal)
            : this.maskWidth
          }px)`;
        break;
    }
    return this.sanitizer.bypassSecurityTrustStyle(str);
  }

}
<ng2-kw-masked-stack
    bgColor = "white"
    frontColor = "black"
    
    maskWidth = "100"
    maskHeight = "100"
  
    [value] = "value"
    
    uniqueId = "orb">
  
    <svg:circle
      cx = "50"
      cy = "50"
      r = "50"/>
  </ng2-kw-masked-stack>

Gauge

Using:

circle

CSS

stroke-dasharray

Tricky parts:

calculating the length of stroke

10

dash: 5

gap: 25

strokeDasharray: 5 25

dashArray

10

dash: 5

gap: 5

strokeDasharray: 5 5

10

dash: 10

gap: 40

strokeDasharray: 10 40

<svg viewBox = "0 0 200 200">  
  <g>
    <circle
      [attr.r] = "bgRadius"
      [style.fill] = bgColor/>

    <g
      *ngFor = "let segment of sortedSegments">
      <circle
        [style.stroke] = segment.bgColor
        [style.strokeWidth] = segment.borderWidth
        [attr.r] = segment.computedRadius/>

      <circle
        [style.transition] = "'stroke-dasharray ' + animationSecs + 's'"
        [style.stroke] = segment.color
        [style.strokeWidth] = segment.borderWidth
        [style.strokeDasharray] = "segmentsLoaded ? segment.strokeProgress : segment.strokeEmptyProgress"
        [style.strokeLinecap] = "rounded ? 'round' : ''"
        [attr.r] = segment.computedRadius/>
    </g>
  </g>



  <g
    transform = "translate(100, 100)">
    <text
      *ngFor = "let label of labels"
      [attr.x] = label.x
      [attr.y] = label.y
      [style.fill] = label.color
      [style.fontSize] = label.fontSize
      text-anchor = middle>
      {{label.text}}
    </text>
  </g>
</svg>
@Component({
  selector: 'ng2-kw-gauge',
  templateUrl: './gauge.component.html',
  styleUrls: ['./gauge.component.scss']
})
export class GaugeComponent implements OnInit {
  @Input() bgRadius: number = 100;
  @Input() bgColor: string;
  @Input() rounded: boolean = true;
  @Input() reverse: boolean = false;
  @Input() animationSecs: number = 0.5;

  @Input() labels: GaugeLabel[];

  @Input()
  set segments(segments: GaugeSegment[]) {
    this.segmentsLoaded = false;
    this.sortedSegments = this.sortSegments(segments);

    Observable.timer(0)
      .first()
      .subscribe(() => this.segmentsLoaded = true);
  }
  sortedSegments: GaugeSegment[];
  segmentsLoaded: boolean = false;


  constructor() { }

  ngOnInit() {
  }

  sortSegments(segments: GaugeSegment[]) {
    return segments.sort((a: GaugeSegment, b: GaugeSegment) => {
      if (this.reverse) {
        return (a.value / a.goal > b.value / b.goal) ? 1 : -1;
      } else {
        return (a.value / a.goal > b.value / b.goal) ? -1 : 1;
      }
    });
  }


}

Duotone image

Using:

image

filter

Tricky parts:

color calculations

Filters

colorMatrix

blur

blending

shadow

lighting

...

R 0 0 0 0

0 G 0 0 0

0 0 B 0 0

0 0 0 A 0

color matrix

input
R G B A M
R

G

B

A
output
<svg
  attr.viewBox = "0 0 {{width}} {{height}}"
  [attr.width] = "width"
  [attr.height] = "height"
  preserveAspectRatio="xMidYMid slice">
  <defs>
    <filter id="duotone-filter">
      <feColorMatrix
        type="matrix"
        [attr.values]= "duotoneMatrix"/>
    </filter>    
  </defs>

  <image
    [attr.width] = "width"
    [attr.height] = "height"
    filter="url(#duotone-filter)"
    [attr.xlink:href] = "src"/>
</svg>
@Component({
  selector: 'ng2-kw-duotone-image',
  templateUrl: './duotone-image.component.html',
  styleUrls: ['./duotone-image.component.scss']
})
export class DuotoneImageComponent implements OnInit {
  @Input() src: string;
  @Input() width: number;
  @Input() height: number;

  @Input()
  set darkColor(color: string) {
    this._darkColor = this.hexToRgb(color);
  }

  @Input()
  set lightColor(color: string) {
    this._lightColor = this.hexToRgb(color);
  }


  _darkColor: { r: number, g: number, b: number };
  _lightColor: { r: number, g: number, b: number };



  constructor() { }

  ngOnInit() {
  }


  hexToRgb(hex) {
    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
    var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
    hex = hex.replace(shorthandRegex, function(m, r, g, b) {
      return r + r + g + g + b + b;
    });

    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
      r: parseInt(result[1], 16),
      g: parseInt(result[2], 16),
      b: parseInt(result[3], 16)
    } : null;
  }

  get duotoneMatrix() {
    let value = [];
    value = value.concat(
      [this._lightColor.r / 256 - this._darkColor.r / 256, 0, 0, 0, this._darkColor.r / 256]);
    value = value.concat(
      [this._lightColor.g / 256 - this._darkColor.g / 256, 0, 0, 0, this._darkColor.g / 256]);
    value = value.concat(
      [this._lightColor.b / 256 - this._darkColor.b / 256, 0, 0, 0, this._darkColor.b / 256]);
    value = value.concat([0, 0, 0, 1, 0]);

    return value;
  }
}

Funnel Graph

Using:

path

CSS animations

subcomponents

Tricky parts:

learning to build paths

dealing with linter

PATH

The selector of the component "FunnelSegmentComponent" should have prefix "ng2-kw" (https://goo.gl/cix8BY)              

Linter

<svg [attr.viewBox] = "viewBox">
  <g ng2-kw-funnel-segment
    *ngFor = "let segment of _segments; let i = index"
    [from] = "i ? _segments[i-1].value : undefined"
    [to] = "segment.value"
    [color] = "segment.color"
    [labelColor] = "segment.labelColor"
    [max] = "_segments[0].value"
    [max] = "_segments[_segments.length-1].value"
    [width] = "segmentWidth"
    [height] = "funnelHeight"
    [graphMode] = "graphMode"
    [attr.transform] = "getSegmentOffset(i)">
  </g>
</svg>
protected drawSegment(segment: Segment): string {
    // from axe right
    // to previous right
    // to end this.slope
    // to current left

    // to -current left
    // to -end this.slope
    // to -previous right

    return `
      M0, ${this.height / 2}
      ${Point.getRoundedCorner(
        {
          x: 0,
          y: this.height / 2 - segment.start.y / 2
        }, {
          x: this.width * this.slope,
          y: this.height / 2 - segment.end.y / 2
        }, {
          x: this.width,
          y: this.height / 2 - segment.end.y / 2
        },
        this.width * this.RADIUS)}
      

      ${Point.getRoundedCorner(
        {
          x: this.width,
          y: this.height / 2 + segment.end.y / 2
        }, {
          x: this.width * this.slope,
          y: this.height / 2 + segment.end.y / 2
        }, {
          x: 0,
          y: this.height / 2 + segment.start.y / 2
        },
        this.width * this.RADIUS)}
      Z`;
  }


static getRoundedCorner(start: Point, mid: Point, end: Point, radius: number) {
    // y = mx + b
    let m1 = (start.y - mid.y) / (start.x - mid.x);
    let m2 = (mid.y - end.y) / (mid.x - end.x);
    let b1 = mid.y - m1 * mid.x;
    let b2 = mid.y - m2 * mid.x;

    let direction = start.x < end.x ? 1 : -1;
    let startmid: Point = new Point({
      x: mid.x - radius * direction,
      y: m1 * (mid.x - radius * direction) + b1
    });

    let midend: Point = new Point({
      x: mid.x + radius * direction,
      y: m2 * (mid.x + radius * direction) + b2
    });


    return `L${start.x},${start.y}
      L${startmid.x},${startmid.y}
      Q${mid.x},${mid.y},
       ${midend.x},${midend.y}
      L${end.x},${end.y}`;
  }

Animation

CSS animations

Morph through script

SMIL gettingh deprecated

Resources

Sara Soueidan

She's just... great

 

https://sarasoueidan.com/tags/svg

MDN: SVG

Mozilla SVG reference with examples

 

https://developer.mozilla.org/en-US/docs/Web/SVG

Reference

SVG path syntax

Reference

Greensock

Animation library with a huge background expertise

 

http://greensock.com

Animation

SMIL alternatives

Codrops

Curated examples pushing imagination forward

 

http://tympanus.net/codrops/?s=svg&search-type=posts​

Codepen

See what others are doing

 

http://codepen.io/search/pens?q=svg

Inspiration

Moltes gràcies

@subarroca

Angular Beers

Angular2 ♥ SVG ¦ angularbeers

By Salvador Subarroca

Angular2 ♥ SVG ¦ angularbeers

  • 2,256