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

Viewed 2257

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

3 Answers

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

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

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à!

Related