6

Angular CDK drag-drop with zoom by CSS-property “transform: scale(0.5)” doesn’t work as expected.

If the outer-DIV is scaled by CSS-property “transform: scale(0.5)” the drag doesn’t align properly with the mouse pointer. This happens as soon as the scale is unequal to 1.

Here an example: https://stackblitz.com/edit/angular-2q1mte

I’m aware of this post Drag and drop with pinch zoom doesn't work as expected and therefore the

“@Input('cdkDragConstrainPosition') constrainPosition: (point: Point, dragRef: DragRef) => Point”.

But how to write the custom logic to map the drag properly with the pointer? Or is there any other solution to provide the zoom functionality but keep the drag aligned properly with the mouse pointer?

Any help appreciated

Harsha Murupudi
  • 579
  • 1
  • 6
  • 19
pguysor
  • 63
  • 1
  • 6

3 Answers3

4

I've managed to get it working in Angular 12 without using calc() but keeping the transorm: scale(x):

Template:

<div cdkDrag (cdkDragStarted)="startDragging($event)" [cdkDragFreeDragPosition]="dragPosition" [cdkDragConstrainPosition]="dragConstrainPoint">

Component:

_dragPosition: { x: number, y: number } = {x: 0, y: 0};

get dragPosition(): { x: number, y: number } {
  return this._dragPosition;
}


dragConstrainPoint = (point: any, dragRef: DragRef) => {
  let zoomMoveXDifference = 0;
  let zoomMoveYDifference = 0;
  if (this.scale != 1) {
    zoomMoveXDifference = (1 - this.scale) * dragRef.getFreeDragPosition().x;
    zoomMoveYDifference = (1 - this.scale) * dragRef.getFreeDragPosition().y;
  }

  return {
    x: point.x + zoomMoveXDifference as number,
    y: point.y + zoomMoveYDifference as number
  };
};

startDragging(event: CdkDragStart) {
  const position = {
    x: this.dragPosition.x * this.scale,
    y: this.dragPosition.y * this.scale
  };
  event.source._dragRef.setFreeDragPosition(position);
  (event.source._dragRef as any)._activeTransform = this.dragPosition;
  (event.source._dragRef as any)._applyRootElementTransform(this.dragPosition.x, this.dragPosition.y);
}
Mrniach
  • 41
  • 1
3

i got the problem too.

Here the solution : initialize a scale variable and use it the dragConstrainPoint

  // change position of object while zooming ! (cdk fix)
    dragConstrainPoint = (point, dragRef) => {
      let zoomMoveXDifference = 0; 
      let zoomMoveYDifference = 0; 
      if (this._scale !- 1) {
       zoomMoveXDifference = (1 - this._scale) * dragRef.getFreeDragPosition().x;
       zoomMoveYDifference = (1 - this._scale) * dragRef.getFreeDragPosition().y;
      }
      return {
        x: point.x + zoomMoveXDifference ,
        y: point.y + zoomMoveYDifference
      };
Jimmy Pannier
  • 261
  • 1
  • 2
  • 16
  • Many thanks. It solves the problem regarding the mouse pointer alignment. However, if you drag&drop an item twice, the second time it just jumps away from the mouse pointer. The freeDragPosition is not set back to 0,0 on a new drag-start. Check out the problem here [https://stackblitz.com/edit/angular-2q1mte-bf9u2y](https://stackblitz.com/edit/angular-2q1mte-bf9u2y). Any ideas how to solve this issue? – pguysor Oct 03 '20 at 23:37
  • Try to add (cdkDragEnded)="endDragging($event)" Implement the method endDragging : set manually the position x and y of your div ( get new position via event.source.getRootElement().getBoundingClientRect() ) and finally reset the cdkDrag like that: (event.source as CdkDrag).reset(); – Jimmy Pannier Oct 04 '20 at 09:43
  • x and y of the new position have to be divided by scale when manually setting the new position – Jimmy Pannier Oct 05 '20 at 05:46
  • Many thanks. Unfortunately (after hours of trying), I still can't get the correct behaviour. Here [https://stackblitz.com/edit/angular-2q1mte-sqhrbp](https://stackblitz.com/edit/angular-2q1mte-sqhrbp) I just implemented your suggestion. Really appreciate your hints. – pguysor Oct 06 '20 at 14:19
  • endDragging($event) { const elementMoving = $event.source.getRootElement(); const elementMovingRect: ClientRect = elementMoving.getBoundingClientRect(); let div = document.getElementById('dddiv'); let xpos =(elementMovingRect.left-elementMoving.parentElement.getBoundingClientRect().left)/this.zoomScale; div.style.left = xpos + "px"; let ypos = (elementMovingRect.top-elementMoving.parentElement.getBoundingClientRect().top)/this.zoomScale; div.style.top = ypos + "px"; const cdkDrag = $event.source as CdkDrag; cdkDrag.reset(); } – Jimmy Pannier Oct 06 '20 at 20:13
  • And you will have to change your implementation of cdkDragBoundary=".example-boundary" because it works like a charm when you remove it. https://angular-2q1mte-tmzbto.stackblitz.io – Jimmy Pannier Oct 06 '20 at 20:16
  • Many thanks for taking the time! It works perfectly now. Best regards. – pguysor Oct 08 '20 at 06:42
1

I had the same issue with scaling a full scene with drag and drop using CDK material with Angular 11.

The dragConstrainPoint solution did not work for my context. The idea is really to avoid transform scale or zoom, has it mess with the mouse and objects positions, making it a nightmare with multiplayer drag & drop.

My solution works well in multiplayer having different scene scales

Solution: I have used CSS variable --scale on a wrapper div, with dynamic scale calculation

<div class="game-wrapper" style="{{'--scale:'+ gameScale}}">

In Angular the scene scale is calculated like this:

...
  private _unsubscriber$: Subject<any> = new Subject();
  public screenWidth$: BehaviorSubject<number> = new BehaviorSubject(null);
  public screenHeight$: BehaviorSubject<number> = new BehaviorSubject(null);
  public gameScale: number = 1;

public constructor(
    @Inject(DOCUMENT) private document: any
  ) { }
...


ngOnInit(): void {
   
    //Manage screen width
    this._setScreenResolution(window.innerWidth, window.innerHeight);
    fromEvent(window, 'resize')
      .pipe(
        debounceTime(1000),
        takeUntil(this._unsubscriber$)
      ).subscribe((evt: any) => {
      this._setScreenResolution(evt.target.innerWidth, evt.target.innerHeight);
    });
  }

  ngOnDestroy() {
    this._unsubscriber$.next();
    this._unsubscriber$.complete();
  }

  private _setScreenResolution(width: number, height: number): void {

    this.screenWidth$.next(width);
    this.screenHeight$.next(height);

    let scaleX = windowWidth / this.sceneWidth;
    let scaleY = windowHeight / this.sceneHeight;
    let scale = scaleX;

    if(scaleX > scaleY) {
      scale = scaleY;
    }
    //console.log("getGameScale", scale.toFixed(3));
    scale = parseFloat(scale.toFixed(3));
    this.gameScale = scale;
    
  }

In the HTML templates and CSS, at each time pixels are involved, we add the calc().

Here some exemples of CSS with those calculs

width: calc(18px * var(--scale));
padding: calc(1px * var(-scale)) calc(6px * var(-scale));
background-size: calc(30px * var(-scale)) calc(30px * var(-scale)), cover;
margin: calc(20px * var(-scale)) calc(30px * var(-scale));
bottom: calc(300px * var(--scale));
border-radius: calc(4px * var(--scale));
font-size: calc(21px * var(--scale));
left: calc(10px * var(--scale));
top: calc(4px * var(--scale));
box-shadow: 0 0 0 calc(4px * var(--scale)) rgba(0,0,0,0.1) inset;
border: calc(2px * var(--scale)) solid #6898f3;
height: calc(32px * var(--scale));
min-height: calc(26px * var(--scale));
line-height: calc(42px * var(--scale));

For the local CDK drag move and drag end values to be well sent to other multiplayers, you need to scale them by dividing by the scale:

  dragMoved(event: CdkDragMove) {

      let {offsetLeft, offsetTop} = event.source.element.nativeElement;
      let {x, y} = event.distance;

      x = x / this.gameScale;
      y = y / this.gameScale;
      offsetLeft = offsetLeft / this.gameScale;
      offsetTop = offsetTop / this.gameScale;

      //Remove scene offset from calcul (ex. for aside left menu)
      let xpos = ((offsetLeft + x) - this.offset.x);
      let ypos = ((offsetTop + y) - this.offset.y);
      let position: any = {x: xpos, y: ypos};
    
      //The position is then sent to a multiplayer management relay message...
    
  }

//same for drag end + do not forget to reset the transform 3D at the end of the function
dragEnded(event: CdkDragEnd) {
...

    //The position at drag end is set on the object and for all other players

    //Reset translate3D
    //event.source._dragRef.setFreeDragPosition({x: 0, y: 0});
    event.source._dragRef.reset();

}

Voilà!

Dharman
  • 30,962
  • 25
  • 85
  • 135