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>
https://github.com/subarroca/ng2-masked-stack
npm i ng2-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;
}
});
}
}
https://github.com/subarroca/ng2-kw-gauge
npm i ng2-kw-gauge
Duotone image

based on: http://codepen.io/jmperez/pen/LGqaxQ


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;
}
}
https://subarroca.github.io/ng2-kw-duotone-image
npm i ng2-kw-duotone-image
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
MDN: SVG
Reference
SVG path syntax
Reference
Greensock
Animation
SMIL alternatives
Codrops
Codepen
Inspiration
Moltes gràcies

@subarroca

Angular Beers
Angular2 ♥ SVG ¦ angularbeers
By Salvador Subarroca
Angular2 ♥ SVG ¦ angularbeers
- 2,303