2017-06-22 12:07:42 +03:00
|
|
|
import {
|
2017-07-06 22:33:06 +03:00
|
|
|
Component, HostListener, ViewChild, ElementRef, Input, Output, EventEmitter, AfterViewInit, OnChanges,
|
2017-07-13 12:04:21 +03:00
|
|
|
ChangeDetectorRef,
|
2017-06-22 12:07:42 +03:00
|
|
|
} from '@angular/core';
|
|
|
|
|
|
|
|
const VIEW_BOX_SIZE = 300;
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'ngx-temperature-dragger',
|
|
|
|
templateUrl: './temperature-dragger.component.html',
|
|
|
|
styleUrls: ['./temperature-dragger.component.scss'],
|
|
|
|
})
|
2017-07-06 22:33:06 +03:00
|
|
|
export class TemperatureDraggerComponent implements AfterViewInit, OnChanges {
|
2017-06-22 12:07:42 +03:00
|
|
|
|
|
|
|
@ViewChild('svgRoot') svgRoot: ElementRef;
|
|
|
|
|
2017-07-06 22:33:06 +03:00
|
|
|
@Input() fillColors: string|string[] = '#2ec6ff';
|
|
|
|
@Input() disableArcColor: string = '#999999';
|
2017-06-22 12:07:42 +03:00
|
|
|
@Input() bottomAngle: number = 90;
|
2017-07-07 19:54:49 +03:00
|
|
|
@Input() arcThickness: number = 20; // CSS pixels
|
2017-07-13 11:19:57 +03:00
|
|
|
@Input() thumbRadius: number = 15; // CSS pixels
|
|
|
|
@Input() thumbDashRadius: number = null;
|
|
|
|
@Input() maxLeap: number = 0.4;
|
2017-06-22 12:07:42 +03:00
|
|
|
|
2017-07-07 19:54:49 +03:00
|
|
|
value: number = 50;
|
2017-06-22 12:07:42 +03:00
|
|
|
@Output('valueChange') valueChange = new EventEmitter<Number>();
|
|
|
|
@Input('value') set setValue(value) {
|
|
|
|
this.value = value;
|
|
|
|
}
|
|
|
|
|
2017-07-07 19:54:49 +03:00
|
|
|
@Input() min: number = 0; // min output value
|
|
|
|
@Input() max: number = 100; // max output value
|
|
|
|
|
|
|
|
@Output() power = new EventEmitter<boolean>();
|
|
|
|
|
2017-07-13 11:19:57 +03:00
|
|
|
@HostListener('window:mouseup', ['$event'])
|
2017-06-22 12:07:42 +03:00
|
|
|
onMouseUp(event) {
|
|
|
|
this.recalculateValue(event);
|
|
|
|
this.isMouseDown = false;
|
|
|
|
}
|
|
|
|
|
2017-07-13 11:19:57 +03:00
|
|
|
@HostListener('window:mousemove', ['$event'])
|
2017-06-22 12:07:42 +03:00
|
|
|
onMouseMove(event: MouseEvent) {
|
|
|
|
this.recalculateValue(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
@HostListener('window:resize', ['$event'])
|
|
|
|
onResize(event) {
|
|
|
|
this.invalidate();
|
|
|
|
}
|
|
|
|
|
2017-07-07 19:54:49 +03:00
|
|
|
off: boolean = false;
|
|
|
|
oldValue: number;
|
|
|
|
|
2017-06-22 12:07:42 +03:00
|
|
|
scaleFactor: number = 1;
|
|
|
|
bottomAngleRad = 0;
|
|
|
|
radius: number = 100;
|
|
|
|
translateXValue = 0;
|
|
|
|
translateYValue = 0;
|
|
|
|
thickness = 6;
|
|
|
|
pinRadius = 10;
|
2017-07-13 11:19:57 +03:00
|
|
|
pinDashRadius = null;
|
2017-06-22 12:07:42 +03:00
|
|
|
colors: any = [];
|
|
|
|
|
|
|
|
styles = {
|
|
|
|
viewBox: '0 0 300 300',
|
|
|
|
arcTranslateStr: 'translate(0, 0)',
|
|
|
|
clipPathStr: '',
|
|
|
|
gradArcs: [],
|
|
|
|
nonSelectedArc: {},
|
2017-07-13 11:19:57 +03:00
|
|
|
thumbPosition: { x: 0, y: 0 },
|
2017-06-22 12:07:42 +03:00
|
|
|
blurRadius: 15,
|
|
|
|
};
|
|
|
|
|
|
|
|
private isMouseDown = false;
|
2017-07-06 22:33:06 +03:00
|
|
|
private init = false;
|
2017-06-22 12:07:42 +03:00
|
|
|
|
2017-07-13 12:04:21 +03:00
|
|
|
constructor(private changeDetectorRef: ChangeDetectorRef) {
|
2017-07-07 19:54:49 +03:00
|
|
|
this.oldValue = this.value;
|
2017-06-22 12:07:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
ngAfterViewInit(): void {
|
|
|
|
this.invalidate();
|
2017-07-06 22:33:06 +03:00
|
|
|
this.init = true;
|
2017-07-13 12:04:21 +03:00
|
|
|
this.changeDetectorRef.detectChanges();
|
2017-07-06 22:33:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
ngOnChanges(): void {
|
|
|
|
if (this.init) {
|
|
|
|
this.invalidate();
|
|
|
|
}
|
2017-06-22 12:07:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
mouseDown(event) {
|
|
|
|
this.isMouseDown = true;
|
2017-07-07 19:54:49 +03:00
|
|
|
if (!this.off) {
|
2017-07-13 11:19:57 +03:00
|
|
|
this.recalculateValue(event, true);
|
2017-07-07 19:54:49 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switchPower() {
|
|
|
|
this.off = !this.off;
|
|
|
|
this.power.emit(!this.off);
|
|
|
|
|
|
|
|
if (this.off) {
|
|
|
|
this.oldValue = this.value;
|
|
|
|
this.value = this.min;
|
|
|
|
} else {
|
|
|
|
this.value = this.oldValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.invalidatePinPosition();
|
2017-06-22 12:07:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
private invalidate(): void {
|
|
|
|
this.bottomAngleRad = TemperatureDraggerComponent.toRad(this.bottomAngle);
|
|
|
|
this.calculateVars();
|
|
|
|
|
|
|
|
this.invalidateClipPathStr();
|
|
|
|
this.invalidateGradientArcs();
|
|
|
|
this.invalidatePinPosition();
|
|
|
|
}
|
|
|
|
|
|
|
|
private calculateVars() {
|
|
|
|
this.bottomAngleRad = TemperatureDraggerComponent.toRad(this.bottomAngle);
|
|
|
|
this.colors = (typeof this.fillColors === 'string') ? [this.fillColors] : this.fillColors;
|
|
|
|
|
|
|
|
const baseRadius: number = VIEW_BOX_SIZE / 2;
|
|
|
|
const halfAngle = this.bottomAngleRad / 2;
|
|
|
|
|
|
|
|
const svgBoundingRect = this.svgRoot.nativeElement.getBoundingClientRect();
|
|
|
|
const svgAreaFactor = svgBoundingRect.width / svgBoundingRect.height;
|
|
|
|
const svgHeight = VIEW_BOX_SIZE / svgAreaFactor;
|
2017-07-13 11:19:57 +03:00
|
|
|
const thumbMaxRadius = Math.max(this.thumbRadius, this.thumbDashRadius);
|
|
|
|
const thumbMargin = 2 * thumbMaxRadius > this.arcThickness
|
|
|
|
? (thumbMaxRadius - this.arcThickness / 2) / this.scaleFactor
|
2017-06-22 12:07:42 +03:00
|
|
|
: 0;
|
|
|
|
|
|
|
|
this.scaleFactor = svgBoundingRect.width / VIEW_BOX_SIZE;
|
|
|
|
this.styles.viewBox = `0 0 ${VIEW_BOX_SIZE} ${svgHeight}`;
|
|
|
|
|
|
|
|
|
|
|
|
const circleFactor = this.bottomAngleRad <= Math.PI
|
|
|
|
? ( 2 / (1 + Math.cos(halfAngle)) )
|
|
|
|
: ( 2 * Math.sin(halfAngle) / (1 + Math.cos(halfAngle)) );
|
|
|
|
if (circleFactor > svgAreaFactor) {
|
|
|
|
if (this.bottomAngleRad > Math.PI) {
|
2017-07-13 11:19:57 +03:00
|
|
|
this.radius = (VIEW_BOX_SIZE - 2 * thumbMargin) / (2 * Math.sin(halfAngle));
|
2017-06-22 12:07:42 +03:00
|
|
|
} else {
|
2017-07-13 11:19:57 +03:00
|
|
|
this.radius = VIEW_BOX_SIZE / 2 - thumbMargin;
|
2017-06-22 12:07:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
2017-07-13 11:19:57 +03:00
|
|
|
this.radius = (svgHeight - 2 * thumbMargin) / (1 + Math.cos(halfAngle));
|
2017-06-22 12:07:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
this.translateXValue = VIEW_BOX_SIZE / 2 - this.radius;
|
|
|
|
this.translateYValue = (svgHeight) / 2 - this.radius * (1 + Math.cos(halfAngle)) / 2;
|
|
|
|
|
|
|
|
this.styles.arcTranslateStr = `translate(${this.translateXValue}, ${this.translateYValue})`;
|
|
|
|
|
|
|
|
this.thickness = this.arcThickness / this.scaleFactor;
|
2017-07-13 11:19:57 +03:00
|
|
|
this.pinRadius = this.thumbRadius / this.scaleFactor;
|
|
|
|
this.pinDashRadius = this.thumbDashRadius && this.thumbDashRadius / this.scaleFactor;
|
2017-06-22 12:07:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
private calculateClipPathSettings() {
|
|
|
|
const halfAngle = this.bottomAngleRad / 2;
|
|
|
|
const innerRadius = this.radius - this.thickness;
|
|
|
|
|
|
|
|
const xStartMultiplier = 1 - Math.sin(halfAngle);
|
|
|
|
const yMultiplier = 1 + Math.cos(halfAngle);
|
|
|
|
const xEndMultiplier = 1 + Math.sin(halfAngle);
|
|
|
|
|
|
|
|
return {
|
|
|
|
outer: {
|
|
|
|
start: {
|
|
|
|
x: xStartMultiplier * this.radius,
|
|
|
|
y: yMultiplier * this.radius,
|
|
|
|
},
|
|
|
|
end: {
|
|
|
|
x: xEndMultiplier * this.radius,
|
|
|
|
y: yMultiplier * this.radius,
|
|
|
|
},
|
|
|
|
radius: this.radius,
|
|
|
|
},
|
|
|
|
inner: {
|
|
|
|
start: {
|
|
|
|
x: xStartMultiplier * innerRadius + this.thickness,
|
|
|
|
y: yMultiplier * innerRadius + this.thickness,
|
|
|
|
},
|
|
|
|
end: {
|
|
|
|
x: xEndMultiplier * innerRadius + this.thickness,
|
|
|
|
y: yMultiplier * innerRadius + this.thickness,
|
|
|
|
},
|
|
|
|
radius: innerRadius,
|
|
|
|
},
|
|
|
|
thickness: this.thickness,
|
|
|
|
big: this.bottomAngleRad < Math.PI ? '1' : '0',
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private invalidateClipPathStr() {
|
|
|
|
const s = this.calculateClipPathSettings();
|
|
|
|
|
|
|
|
let path = `M ${s.outer.start.x},${s.outer.start.y}`; // Start at startangle top
|
|
|
|
|
|
|
|
// Outer arc
|
2017-07-06 22:33:06 +03:00
|
|
|
// Draw an arc of radius 'radius'
|
|
|
|
// Arc details...
|
|
|
|
path += ` A ${s.outer.radius},${s.outer.radius}
|
|
|
|
0 ${s.big} 1
|
|
|
|
${s.outer.end.x},${s.outer.end.y}`; // Arc goes to top end angle coordinate
|
2017-06-22 12:07:42 +03:00
|
|
|
|
|
|
|
// Outer to inner connector
|
2017-07-06 22:33:06 +03:00
|
|
|
path += ` A ${s.thickness / 2},${s.thickness / 2}
|
|
|
|
0 1 1
|
|
|
|
${s.inner.end.x},${s.inner.end.y}`;
|
2017-06-22 12:07:42 +03:00
|
|
|
|
|
|
|
// Inner arc
|
2017-07-06 22:33:06 +03:00
|
|
|
path += ` A ${s.inner.radius},${s.inner.radius}
|
|
|
|
1 ${s.big} 0
|
|
|
|
${s.inner.start.x},${s.inner.start.y}`;
|
2017-06-22 12:07:42 +03:00
|
|
|
|
|
|
|
// Outer to inner connector
|
2017-07-06 22:33:06 +03:00
|
|
|
path += ` A ${s.thickness / 2},${s.thickness / 2}
|
|
|
|
0 1 1
|
|
|
|
${s.outer.start.x},${s.outer.start.y}`;
|
2017-06-22 12:07:42 +03:00
|
|
|
|
|
|
|
// Close path
|
|
|
|
path += ' Z';
|
|
|
|
this.styles.clipPathStr = path;
|
|
|
|
}
|
|
|
|
|
|
|
|
private calculateGradientConePaths(angleStep) {
|
|
|
|
const radius = this.radius;
|
|
|
|
|
|
|
|
function calcX(angle) {
|
|
|
|
return radius * (1 - 2 * Math.sin(angle));
|
|
|
|
}
|
|
|
|
|
|
|
|
function calcY(angle) {
|
|
|
|
return radius * (1 + 2 * Math.cos(angle));
|
|
|
|
}
|
|
|
|
|
|
|
|
const gradArray = [];
|
|
|
|
|
|
|
|
for (let i = 0, currentAngle = this.bottomAngleRad / 2; i < this.colors.length; i++, currentAngle += angleStep) {
|
|
|
|
gradArray.push({
|
|
|
|
start: { x: calcX(currentAngle), y: calcY(currentAngle) },
|
|
|
|
end: { x: calcX(currentAngle + angleStep), y: calcY(currentAngle + angleStep) },
|
|
|
|
big: Math.PI <= angleStep ? 1 : 0,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return gradArray;
|
|
|
|
}
|
|
|
|
|
|
|
|
private invalidateGradientArcs() {
|
|
|
|
const radius = this.radius;
|
|
|
|
|
|
|
|
function getArc(des) {
|
2017-07-06 22:33:06 +03:00
|
|
|
return `M ${radius},${radius}
|
|
|
|
L ${des.start.x},${des.start.y}
|
|
|
|
A ${2 * radius},${2 * radius}
|
|
|
|
0 ${des.big} 1
|
|
|
|
${des.end.x},${des.end.y}
|
|
|
|
Z`;
|
2017-06-22 12:07:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
const angleStep = (2 * Math.PI - this.bottomAngleRad) / this.colors.length;
|
|
|
|
const s = this.calculateGradientConePaths(angleStep);
|
|
|
|
|
|
|
|
this.styles.gradArcs = [];
|
|
|
|
for (let i = 0; i < s.length; i++) {
|
|
|
|
const si = s[i];
|
|
|
|
const arcValue = getArc(si);
|
|
|
|
this.styles.gradArcs.push({
|
|
|
|
color: this.colors[i],
|
|
|
|
d: arcValue,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this.styles.blurRadius = 2 * radius * Math.sin(angleStep / 6);
|
|
|
|
}
|
|
|
|
|
|
|
|
private invalidateNonSelectedArc() {
|
2017-07-07 19:54:49 +03:00
|
|
|
const angle = this.bottomAngleRad / 2 + (1 - this.getValuePercentage()) * (2 * Math.PI - this.bottomAngleRad);
|
2017-06-22 12:07:42 +03:00
|
|
|
this.styles.nonSelectedArc = {
|
|
|
|
color: this.disableArcColor,
|
2017-07-06 22:33:06 +03:00
|
|
|
d: `M ${this.radius},${this.radius}
|
|
|
|
L ${this.radius},${3 * this.radius}
|
|
|
|
A ${2 * this.radius},${2 * this.radius}
|
|
|
|
1 ${angle > Math.PI ? '1' : '0'} 0
|
|
|
|
${this.radius + this.radius * 2 * Math.sin(angle)},${this.radius + this.radius * 2 * Math.cos(angle)}
|
|
|
|
Z`,
|
2017-06-22 12:07:42 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private invalidatePinPosition() {
|
|
|
|
const radiusOffset = this.thickness / 2;
|
|
|
|
const curveRadius = this.radius - radiusOffset;
|
2017-07-07 19:54:49 +03:00
|
|
|
const actualAngle = (2 * Math.PI - this.bottomAngleRad) * this.getValuePercentage() + this.bottomAngleRad / 2;
|
2017-07-13 11:19:57 +03:00
|
|
|
this.styles.thumbPosition = {
|
2017-06-22 12:07:42 +03:00
|
|
|
x: curveRadius * (1 - Math.sin(actualAngle)) + radiusOffset,
|
|
|
|
y: curveRadius * (1 + Math.cos(actualAngle)) + radiusOffset,
|
|
|
|
};
|
|
|
|
this.invalidateNonSelectedArc();
|
|
|
|
}
|
|
|
|
|
2017-07-13 11:19:57 +03:00
|
|
|
private recalculateValue(event, allowJumping = false) {
|
2017-07-10 20:13:49 +03:00
|
|
|
if (this.isMouseDown && !this.off) {
|
2017-06-22 12:07:42 +03:00
|
|
|
const rect = this.svgRoot.nativeElement.getBoundingClientRect();
|
|
|
|
const center = {
|
|
|
|
x: rect.left + VIEW_BOX_SIZE * this.scaleFactor / 2,
|
|
|
|
y: rect.top + (this.translateYValue + this.radius) * this.scaleFactor,
|
|
|
|
};
|
|
|
|
let actualAngle = Math.atan2(center.x - event.clientX, event.clientY - center.y);
|
|
|
|
if (actualAngle < 0) {
|
|
|
|
actualAngle = actualAngle + 2 * Math.PI;
|
|
|
|
}
|
|
|
|
|
2017-07-13 11:19:57 +03:00
|
|
|
const previousRelativeValue = this.getValuePercentage();
|
|
|
|
let relativeValue = 0;
|
2017-06-22 12:07:42 +03:00
|
|
|
if (actualAngle < this.bottomAngleRad / 2) {
|
2017-07-13 11:19:57 +03:00
|
|
|
relativeValue = 0;
|
2017-06-22 12:07:42 +03:00
|
|
|
} else if (actualAngle > 2 * Math.PI - this.bottomAngleRad / 2) {
|
2017-07-13 11:19:57 +03:00
|
|
|
relativeValue = 1;
|
2017-06-22 12:07:42 +03:00
|
|
|
} else {
|
2017-07-13 11:19:57 +03:00
|
|
|
relativeValue = (actualAngle - this.bottomAngleRad / 2) / (2 * Math.PI - this.bottomAngleRad);
|
2017-06-22 12:07:42 +03:00
|
|
|
}
|
|
|
|
|
2017-07-13 11:19:57 +03:00
|
|
|
const value = this.toValueNumber(relativeValue);
|
2017-07-07 19:54:49 +03:00
|
|
|
|
2017-07-13 11:19:57 +03:00
|
|
|
if (this.value !== value && (allowJumping || Math.abs(relativeValue - previousRelativeValue) < this.maxLeap)) {
|
2017-06-22 12:07:42 +03:00
|
|
|
this.value = value;
|
|
|
|
this.valueChange.emit(this.value);
|
|
|
|
this.invalidatePinPosition();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-07 19:54:49 +03:00
|
|
|
private getValuePercentage() {
|
|
|
|
return (this.value - this.min) / (this.max - this.min);
|
|
|
|
}
|
|
|
|
|
2017-07-13 11:19:57 +03:00
|
|
|
private toValueNumber(factor) {
|
|
|
|
return factor * (this.max - this.min) + this.min;
|
|
|
|
}
|
|
|
|
|
2017-06-22 12:07:42 +03:00
|
|
|
private static toRad(angle) {
|
|
|
|
return Math.PI * angle / 180;
|
|
|
|
}
|
|
|
|
}
|