【css3d动画】react+ts实现3d旋转环绕banner效果

静态效果图

在这里插入图片描述

功能

  • 拖动后会默认居中当前最靠近中间元素
  • 点击靠近中间位置的左右两个元素可以切换剧中元素
  • 可选择hover时添加视频动画

代码

import React, { useState, MouseEvent, useMemo, useEffect } from 'react';
import { useEventListener, useThrottleFn } from 'ahooks';
import cx from './index.module.less';
// 采用了classNames的样式写法

interface Props {
  imgWidth: number;
  imgHeight: number;
}
interface ListItem {
  img: string;
  video: string;
  index: number;
}

const imgList = [
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRc-daxzQsmo3cQ7M0TM4hOz3pX-1WKzB-V-w&usqp=CAU',
    video: '',
  },
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQXhun_JTTMmKDhvGv0vELHx41BS5hVL58mDA&usqp=CAU',
    video: '',
  },
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR1kno3P3RqG6qB-uJYBlglsznLVf4XLY_34A&usqp=CAU',
    video: '',
  },
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTz-VVabwdSkWiTFe4WTzj1WkD3ILkrEV0dQw&usqp=CAU',
    video: '',
  },
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRc-daxzQsmo3cQ7M0TM4hOz3pX-1WKzB-V-w&usqp=CAU',
    video: '',
  },
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQXhun_JTTMmKDhvGv0vELHx41BS5hVL58mDA&usqp=CAU',
    video: '',
  },
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR1kno3P3RqG6qB-uJYBlglsznLVf4XLY_34A&usqp=CAU',
    video: '',
  },
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTz-VVabwdSkWiTFe4WTzj1WkD3ILkrEV0dQw&usqp=CAU',
    video: '',
  },
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRc-daxzQsmo3cQ7M0TM4hOz3pX-1WKzB-V-w&usqp=CAU',
    video: '',
  },
  {
    img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQXhun_JTTMmKDhvGv0vELHx41BS5hVL58mDA&usqp=CAU',
    video: '',
  },
];

export const Banner3D = ({ imgWidth = 400, imgHeight = 400 }: Props) => {
  const [clientWidth, setClientWidth] = useState(document.documentElement.clientWidth);
  // 鼠标是否处于拖动状态
  const [mouseDrag, setMouseDrag] = useState(false);
  // 鼠标拖动距离
  const [mouseX, setMouseX] = useState(0);
  // 当前旋转的角度
  const [rotateY, setRotateY] = useState(0);
  const [listLength] = useState(imgList.length);
  // 元素的凭据旋转角度
  const [average] = useState(360 / listLength);
  // 停止拖动时自动居中最近元素使用长时间动画
  const [showSlowAnimation, setShowSlowAnimation] = useState(true);
  // 当前剧中元素的下标
  const [activeIndex, setActiveIndex] = useState(0);
  // 为每一个元素带上固定下标及相对父级的旋转角度
  const list: ListItem[] = useMemo(() => {
    return imgList.map((item, index) => {
      return { ...item, index, rotateY: index * (360 / listLength) };
    });
  }, [imgList]);

  useEventListener('resize', () => {
    const { clientWidth } = document.documentElement;
    setClientWidth(clientWidth);
  });
  const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
    setShowSlowAnimation(false);
    setMouseDrag(true);
    setMouseX(e.clientX);
  };
  const handleMouseUp = () => {
    setMouseDrag(false);
    setShowSlowAnimation(true);
    setMouseX(0);
    // 鼠标松开时旋转至最近的一个分割角度居中图案
    setRotateY(average * Math.round(rotateY / average));
  };
  const drag = (e: MouseEvent<Element, MouseEvent>) => {
    const oldX = mouseX;
    const newX = e.clientX;
    if (oldX !== 0) {
      // 跟据鼠标移动距离与窗口宽度的比例计算当前旋转的角度
      let constance = newX - oldX;
      setRotateY(rotateY - (180 * constance) / clientWidth);
    }
    setMouseX(newX);
  };
  const { run: dragThrottle } = useThrottleFn(drag, {
    // 拖动节流
    wait: 100,
  });
  const handleMouseMove = (e: any) => {
    if (mouseDrag) dragThrottle(e);
  };

  // 3d视角圆的半径
  const translateZ = Math.max(((imgWidth + 50) * (listLength / 2)) / Math.PI, clientWidth / 2);

  const handleClick = (item: ListItem) => {
    // 点击居中元素左右两边的banner切换居中图像
    if (item.index - 1 === activeIndex || activeIndex - item.index === listLength - 1) setRotateY(rotateY - average);
    if (item.index + 1 === activeIndex || item.index - activeIndex === listLength - 1) setRotateY(rotateY + average);
  };
  useEffect(() => {
    if (mouseDrag) return;
    if (rotateY % 360 === 0) {
      // 当旋转一周时切换居中下标为默认0
      setActiveIndex(0);
      return;
    }
    const activeItem = list.find((item: any) => {
      if (rotateY >= 0) {
        return Math.round(Math.abs(item.rotateY)) === Math.round(Math.abs((rotateY % 360) - 360));
      } else {
        return Math.round(Math.abs(item.rotateY)) === Math.round(Math.abs(rotateY % 360));
      }
    });
    // 根据旋转角度设置居中下标
    if (activeItem) setActiveIndex(activeItem.index);
  }, [rotateY, average]);
  useEventListener('mouseup', handleMouseUp);
  useEventListener('mousemove', handleMouseMove);
  return (
    <div className={cx('banner-container')} style={{ height: `${imgHeight}px` }} onMouseDown={handleMouseDown}>
      <div
        className={cx('box', { 'slow-animation': showSlowAnimation })}
        style={{ width: `${imgWidth}px`, height: `${imgHeight}px`, transform: `rotateY(${rotateY}deg) ` }}>
        {list.map(item => {
          return (
            <div
              key={item.index}
              onClick={() => {
                handleClick(item);
              }}
              className={cx('img-box', `item${item.index}`, {
                'near-active':
                  item.index === (activeIndex + 1 >= listLength ? 0 : activeIndex + 1) ||
                  item.index === (activeIndex - 1 >= 0 ? activeIndex - 1 : listLength - 1),
                active: activeIndex === item.index,
              })}
              style={{
                backgroundImage: `url('${item.img}')`,
                transform: `rotateY(${item.index * (360 / listLength)}deg) translateZ(-${translateZ}px) rotateX(0deg)`,
              }}>
              {item.video && (
                <video preload="auto" loop muted autoPlay>
                  <source src={item.video} />
                </video>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default Banner3D;


  • module.less样式文件
.banner-container{
  width: 100%;
  overflow: hidden;
  user-select: none;
  cursor: pointer;
  max-width: 1920px;
  margin: 0 auto;
  min-width: 1350px;
  perspective:2000px;
  display: flex;
  align-items: center;
  justify-content: center;
  .box{
    transform-style: preserve-3d;
    transform: rotateY(0deg);
    transition: all 0.1s ease-out;
    &.slow-animation{
      transition: all 1s ease-out;
    }
  }
  .img-box{
    width: 100%;
    height: 100%;
    position: absolute;
    overflow: hidden;
    backface-visibility: hidden;
    transform-origin: 50% 50%;
    transform: rotateY(180deg);
    transition-delay: 300ms;
    background-position: center center;
    background-size: cover;
    background-repeat: no-repeat;
    border-radius: 4px;
    img {
      display: block;
      width: 100%;
      height: 100%;
      user-select: none;
    }
    video{
      display: none;
      position: absolute;
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
      visibility: hidden;
    }
    &.active,&.near-active:hover{
      video{
        display: block;
        visibility: visible;
      }
    }
  }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
React TS是一个用于构建用户界面的JavaScript库,而VR看房可以通过使用WebVR技术来实现。在React TS实现VR看房需要以下步骤: 1. 了解WebVR技术:WebVR是一种使Web应用程序能够在虚拟现实设备上运行的技术,它提供了一种在虚拟现实设备上渲染内容的方式。 2. 导入WebVR库:在React TS项目中,需要导入与WebVR相关的库,例如A-Frame或React 360等,这些库提供了一些用于构建VR界面的组件和功能。 3. 创建VR场景:使用React TS的组件化开发方式,可以通过在项目中创建VR场景组件来构建VR看房的界面。可以使用库中提供的组件来构建3D场景、添加虚拟现实设备的交互等。 4. 加载房屋模型:在VR场景中加载房屋模型,可以使用库中提供的加载器将3D房屋模型导入到场景中,并设置适当的位置和缩放。 5. 添加交互功能:为了实现VR看房的功能,需要添加一些交互功能,例如移动、旋转和缩放房屋模型,点击房间以获取更多信息等。可以使用库中提供的交互组件或自定义事件处理程序来实现这些功能。 6. 兼容不同的设备:考虑到不同的虚拟现实设备,需要在React TS项目中进行一些适配工作,以确保VR看房界面在不同设备上的兼容性。 总的来说,使用React TS实现VR看房需要对WebVR技术有一定的了解,并结合具体的库和组件来构建VR场景、加载房屋模型,并添加交互功能。不同设备的兼容性也是一个需要考虑的因素。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.温m_

如果喜欢可以支持打赏小哥!!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值