Как реализовать баннер на главной странице Bilibili с помощью Vue 3 и Canvas?

Vue.js
Как реализовать баннер на главной странице Bilibili с помощью Vue 3 и Canvas?

Есть волшебное место, где можно посмотреть драмы и варьете, а также короткие видео прямые трансляции и игры.Сегодня посмотрим, к какому брату пойдет деревенский тиран, а завтра, в какой тюрьме сидит Чжан Сан.

Правильно, я про Bilibili Barrage Network. Некоторое время назад, когда я ловил рыбу, я обнаружил, что баннер этого сайта изменился. Изменение. Да, интересно, я нажал F12, чтобы понаблюдать за волной, и обнаружил что это было достигнуто с помощью нескольких изображений и CSS.Vue 3.0Он был выпущен, я хотел сделать демо и написать его, поэтому я использовалviteБыл построен простой проект.Эффект баннера Bilibili был реализован с использованием Composition API Vue 3.0 и Canvas.Конкретный эффект может перейти к моемуGitHubПроверить.

Сначала создайте проект с помощью vite

vite — это новый инструмент сборки, официально выпущенный vue, его характеристика — одно слово:быстроНасколько это быстро, то есть вы только что открыли браузер и готовитесьснять штаныВведите localhost, и он будет построен.Я не знаю, правда ли это, я просто шучу

# 在你的命令行直接输入
npm init vite-app bilibili-autumn

В это время будет автоматически загружен cli-инструмент vite, и в текущем каталоге будет создана папка bilibili-autumn, а затем в командной строке будут выведены некоторые подсказки.

Done. Now run:

  cd bilibili-autumn
  npm install (or `yarn`)
  npm run dev (or `yarn dev`)

Следуйте подсказкам для запуска проекта, вся структура каталогов проекта такая

.
├── index.html
├── package.json
├── public
│   └── favicon.ico
└── src
    ├── App.vue
    ├── assets
    │   └── logo.png
    ├── components
    │   └── HelloWorld.vue
    ├── index.css
    └── main.js

4 directories, 8 files

Далее мы начинаем реализовывать этот эффект баннера

Соберите необходимые материалы

Нажмите F12, чтобы открыть консоль, просмотреть DOM-структуру баннера и отключить все графические материалы (щелкните правой кнопкой мыши).Open in new tab + Ctrl Sсохранить изображение)

Поместите сохраненный материал изображения в проектsrc/assetsв папке

начать кодирование

может открыть первымДокументация для Vue 3.0Посмотрите, что изменилось в API.

Выберите некоторые фрагменты кода ниже, полный код и эффекты можно щелкнутьGithubПроверить

1. СоздайтеBanner.vueкомпоненты

<template>
  <div ref="placeholder" class="banner">
    <img class="banner-placeholder" src="/src/assets/full-bg.png" />
    <canvas :ref="(el) => (layers.bg = el)" class="banner-layer"></canvas>
    <canvas :ref="(el) => (layers.twotwo = el)" class="banner-layer"></canvas>
    <canvas :ref="(el) => (layers.land = el)" class="banner-layer"></canvas>
    <canvas :ref="(el) => (layers.ground = el)" class="banner-layer"></canvas>
    <canvas
      :ref="(el) => (layers.threethree = el)"
      class="banner-layer"
    ></canvas>
    <canvas :ref="(el) => (layers.grass = el)" class="banner-layer"></canvas>
  </div>
</template>

Основная идея реализации состоит в том, чтобы использовать Canvas для рисования изображений каждого слоя, который используется здесь.<img>Добавьте статическое фоновое изображение, чтобы растянуть родительский элемент.<div>достигать<canvas>Адаптивный размер

2. Логика рисования слоев

Установив разные слои изображенийfilter: blur()Для достижения разной глубины резкости смещение достигается установкой разных начальных координат при отрисовке каждого кадра картинки

function draw(image, config) {
  const { sx = 0, sy = 0, sw, sh, blur: b } = config || {}
  return {
    to(canvas) {
      const ctx = canvas.getContext('2d')
      ctx.imageSmoothingEnabled = true
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ctx.filter = `blur(${b}px)`
      ctx.drawImage(image, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height)
    },
  }
}

3. Получите ссылку на изображение и элемент Canvas в соответствующем жизненном цикле и нарисуйте изображение на Canvas.

// 注意这里 vite 会对 assets 的资源做处理, 返回的是资源的地址
import bg from '/src/assets/bg.png'

const images = reactive({
  bg: null,
})

function buildImage(src) {
  return new Promise((resolve) => {
    const image = new Image()
    image.onload = () => resolve(image)
    image.src = src
  })
}

onBeforeMount(() => {
  buildImage(bg).then((img) => (images.bg = img))
})

onMounted(() => {
  watch(
    () => images.bg,
    () => draw(images.bg, config.bg).to(layers.bg)
  )
})

4. Отслеживайте события мыши, чтобы обрабатывать изменения глубины резкости и смещения.

const enterPoint = {}
placeholder.value.addEventListener('mouseenter', (e) => {
  const { width } = placeholder.value.getBoundingClientRect()
  enterPoint.x = e.clientX
  enterPoint.w = width
})

// 鼠标移动时, 计算当前位置与初始位置的距离的比例
placeholder.value.addEventListener('mousemove', (e) => {
  const v = e.clientX - enterPoint.x
  const ratio = v / enterPoint.w

  requestAnimationFrame(() => render(ratio))
})

// 鼠标离开时, 缓慢恢复至初始帧状态(匀速地过渡)
placeholder.value.addEventListener('mouseout', (e) => {
  const v = e.clientX - enterPoint.x
  let ratio = v / enterPoint.w
  const gap = 0.08 * (ratio < 0 ? 1 : -1)

  requestAnimationFrame(tick)
  function tick() {
    if (gap * ratio < 0) {
      ratio = ratio + gap
      render(ratio)
      requestAnimationFrame(tick)
    } else {
      if (images.bg) {
        draw(images.bg, config.bg).to(layers.bg)
      }
    }
  }
})

// 根据鼠标移动时位置与初始位置的距离比例计算景深和位移
function render(ratio) {
  if (ratio < 0 && images.bg) {
    const c = { ...config.bg }
    c.blur = c.blur + ratio * c.blur
    draw(images.bg, c).to(layers.bg)
  }
}

5. Отрисовка подмигивающей рамки персонажа

setTimeout(wink, 4800)
async function wink() {
  await new Promise((r) => setTimeout(r, 50))
  images.twotwoClosingEye &&
    draw(images.twotwoClosingEye, config.twotwo).to(layers.twotwo)
  await new Promise((r) => setTimeout(r, 50))
  images.twotwoCloseEye &&
    draw(images.twotwoCloseEye, config.twotwo).to(layers.twotwo)
  await new Promise((r) => setTimeout(r, 50))
  images.twotwoOpeningEye &&
    draw(images.twotwoOpeningEye, config.twotwo).to(layers.twotwo)
  await new Promise((r) => setTimeout(r, 50))
  images.twotwo && draw(images.twotwo, config.twotwo).to(layers.twotwo)
  setTimeout(wink, 4800)
}

6. Перерисовывать при изменении размера области просмотра

function resize(layers) {
  return {
    with({ width, height }) {
      for (const canvas of Object.values(layers)) {
        canvas.width = width
        canvas.height = height
      }
    },
  }
}

onMounted(async () => {
  resize(layers).with(placeholder.value.getBoundingClientRect())

  window.addEventListener('resize', () => {
    resize(layers).with(placeholder.value.getBoundingClientRect())
    images.bg && draw(images.bg, config.bg).to(layers.bg)
  })
})

добиться эффекта

Обратите внимание, что GIF немного великоват, и записанный эффект не очень заметен.

Наконец, давайте угадаем, сможет ли S10 LPL снова выиграть чемпионат?