Krpano:打造全景漫游体验—全景开发(二)

前言

在我们预览全景的时候,场景的切换一般都是通过皮肤导航栏来进行切换的,原本是使用MAKE VTOUR Droplet.exe 自动生成的vtour文件夹中默认皮肤xml

也就是主xml中引入的vtourskin.xml,但是ui实在是太丑了,自定义样式比较困难

于是就不使用xml来展示皮肤导航了,而是自己实现一个这样的皮肤导航,通过js来和krpano进行交互,这是实现后的效果,可以显示/隐藏皮肤导航,通过点击,滚轮以及拖动来控制皮肤导航

实现自定义皮肤导航

实现步骤:

  1. 定义组件
  2. 实现皮肤导航隐藏/显示
  3. 实现滚轮滑动效果
  4. 实现鼠标拖动效果
  5. 实现点击切换场景

1.定义组件

新建一个SkinControl组件,并且定义接口描述组件需要传入的属性:

  • show:用于控制导航条的显示与隐藏
  • scenes:场景列表
  • onSceneChange:场景切换事件处理函数,接受一个场景对象作为参数
js 复制代码
import React, { memo } from 'react'
import type { FC, ReactNode } from 'react'
import type { ISceneType } from '@/views/pano/edit/type'
import { SkinControlWrapper } from '.'

interface IProps {
  children?: ReactNode
  /**
   * boolean: 导航条显示/隐藏
   */
  show: boolean
  /**
   * scenes: 场景列表
   */
  scenes: ISceneType[]
  /**
   * onSceneChange: 场景切换回调
   */
  onSceneChange?: (scene: ISceneType) => void
}

const SkinControl: FC<IProps> = ({ show, scenes = [], onSceneChange }) => {
  return (
    <SkinControlWrapper>
         <div></div>
    </SkinControlWrapper>
  )
}

export default memo(SkinControl)

2. 实现皮肤导航隐藏/显示

皮肤导航隐藏/显示主要通过高度来控制,给div一个class名为skin_control_bar,height设置为0,overflow设置为hidden,背景色设为黑色,透明度给个0.3,再根据show属性动态添加class名为skin_control_show,skin_control_show里height设为100px

html 复制代码
<SkinControlWrapper>
  <div className={`skin_control_bar ${show ? 'skin_control_show' : ''}`}>
  </div>
</SkinControlWrapper>
css 复制代码
.skin_control_bar {
    box-sizing: border-box;
    background: rgba(0, 0, 0, 0.3);
    height: 0;
    overflow: hidden;
    padding: 0 36px;
    position: relative;
    width: 100%;
}
.skin_control_show {
    height: 100px;
}

emm...有点生硬,skin_control_bar里再给个高度的过渡效果:transition: height 0.15s linear;

3.实现滚轮滑动效果

先将场景展示在皮肤导航里,使用绝对定位+监听鼠标滚轮事件就可以实现滚轮的滑动效果

3.1定位

添加一个class名为carousel的元素作为场景列表的容器,设置容器的定位方式为相对定位,高度为100px,容器的溢出处理方式为隐藏,最后加上pointer-events: none; 防止鼠标事件的穿透

然后在容器里遍历场景列表,添加class名为carousel-item的div,设置绝对定位,top值为0,left为50%

html 复制代码
<SkinControlWrapper>
  <div className={`skin_control_bar ${show ? 'skin_control_show' : ''}`}>
    <div className="carousel">
      {scenes.map((item, index) => {
        return (
          <div className="carousel-item" key={index} onClick={() => handleClick(item, index)}>
            <div className="carousel-item_box">
              <img src={OSS_PATH + '/' + item.thumbUrl} alt="" />
            </div>
          </div>
        )
      })}
    </div>
  </div>
</SkinControlWrapper>
css 复制代码
.carousel {
    position: relative;
    z-index: 1;
    height: 100px;
    overflow: hidden;
    pointer-events: none;
    .carousel-item {
      cursor: pointer;
      box-sizing: border-box;
      width: 100px;
      height: 100px;
      padding: 5px;
      overflow: hidden;
      position: absolute;
      top: 0;
      left: 50%;
      .carousel-item_box {
        box-sizing: border-box;
        width: 100%;
        height: 100%;
        border: 2px solid #fff;
        border-radius: 5px;
        img {
          width: 100%;
          height: 100%;
          object-fit: cover;
          pointer-events: none;
          border-radius: 5px;
        }
      }
    }
  }

3.2排列

所有元素都重叠在一起了,我们需要进行一个初始的排列,先定义一些计算用到的变量:

js 复制代码
const [progress, setProgress] = useState(50)
const speedWheel = 1 / scenes.length
const speedDrag = -0.15

useRef获取容器元素

ini 复制代码
const carouselRef = useRef<HTMLDivElement>(null)
<div ref={carouselRef}></div>

在CSS类.carousel-item中添加自定义的css变量:

  • --count: 0,子元素列表的数量;
  • --active:0,子元素的索引值
  • --x: 默认值calc(calc(var(--active) * var(--count)) * 100%),子元素的水平位移
css 复制代码
.carousel-item {
    --count: 0;
    --active: 0;
    --x: calc(calc(var(--active) * var(--count)) * 100%);
    cursor: pointer;
    box-sizing: border-box;
    width: 100px;
    height: 100px;
    padding: 5px;
    overflow: hidden;
    position: absolute;
    top: 0;
    left: 50%;
    user-select: none;
    transform-origin: 0% 100%;
    pointer-events: all;
    // 水平偏移
    transform: translate(var(--x));
    // 动画过渡
    transition: transform 0.8s cubic-bezier(0, 0.02, 0, 1);
}

初始化执行animate函数,获取容器中的子元素列表和子元素列表的数量,并根据进度值progress计算当前处于中间元素的索引

js 复制代码
useEffect(() => {
    if (scenes.length) {
        animate()
    }
}, [scenes, progress])

// 初始化
const animate = () => {
    const childNodes = carouselRef.current?.childNodes
    const childNodesLength = carouselRef.current?.childNodes.length || 0

    const p = Math.max(0, Math.min(progress, 100))
    const a = Math.ceil((p / 100) * (childNodesLength - 1))
    setProgress(p)
}

然后在遍历每个子元素,设置每个子元对应的css变量

js 复制代码
// 排列元素位置
const displayItems = (item: HTMLElement, index: number, active: number, length: number) => {
    item.style.setProperty('--count', length.toString())
    item.style.setProperty('--active', ((index - active) / length).toString())
}
  
const animate = () => {
    // ...
    
    childNodes?.forEach((item, index) =>
      displayItems(item as HTMLElement, index, a, childNodesLength)
    )
}

为容器添加滚轮监听事件,计算滚动时候滚动的进度,计算方式为滚轮的滚动量 * speedWheel,再重新setState更新progress,重新排列元素位置

html 复制代码
<div className="carousel" ref={carouselRef} onWheel={handleWheel}>
    {scenes.map((item, index) => {
        return (
            <div className="carousel-item" key={index} onClick={() => handleClick(item, index)}>
                <div className="carousel-item_box">
                    <img src={OSS_PATH + '/' + item.thumbUrl} alt="" />
                </div>
            </div>
        )
    })}
</div>
js 复制代码
const handleWheel = (e: React.WheelEvent<HTMLDivElement>) => {
    const wheelProgress = e.deltaY * speedWheel
    setProgress(progress + wheelProgress)
}

4. 实现鼠标拖动效果

实现鼠标拖动效果就要监听鼠标事件,记录鼠标按下的位置和鼠标状态,在拖动的过程中重新计算progress的值,重新排列元素位置,当鼠标离开元素或者鼠标释放的时候清除鼠标按下的状态

html 复制代码
 <div
    className="carousel"
    ref={carouselRef}
    onWheel={handleWheel}
    onMouseDown={handleMouseDown}
    onMouseMove={handleMouseMove}
    onMouseUp={handleMouseUp}
    onMouseLeave={handleMouseLeave}
    >
        {scenes.map((item, index) => {
        return (
            <div className="carousel-item" key={index} onClick={() => handleClick(item, index)}>
                <div className="carousel-item_box">
                    <img src={OSS_PATH + '/' + item.thumbUrl} alt="" />
                </div>
            </div>
        )
    })}
</div>
js 复制代码
const handleMouseDown = (
        e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
) => {
    // 是否移动端触发
    const isTouch = 'touches' in e
    // 标记鼠标状态
    setIsDown(true)
    // 记录鼠标按下位置
    setStartX(isTouch ? e.touches && e.touches[0].clientX : e.clientX)
}

const handleMouseMove = (
    e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
) => {
    if (!isDown) return
    const isTouch = 'touches' in e
    const x = isTouch ? e.touches[0].clientX : e.clientX
    const mouseProgress = (x - startX) * speedDrag
    setProgress(progress + mouseProgress)
    setStartX(x)
}

// 清除鼠标状态
const handleMouseUp = () => {
    setIsDown(false)
}
const handleMouseLeave = () => {
    setIsDown(false)
}

鼠标拖动效果也是实现了,为了适配移动端也要给容器绑定touch事件

html 复制代码
<div
    className="carousel"
    ref={carouselRef}
    onWheel={handleWheel}
    onMouseDown={handleMouseDown}
    onMouseMove={handleMouseMove}
    onMouseUp={handleMouseUp}
    onMouseLeave={handleMouseLeave}
    onTouchStart={handleMouseDown}
    onTouchMove={handleMouseMove}
    onTouchEnd={handleMouseUp}
>
    // ...
</div>

5. 实现点击切换场景

当我们点击皮肤导航中的场景时需要展示对应的全景,所以为场景对应的元素绑定点击事件,点击的时候根据场景的name属性来调用krpano中的loadscene方法来切换全景,并且重新计算progress值,使点击的场景位置处于皮肤导航中间

html 复制代码
{scenes.map((item, index) => {
    return (
        <div className="carousel-item" key={index} onClick={() => handleClick(item, index)}>
            <div className="carousel-item_box">
                <img src={OSS_PATH + '/' + item.thumbUrl} alt="" />
            </div>
        </div>
    )
})}
js 复制代码
const handleClick = (item: ISceneType, i: number) => {
    kp.call(`loadscene(${item.scene_id}, null, MERGE, BLEND(1))`)
    setProgress((i / (carouselRef.current?.childNodes.length || 0)) * 100 + 10)
}

结尾

以上就是我对自定义皮肤导航的实现,如果还有更好的方式也欢迎大家一起分享交流,学习学习😊🤞💋😘

相关推荐
J总裁的小芒果9 分钟前
Vue3 el-table 默认选中 传入的数组
前端·javascript·elementui·typescript
Lei_zhen9612 分钟前
记录一次electron-builder报错ENOENT: no such file or directory, rename xxxx的问题
前端·javascript·electron
咖喱鱼蛋14 分钟前
Electron一些概念理解
前端·javascript·electron
yqcoder15 分钟前
Vue3 + Vite + Electron + TS 项目构建
前端·javascript·vue.js
鑫宝Code33 分钟前
【React】React Router:深入理解前端路由的工作原理
前端·react.js·前端框架
Mr_Xuhhh2 小时前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法
永乐春秋3 小时前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端
鸽鸽程序猿3 小时前
【前端】CSS
前端·css
ggdpzhk3 小时前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
学不会•5 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html