angular 2.2.0
Scalable Vector Graphic
XML Markup
CSS styling
<circle />
<rect/>
<polygon/>
<path/>
<image/>
<text/>
SMIL animations (getting deprecated)
CSS animations
<clipPath />
<filter/>
colorMatrix
blur
blending
shadow
lighting
...
160px (or em, ...)
200px (or em, ...)
392
314
xOrigin yOrigin width height
viewBox = "0 0 392 314"
SVG components = HTML components
editable attrs → attr.x
CSS per component
subcomponents are trickier and break linter
<g subComponent></g>
data-binding
events
elements → svg:circle
Web animation → angular animation
bind events on elements
-
node per element → worse performance
one element →better performance
redraw on demand →better performance
redraw each frame → worse performance
Bad for intense refreshing
Bad for interaction
Good for interactions and few nodes (regular graphics nowadays)
Good for animated textures
rect
CSS
mask
unique mask id
<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
circle
CSS
stroke-dasharray
calculating the length of stroke
10
dash: 5
gap: 5
strokeDasharray: 5 5
10
dash: 15
gap: 40
strokeDasharray: 15 40
10
dash: 5
gap: 40
strokeDasharray: 5 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
based on: http://codepen.io/jmperez/pen/LGqaxQ
image
filter
color calculations
R 0 0 0 0 0 G 0 0 0 0 0 B 0 0 0 0 0 A 0
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
path
CSS animations
subcomponents
learning to build paths
dealing with linter
The selector of the component "FunnelSegmentComponent" should have prefix "ng2-kw" (https://goo.gl/cix8BY)
<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}`;
}
CSS animations
Morph through script
SMIL getting deprecated
@Component({
selector: 'app-logo',
templateUrl: './app-logo.component.html',
styleUrls: ['./app-logo.component.scss'],
animations: [
trigger('aState', [
state('small', style({transform: 'scale(1)'})),
state('large', style({transform: 'scale(4.2)'})),
transition('small => large', animate('1s ease', keyframes([
style({transform: 'scale(1)', offset: 0}),
style({transform: 'scale(0.7) rotate(15deg)', offset: 0.15}),
style({transform: 'scale(1)', offset: 0.3}),
style({transform: 'scale(4.2)', offset: 1})
]))),
transition('large => small', animate('1s ease', keyframes([
style({transform: 'scale(4.2)', offset: 0}),
style({transform: 'scale(5) rotate(-15deg)', offset: 0.15}),
style({transform: 'scale(4.2)', offset: 0.3}),
style({transform: 'scale(1)', offset: 1})
])))
])
]
})