feat(graohsComponent): added vector configuration
This commit is contained in:
@@ -10,87 +10,68 @@
|
|||||||
|
|
||||||
<app-graphs [config]="config"></app-graphs>
|
<app-graphs [config]="config"></app-graphs>
|
||||||
|
|
||||||
<ul *ngIf="false">
|
<aside class="col-sm-4 col-lg-3 col-xl-3">
|
||||||
<li>
|
|
||||||
<label>
|
|
||||||
<div>Points</div>
|
|
||||||
<input type="number" name="points" [(ngModel)]="canvasParam.points" max="10" min="0" step="1">
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<label>
|
|
||||||
<div>Margin X</div>
|
|
||||||
<input type="number" name="marginX" [(ngModel)]="canvasParam.margin.x" max="1" min="0" step="0.1">
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<label>
|
|
||||||
<div>Margin Y</div>
|
|
||||||
<input type="number" name="marginY" [(ngModel)]="canvasParam.margin.y" max="1" min="0" step="0.1">
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<label>
|
|
||||||
<div>Stroke Width</div>
|
|
||||||
<input type="number" name="strokeWidth" [(ngModel)]="canvasParam.stroke.width" max="10" min="0" step="0.1">
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<label>
|
|
||||||
<div>Spread lines</div>
|
|
||||||
<input type="number" name="spread" [(ngModel)]="canvasParam.spread" max="20" min="5" step="1">
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<label>
|
|
||||||
<div>Show Grid?</div>
|
|
||||||
<input type="checkbox" name="showGrid" [(ngModel)]="canvasParam.showGrid">
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<aside>
|
|
||||||
<form [formGroup]="configForm" (ngSubmit)="updateGraphs()" novalidate>
|
<form [formGroup]="configForm" (ngSubmit)="updateGraphs()" novalidate>
|
||||||
|
<div class="pb-5">
|
||||||
|
<h5>Seitenverhältnis</h5>
|
||||||
|
<hr>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-control-label">
|
<label class="form-control-label">
|
||||||
Breite <small>(bspw. 42, -12, 3.2)</small>
|
Breite
|
||||||
</label>
|
</label>
|
||||||
<input type="number" class="form-control" formControlName="width">
|
<input type="number" class="form-control" formControlName="width">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-control-label">
|
<label class="form-control-label">
|
||||||
Höhe <small>(bspw. 42, -12, 3.2)</small>
|
Höhe
|
||||||
</label>
|
</label>
|
||||||
<input type="number" class="form-control" formControlName="height">
|
<input type="number" class="form-control" formControlName="height">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-control-label">
|
<label class="form-control-label">
|
||||||
Duktus <small>(Strichstärke)</small>
|
Linienstärke
|
||||||
</label>
|
</label>
|
||||||
<input type="number" class="form-control" formControlName="stroke" min="0.1" max="10" step="0.1">
|
<input type="number" class="form-control" formControlName="stroke" min="0.1" max="10" step="0.1">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pb-5">
|
||||||
|
<h5>Vektoren</h5>
|
||||||
|
<hr>
|
||||||
|
<ng-container formGroupName="vectors">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-control-label">
|
<label class="form-control-label">
|
||||||
Anfangsvektor
|
Anfangsvektor
|
||||||
</label>
|
</label>
|
||||||
<select class="form-control" formControlName="directionStart">
|
<select class="form-control" formControlName="start">
|
||||||
<option value="0">Oben</option>
|
<option value="1">Oben</option>
|
||||||
<option value="90">Rechts</option>
|
<option value="0.5">Rechts</option>
|
||||||
<option value="180">Unten</option>
|
<option value="0">Unten</option>
|
||||||
<option value="270">Links</option>
|
<option value="1.5">Links</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-control-label">
|
<label class="form-control-label">
|
||||||
Endvektor
|
Endvektor
|
||||||
</label>
|
</label>
|
||||||
<select class="form-control" formControlName="directionEnd">
|
<select class="form-control" formControlName="end">
|
||||||
<option value="0">Oben</option>
|
<option value="1">Oben</option>
|
||||||
<option value="90">Rechts</option>
|
<option value="0.5">Rechts</option>
|
||||||
<option value="180">Unten</option>
|
<option value="0">Unten</option>
|
||||||
<option value="270">Links</option>
|
<option value="1.5">Links</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-control-label">
|
||||||
|
Vektordehnung
|
||||||
|
</label>
|
||||||
|
<input type="number" class="form-control" formControlName="range" min="0" max="1" step="0.1">
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<h5 class="mt-2">Ausdehnung</h5>
|
||||||
|
<!-- <hr>
|
||||||
|
<small>Anzahl der Schwingungen mittels Knotenpunkten bestimmen und die Skalierung auf der Leinwand einstellen.</small> -->
|
||||||
|
<hr>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-control-label">
|
<label class="form-control-label">
|
||||||
Skalierung
|
Skalierung
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@@ -44,5 +45,5 @@ aside {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 3rem;
|
padding: 3rem;
|
||||||
background: rgba(251, 252, 253, 0.8);
|
background: rgba(251, 252, 253, 0.9);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ export class AppComponent implements OnInit {
|
|||||||
public configForm: FormGroup;
|
public configForm: FormGroup;
|
||||||
|
|
||||||
@HostListener('mousewheel', ['$event'])
|
@HostListener('mousewheel', ['$event'])
|
||||||
private onWheelUp(event) {
|
private onMousewheel(event) {
|
||||||
const delta = Math.sign(event.deltaY);
|
const delta = Math.sign(event.deltaY);
|
||||||
const step = 0.01;
|
const step = env.controls.wheelStep;
|
||||||
|
|
||||||
this.config = {...this.configForm.value};
|
this.config = {...this.configForm.value};
|
||||||
|
|
||||||
@@ -36,13 +36,11 @@ export class AppComponent implements OnInit {
|
|||||||
this.config.scale -= step;
|
this.config.scale -= step;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.config.scale = Math.round(this.config.scale * 100) / 100;
|
this.config.scale = Math.round(this.config.scale / step) * step;
|
||||||
|
|
||||||
this.configForm.reset({...this.config});
|
this.configForm.reset({...this.config});
|
||||||
this.updateGraphs();
|
this.updateGraphs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.canvasParam = {
|
this.canvasParam = {
|
||||||
colors: {
|
colors: {
|
||||||
@@ -67,6 +65,10 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.configForm.reset({...this.config});
|
this.configForm.reset({...this.config});
|
||||||
|
|
||||||
|
// this.configForm.valueChanges.subscribe(val => {
|
||||||
|
// console.log('form changes(appComponent)', this.config);
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateGraphs() {
|
public updateGraphs() {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class GraphsComponent implements AfterViewInit, OnChanges {
|
|||||||
|
|
||||||
public graphs: Graph[];
|
public graphs: Graph[];
|
||||||
public canvas: any | null;
|
public canvas: any | null;
|
||||||
|
public matrix: any | null;
|
||||||
|
|
||||||
@Input() config: any;
|
@Input() config: any;
|
||||||
|
|
||||||
@@ -51,50 +52,43 @@ export class GraphsComponent implements AfterViewInit, OnChanges {
|
|||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
this.updateCanvas();
|
this.updateCanvas();
|
||||||
|
this.updateMatrix();
|
||||||
this.updateGraphs();
|
this.updateGraphs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateGraphs(): void {
|
private updateGraphs(): void {
|
||||||
const matrix = this.matrix;
|
this.graphs = [this.adjustGraph('start'), this.adjustGraph('end')];
|
||||||
|
console.log('graphs component (updateGraphs):', this.graphs);
|
||||||
this.graphs = [...[this.adjustGraph('start'), this.adjustGraph('end')]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private adjustGraph(from: string) {
|
private adjustGraph(from: string) {
|
||||||
const matrix = this.matrix;
|
|
||||||
const to = this.flipflop(from);
|
const to = this.flipflop(from);
|
||||||
|
const startPoint = { x: this.matrix[from].x, y: this.matrix[from].y };
|
||||||
|
const endPoint = { x: this.matrix[to].x, y: this.matrix[to].y };
|
||||||
|
|
||||||
|
console.error(from, '->', to);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: `${from}-to-${to}`,
|
||||||
start: {
|
start: {
|
||||||
coords: { x: matrix[from].x, y: matrix[from].y },
|
coords: startPoint,
|
||||||
direction: this.config.directionStart,
|
direction: this.config.vectors[from],
|
||||||
color: env.guilloche.colors[from]
|
color: env.guilloche.colors.start
|
||||||
}, end: {
|
}, end: {
|
||||||
coords: { x: matrix[to].x, y: matrix[to].y },
|
coords: endPoint,
|
||||||
direction: this.config.directionEnd,
|
direction: this.config.vectors[to],
|
||||||
color: env.guilloche.colors[to]
|
color: env.guilloche.colors.end
|
||||||
},
|
},
|
||||||
stroke: this.config.stroke,
|
stroke: this.config.stroke,
|
||||||
nodes: []
|
nodes: [
|
||||||
|
this.vectorPoint(startPoint, this.config.vectors[from]),
|
||||||
|
this.vectorPoint(endPoint, this.config.vectors[to])
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// {
|
|
||||||
// start: {
|
|
||||||
// coords: { x: matrix.end.x, y: matrix.end.y },
|
|
||||||
// direction: this.config.directionEnd,
|
|
||||||
// color: env.guilloche.colors.start
|
|
||||||
// }, end: {
|
|
||||||
// coords: { x: matrix.start.x, y: matrix.start.y },
|
|
||||||
// direction: this.config.directionStart,
|
|
||||||
// color: env.guilloche.colors.end
|
|
||||||
// },
|
|
||||||
// stroke: this.config.stroke,
|
|
||||||
// nodes: []
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private flipflop(direction: string) {
|
private flipflop(x: string) {
|
||||||
return (direction === 'start') ? 'end' : 'start';
|
return (x === 'start') ? 'end' : 'start';
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateCanvas(): void {
|
private updateCanvas(): void {
|
||||||
@@ -109,15 +103,17 @@ export class GraphsComponent implements AfterViewInit, OnChanges {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private get matrix() {
|
private updateMatrix() {
|
||||||
const totalArea = Math.abs(this.canvas.clientWidth * this.canvas.clientHeight);
|
const totalArea = Math.abs(this.canvas.clientWidth * this.canvas.clientHeight);
|
||||||
const totalCenter = this.centerPoint(this.canvas.clientWidth, this.canvas.clientHeight);
|
const totalCenter = this.centerPoint(this.canvas.clientWidth, this.canvas.clientHeight);
|
||||||
|
|
||||||
const baseArea = Math.abs(this.config.width * this.config.height);
|
const baseArea = Math.abs(this.config.width * this.config.height);
|
||||||
const baseScale = Math.pow(totalArea / baseArea * this.config.scale, 0.5);
|
const baseScale = Math.pow(totalArea / baseArea * this.config.scale, 0.5);
|
||||||
const baseCenter = this.centerPoint( baseScale * this.config.width, baseScale * this.config.height);
|
const baseWidthScaled = baseScale * this.config.width;
|
||||||
|
const baseHeightScaled = baseScale * this.config.height;
|
||||||
|
const baseCenter = this.centerPoint(baseWidthScaled, baseHeightScaled);
|
||||||
|
|
||||||
return {
|
this.matrix = {
|
||||||
start: {
|
start: {
|
||||||
x: totalCenter.x - baseCenter.x,
|
x: totalCenter.x - baseCenter.x,
|
||||||
y: totalCenter.y + baseCenter.y
|
y: totalCenter.y + baseCenter.y
|
||||||
@@ -125,7 +121,29 @@ export class GraphsComponent implements AfterViewInit, OnChanges {
|
|||||||
end: {
|
end: {
|
||||||
x: totalCenter.x + baseCenter.x,
|
x: totalCenter.x + baseCenter.x,
|
||||||
y: totalCenter.y - baseCenter.y
|
y: totalCenter.y - baseCenter.y
|
||||||
}
|
},
|
||||||
|
width: baseWidthScaled,
|
||||||
|
height: baseHeightScaled
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private vectorPoint(point: Point, direction: number) {
|
||||||
|
const range = this.Δ(this.matrix.start, this.matrix.end) * this.config.vectors.range;
|
||||||
|
|
||||||
|
console.log('graphs component(vectorPoint)', point, direction);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: range * Math.sin(Math.PI * direction) + point.x,
|
||||||
|
y: range * Math.cos(Math.PI * direction) + point.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate distance between to points with coordinates.
|
||||||
|
* @param a
|
||||||
|
* @param b
|
||||||
|
*/
|
||||||
|
private Δ(a: Point, b: Point) {
|
||||||
|
return Math.pow(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2), 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as Shape from 'd3-shape';
|
|||||||
import * as Random from 'd3-random';
|
import * as Random from 'd3-random';
|
||||||
import * as Drag from 'd3-drag';
|
import * as Drag from 'd3-drag';
|
||||||
|
|
||||||
|
import { environment as env } from './../../environments/environment';
|
||||||
import { Config } from './../models/config.model';
|
import { Config } from './../models/config.model';
|
||||||
import { Point } from './../models/point.model';
|
import { Point } from './../models/point.model';
|
||||||
import { Param } from './../models/param.model';
|
import { Param } from './../models/param.model';
|
||||||
@@ -27,16 +28,16 @@ export class GuillocheDirective implements OnChanges {
|
|||||||
) {
|
) {
|
||||||
this.group = Selection.select(el.nativeElement);
|
this.group = Selection.select(el.nativeElement);
|
||||||
this.canvas = Selection.select(this.canvasService.get);
|
this.canvas = Selection.select(this.canvasService.get);
|
||||||
this.gradientId = 'linear';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
console.log('guilloche directive (changes)', changes.graph.currentValue);
|
console.log('guilloche directive (changes)', changes.graph.currentValue);
|
||||||
|
this.defineGradient();
|
||||||
this.drawGraph();
|
this.drawGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawGraph(): void {
|
private defineGradient(): void {
|
||||||
console.log('guilloche directive(drawGraph)', this.graph);
|
this.gradientId = `gradient-${this.graph.id}`;
|
||||||
|
|
||||||
const defs = this.group.append('defs');
|
const defs = this.group.append('defs');
|
||||||
const grad = defs.append('linearGradient')
|
const grad = defs.append('linearGradient')
|
||||||
@@ -47,19 +48,21 @@ export class GuillocheDirective implements OnChanges {
|
|||||||
grad.append('stop')
|
grad.append('stop')
|
||||||
.attr('stop-color', this.graph.end.color)
|
.attr('stop-color', this.graph.end.color)
|
||||||
.attr('offset', '100%');
|
.attr('offset', '100%');
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawGraph(): void {
|
||||||
|
const points = [this.graph.start.coords, ...this.graph.nodes, this.graph.end.coords];
|
||||||
|
|
||||||
this.group.append('path')
|
this.group.append('path')
|
||||||
.attr('d', Shape.line()
|
.attr('d', Shape.line()
|
||||||
.x(p => p.x)
|
.x(p => p.x)
|
||||||
.y(p => p.y)
|
.y(p => p.y)
|
||||||
.curve(Shape.curveBasis)([
|
.curve(Shape.curveBasis)(points))
|
||||||
this.graph.start.coords,
|
|
||||||
this.graph.end.coords
|
|
||||||
]))
|
|
||||||
.attr('stroke', `url(#${this.gradientId})`)
|
.attr('stroke', `url(#${this.gradientId})`)
|
||||||
.attr('stroke-width', this.graph.stroke)
|
.attr('stroke-width', this.graph.stroke)
|
||||||
.attr('fill', 'none');
|
.attr('fill', 'none');
|
||||||
|
|
||||||
|
if (!env.production) {
|
||||||
this.group.append('circle')
|
this.group.append('circle')
|
||||||
.attr('cx', this.graph.start.coords.x)
|
.attr('cx', this.graph.start.coords.x)
|
||||||
.attr('cy', this.graph.start.coords.y)
|
.attr('cy', this.graph.start.coords.y)
|
||||||
@@ -75,5 +78,18 @@ export class GuillocheDirective implements OnChanges {
|
|||||||
.attr('stroke-width', 1)
|
.attr('stroke-width', 1)
|
||||||
.attr('fill-opacity', 0)
|
.attr('fill-opacity', 0)
|
||||||
.attr('stroke', this.graph.end.color);
|
.attr('stroke', this.graph.end.color);
|
||||||
|
|
||||||
|
this.graph.nodes.forEach(point => {
|
||||||
|
this.group.append('circle')
|
||||||
|
.attr('cx', point.x)
|
||||||
|
.attr('cy', point.y)
|
||||||
|
.attr('r', 5)
|
||||||
|
.attr('stroke-width', 1)
|
||||||
|
.attr('fill-opacity', 0)
|
||||||
|
.attr('stroke', 'darkgray');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('guilloche directive(drawGraph)', this.graph);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,20 @@ const fb = new FormBuilder();
|
|||||||
export let ConfigForm: FormGroup = fb.group({
|
export let ConfigForm: FormGroup = fb.group({
|
||||||
width: fb.control('', Validators.required),
|
width: fb.control('', Validators.required),
|
||||||
height: fb.control('', Validators.required),
|
height: fb.control('', Validators.required),
|
||||||
directionStart: fb.control('', Validators.compose([
|
vectors: fb.group({
|
||||||
|
start: fb.control('', Validators.compose([
|
||||||
Validators.min(0),
|
Validators.min(0),
|
||||||
Validators.max(360)
|
Validators.max(2)
|
||||||
])),
|
])),
|
||||||
directionEnd: fb.control('', Validators.compose([
|
end: fb.control('', Validators.compose([
|
||||||
Validators.min(0),
|
Validators.min(0),
|
||||||
Validators.max(360)
|
Validators.max(2)
|
||||||
])),
|
])),
|
||||||
|
range: fb.control('', Validators.compose([
|
||||||
|
Validators.min(0),
|
||||||
|
Validators.max(1)
|
||||||
|
]))
|
||||||
|
}),
|
||||||
nodes: fb.control('', Validators.compose([
|
nodes: fb.control('', Validators.compose([
|
||||||
Validators.min(1),
|
Validators.min(1),
|
||||||
Validators.max(10)
|
Validators.max(10)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Point } from './point.model';
|
import { Point } from './point.model';
|
||||||
|
|
||||||
export interface Graph {
|
export interface Graph {
|
||||||
|
id: string;
|
||||||
start: {
|
start: {
|
||||||
coords: Point;
|
coords: Point;
|
||||||
direction: number; // degree between 0 and 360
|
direction: number; // degree between 0 and 360
|
||||||
|
|||||||
@@ -6,11 +6,17 @@ export const environment = {
|
|||||||
end: '#5eb1bd'
|
end: '#5eb1bd'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
controls: {
|
||||||
|
wheelStep: 0.01
|
||||||
|
},
|
||||||
formDefaults: {
|
formDefaults: {
|
||||||
width: 9,
|
width: 9,
|
||||||
height: 16,
|
height: 16,
|
||||||
directionStart: 0,
|
vectors: {
|
||||||
directionEnd: 180,
|
start: 1,
|
||||||
|
end: 0,
|
||||||
|
range: 0.3
|
||||||
|
},
|
||||||
nodes: 3,
|
nodes: 3,
|
||||||
stroke: 1,
|
stroke: 1,
|
||||||
scale: 0.3
|
scale: 0.3
|
||||||
|
|||||||
@@ -10,14 +10,20 @@ export const environment = {
|
|||||||
end: '#0067cc'
|
end: '#0067cc'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
controls: {
|
||||||
|
wheelStep: 0.01
|
||||||
|
},
|
||||||
formDefaults: {
|
formDefaults: {
|
||||||
width: 9,
|
width: 9,
|
||||||
height: 16,
|
height: 16,
|
||||||
directionStart: 0,
|
vectors: {
|
||||||
directionEnd: 180,
|
start: 1,
|
||||||
|
end: 0,
|
||||||
|
range: 0.3
|
||||||
|
},
|
||||||
nodes: 3,
|
nodes: 3,
|
||||||
stroke: 1,
|
stroke: 1,
|
||||||
scale: 0.3
|
scale: 0.1
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user