Rotating psuedo-3d carousel in Angular

Viewed 314

I'm trying to create a psuedo-3d carousel with 5 items like the following (and have them cycle around):

enter image description here

I found this great stackblitz as a starting point, and have been playing with it for hours trying to make it have 5 items instead of 3 but I'm having a really hard time understanding how it works.

If someone smarter than me can help me understand how to make this 5 items it would be great. In particular, I dont understand how this movements variable should be configured, and that is probably the entire secret sauce here.

movements = [
    { pos: 0, right: [1, 2], left: [8, 7] },
    { pos: 2, right: [3, 4, 5, 6, 7], left: [1, 0] },
    { pos: 7, right: [8, 0], left: [6, 5, 4, 3, 2] }
];
2 Answers

sorry, I think I the author. and I explained very bad the code. See that you see in your carousel 5 items at time, So draw a circunference and divide in 8 part. numbered so the most bottom as 0, and to the left 1, 2,... some like

    4
 3     5
2       6
 1     7
    0

The animates (the faces you see) are 0,1,2,6 and 7

animates = [0, 1,2,6,7];

And the position are

movements = [
    { pos: 0, right: [1], left: [7] },
    { pos: 1, right: [2], left: [0] },
    { pos: 2, right: [3,4,5,6], left: [1] },
    { pos: 6, right: [7], left: [5,4,3,2] },
    { pos: 7, right: [0], left: [6] },
  ];

that, e.g. the face is in position 0, to the rigth goes to position 1, to the left goes to position 7 the face is in position 1, to the rigth goes to position 1, to the left goes to position 0 the face is in position 2, to the rigth goes to positions 3,4,5 and 6, to the left goes to position 1.

Well, you need take carefull, when we calculate the angle, you divided by 8 (not by 9)

const animations = mov[direction].map(m => {
    const angle = (m * 2 * Math.PI) / 8; //<--see the "8"   
    ....
}

You can choose, instead of divided by 8, divided by 16, to make the movement more realistic. In this case you see the faces

          8
       7     9
    6          10
  5              11 
 4                12
  3              13
    2          14
      1      15
          0

See that in this case the faces visible are 0,2,4,12 y 14

animates=[0,2,4,12,14]

The positions are

movements = [
    { pos: 0, right: [1,2], left: [15,14] },
    { pos: 2, right: [3,4], left: [1,0] },
    { pos: 4, right: [5,6,7,8,9,10,11,12], left: [3,2] },
    { pos: 12, right: [13,14], left: [11,10,9,8,7,6,5,4] },
    { pos: 14, right: [15,0], left: [13,12] },
  ];

And you need divided by 16

const animations = mov[direction].map(m => {
    const angle = (m * 2 * Math.PI) / 16; //<--see the "16"   
    ....
}

I forked the stackblitz here

Update the carousel only move to left, rigth one step. We can improve it if we defined other movements to allow move to left and right two steps

movementsTwo=[
    { pos: 0, right: [1,2,3,4], left: [15,14,13,12] },
    { pos: 2, right: [3,4,5,6,7,8,9,10,11,12], left: [1,0,15,14] },
    { pos: 4, right: [5,6,7,8,9,10,11,12,13,14], left: [3,2,1,0] },
    { pos: 12, right: [13,14,15,0], left: [11,10,9,8,7,6,5,4,3,2] },
    { pos: 14, right: [15,0,1,2], left: [13,12,11,10,9,8,7,6,5,4] },

  ]

And change the function animateViews to allow move one or two steps

  animateViews(direction: string,steps:number=1) {
    this.animates.forEach((x: number, index: number) => {
      //here use one or another array
      const movements=steps==1?this.movements:this.movementsTwo;
      const mov = movements.find(m => m.pos == x);
      ...
  }

This allow us write a function to "move to front" one element

  indexToFront(index: any) {
    index = +index;
    const pos = this.animates[+index];
    if (pos) {
      const mov = this.movements.find((x) => x.pos == pos);
      const mov2 = this.movementsTwo.find((x) => x.pos == pos);
      const anim = { direction: 'right', steps: 1 };
      if (
        mov.left[mov.left.length - 1] == 0 ||
        mov2.left[mov2.left.length - 1] == 0
      )
        anim.direction = 'left';
      if (
        mov2.left[mov2.left.length - 1] == 0 ||
        mov2.right[mov2.right.length - 1] == 0
      )
        anim.steps = 2;

      this.animateViews(anim.direction, anim.steps);
    }
  }

Well, the only is write a select

<select #select (change)="indexToFront(select.value)">
  <option *ngFor="let i of [0,1,2,3,4]">{{i}}</option>
</select>

Or add a (click) event to our "carousel"

<div style="position:relative;margin-top:150px">
  <div class="carousel" >
    <div #element *ngFor="let i of [0,2,4,12,14];let index=index" 
       (click)="animates[index]!=0 && indexToFront(index)" 
       class="carousel__cell">{{index}}
      </div>
</div>

I create another stackblitz

NOTE: Really "harcoded" the movemnet don't like so much. If I have a bit time I'll try to create a component, but I can not promise when :(

Update 2 I made a component speudo-carousel for 5 images. The main change is that, in this one, I use transfom to move the "cards". So the function animateView becomes like

animateViews(direction: string, steps: number = 1) {
    this.animates.forEach((x: number, index: number) => {
      const movements = steps == 1 ? this.movements : this.movementsTwo;
      const mov = movements.find((m) => m.pos == x);
      const item = this.itemsView.find((_x, i) => i == index);
      const animations = mov[direction].map((m) => {
        const angle = (m * 2 * Math.PI) / 16;
        const scale =
          (1 + this.minScale) / 2 + ((1 - this.minScale) / 2) * Math.cos(angle);
        const applystyle = {
          transform:'translate('+
               (this.radius * Math.sin(angle)) + 'px,'+
               (Math.floor(this.top * scale)-this.top)+'px) 
               scale('+scale+')',
          'z-index': Math.floor(100 * scale),
        };
        return animate(
          this.timer / mov[direction].length + 'ms',
          style(applystyle)
        );
      });
      ....
    });
  }

This makes easer the .css that is only

.carousel{
  display:flex;
  margin-left:auto;
  margin-right:auto;
  margin-top:20px;

}
.carousel::after{
  content:' ';
  display: block;
  clear: both;
}

.carousel>div{
  float:left;
  margin-right:-100%;
  cursor:pointer;
  transform-origin: top left;
}

.carousel>div img {
  border-radius: 1rem;
  box-shadow: 0 .5rem 1rem rgba(0,0,0,.35);
}

So the component are not so dependent for the sizes of the images. See also that I add a trasnformY to give a sensation to "view from above"

The changes in a new stackblitz

NOTE: By the moment, if we want to change the .css we can use the styles.css and write, e.g.

.custom .carousel>div img {
  border-radius: 50%!important;
  border:2px solid white;

}

NOTE: It's a continuation of my another response.

Finally I make a component that allow ever odd number of figures.

The first is simplify the values of animations, instead numberer skip one, I numered from 0 to 2*length-2. So, e.g.

  Five images            three images
      4
   3     5                   2  
  2       6               1     3
   1     7                   0
      0
animates=[0,1,2,6,7]   animates=[0,1,3]

Don't worry, we create the array when we pass the images in the @Input

@Input('images') set _(value:string[])
  {
    this.images=value;
    this.length=value.length;
    this.array=new Array(this.length).fill(0).map((_,x)=>x)
    this.animates=new Array(this.length*2-2).fill(0)
              .map((_,x)=>x)
              .filter(x=>(x <= this.length / 2 || x > (3 * this.length) / 2 - 2))
    
  }

So first we are calculate the array "pos" using a function. See that in this case the array pos can hold numbers and fraccions. e.g. in one case can be like [3.5, 3] or [5.5, 5, 4.5, 4, 3.5, 3, 2.5, 2]

getMovement(posIni: number, incr: number, steps: number, length: number) {
    if (steps == 0) return [posIni];
    const pos = [];
    let index = posIni;
    let cont = 0;
    while (cont < steps) {
      index += incr / 2;
      index = (index + 2 * length - 2) % (2 * length - 2);
      if ((index * 2) % 2 == 0) {
        pos.push(index);
        if (index <= length / 2 || index > (3 * length) / 2 - 2) cont++;
      } else pos.push(index);
    }
    return pos;
  }

An auxiliar function give us the "animations"

getAnimation(pos: number, length: number, timer: number) {
    const angle = (pos * 2 * Math.PI) / (2 * length - 2);
    const scale =
      (1 + this.minScale) / 2 + ((1 - this.minScale) / 2) * Math.cos(angle);
    const applystyle = {
      transform:
        'translate(' +
        this.radius * Math.sin(angle) +
        'px,' +
        (Math.floor(this.top * scale) - this.top) +
        'px) scale(' +
        scale +
        ')',
      'z-index': Math.floor(100 * scale),
    };
    return animate(timer + 'ms', style(applystyle));
  }

And our function "animateViews" becomes like

  animateViews(direction: number, steps: number = 1) {
    this.animates.forEach((x: number, index: number) => {
      const pos = this.getMovement(x, direction, steps, this.length);
      const time = this.timer / pos.length;
      const animations = pos.map((x) => this.getAnimation(x, this.length, time));
      const item = this.itemsView.find((_x, i) => i == index);

      const myAnimation = this.builder.build(animations);
      this.player = myAnimation.create(item.nativeElement);
      this.player.onDone(() => {
        this.animates[index] = pos[pos.length - 1];
      });
      this.player.play();
    });
  }

Finally, when we what to bring an image to front we simple calculate the neccessary steps to get the "0" position

  indexToFront(index: any) {
    const pos=this.animates[index]
    if (pos != 0) {
      const steps = pos<=this.length/2?-pos:2*this.length-2-pos
      const factor=steps<0?-1:1;
      this.animateViews(factor,factor*steps)
    }
    this.select.emit(index);
  }

the final stackblitz

Related