基于英雄联盟人物的加载动画,奇怪的需求又增加了!

5,015 阅读3分钟

1、背景

前两天老板找到我说有一个需求,要求使用英雄联盟的人物动画制作一个加载中的组件,类似于下面这样:

iShot_2024-06-06_18.09.55.gif

我定眼一看:这个可以实现,但是需要UI妹子给切图。

老板:UI? 咱们啥时候招的UI !

我:老板,那不中呀,不切图弄不成呀。

老板:下个月绩效给你A。

我:那中,管管管。

2、调研

发动我聪明的秃头,实现这个需求有以下几种方案:

  • 切动画帧,没有UI不中❎。
  • 去lol客户端看看能不能搞到什么美术素材,3D模型啥的,可能行❓
  • 问下 gpt4o,有没有哪个老表收集的有lol英雄的美术素材,如果有那就更得劲了✅。

经过我一番搜索,发现了这个网站:model-viewer,收集了很多英雄联盟的人物模型,模型里面还有各种动画,还给下载。老表,这个需求稳了50%了!

image-20240606182312802.png

接下来有几种选择:

  • 将模型动画转成动画帧,搞成雪碧图,较为麻烦,且动画不支持切换。
  • 直接加载模型,将模型放在进度条上,较为简单,支持切换不同动画,而且可以自由过渡。就是模型文件有点大,初始化加载可能耗时较长。但是后续缓存一下就好了。

聪明的我肯定先选第二个方案呀,你糊弄我啊,我糊弄你。

3、实现

web中加载模型可以使用谷歌基于threejs封装的 model-viewer, 使用现代的 web component 技术。简单易用。

先初始化一个vue工程

 npm create vue@latest

然后将里面的初始化的组件和app.vue里面的内容都删除。

安装model-viewer依赖:

npm i three // 前置依赖
npm i @google/model-viewer

修改vite.config.js,将model-viewer视为自定义元素,不进行编译

import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue({
      template: {
        // 添加以下内容
        compilerOptions: {
          isCustomElement: (tag) => ['model-viewer'].includes(tag)
        }
      }
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  assetsInclude: ['./src/assets/heros/*.glb']
})
​

新建 src/components/LolProgress.vue

<template>
  <div class="progress-container">
    <model-viewer
      :src="hero.src"
      disable-zoom
      shadow-intensity="1"
      :camera-orbit="hero.cameraOrbit"
      class="model-viewer"
      :style="heroPosition"
      :animation-name="animationName"
      :camera-target="hero.cameraTarget"
      autoplay
      ref="modelViewer"
    ></model-viewer>
    <div
      class="progress-bar"
      :style="{ height: strokeWidth + 'px', borderRadius: strokeWidth / 2 + 'px' }"
    >
      <div class="progress-percent" :style="currentPercentStyle"></div>
    </div>
  </div>
</template>
​
<script setup lang="ts">
import { computed, onMounted, ref, watch, type PropType } from 'vue'
/** 类型 */
interface Hero {
  src: string
  cameraOrbit: string
  progressAnimation: string
  finishAnimation: string
  finishAnimationIn: string
  cameraTarget: string
  finishDelay: number
}
type HeroName = 'yasuo' | 'yi'type Heros = {
  [key in HeroName]: Hero
}
const props = defineProps({
  hero: {
    type: String as PropType<HeroName>,
    default: 'yasuo'
  },
  percentage: {
    type: Number,
    default: 100
  },
  strokeWidth: {
    type: Number,
    default: 10
  },
  heroSize: {
    type: Number,
    default: 150
  }
})
​
const modelViewer = ref(null)
​
const heros: Heros = {
  yasuo: {
    src: '/src/components/yasuo.glb',
    cameraOrbit: '-90deg 90deg',
    progressAnimation: 'Run2',
    finishAnimationIn: 'yasuo_skin02_dance_in',
    finishAnimation: 'yasuo_skin02_dance_loop',
    cameraTarget: 'auto auto 0m',
    finishDelay: 2000
  },
  yi: {
    src: '/src/components/yi.glb',
    cameraOrbit: '-90deg 90deg',
    progressAnimation: 'Run',
    finishAnimationIn: 'Dance',
    finishAnimation: 'Dance',
    cameraTarget: 'auto auto 0m',
    finishDelay: 500
  }
}
​
const heroPosition = computed(() => {
  const percentage = props.percentage > 100 ? 100 : props.percentage
  return {
    left: `calc(${percentage + '%'} - ${props.heroSize / 2}px)`,
    bottom: -props.heroSize / 10 + 'px',
    height: props.heroSize + 'px',
    width: props.heroSize + 'px'
  }
})
​
const currentPercentStyle = computed(() => {
  const percentage = props.percentage > 100 ? 100 : props.percentage
  return { borderRadius: `calc(${props.strokeWidth / 2}px - 1px)`, width: percentage + '%' }
})
​
const hero = computed(() => {
  return heros[props.hero]
})
​
const animationName = ref('')
​
watch(
  () => props.percentage,
  (percentage) => {
    if (percentage < 100) {
      animationName.value = hero.value.progressAnimation
    } else if (percentage === 100) {
      animationName.value = hero.value.finishAnimationIn
      setTimeout(() => {
        animationName.value = hero.value.finishAnimation
      }, hero.value.finishDelay)
    }
  }
)
onMounted(() => {
  setTimeout(() => {
    console.log(modelViewer.value.availableAnimations)
  }, 2000)
})
</script>
<style scoped>
.progress-container {
  position: relative;
  width: 100%;
}
.model-viewer {
  position: relative;
  background: transparent;
}
.progress-bar {
  border: 1px solid #fff;
  background-color: #666;
  width: 100%;
}
.progress-percent {
  background-color: aqua;
  height: 100%;
  transition: width 100ms ease;
}
</style>

组件非常简单,核心逻辑如下:

  • 根据传入的英雄名称加载模型

  • 指定每个英雄的加载中的动画,

  • 加载100%,切换完成动作进入动画和完成动画即可。

  • 额外的细节处理。

    最后修改 app.vue:

    <script setup lang="ts">
    import { ref } from 'vue'
    import LolProgress from './components/LolProgress.vue'
    const percentage = ref(0)
    setInterval(() => {
      percentage.value = percentage.value + 1
    }, 100)
    </script><template>
      <main>
        <LolProgress
          :style="{ width: '200px' }"
          :percentage="percentage"
          :heroSize="200"
          hero="yasuo"
        />
      </main>
    </template><style scoped></style>

这不就完成了吗,先拿给老板看看。

老板:换个女枪的看看。

我:好嘞。

iShot_2024-06-06_19.08.49.gif

老板:弄类不赖啊小伙,换个俄洛伊的看看。

4、总结

通过本次需求,了解到了 model-viewer组件。

老板招个UI妹子吧。

在线体验:github-pages