March passed for several weeks, this day finally dropped the shadow of the winter, slowly began to heat up, sunny day is very good, you can see the story of the small yellow flowers, and can be in fine spirits began to toss up.

The first thing I do at work every day is to open netease cloud music (WIN PC version), find a playlist and start listening to songs, and then open VSCODE and keep coding… One day code tired to have a rest for a while, point a little cloud music, inadvertently found that it is simply a magical existence, everywhere is a variety of components, there are menus, running lights, cards,pageHeader, slider bar… When business code is tired, it is easy to do something interesting. Well, I will start to make my own netease cloud music.

All things are hard to begin with, and it’s even harder to start with a hard bone.

Technical Stack Description

Front end: Typescript/React/umijs/sass/ant-design/ ICONS back end: Java/SpringBoot/Mybatis/MySQL

Notable Carousel

I chose this as the first component not only because it was a bit difficult, but also because I couldn’t help noticing this thing rolling around in front of my eyes every day.

Because it is a C-terminal product, compared with ANTD’s merry-go-round, cloud music’s merry-go-round is more flowery, there is certain overlap between cards, and it is too comfortable and smooth.

Element 1, the card



1. Card with rounded corners (Width :542px; height:196px).

2. There is a red corner mark like exclusive/debut in the lower right corner (height 28, width variable, color :#EC4141, note: The main color of netease products is this red, we will directly call this color value when it comes to the main red in the future)

3. Click the card to trigger an event

Element 2, row the lattice

Element 3, the button

Next we’ll implement each widget one by one, and finally animate it

1. The card

Component parameters are relatively simple. Look at our component types

1.1 Component Types

interfaceCardProps { onClick? :() = > void; // Call back the click eventtext? :string;// The text in the lower right corner of the cardclassName? :string;
}
Copy the code

1.2 Component Code

const Card: React.FC<CardProps> = ({ children, onClick, text, className }) = > {
  const classes = classnames('carousel-card-container', className);
  return (
    <div className={classes} onClick={onClick}>
      {React.cloneElement(children as React.ReactElement, {
        style: { width: '100%', height: '100%' },
      })}
      <div className="carousel-card-text">{text}</div>
    </div>
  );
};
export default Card;
Copy the code

1.3 style

.carousel-card-container { position: relative; border-radius: 8px; overflow: hidden; } .carousel-card-text { position: absolute; bottom: 0; right: 0; height: 22px; padding: 4px 8px; background: $primary-color; $EC4141 color: # FFF; $EC4141 color: # FFF; font-size: 12px; line-height: 14px; border-top-left-radius: 8px; border-bottom-right-radius: 8px; }Copy the code

1.4 pay attention to the point

1. CloneElement appends width and height to children so that the width and height of subsequent child elements match that of the parent element. Such as:

<Card><img src="http://example.com/xxx.png"/></Card>
Copy the code

No matter how many pixels the IMG is compressed to fit the parent element

1.5 rendering

<Card text="Exclusive">
  <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8835bba0ccdc4fd58a92c29f6af17580~tplv-k3u1fbpfcp-zoom-1.image"/>
</Card>
Copy the code

Seems to be ok

Element two lattice

2.1 a point

Through careful observation, it can be concluded as follows: 1. There are two states: red when selected and gray when unselected 2. Support mouse hover when the callback event is concluded, our code part is still configuration first

2.2 Component Types

interface DotProps { isSelected? : boolean; isHover? : (hover: boolean) => void; style? : React.CSSProperties; }Copy the code

2.3 Component Code

const Dot: React.FC<DotProps> = ({ isSelected, isHover, style }) = > {
  const classes = classnames('carousel-dot-container', {
    'carousel-dot-selected': isSelected,
  });
  return (
    <div
      className={classes}
      style={style}
      onMouseEnter={()= >isHover? .(true)} onMouseLeave={() => isHover? .(false)} />
  );
};

export default Dot;
Copy the code

2.4 Style Code

.carousel-dot-container{
  width: 6px;
  height: 6px;
  border-radius: 50%; // Add a square with a big rounded corner to make a prototypemargin: 0 4px 0 4px; // It is used for the space between the latticesbackground-color: #E6E6E6;
}

.carousel-dot-selected {
  background-color$primary-color = $primary-colorCopy the code

2.5 lattice

The first induction is as follows: 1. Input the number of dot matrix 2. The current selected point 3

2.6 Component Types

interface DotsProps {
  count: number;
  current: number; onChange? :(index: number) = > void; style? : React.CSSProperties; }Copy the code

2.7 Component Code

const Dots: React.FC<DotsProps> = ({ count, current, onChange }) = > {
  return (
    <div className="carousel-dots-container">
      {new Array(count).fill(0).map((curr, index) => {
        return (
          <Dot
            key={index}
            isSelected={current= = =index}
            isHover={()= >onChange? .(index)} /> ); })}</div>
  );
};

export default Dots;
Copy the code

2.8 Style Code

// Dot matrix style.carousel-dots-container {
  display: flex;
  justify-content:center;
}
Copy the code

2.9 rendering

import React, { useState } from 'react'
import Dots from '@/components/carousel/dots/Dots'
const Test = () = >{
  const [current,setCurrent] = useState(0)
  return (
    <div>
      <Dots count={9} current={current} onChange={i= >setCurrent(i)}/>
    </div>)}export default Test
Copy the code

3 button

The button component is the simplest part, where we represent the left and right buttons as two states of the base component

3.1 Component Code

import React from 'react';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import classnames from 'classnames';
interfaceButtonProps { onClick? :() = > void;
  placement: 'left' | 'right'; className? :string;
}

const Button: React.FC<ButtonProps> = ({ onClick, placement, className }) = > {
  const classes = classnames(className, 'carousel-button-container');
  return (
    <div className={classes} onClick={()= >onClick? .()}> {placement === 'left' ? (<LeftOutlined className="carousel-button-icon" />
      ) : (
        <RightOutlined className="carousel-button-icon" />
      )}
    </div>
  );
};
export default Button;
Copy the code

3.2 Style Code

.carousel-button-container{
  height: 30px;
  width: 30px;
  border-radius: 50%;
  background: $grey-7; //$grey-x is a series of shades of greyopacity: 0.5;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
}

.carousel-button-icon {
  font-size: 12px;
  color: $grey-5;
  font-weight: 600;
  &:hover, &.hover {
    color: $grey-1; }}Copy the code

3. It’s empowering

So far, we have realized the three basic components of the flash-through card/dot matrix/button, and then come to the important part, that is, how to layout, how to achieve this switch animation?

3.1 Animation Decomposition

The whole process of animation switching is very fast, about 0.5s to complete a frame. After screen recording, 0.25 times speed + frame by frame analysis, we summarize the details of the animation. I will not talk about the specific decomposition process, but directly focus on the key points: 1. At the front of the whole entertaining diversions, a total of three images, left | in the | right, the left and right, respectively, a certain degree of scale (CSS 3 can use the scale to be realized in the x, y of scaling, do not need to manually modify the element size), middle figure z – index, the highest priority is always in the forefront, covering about two figure. 2. All other images are stacked behind the middle image and hidden. When stacked, they need to be scaled, and the scale value is the same as the left and right images. The whole forward switching logic is as follows: left-middle-right in the current frame, left-bottom in the next frame, middle to left, right to middle, and the card at the top of the pile to right 4. 5. When the mouse hover to running lantern switch pause, mouse hover dot matrix, directly switch the corresponding card hover dot matrix

3.2 Component Code

The component code itself is not too difficult, just note that getClasses is used to dynamically generate the className of each card

import React from 'react';
import Card from './card/Card';
import Dots from './dots/Dots';
import Button from './button/Button';
import classnames from 'classnames';
interface CarouselProps {
  cards: ArrayThe < {text: string; img: string} >. className? :string
}
const Carousel: React.FC<CarouselProps> = ({ cards,className }) = > {
  const [current, setCurrent] = React.useState(0); // The current card index value
  const countRef = React.useRef(0);
  countRef.current = current;
  const [isHover, setHover] = React.useState(false); // Whether the mouse hover on the running lamp
  const useHoverRef = React.useRef(false);
  useHoverRef.current = isHover;
  const getClasses = (index: number) = > {
    const isLeft =
      index === current - 1 || (current === 0 && index === cards.length - 1);
    const isRight =
      index === current + 1 || (current === cards.length - 1 && index === 0);
    const isCurrent = current === index;
    return classnames('carousel-card-size',className, {
      'carousel-card-dock': !(isLeft || isRight || isCurrent),
      'carousel-card-middle': isCurrent,
      'carousel-card-left': isLeft,
      'carousel-card-right': isRight,
    });
  };
  if (cards.length < 3) {
    throw new Error('No less than 3 elements in a revolving lantern');
  }

  const step = (directon: 1 | -1, source: 'manual' | 'auto') = > {
    if (useHoverRef.current && source === 'auto') {
      return;
    }
    if (directon === 1) {
      if (countRef.current === cards.length - 1) {
        setCurrent(0);
      } else {
        setCurrent((current) = > current + 1); }}if (directon === -1) {
      if (countRef.current === 0) {
        setCurrent(cards.length - 1);
      } else {
        setCurrent((current) = > current - 1); }}};// Set the timer scrolling interval to 4s
  React.useEffect(() = > {
    const timer = setInterval(() = > {
      step(1.'auto');
    }, 4000);
    return () = > clearInterval(timer); } []);return (
    <div
      className="carousel-container"
      onMouseEnter={()= > setHover(true)}
      onMouseLeave={() => setHover(false)}
    >
    <div className="carousel-cards">
      {cards.map((card, index) => (
        <Card key={index} text={card.text} className={getClasses(index)}>
          <img src={card.img} />
        </Card>
      ))}
      {isHover ? (
        <Button
          placement="left"
          className="carousel-button-left"
          onClick={()= > step(-1, 'manual')}
        />
      ) : null}
      {isHover ? (
        <Button
          placement="right"
          className="carousel-button-right"
          onClick={()= > step(1, 'manual')}
        />
      ) : null}
      </div>
      <div className="carousel-bottom-dots">
        <Dots
          count={cards.length}
          current={current}
          onChange={(i)= > setCurrent(i)}
        />
      </div>
    </div>
  );
};

export default Carousel;

Copy the code

3.3 Style Code

From the decomposition of the animation above, there are four types of CARDS left in the | | | right card pile, on a carousel – card – left, carousel – card – middle, carousel – card – right, carousel – card – the dock Focus on a few attributes: Transform-origin transfers the center of the element, otherwise there will be a gap when the left and right cards scale towards the default center

After looking at the CSS code below, you might notice why the carousel-card-right position is computed left instead of right:0. It’s because transition can only animate the same measurable CSS properties,left and Righ Transition doesn’t work when t is present at the same time, so we have to make all cards left or all cards right.

$carousel-card-width: 542px! default;@import './button/style'; // Introduce sub-component style code@import './card/style';
@import './dots/style';
.carousel-container{
  width: 100%;
  position: relative;
  height: 250px;
}

.carousel-cards {
  height: 196px;
  .carousel-card-size {
    width: $carousel-card-width;
    height: 196px;
  }
  
  .carousel-card-middle {
    z-index: 99;
    position: absolute;
    left: calc(50% - #{$carousel-card-width/2}); 
    bottom: 0;
    top: 0;
    margin: auto;
    transition: all 0.5 s;
  }
  
  .carousel-card-left {
    position: absolute;
    z-index: 98;
    top: 0;
    bottom: 0;
    left: 0;
    margin-top: auto;
    margin-bottom: auto;
    transform: scale(0.82.0.82); // Card scalingtransform-origin: 0% 50%; // The element center of the left card moves to the far lefttransition: all 0.5 s;
  }
  
  .carousel-card-right {
    position: absolute;
    z-index: 98;
    top: 0;
    bottom: 0;
    left: calc(100% - #{$carousel-card-width}); 
    margin-top: auto;
    margin-bottom: auto;
    transform: scale(0.82.0.82);
    transform-origin: 100% 50%; // The element center of the right card moves to the far righttransition: all 0.5 s;
  }
  
  .carousel-card-dock {
    position: absolute;
    left: calc(50% - #{$carousel-card-width/2});
    bottom: 0;
    top: 0;
    margin: auto;
    transition: all 0.5 s;
    transform: scale(0.82.0.82);
  }
  
  .carousel-button-left {
    position: absolute;
    left: 15px;
    bottom: 0;
    top: 0;
    margin: auto;
    z-index: 100;
  }
  
  .carousel-button-right {
    position: absolute;
    right: 15px;
    bottom: 0;
    top: 0;
    margin: auto;
    height: 30px;
    z-index: 100; }}.carousel-bottom-dots {
  position: absolute;
  width: 100%;
  bottom: 0;
  text-align: center;
}
Copy the code

3.4 rendering

import React, { useState } from 'react'
import Carousel from '@/components/carousel/Carousel'
const Test = () = >{
  const carouselCards = [
    {text:'exclusive'.img:'/resourcebed/picture? md5=9062dc80bb47556c3565efa18f1dcc32'},
    {text:'exclusive'.img:'/resourcebed/picture? md5=5624a16a955d3f52088afe8b5eb5bb12'},
    {text:'New album debut'.img:'/resourcebed/picture? md5=98cfacc930a291d70d72eb1b447fe2b0'},
    {text:'the MV start'.img:'/resourcebed/picture? md5=d6d853c35dd5d0fffec8a7509a13c070'},
    {text:'exclusive'.img:'/resourcebed/picture? md5=2b97b3ec46feb33141d313e5379d8fd8'},
    {text:'New song debut'.img:'/resourcebed/picture? md5=ae05e981386806b0dde1f848f7333937'},
    {text:'exclusive'.img:'/resourcebed/picture? md5=cfc889c49b92312bcb26fe8fc7e1e896'},
    {text:'New song debut'.img:'/resourcebed/picture? md5=6e9bdc761b49f6dc95bfda35b1981636'},
    {text:'exclusive'.img:'/resourcebed/picture? md5=54298ef6444d6fa77584922e883ac5ca'}]return (
    <div>
      <Carousel cards={carouselCards}/> 
    </div>)}export default Test
Copy the code

conclusion

Thank you for reading this article. Actually, when WRITING this article, the whole dismantling work has been done a lot, now almost all the available components of cloud music have been abstracted out, I am writing the background CRUD, and I will pair some interfaces successively later. After the whole work is completed, I will consider how to separate this component library, of course, this is another no small challenge ~~ finally, the first time to write an article in nuggets, if there is any improper, please correct, thank you!

Warehouse address and Demo address

GITHUB

DEMO