Первый взгляд на эффект
С музыкальными эффектами поддерживает только Google Chrome
Простой? У нас может не хватать глаз для открытия простоты, но нам не хватает рук для совершенной простоты.
Этот анимационный эффект был замечен в приложении NetEase Cloud Music, и он был реализован с менталитетом просмотра холста.
Теперь приступайте к анализу и реализуйте требуемый функционал
Список требований
- См. ссылку выше для эффекта.
- Настраиваемые параметры (размер, цвет и т. д.) необходимы для различных сценариев использования.
- Нужно анимировать в соответствии с ритмом музыки
приступить к реализации
1. Настройка параметров
В соответствии с эффектом мы можем изначально думать о параметрах, которые необходимо установить следующим образом, а конкретные значения параметров могут быть дополнительно изменены в последующих реализациях.
const originParams = {
cover: '', // 中心的封面图
size: 500, // 画布 canvas 的尺寸
radius: 100, // 封面图,中心圆的半径,小于零则为容器的百分比
interval: [500, 1500], // 涟漪出现的最小频率(毫秒)
centerColor: '#ddd', // 封面图位置的颜色(在没有封面图时显示)
borderWidth: 5, // 封面图边框的宽度
borderColor: '#aaa', // 封面图边框的颜色
rippleWidth: 4, // 涟漪圆环的宽度
rippleColor: '#fff', // 涟漪颜色
pointRadius: 8, // 涟漪圆点的半径
rotateAngle: .3, // 封面图每帧旋转的角度
}
2. Напишите конструктор
Мы знаем, что принцип мультипликации заключается в скрытой замене похожей картинки, когда мозг не обращает внимания на реакцию, так что пошаговая замена образует анимацию, и это пошаговое можно назвать帧
.
Итак, при рендеринге кругов и точек нам нужен массив для хранения их положения каждый раз, когда они рендерятся.
Кроме того, нам нужны некоторые необходимые суждения об инициализации и объявление некоторых общедоступных параметров.
Таким образом, мы можем написать следующий конструктор
class Ripple {
constructor(container, params = {}) {
const originParams = {
cover: '',
size: 500,
radius: 100,
interval: [500, 1500],
centerColor: '#ddd',
borderWidth: 5,
borderColor: '#aaa',
rippleWidth: 4,
rippleColor: '#fff',
pointRadius: 8,
rotateAngle: .3,
}
this.container = typeof container === "string" ? document.querySelector(container) : container
this.params = Object.assign(originParams, params)
this.cover = this.params.cover
this.radius = this.params.radius < 1 ? this.params.size * this.params.radius : this.params.radius
this.center = this.params.size / 2 // 中心点
this.rate = 0 // 记录播放的帧数
this.frame = null // 帧动画,用于取消
this.rippleLines = [] // 存储涟漪圆环的半径
this.ripplePoints = [] // 存储涟漪点距离中心点的距离
}
}
3. Инициализируйте контейнер
Нелегко визуализировать и вращать изображения на холсте, поэтому вcover
При передаче параметров передатьimg
метка для рендеринга.
Кроме того, нам нужен другой необходимый CSS для добавления к элементу.
class Ripple{
initCanvas() {
this.container.innerHTML = `<canvas width="${this.params.size}" height="${this.params.size}"></canvas>${this.cover ? `<img src="${this.cover}" alt="">` : ''}`
this.cover = this.container.querySelector('img')
this.canvas = this.container.querySelector('canvas')
this.ctx = this.canvas.getContext('2d')
this.rotate = 0
const containerStyle = { ... }
const canvasStyle = { ... }
const coverStyle = { ... }
utils.addStyles(this.container, containerStyle)
utils.addStyles(this.canvas, canvasStyle)
utils.addStyles(this.cover, coverStyle)
this.strokeBorder()
}
}
4. Нарисуйте круг
Основное использование холста не будет вдаваться в подробности.
class Ripple{
strokeCenterCircle() {
const ctx = this.ctx
ctx.beginPath()
ctx.arc(this.center, this.center, this.radius, 0, 2 * Math.PI)
ctx.closePath()
ctx.fillStyle = this.params.centerColor
ctx.fill()
}
strokeBorder() {
const ctx = this.ctx
ctx.beginPath()
ctx.arc(this.center, this.center, this.radius + this.params.borderWidth / 2, 0, 2 * Math.PI)
ctx.closePath()
ctx.strokeStyle = this.params.borderColor
ctx.lineWidth = 5
ctx.stroke()
}
}
5. Нарисуйте круг
Разве это не просто круг плюс круг?
class Ripple{
drawRipple() {
const ctx = this.ctx
// 画外圈
ctx.beginPath()
ctx.arc(this.center, this.center, 200, 0, Math.PI * 2)
ctx.strokeStyle = 'rgba(255,255,255,0.4)'
ctx.lineWidth = this.params.rippleWidth
ctx.stroke()
// 画点
ctx.beginPath()
ctx.arc(this.center - 200/Math.sqrt(2), this.center - 200/Math.sqrt(2), this.params.pointRadius, 0, 2 * Math.PI)
ctx.closePath()
ctx.fillStyle = 'rgba(255,255,255,0.4)'
ctx.fill()
}
}
Итак, возникает следующая проблема
Причина появления также очень проста.Прозрачность перекрывающейся части двух полупрозрачных графиков обязательно увеличится, поэтому решить ее можно только нарисовав неполный круг (просто отделив часть точки).Решение выглядит следующим образом:
Для того, чтобы это было легко увидеть, линия соединения немного смещена внутрь, поэтому наша задача состоит в том, чтобы узнать r, R и найти угол θ, решение не будет подробно объясняться, это приложение средней школы математика. Мы можем получить угол какMath.asin(R / r / 2) * 4
.
6. Продолжайте рисовать круги
Совпадение круга и точки решено, теперь нужно обновлять их положение при каждом обновлении, если условие выполнено, нужно добавить новый круг и точку, если радиус круга превышает холст , затем удалите соответствующие данные
class Ripple{
strokeRipple() {
// 当圆环大小超出画布时,删除改圆环数据
if (this.rippleLines[0] > this.params.size) {
this.rippleLines.shift()
this.ripplePoints.shift()
}
// 当达到条件时,添加数据
if (this.rate - this.lastripple >= this.minInterval) {
this.rippleLines.push({
r: this.radius + this.params.borderWidth + this.params.rippleWidth / 2,
color: utils.getRgbColor(this.params.rippleColor)
})
this.ripplePoints.push({
angle: utils.randomAngle()
})
// 更新添加时间
this.lastripple = this.rate
}
// 计算下一次渲染的位置数据
this.rippleLines = this.rippleLines.map((line, index) => ...)
this.ripplePoints = this.rippleLines.map((line, index) => ...)
// 根据新的数据渲染
this.strokeRippleLine()
this.strokeRipplePoint()
}
}
6. Начать анимацию
Данные обновляются каждый раз, когда они отображаются, иrequestAnimationFrame
Хранится вthis.frame
, это удобно отменить.
class Ripple{
animate() {
this.ctx.clearRect(0, 0, this.params.size, this.params.size)
this.strokeRipple()
this.strokeBorder()
...
var that = this
this.frame = requestAnimationFrame(function () {
that.animate()
})
}
}
7. Добавьте музыкальные ритмы
Принцип заключается в следующем:
- Создавать
<audio>
, выполните соответствующие настройки (автовоспроизведение, дисплей управления и т. д.) - Создайте AudioContext, установите источник звука на этот
- Создайте AnalyserNode и свяжите AnalyserNode с аудиоисточником AudioContext.
- Если вам нужно одновременно добиться эффекта проигрывания дисплея, вам необходимо подключить Analyzer к AudioContext, иначе данные будут приходить только без звука.
- Создайте место для рисования (это может быть обычный элемент HTML или холст, SVG, WebGL и т. д.)
- Регулярно получать аудиоданные от AnalyserNode (это могут быть данные во временной или частотной области, а синхронизация может быть выполнена с помощью requestAnimationFrame или setTimeout)
- Используйте полученные данные для визуального проектирования и рисования изображений
Однако может бытьaudiocontext
В сафари невозможно получить аудиоинформацию в режиме реального времени, если кто знает об этом, сообщите пожалуйста.
Так что в реализации этой части и говорить не о чем, если интересно, можете проверить непосредственно.исходный кода такжевыполнить
напиши в конце
Реализация приведенной выше анимации действительно не сложна, но в процессе реализации можно больше внимания уделить тому, как организовать код и как спроектировать интерфейс (как облегчить использование, расширить возможности настройки и сократить количество операций). ). Эти вещи были написаны легко и несерьезно при написании вышеприведенного туториала, который упоминался одним росчерком или не упоминался вообще, но осознать настоящие потребности можно только тогда, когда напишешь их сам. хорошо запишите реализацию кода, которую вы только что видели. , просто посмотрите на эффект, и напишите подобную анимацию (с музыкой) сами.В конце концов, мы не лишены глаз, чтобы обнаружить простоту, но то, что может отсутствовать, является совершенным и простые руки.
некоторые другие предметы
Я думаю, что это хорошо, но никто, кажется, не знает...