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)
}

结尾

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

相关推荐
zhougl9961 小时前
html处理Base文件流
linux·前端·html
花花鱼1 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_2 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo3 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡5 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木6 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
難釋懷7 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript