My user should be able to move (or rotate) an object by mouse in a canvas. When mouse events occur the screen coordinates are used to calculate the delta (direction and length) to the last event. Nothing special...
- mousedown (get the first coordinate)
- mousemove (get nth coordinate, calculate deltaXY, move object by deltaXY)
- mouseup (same as step 2 and stop the mousemove and mouseup event handling)
After this chain of events it should be possible to repeat the same action.
This outdated example works as expected, after removing the toRx
calls. But here the delta to the first coordinate is determined: github.com:rx-draggable
Here is my effort to adapt the code from the example:
@Component({
selector: 'home',
providers: [Scene],
template: '<canvas #canvas id="3dview"></canvas>'
})
export class Home {
@ViewChild('canvas') canvas: ElementRef;
private scene: Scene;
private mousedrag = new EventEmitter();
private mouseup = new EventEmitter<MouseEvent>();
private mousedown = new EventEmitter<MouseEvent>();
private mousemove = new EventEmitter<MouseEvent>();
private last: MouseEvent;
private el: HTMLElement;
@HostListener('mouseup', ['$event'])
onMouseup(event: MouseEvent) { this.mouseup.emit(event); }
@HostListener('mousemove', ['$event'])
onMousemove(event: MouseEvent) { this.mousemove.emit(event); }
constructor(@Inject(ElementRef) elementRef: ElementRef, scene: Scene) {
this.el = elementRef.nativeElement;
this.scene = scene;
}
@HostListener('mousedown', ['$event'])
mouseHandling(event) {
event.preventDefault();
console.log('mousedown', event);
this.last = event;
this.mousemove.subscribe({next: evt => {
console.log('mousemove.subscribe', evt);
this.mousedrag.emit(evt);
}});
this.mouseup.subscribe({next: evt => {
console.log('mousemove.subscribe', evt);
this.mousedrag.emit(evt);
this.mousemove.unsubscribe();
this.mouseup.unsubscribe();
}});
}
ngOnInit() {
console.log('init');
this.mousedrag.subscribe({
next: evt => {
console.log('mousedrag.subscribe', evt);
this.scene.rotate(
evt.clientX - this.last.clientX,
evt.clientY - this.last.clientY);
this.last = evt;
}
});
}
...
}
It works only for one cycle. After the mouseup
event i got this error:
Uncaught EXCEPTION: Error during evaluation of "mousemove"
ORIGINAL EXCEPTION: ObjectUnsubscribedError
ERROR CONTEXT: EventEvaluationErrorContext
The cancellation of the mousemove
subscription does not work. The error is repeating for all following mousemoves.
Do you have an idea what is wrong with my code? Is there a different elegant approach to solve this problem?
I believe your problem lies in the difference between unsubscribe()
and remove(sub : Subscription)
on an EventEmitter
. But it is possible to do it without the use of subscriptions (except the ones created by a @HostListener
) and make it easy to read. I've rewritten your code a little. You might consider though placing your mouseup
event
on the document
or window
, otherwise you get weird behaviour if you release your mouse outside the canvas.
Warning: untested code ahead
@Component({
selector: 'home',
providers: [Scene],
template: '<canvas #canvas id="3dview"></canvas>'
})
export class Home {
@ViewChild('canvas')
canvas: ElementRef;
private scene: Scene;
private last: MouseEvent;
private el: HTMLElement;
private mouseDown : boolean = false;
@HostListener('mouseup')
onMouseup() {
this.mouseDown = false;
}
@HostListener('mousemove', ['$event'])
onMousemove(event: MouseEvent) {
if(this.mouseDown) {
this.scene.rotate(
event.clientX - this.last.clientX,
event.clientY - this.last.clientY
);
this.last = event;
}
}
@HostListener('mousedown', ['$event'])
onMousedown(event) {
this.mouseDown = true;
this.last = event;
}
constructor(elementRef: ElementRef, scene: Scene) {
this.el = elementRef.nativeElement;
this.scene = scene;
}
}
The problem that you have is that the code is not reactive. In reactive programming all behaviors should be defined at decoration time and only one subscription is required.
Here is an example: Angular2/rxjs mouse translation/rotation
import {Component, NgModule, OnInit, ViewChild} from '@angular/core'
import {BrowserModule, ElementRef, MouseEvent} from '@angular/platform-browser'
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMapTo';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/combineLatest';
import 'rxjs/add/operator/startWith';
@Component({
selector: 'my-app',
styles: [`
canvas{
border: 1px solid red;
}`],
template: `
<div>
<h2>translate/Rotate by mouse</h2>
<canvas #canvas id="3dview"></canvas>
<p>Translate by delta: {{relativeTo$|async|json}}</p>
<p>Rotate by angle: {{rotateToAngle$|async|json}}</p>
</div>
`
})
export class App extends OnInit {
@ViewChild('canvas')
canvas: ElementRef;
relativeTo$: Observable<{dx:number, dy:number, start: MouseEvent}>;
rotateToAngle$: Observable<{angle:number, start: MouseEvent}>;
ngOnInit() {
const canvasNE = this.canvas.nativeElement;
const mouseDown$ = Observable.fromEvent(canvasNE, 'mousedown');
const mouseMove$ = Observable.fromEvent(canvasNE, 'mousemove');
const mouseUp$ = Observable.fromEvent(canvasNE, 'mouseup');
const moveUntilMouseUp$= mouseMove$.takeUntil(mouseUp$);
const startRotate$ = mouseDown$.switchMapTo(moveUntilMouseUp$.startWith(null));
const relativePoint = (start: MouseEvent, end: MouseEvent): {x:number, y:number} =>
(start && end && {
dx: start.clientX - end.clientX,
dy: start.clientY - end.clientY,
start: start
} || {});
this.relativeTo$ = startRotate$
.combineLatest(mouseDown$)
.map(arr => relativePoint(arr[0],arr[1]));
this.rotateToAngle$ = this.relativeTo$
.map((tr) => ({angle: Math.atan2(tr.dy, tr.dx), start: tr.start}));
// this.relativeTo$.subscribe(console.log.bind(console,'rotate:'));
// this.rotateToAngle$.subscribe(console.log.bind(console,'rotate 0:'));
}
}
@NgModule({
imports: [ BrowserModule ],
declarations: [ App ],
bootstrap: [ App ]
})
export class AppModule {}
来源:https://stackoverflow.com/questions/36056628/angular2-mouse-event-handling-movement-relative-to-current-position