From 09786be1d78b77189fabf55d3f957c9bf8a4be48 Mon Sep 17 00:00:00 2001 From: Michael Czechowski Date: Mon, 13 Aug 2018 13:27:39 +0200 Subject: [PATCH] enhanced graph shifting --- package.json | 2 +- src/app/app.component.html | 4 +- src/app/app.component.ts | 14 +- src/app/app.module.ts | 6 +- src/app/components/graphs.component.html | 4 +- src/app/components/graphs.component.ts | 43 ++--- src/app/directives/guilloche.directive.ts | 162 ++++++++++++++---- src/app/models/point.model.ts | 1 + src/app/services/animation.service.ts | 19 +- src/app/services/graph.service.ts | 76 ++++++++ ...{arithmetic.service.ts => math.service.ts} | 53 +++++- src/environments/environment.ts | 5 +- 12 files changed, 308 insertions(+), 81 deletions(-) create mode 100644 src/app/services/graph.service.ts rename src/app/services/{arithmetic.service.ts => math.service.ts} (63%) diff --git a/package.json b/package.json index ce92a21..f3df2d0 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ ], "scripts": { "ng": "ng", - "start": "ng serve", + "start": "ng serve --aot -o", "build": "ng build --prod", "test": "ng test", "lint": "ng lint", diff --git a/src/app/app.component.html b/src/app/app.component.html index a651ac9..dfccfcb 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -99,8 +99,8 @@ Animation
- - + +
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 17b03dc..ceed6a5 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,3 @@ -import { AnimationService } from './services/animation.service'; /** * Copyright (C) 2018 Michael Czechowski * This program is free software; you can redistribute it and/or modify it @@ -29,6 +28,8 @@ import { Config } from './models/config.model'; import { CanvasService } from './services/canvas.service'; import { HistoryService } from './services/history.service'; import { Graph } from './models/graph.model'; +import { GraphService } from './services/graph.service'; +import { AnimationService } from './services/animation.service'; @Component({ selector: 'app-root', @@ -49,6 +50,7 @@ export class AppComponent implements OnInit { constructor( private canvasService: CanvasService, private historyService: HistoryService, + private graphService: GraphService, ) { moment.locale('de'); @@ -100,4 +102,14 @@ export class AppComponent implements OnInit { this.configForm.reset({...history.config}); this.restoredHistory = history; } + + public startAnimation() { + this.animationActive = true; + this.graphService.startAnimation(); + } + + public stopAnimation() { + this.animationActive = false; + this.graphService.stopAnimation(); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7307278..1d87154 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -25,7 +25,8 @@ import { GuillocheDirective } from './directives/guilloche.directive'; import { CanvasService } from './services/canvas.service'; import { HistoryService } from './services/history.service'; import { AnimationService } from './services/animation.service'; -import { ArithmeticService } from './services/arithmetic.service'; +import { MathService } from './services/math.service'; +import { GraphService } from './services/graph.service'; @NgModule({ declarations: [ @@ -43,7 +44,8 @@ import { ArithmeticService } from './services/arithmetic.service'; CanvasService, HistoryService, AnimationService, - ArithmeticService, + MathService, + GraphService, ], bootstrap: [AppComponent] }) diff --git a/src/app/components/graphs.component.html b/src/app/components/graphs.component.html index 37a7bbd..389ac28 100644 --- a/src/app/components/graphs.component.html +++ b/src/app/components/graphs.component.html @@ -1,3 +1,5 @@ - + + + diff --git a/src/app/components/graphs.component.ts b/src/app/components/graphs.component.ts index 59e6d26..195fd39 100644 --- a/src/app/components/graphs.component.ts +++ b/src/app/components/graphs.component.ts @@ -25,10 +25,11 @@ import { environment as env } from '../../environments/environment'; import { CanvasService } from './../services/canvas.service'; import { HistoryService } from './../services/history.service'; import { AnimationService } from '../services/animation.service'; -import { ArithmeticService } from '../services/arithmetic.service'; +import { MathService } from '../services/math.service'; import { GuillocheDirective } from './../directives/guilloche.directive'; import { Graph } from '../models/graph.model'; import { Point } from '../models/point.model'; +import { GraphService } from '../services/graph.service'; @Component({ selector: 'app-graphs', @@ -48,7 +49,6 @@ export class GraphsComponent implements OnChanges, OnInit { private hash: string; private animation: Observable; private timer: Observable; - private animationSteps: any; @Input() config: any; @Input() restoredHistory: any; @@ -66,7 +66,8 @@ export class GraphsComponent implements OnChanges, OnInit { private canvasService: CanvasService, private historyService: HistoryService, private animationService: AnimationService, - private arithmetics: ArithmeticService + private math: MathService, + private graphService: GraphService ) { this.genLoadedAllGraphs = this.countLoadedGraphs(); this.timer = interval(500); @@ -90,19 +91,6 @@ export class GraphsComponent implements OnChanges, OnInit { this.graphs = this.restoredHistory.graphs; this.hash = this.restoredHistory.hash; } - - if (changes.animationActive) { - if (this.animationActive) { - this.animationSteps = setInterval(() => this.animateGraph(), 50); - } else { - if (this.animationSteps) { - clearInterval(this.animationSteps); - } - } - } - } - private animateGraph() { - this.graphs = this.animationService.animate(this.graphs); } private saveHistory() { @@ -110,6 +98,10 @@ export class GraphsComponent implements OnChanges, OnInit { this.historyService.save(this.graphs, this.config); } + private saveGraph() { + this.graphService.set(this.graphs); + } + private updateGraphs(): void { const genShiftStart = this.shiftPoint(this.matrix.start, this.config.vectors.start); const genShiftEnd = this.shiftPoint(this.matrix.end, this.config.vectors.end); @@ -130,6 +122,7 @@ export class GraphsComponent implements OnChanges, OnInit { this.graphs = curveList.map(curve => this.adjustGraph(curve)); this.hash = this.historyService.hash(this.graphs); this.saveHistory(); + this.saveGraph(); } private adjustGraph(curve) { @@ -147,7 +140,7 @@ export class GraphsComponent implements OnChanges, OnInit { const generatedPoints = []; for (let i = 0; i < this.config.nodes; i++) { - generatedPoints.push(this.arithmetics.randomPoint(this.matrix, this.config.overlap)); + generatedPoints.push(this.math.randomPoint(this.matrix, this.config.overlap)); } return generatedPoints; @@ -165,13 +158,13 @@ export class GraphsComponent implements OnChanges, OnInit { private updateMatrix() { const totalArea = Math.abs(this.canvas.clientWidth * this.canvas.clientHeight); - const totalCenter = this.arithmetics.centerPoint(this.canvas.clientWidth, this.canvas.clientHeight); + const totalCenter = this.math.centerOfArea(this.canvas.clientWidth, this.canvas.clientHeight); const baseArea = Math.abs(this.config.width * this.config.height); const baseScale = Math.pow(totalArea / baseArea * this.config.scale, 0.5); const baseWidthScaled = baseScale * this.config.width; const baseHeightScaled = baseScale * this.config.height; - const baseCenter = this.arithmetics.centerPoint(baseWidthScaled, baseHeightScaled); + const baseCenter = this.math.centerOfArea(baseWidthScaled, baseHeightScaled); this.matrix = { start: { @@ -189,7 +182,7 @@ export class GraphsComponent implements OnChanges, OnInit { } private genVectorPoint(point: Point, vector: number) { - const range = this.arithmetics.Δ(this.matrix.start, this.matrix.end) * this.config.vectors.range; + const range = this.math.Δ(this.matrix.start, this.matrix.end) * this.config.vectors.range; return { x: range * Math.sin(Math.PI * vector) + point.x, @@ -215,7 +208,7 @@ export class GraphsComponent implements OnChanges, OnInit { private *shiftNumber(space: number, vector: number) { let current = 0; let index = 0; - const sign = this.flipSign(); + const sign = this.math.flipSign(); while (true) { yield current = sign.next().value * index * space + current; @@ -223,14 +216,6 @@ export class GraphsComponent implements OnChanges, OnInit { } } - private *flipSign() { - let sign = 1; - - while (true) { - yield sign = sign * (-1); - } - } - public prepareGuillocheExport(guillocheElement) { if (this.genLoadedAllGraphs.next().value) { this.svgChange.emit(this.svgElementRef); diff --git a/src/app/directives/guilloche.directive.ts b/src/app/directives/guilloche.directive.ts index af9cfed..fcbf4ea 100644 --- a/src/app/directives/guilloche.directive.ts +++ b/src/app/directives/guilloche.directive.ts @@ -14,11 +14,13 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ElementRef, HostListener, Output, EventEmitter, Input, Directive, OnChanges, SimpleChanges } from '@angular/core'; +import { ElementRef, HostListener, Output, EventEmitter, Input, Directive, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import * as Selection from 'd3-selection'; import * as Shape from 'd3-shape'; import * as Random from 'd3-random'; import * as Drag from 'd3-drag'; +import * as Ease from 'd3-ease'; +import * as Timer from 'd3-timer'; import { environment as env } from './../../environments/environment'; import { Config } from './../models/config.model'; @@ -26,33 +28,74 @@ import { Graph } from './../models/graph.model'; import { Point } from './../models/point.model'; import { Param } from './../models/param.model'; import { CanvasService } from './../services/canvas.service'; -import { ArithmeticService } from './../services/arithmetic.service'; +import { MathService } from './../services/math.service'; +import { GraphService } from '../services/graph.service'; +import { AnimationService } from './../services/animation.service'; +import { spread } from 'q'; @Directive({ selector: '[guilloche]' }) -export class GuillocheDirective implements OnChanges { +export class GuillocheDirective implements OnChanges, OnInit { private canvas: any; private group: any; + private animationInterval: any; + private x: any; + private y: any; @Input() graph: Graph; @Input() matrix: any; @Input() config: any; + @Input() animate: boolean; @Output() guillocheChange = new EventEmitter(); constructor( private canvasService: CanvasService, private el: ElementRef, - private arithmetics: ArithmeticService + private math: MathService, + private graphService: GraphService, + private animationService: AnimationService ) { this.group = Selection.select(el.nativeElement); this.canvas = Selection.select(this.canvasService.get); } + ngOnInit() { + // console.log('guilloche:init'); + // Timer.timer(function(elapsed) { + // let t = (elapsed % 3000) / 3000; + // console.log(t); + // // dot1.attr("cx", x(t)).attr("cy", y(ease(t))); + // // dot2.attr("cy", y(ease(t))); + // }); + + // console.log(Ease.easeLinear(0.5)); + // const t = Timer.timer(function(elapsed) { + // if (elapsed > 200) { + // t.stop(); + // } + // }, 1000); + } + ngOnChanges(changes: SimpleChanges) { - // console.log(this.graph); + // @todo modify graph here instead of in graphs.component.ts + this.group.selectAll('*').remove(); + + // console.log('guilloche:changes', changes); + + if (this.graphService.isAnimated) { + console.log('is animated'); + // this.graphService.startAnimation(); + this.animationInterval = setInterval(() => this.animateGraph(), 60); + } else { + if (this.animationInterval) { + console.log('not animated'); + // this.graphService.stopAnimation(); + clearInterval(this.animationInterval); + } + } const points = [ this.graph.start.point, @@ -63,6 +106,18 @@ export class GuillocheDirective implements OnChanges { this.guillocheChanged(); } + private animateGraph() { + this.group.selectAll('*').remove(); + this.graph = this.animationService.animate(this.graph); + // this.saveGraph(); + const points = [ + this.graph.start.point, + ...this.graph.nodes, + this.graph.end.point + ]; + this.spreadLines(points); + } + public guillocheChanged() { this.guillocheChange.emit(this.el.nativeElement); } @@ -77,50 +132,93 @@ export class GuillocheDirective implements OnChanges { .attr('stroke-width', this.graph.stroke) .attr('fill', 'none'); - if (!env.production) { + if (env.grid) { this.showGrid(); } } private spreadLines(points: Point[]) { - const indexMiddle = Math.floor(points.length * 0.5); - const pointMiddle = points[indexMiddle]; - const closestCenter = this.arithmetics.getClosestCenter(pointMiddle, this.matrix); - const radius = this.arithmetics.Δ(pointMiddle, closestCenter); - const spreadPoints = []; - const pies = 80; + const shiftedMedians = []; + const medianPoint = this.math.centerOfCurve(points); + const medianIndex = this.math.medianIndex(points); + const genshiftedMedians = this.graphService.spreadOrthogonal(medianPoint, 20); - for (let i = 0; i < pies; i++) { - spreadPoints.push({ - x: radius * Math.cos(2 * i * Math.PI / pies) + closestCenter.x, - y: radius * Math.sin(2 * i * Math.PI / pies) + closestCenter.y, + for (let i = 0; i < this.config.spread; i++) { + shiftedMedians.push(genshiftedMedians.next().value); + } + + // const indexMiddle = Math.floor(points.length * 0.5); + // const pointMiddle = points[indexMiddle]; + // const closestCenter = this.math.getClosestCenter(pointMiddle, this.matrix); + // const radius = this.math.Δ(pointMiddle, closestCenter); + // const shiftedMedians = []; + // const pies = 200; + + // for (let i = 0; i < pies; i++) { + // shiftedMedians.push({ + // x: radius * Math.cos(2 * i * Math.PI / pies) + closestCenter.x, + // y: radius * Math.sin(2 * i * Math.PI / pies) + closestCenter.y, + // }); + // } + + // shiftedMedians.sort((a, b) => { + // // Good possibility to align orientation points outsite + // return this.math.Δ(b, pointMiddle) - this.math.Δ(a, pointMiddle); + // }); + + // console.log(shiftedMedians); + + // shiftedMedians.some((point, index) => { + // points[indexMiddle] = point; + + // this.drawGraph(points); + + // return index === this.config.spread - 1; + // }); + if (env.grid) { + [medianPoint, ...shiftedMedians].forEach((point, index) => { + this.group.append('circle') + .attr('cx', point.x) + .attr('cy', point.y) + .attr('r', 10 / index) + .attr('fill-opacity', 0.6) + .attr('fill', 'darkgray'); }); } - spreadPoints.sort((a, b) => { - // Good possibility to align orientation points outsite - return this.arithmetics.Δ(b, pointMiddle) - this.arithmetics.Δ(a, pointMiddle); - }); - - spreadPoints.some((point, index) => { - points[indexMiddle] = point; - - this.drawGraph(points); - - return index === this.config.spread - 1; + shiftedMedians.forEach(median => { + const shiftedGraph = points.slice(); + shiftedGraph.splice(medianIndex, 1, median); + this.drawGraph(shiftedGraph); }); + // this.drawGraph(points); } + // private animateRange(n: number) { + // return Ease.scaleLinear().range([n, n + 100]); + // } + private showGrid() { - this.graph.nodes.forEach(point => { - this.group.append('circle') + this.graph.nodes.forEach((point, index) => { + const circle = this.group.append('g'); + // const xRange = this.animateRange(point.x); + // const yRange = this.animateRange(point.y); + + circle.append('circle') .attr('cx', point.x) .attr('cy', point.y) .attr('r', 3) - .attr('stroke-width', 0.1) - .attr('fill-opacity', 0) - .attr('stroke', 'darkgray'); + .attr('fill-opacity', 0.6) + .attr('fill', this.graph.color); + + circle.append('text') + .attr('x', point.x) + .attr('y', point.y) + .attr('dx', 8) + .attr('dy', 15) + .attr('fill', this.graph.color) + .text(index); }); } } diff --git a/src/app/models/point.model.ts b/src/app/models/point.model.ts index aff2e70..f1103e6 100644 --- a/src/app/models/point.model.ts +++ b/src/app/models/point.model.ts @@ -18,4 +18,5 @@ export interface Point { x: number; y: number; color?: string; + ascent?: number; } diff --git a/src/app/services/animation.service.ts b/src/app/services/animation.service.ts index 2f37612..4251f39 100644 --- a/src/app/services/animation.service.ts +++ b/src/app/services/animation.service.ts @@ -19,7 +19,7 @@ import { interval, Observable } from 'rxjs'; import * as Selection from 'd3-selection'; import { Graph } from '../models/graph.model'; -import { ArithmeticService } from './arithmetic.service'; +import { MathService } from './math.service'; import { HistoryService } from './history.service'; @Injectable() @@ -33,27 +33,28 @@ export class AnimationService { // private subscribtion: any; constructor( - private arithmetics: ArithmeticService, + private math: MathService, private historyService: HistoryService, ) { } - public animate(initialGraphs: Graph[]) { - const newGraphs = initialGraphs.slice(); + // public animate(initialGraphs: Graph[]) { + public animate(initialGraph: Graph) { + // const newGraphs = initialGraphs.slice(); - return newGraphs.map(graph => { + // return newGraphs.map(graph => { - const newGraph = Object.assign({}, graph); + const newGraph = Object.assign({}, initialGraph); const indexMiddle = Math.floor(newGraph.nodes.length * 0.5); const pointMiddle = newGraph.nodes[indexMiddle]; newGraph.nodes.splice(indexMiddle, 1, { - x: pointMiddle.x - 1, - y: pointMiddle.y + 1, + x: pointMiddle.x - 2, + y: pointMiddle.y + 2, }); return newGraph; - }); + // }); } } diff --git a/src/app/services/graph.service.ts b/src/app/services/graph.service.ts new file mode 100644 index 0000000..c053a64 --- /dev/null +++ b/src/app/services/graph.service.ts @@ -0,0 +1,76 @@ +import { Validators } from '@angular/forms'; +/** + * Copyright (C) 2018 Michael Czechowski + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import * as Selection from 'd3-selection'; + +import { MathService } from './math.service'; +import { Graph } from './../models/graph.model'; +import { Point } from './../models/point.model'; + +@Injectable() +export class GraphService { + private graphs: Graph[]; + private animation: boolean | null; + + constructor( + private math: MathService + ) {} + + public get() { + return this.graphs; + } + + public set(newGraphs: Graph[]) { + // console.log('GraphService:set', newGraphs); + this.graphs = newGraphs; + } + + public get isAnimated() { + return this.animation; + } + + public startAnimation() { + this.animation = true; + } + + public stopAnimation() { + this.animation = false; + } + + public *spreadOrthogonal(start: Point, spacing: number) { + const sign = this.math.flipSign(); + let currentPoint = start; + let i = 0; + + while (true) { + const currentSpacing = sign.next().value * spacing * i; + currentPoint = this.shiftPoint(currentPoint, start.ascent, currentSpacing); + + yield currentPoint; + + i++; + } + } + + public shiftPoint(point: Point, radians: number, spacing: number) { + return { + x: Math.sin(radians * Math.PI) * spacing + point.x, + y: Math.cos(radians * Math.PI) * spacing + point.y + }; + } +} diff --git a/src/app/services/arithmetic.service.ts b/src/app/services/math.service.ts similarity index 63% rename from src/app/services/arithmetic.service.ts rename to src/app/services/math.service.ts index f79ff79..aa5cde9 100644 --- a/src/app/services/arithmetic.service.ts +++ b/src/app/services/math.service.ts @@ -19,9 +19,10 @@ import * as Selection from 'd3-selection'; import * as Random from 'd3-random'; import { Point } from './../models/point.model'; +import { Graph } from './../models/graph.model'; @Injectable() -export class ArithmeticService { +export class MathService { /** * Calculate distance between to points with coordinates. @@ -64,10 +65,58 @@ export class ArithmeticService { }; } - public centerPoint(width, height): Point { + public centerOfArea(width, height): Point { return { x: width * 0.5, y: height * 0.5 }; } + + public centerOfPoints(p1: Point, p2: Point) { + return { + x: (p1.x + p2.x) * 0.5, + y: (p1.y + p2.y) * 0.5 + }; + } + + public centerOfCurve(curve: Point[]) { + const genMedian = this.medianPoint(curve); + const p1 = genMedian.next().value; + const p2 = genMedian.next().value; + const radians = this.angleRadians(p1, p2); + + return Object.assign(this.centerOfPoints(p1, p2), { ascent: radians }); + } + + public angleRadians(p1: Point, p2: Point) { + return Math.atan2(p2.y - p1.y, p2.x - p1.x); + } + + public angleDegree(p1: Point, p2: Point) { + return this.angleRadians(p1, p2) * 180 / Math.PI; + } + + public medianIndex(list: any): number { + return Math.floor(list.length * 0.5); + } + + public *medianPoint(points: Point[]) { + let index: number; + const list: Point[] = points.slice(); + + while (list) { + index = this.medianIndex(points); + yield list[index]; + + list.splice(index, 1); + } + } + + public *flipSign() { + let sign = 1; + + while (true) { + yield sign = sign * (-1); + } + } } diff --git a/src/environments/environment.ts b/src/environments/environment.ts index a71c77f..4a6a2ec 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -16,6 +16,7 @@ export const environment = { production: false, + grid: false, guilloche: { colors: { primary: '#cb0c4d', @@ -30,11 +31,11 @@ export const environment = { vectors: { start: 1, end: 0, - range: 0.4, + range: 0.4 }, nodes: 4, stroke: 1, spread: 12, - space: 5 + space: 10 } };