mirror of
https://github.com/akveo/ngx-admin.git
synced 2025-12-17 16:00:14 +01:00
feat(dashboard): add temperature dragger component
This commit is contained in:
parent
35e377093a
commit
53778c2053
9 changed files with 362 additions and 3 deletions
|
|
@ -1,3 +1,10 @@
|
|||
@mixin ngx-global-theme() {
|
||||
// any global, non-component styles go here
|
||||
}
|
||||
|
||||
// TODO: probably it makes sense to move it to mixin
|
||||
.nga-theme-cosmic {
|
||||
nga-card nga-card-header + nga-card-body {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* Copyright Akveo. All Rights Reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*/
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
|
@ -11,7 +12,6 @@ import { HttpModule } from '@angular/http';
|
|||
import { CoreModule } from './@core/core.module';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { PagesModule } from './pages/pages.module';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
||||
@NgModule({
|
||||
|
|
@ -24,6 +24,9 @@ import { AppRoutingModule } from './app-routing.module';
|
|||
CoreModule,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||
],
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@
|
|||
<nga-card size="xmedium">
|
||||
<nga-tabset fullWidth>
|
||||
<nga-tab tabTitle="Temperature">
|
||||
<span>Content #1</span>
|
||||
<ngx-temperature-dragger [arcThickness]="15" [knobRadius]="15" [(value)]="currentValue" [bottomAngle]="90"
|
||||
disableArcColor="#2c2961"
|
||||
[fillColors]="['#2ec6ff', '#31ffad', '#7bff24', '#fff024', '#ff6c00']">
|
||||
</ngx-temperature-dragger>
|
||||
</nga-tab>
|
||||
<nga-tab tabTitle="Humidity">
|
||||
<span>Content #2</span>
|
||||
|
|
@ -30,6 +33,8 @@
|
|||
<nga-card-header>
|
||||
Room Management
|
||||
</nga-card-header>
|
||||
<nga-card-body>
|
||||
</nga-card-body>
|
||||
</nga-card>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ import { Component } from '@angular/core';
|
|||
templateUrl: './dashboard.component.html',
|
||||
})
|
||||
export class DashboardComponent {
|
||||
currentValue = 0.5;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { NgaTabsetModule } from '@nga/theme';
|
|||
import { SharedModule } from '../../shared.module';
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
import { StatusCardsComponent } from './status-cards/status-cards.component';
|
||||
import { TemperatureDraggerComponent } from './temperature-dragger/temperature-dragger.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -13,6 +14,7 @@ import { StatusCardsComponent } from './status-cards/status-cards.component';
|
|||
declarations: [
|
||||
DashboardComponent,
|
||||
StatusCardsComponent,
|
||||
TemperatureDraggerComponent,
|
||||
],
|
||||
})
|
||||
export class DashboardModule { }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<svg #svgRoot xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
|
||||
[attr.viewBox]="styles.viewBox" preserveAspectRatio="xMinYMin meet" class="svg-content" (mousedown)="mouseDown($event)">
|
||||
<defs>
|
||||
|
||||
<filter id="blurFilter" x="0" y="0" width="100%" height="100%">
|
||||
<feGaussianBlur in="SourceGraphic" [attr.stdDeviation]="styles.blurRadius" />
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="discrete" tableValues="1 1"/>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
|
||||
<clipPath id="sliderClip">
|
||||
<path [attr.d]="styles.clipPathStr"></path>
|
||||
</clipPath>
|
||||
|
||||
</defs>
|
||||
<g [attr.transform]="styles.arcTranslateStr">
|
||||
|
||||
<g class="toClip" clip-path="url(#sliderClip)">
|
||||
<g class="toFilter" filter="url(#blurFilter)">
|
||||
<path [attr.d]="arc.d" [attr.fill]="arc.color" *ngFor="let arc of styles.gradArcs"></path>
|
||||
</g>
|
||||
<path [attr.d]="styles.nonSelectedArc.d" [attr.fill]="styles.nonSelectedArc.color"></path>
|
||||
</g>
|
||||
|
||||
<circle [attr.cx]="styles.knobPosition.x" [attr.cy]="styles.knobPosition.y" [attr.r]="pinRadius"
|
||||
[attr.stroke-width]="1 / scaleFactor" fill="#FFFFFF" stroke="#666666"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,11 @@
|
|||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.svg-content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
import {
|
||||
Component, HostListener, ViewChild, ElementRef, Input, Output, EventEmitter, AfterViewInit,
|
||||
} from '@angular/core';
|
||||
|
||||
const VIEW_BOX_SIZE = 300;
|
||||
|
||||
@Component({
|
||||
selector: 'ngx-temperature-dragger',
|
||||
templateUrl: './temperature-dragger.component.html',
|
||||
styleUrls: ['./temperature-dragger.component.scss'],
|
||||
})
|
||||
export class TemperatureDraggerComponent implements AfterViewInit {
|
||||
|
||||
@ViewChild('svgRoot') svgRoot: ElementRef;
|
||||
|
||||
@Input() fillColors: String|String[] = '#2ec6ff';
|
||||
@Input() disableArcColor: String = '#999999';
|
||||
@Input() bottomAngle: number = 90;
|
||||
@Input() arcThickness: number = 6; // CSS pixels
|
||||
@Input() knobRadius: number = 10; // CSS pixels
|
||||
|
||||
value: number = 0.5;
|
||||
@Output('valueChange') valueChange = new EventEmitter<Number>();
|
||||
@Input('value') set setValue(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@HostListener('mouseup', ['$event'])
|
||||
onMouseUp(event) {
|
||||
this.recalculateValue(event);
|
||||
this.isMouseDown = false;
|
||||
}
|
||||
|
||||
@HostListener('mousemove', ['$event'])
|
||||
onMouseMove(event: MouseEvent) {
|
||||
this.recalculateValue(event);
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event) {
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
scaleFactor: number = 1;
|
||||
bottomAngleRad = 0;
|
||||
radius: number = 100;
|
||||
translateXValue = 0;
|
||||
translateYValue = 0;
|
||||
thickness = 6;
|
||||
pinRadius = 10;
|
||||
colors: any = [];
|
||||
|
||||
styles = {
|
||||
viewBox: '0 0 300 300',
|
||||
arcTranslateStr: 'translate(0, 0)',
|
||||
clipPathStr: '',
|
||||
gradArcs: [],
|
||||
nonSelectedArc: {},
|
||||
knobPosition: { x: 0, y: 0 },
|
||||
blurRadius: 15,
|
||||
};
|
||||
|
||||
private isMouseDown = false;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
mouseDown(event) {
|
||||
this.isMouseDown = true;
|
||||
this.recalculateValue(event);
|
||||
}
|
||||
|
||||
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;
|
||||
const knobMargin = this.knobRadius > this.arcThickness
|
||||
? (this.knobRadius - this.arcThickness / 2) / this.scaleFactor
|
||||
: 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) {
|
||||
this.radius = (VIEW_BOX_SIZE - 2 * knobMargin) / (2 * Math.sin(halfAngle));
|
||||
} else {
|
||||
this.radius = VIEW_BOX_SIZE / 2 - knobMargin;
|
||||
}
|
||||
|
||||
} else {
|
||||
this.radius = (svgHeight - 2 * knobMargin) / (1 + Math.cos(halfAngle));
|
||||
}
|
||||
|
||||
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;
|
||||
this.pinRadius = this.knobRadius / this.scaleFactor;
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
path += ` A ${s.outer.radius},${s.outer.radius}` + // Draw an arc of radius 'radius'
|
||||
` 0 ${s.big} 1` + // Arc details...
|
||||
` ${s.outer.end.x},${s.outer.end.y}`; // Arc goes to top end angle coordinate
|
||||
|
||||
// Outer to inner connector
|
||||
path += ` A ${s.thickness / 2},${s.thickness / 2}` +
|
||||
` 0 1 1` +
|
||||
` ${s.inner.end.x},${s.inner.end.y}`;
|
||||
|
||||
// Inner arc
|
||||
path += ` A ${s.inner.radius},${s.inner.radius}` +
|
||||
` 1 ${s.big} 0` +
|
||||
` ${s.inner.start.x},${s.inner.start.y}`;
|
||||
|
||||
// Outer to inner connector
|
||||
path += ` A ${s.thickness / 2},${s.thickness / 2}` +
|
||||
` 0 1 1` +
|
||||
` ${s.outer.start.x},${s.outer.start.y}`;
|
||||
|
||||
// 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) {
|
||||
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`;
|
||||
}
|
||||
|
||||
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() {
|
||||
const angle = this.bottomAngleRad / 2 + (1 - this.value) * (2 * Math.PI - this.bottomAngleRad);
|
||||
this.styles.nonSelectedArc = {
|
||||
color: this.disableArcColor,
|
||||
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`,
|
||||
};
|
||||
}
|
||||
|
||||
private invalidatePinPosition() {
|
||||
const radiusOffset = this.thickness / 2;
|
||||
const curveRadius = this.radius - radiusOffset;
|
||||
const actualAngle = (2 * Math.PI - this.bottomAngleRad) * this.value + this.bottomAngleRad / 2;
|
||||
this.styles.knobPosition = {
|
||||
x: curveRadius * (1 - Math.sin(actualAngle)) + radiusOffset,
|
||||
y: curveRadius * (1 + Math.cos(actualAngle)) + radiusOffset,
|
||||
};
|
||||
this.invalidateNonSelectedArc();
|
||||
}
|
||||
|
||||
private recalculateValue(event) {
|
||||
if (this.isMouseDown) {
|
||||
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;
|
||||
}
|
||||
|
||||
let value = 0;
|
||||
if (actualAngle < this.bottomAngleRad / 2) {
|
||||
value = 0;
|
||||
} else if (actualAngle > 2 * Math.PI - this.bottomAngleRad / 2) {
|
||||
value = 1;
|
||||
} else {
|
||||
value = (actualAngle - this.bottomAngleRad / 2) / (2 * Math.PI - this.bottomAngleRad);
|
||||
}
|
||||
|
||||
if (this.value !== value) {
|
||||
this.value = value;
|
||||
this.valueChange.emit(this.value);
|
||||
this.invalidatePinPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static toRad(angle) {
|
||||
return Math.PI * angle / 180;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>NgX Admin Demo</title>
|
||||
<base href="/">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue