2
0

enhanced graph shifting

This commit is contained in:
2018-08-13 13:27:39 +02:00
parent d8714d0606
commit 09786be1d7
12 changed files with 308 additions and 81 deletions

View File

@@ -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",

View File

@@ -99,8 +99,8 @@
Animation
</label>
<div class="btn-group" role="group" aria-label="Basic example">
<button type="button" class="btn" (click)="animationActive = true" [ngClass]="{'btn-outline-primary': !animationActive, 'btn-primary': animationActive}">An</button>
<button type="button" class="btn" (click)="animationActive = false" [ngClass]="{'btn-outline-primary': animationActive, 'btn-primary': !animationActive}">Aus</button>
<button type="button" class="btn" (click)="startAnimation()" [ngClass]="{'btn-outline-primary': !animationActive, 'btn-primary': animationActive}">An</button>
<button type="button" class="btn" (click)="stopAnimation()" [ngClass]="{'btn-outline-primary': animationActive, 'btn-primary': !animationActive}">Aus</button>
</div>
</div>
<div class="dropdown-divider mb-4"></div>

View File

@@ -1,4 +1,3 @@
import { AnimationService } from './services/animation.service';
/**
* Copyright (C) 2018 Michael Czechowski <mail@dailysh.it>
* 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();
}
}

View File

@@ -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]
})

View File

@@ -1,3 +1,5 @@
<svg #svg width="100%" height="100%">
<g guilloche *ngFor="let graph of graphs" [graph]="graph" [matrix]="matrix" [config]="config" (guillocheChange)="prepareGuillocheExport($event)"></g>
<g guilloche [graph]="graphs[0]" [matrix]="matrix" [config]="config" [animate]="true" (guillocheChange)="prepareGuillocheExport($event)"></g>
<g guilloche [graph]="graphs[1]" [matrix]="matrix" [config]="config" [animate]="true" (guillocheChange)="prepareGuillocheExport($event)"></g>
<!-- <g guilloche *ngFor="let graph of graphs" [graph]="graph" [matrix]="matrix" [config]="config" [animate]="true" (guillocheChange)="prepareGuillocheExport($event)"></g> -->
</svg>

Before

Width:  |  Height:  |  Size: 197 B

After

Width:  |  Height:  |  Size: 511 B

View File

@@ -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<Graph[]>;
private timer: Observable<number>;
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);

View File

@@ -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);
});
}
}

View File

@@ -18,4 +18,5 @@ export interface Point {
x: number;
y: number;
color?: string;
ascent?: number;
}

View File

@@ -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;
});
// });
}
}

View File

@@ -0,0 +1,76 @@
import { Validators } from '@angular/forms';
/**
* Copyright (C) 2018 Michael Czechowski <mail@dailysh.it>
* 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
};
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
};