web-dev-qa-db-fra.com

Angular2: gestion des événements de souris (mouvement par rapport à la position actuelle)

Mon utilisateur doit pouvoir déplacer (ou faire pivoter) un objet avec la souris dans un canevas. Lorsque des événements de souris se produisent, les coordonnées d'écran sont utilisées pour calculer le delta (direction et longueur) du dernier événement. Rien de spécial...

  1. mousedown (obtenir la première coordonnée)
  2. mousemove (obtenir la nième coordonnée, calculer deltaXY, déplacer l'objet par deltaXY)
  3. mouseup (identique à l'étape 2 et arrêtez le mousemove et la gestion des événements mouseup)

Après cette chaîne d'événements, il devrait être possible de répéter la même action.

Cet exemple obsolète fonctionne comme prévu, après avoir supprimé les appels toRx. Mais ici, le delta de la première coordonnée est déterminé: github.com:rx-draggable

Voici mes efforts pour adapter le code de l'exemple:

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

Cela ne fonctionne que pour un cycle. Après l'événement mouseup, j'ai eu cette erreur:

EXCEPTION non détectée: erreur lors de l'évaluation de "mousemove"

EXCEPTION ORIGINALE: ObjectUnsubscribedError

CONTEXTE D'ERREUR: EventEvaluationErrorContext

L'annulation de l'abonnement mousemove ne fonctionne pas. L'erreur se répète pour tous les mouvements de souris suivants.

Avez-vous une idée de ce qui ne va pas avec mon code? Existe-t-il une approche élégante différente pour résoudre ce problème?

14
Meiko Rachimow

Je crois que votre problème réside dans la différence entre unsubscribe() et remove(sub : Subscription) sur un EventEmitter. Mais il est possible de le faire sans utiliser d'abonnements (sauf ceux créés par un @HostListener) et le rendre facile à lire. J'ai un peu réécrit votre code. Vous pourriez envisager de placer votre mouseupevent sur le document ou window, sinon vous obtiendrez un comportement étrange si vous relâchez votre souris en dehors du canevas.

Attention: code non testé à l'avance

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

Le problème que vous avez est que le code n'est pas réactif. Dans la programmation réactive, tous les comportements doivent être définis au moment de la décoration et un seul abonnement est requis.

Voici un exemple: translation/rotation de la souris Angular2/rxjs

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 {}
4
user55993