From 53778c2053cf1bbf5d0b0b5da8f7b8ae2e3c0808 Mon Sep 17 00:00:00 2001 From: KostyaDanovsky Date: Thu, 22 Jun 2017 12:07:42 +0300 Subject: [PATCH] feat(dashboard): add temperature dragger component --- src/app/@theme/styles/global.scss | 7 + src/app/app.module.ts | 5 +- .../pages/dashboard/dashboard.component.html | 7 +- .../pages/dashboard/dashboard.component.ts | 1 + src/app/pages/dashboard/dashboard.module.ts | 2 + .../temperature-dragger.component.html | 29 ++ .../temperature-dragger.component.scss | 11 + .../temperature-dragger.component.ts | 302 ++++++++++++++++++ src/index.html | 1 - 9 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.html create mode 100644 src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.scss create mode 100644 src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.ts diff --git a/src/app/@theme/styles/global.scss b/src/app/@theme/styles/global.scss index 94c44a58..000d4ec5 100644 --- a/src/app/@theme/styles/global.scss +++ b/src/app/@theme/styles/global.scss @@ -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; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b86be7ea..29b6568f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 { } diff --git a/src/app/pages/dashboard/dashboard.component.html b/src/app/pages/dashboard/dashboard.component.html index f80c10cb..8e6f9393 100644 --- a/src/app/pages/dashboard/dashboard.component.html +++ b/src/app/pages/dashboard/dashboard.component.html @@ -6,7 +6,10 @@ - Content #1 + + Content #2 @@ -30,6 +33,8 @@ Room Management + + diff --git a/src/app/pages/dashboard/dashboard.component.ts b/src/app/pages/dashboard/dashboard.component.ts index a4539cbc..3ddde498 100644 --- a/src/app/pages/dashboard/dashboard.component.ts +++ b/src/app/pages/dashboard/dashboard.component.ts @@ -6,4 +6,5 @@ import { Component } from '@angular/core'; templateUrl: './dashboard.component.html', }) export class DashboardComponent { + currentValue = 0.5; } diff --git a/src/app/pages/dashboard/dashboard.module.ts b/src/app/pages/dashboard/dashboard.module.ts index 7e7c1f7c..76fdf4b7 100644 --- a/src/app/pages/dashboard/dashboard.module.ts +++ b/src/app/pages/dashboard/dashboard.module.ts @@ -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 { } diff --git a/src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.html b/src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.html new file mode 100644 index 00000000..4edf4494 --- /dev/null +++ b/src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.scss b/src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.scss new file mode 100644 index 00000000..5ebce9a9 --- /dev/null +++ b/src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.scss @@ -0,0 +1,11 @@ +:host { + display: block; + width: 100%; + height: 100%; +} + +.svg-content { + display: block; + width: 100%; + height: 100%; +} diff --git a/src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.ts b/src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.ts new file mode 100644 index 00000000..5f0192ef --- /dev/null +++ b/src/app/pages/dashboard/temperature-dragger/temperature-dragger.component.ts @@ -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(); + @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; + } +} diff --git a/src/index.html b/src/index.html index 4809f39c..00abe18d 100644 --- a/src/index.html +++ b/src/index.html @@ -3,7 +3,6 @@ NgX Admin Demo -