Есть волшебное место, где можно посмотреть драмы и варьете, а также короткие видео прямые трансляции и игры.Сегодня посмотрим, к какому брату пойдет деревенский тиран, а завтра, в какой тюрьме сидит Чжан Сан.
Правильно, я про 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 снова выиграть чемпионат?