Best way to smoothly stop a rotating 3D CSS cube on the camera-facing side?

Viewed 60

I have a small 3D cube written in CSS and JavaScript / Angular that rotates on one axis using the "transform: rotateX" method.

It's designed so that when it's clicked on, it rotates up or down in the mouse's direction - this works fairly well. rotateX is powered by a torque control and a rotation value.

However, when released I'd like it to come to a smooth stop on the "camera-facing" side. This.. is very much a hit or miss. Sometimes it works smoothly, others it decides to jump to the next panel on the cube.

At the moment, I have hardcoded detecting which side it's on but there's probably a better way - this might be part of the problem. With the code I have now, is there a better method to get the stop effect?

In Action: Video of rolling dice under user control

HTML:

<div class="dice"
(mousedown)="mouseDown($event)"
(dblclick)="doubleClick()"
>
<div class="cube" [ngStyle]="{'transform': 'rotateX(' + rotation  + 'deg)'}">
  <div #pane class="face front">4</div>
  <div #pane class="face bottom">3</div>
  <div #pane class="face back">2</div>
  <div #pane class="face top">1</div>
</div>   

JavaScript:

import { Component, OnInit, HostListener, ElementRef, ViewChildren, QueryList } from '@angular/core';

@Component({
  selector: 'roller',
  templateUrl: './roller.component.html',
  styleUrls: ['./roller.component.scss']
})
export class RollerComponent implements OnInit {
  //@ts-ignore
  @ViewChildren("pane", { static: false }) diePanels: QueryList<ElementRef>;

  controlDown: boolean = false;

  rotationSpeed: number = 2;
  friction: number = 0.1;
  frictionCutoff: number = 0.93;

  mouseY: number = 0;
  lastY: number = 0;
  delta: number = 0;
  torque: number = 0;
  rotation: number = 0;
  lastSide: number = 0;
  currentSide: number = 0;
  targetMovement: number = 0;
  torqueTarget: number = 0;
  torqueMaxSpeed: number = 8;

  stopAt: number = 0;

  // Get an array of all dice panels // 
  panels: ElementRef[] = [];

  constructor() { }

  ngOnInit(): void {
    // Lifecycle Placeholder //
  }

  ngAfterViewInit()
  {
    this.panels = this.diePanels.toArray();
    // In case a reference to the parent div is needed //
    // let parent = this.panels[0].nativeElement.parentNode.parentNode;
  }  

  mouseDown(e: any) {
    this.controlDown = true;
    this.mouseY = e.pageY;
    this.lastY = this.mouseY;
  }

  @HostListener('document:mouseup', ['$event'])
  mouseUp()
  {
    // Listen for mouse up when not on div //
    this.controlDown = false;
  }

  @HostListener('document:mousemove', ['$event']) 
  onMouseMove(e) {
    if (this.controlDown)
    {
      // Listen for mouse movement //
      this.mouseY = e.pageY;
    }  
    
  }

  doubleClick()
  {
    // Double Click Test //
    console.log(this.controlDown);
  }

  animate(timeDelta: number)
  {
    // Capture the difference in the click position and current mouse pos
    this.delta = this.mouseY - this.lastY;


    // Clamp Rotation to 0-360 //
    if(this.rotation > 360) {
      this.rotation = 0;
    } else if(this.rotation < 0) {
      this.rotation = 360;
    } 

    // Control Dice when mouse is clicked on it 
    if (this.controlDown)
    {
      this.torque = this.torque * this.frictionCutoff + (this.delta * this.rotationSpeed - this.torque) * this.friction;

      // Limit Torque Speed
      if (this.torque >= this.torqueMaxSpeed)
      {
        this.torque =  this.torqueMaxSpeed;
      } else if (this.torque <= -this.torqueMaxSpeed) {
        this.torque = -this.torqueMaxSpeed;
      } 

      this.rotation -= this.torque;
    } else {
      // Slow dice and stop //
      this.delta = 0;
      this.lastY = 0;
      this.torque = 0;

      let rot = Math.floor(this.rotation);
      // Hardcoding to detect which side is "facing" the viewer //
      if (rot > 0 && rot < 90)
      {
        this.torqueTarget = 90;
        this.currentSide = 0;
      } else if (rot > 90 && rot < 180) {
        this.torqueTarget = 180;
        this.currentSide = 1;
      } else if (rot > 180 && rot < 270) {
        this.torqueTarget = 270;
        this.currentSide = 2;
      } else if (rot > 270 && rot < 360) {
        this.torqueTarget = 360;
        this.currentSide = 3;
      }

      this.rotation = this.lerp(this.rotation, this.torqueTarget, 0.5);
    }
  }

  lerp(start: number, end: number, time: number)
  {
    return (1-time)*start+time*end;
  }

}

Per Request: SCSS

$diceSize: 55px;

.dice {
    width: $diceSize;
    height: $diceSize;
    margin: 335px auto 0;
    cursor: pointer;
    box-shadow: 4px 5px 6px -1px rgba(0,0,0,0.94);
    -webkit-box-shadow: 4px 5px 6px -1px rgba(0,0,0,0.94);
    -moz-box-shadow: 4px 5px 6px -1px rgba(0,0,0,0.94);    
}
  
  .cube {
    transform-style: preserve-3d;
    width: 100%;
    height: 100%;
    position: relative;
    animation-fill-mode: backwards;
  }
  
  .face {
    display: flex;
    position: absolute;
    font-size: 2.5rem;
    align-content: center;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    background: rgb(255, 255, 255);
    background-image: url('./../../../assets/DiceBackground.png');
    background-size: auto 100%;
    border: rgb(68, 68, 68) inset 1px;
    pointer-events: none;
    user-select: none;
  }
  
  .top {
    transform: rotateX(90deg)     
    translateZ(calc($diceSize / 2));
  }
  .bottom {
    transform: rotateX(-90deg) 
    translateZ(calc($diceSize / 2));
  }
  
  .front {
    transform: rotateX(0deg) 
    translateZ(calc($diceSize / 2));
  }

  .back {
    transform: rotateX(-180deg) 
    translateZ(calc($diceSize / 2));
  }
0 Answers
Related