Octopus
Это интерактивный пример работы с FFT и Canvas на голом JS (с помощью AnalyserNode из встроенного в браузер Web Audio API). Анимация управляется звуком. Вся музыка принадлежит правообладателям, переходите на Bandcamp MantisMash по ссылкам на выбранный трек в левом верхнем углу.
https://heymoon.cc/octopus
Узнать подробнее о том, как устроена эта анимация, можно из кода на GitHub. Основная логика находится здесь, в описании объекта Octopus:
const Octopus = {
frequencyData: [],
dividedFreqMax: [],
dividedFreqMaxEver: [],
maxFreqTransition: [],
legs: [],
phases: [],
init(baseWidth, legsCount, jointsCount, animationFrames) {
const self = this;
self.animationFrames = animationFrames;
self.baseWidth = baseWidth;
for (let i = 0; i < legsCount; i++) {
let leg = [];
for (let j = 0; j < jointsCount; j++) {
leg.push({
angle: j === 0 ? 360 * (i / (legsCount - 1)) : 0,
width: ((jointsCount - j) / jointsCount) * baseWidth,
});
}
self.legs.push(leg);
}
self.dividedFreqMax.length = self.legs.length + 3; // otherwise last leg may be inactive due to mp3 compression frequency cutoff
self.dividedFreqMaxEver.length = self.dividedFreqMax.length;
self.maxFreqTransition.length = self.dividedFreqMax.length;
for (let i = 0; i < self.dividedFreqMax.length; i++) {
self.phases.push(0);
self.dividedFreqMax[i] = 1;
self.dividedFreqMaxEver[i] = 1;
self.maxFreqTransition[i] = 1;
}
},
resetFrequencyData() {
const self = this;
for (let i = 0; i < self.dividedFreqMax.length; i++) {
self.dividedFreqMax[i] = 1;
self.dividedFreqMaxEver[i] = 1;
}
},
animate(deltaTime) {
const self = this;
self.frequencyData = new Uint8Array(Player.analyser.frequencyBinCount);
if (Player.context.state === 'running') {
Player.analyser.getByteFrequencyData(self.frequencyData);
}
let sliceSize = Math.floor(self.frequencyData.length / self.dividedFreqMax.length);
let sliceSums = [];
for (let i = 0; i < self.dividedFreqMax.length; i++) {
let tmp_sum = 0;
for (let j = 0; j < sliceSize; j++) {
tmp_sum += self.frequencyData[i * sliceSize + j];
}
sliceSums.push(tmp_sum);
}
const maxFreqTransitionStep = deltaTime / 2500;
sliceSums.forEach((value, index) => {
if (self.dividedFreqMax[index] > 1) {
self.dividedFreqMax[index] -= self.dividedFreqMax[index] / (deltaTime * 10);
if (self.dividedFreqMax[index] < 1) {
self.dividedFreqMax[index] = 1;
}
}
if (value > self.dividedFreqMax[index]) {
self.dividedFreqMax[index] = value;
if (self.dividedFreqMax[index] > self.dividedFreqMaxEver[index]) {
self.dividedFreqMaxEver[index] = value;
}
}
if (self.dividedFreqMax[index]) {
self.phases[index] += (value / self.dividedFreqMax[index]) * deltaTime - (deltaTime / 2);
}
const desiredValue = self.dividedFreqMax[index] / self.dividedFreqMaxEver[index];
if (desiredValue > self.maxFreqTransition[index] + maxFreqTransitionStep) {
self.maxFreqTransition[index] += maxFreqTransitionStep;
} else if (desiredValue < self.maxFreqTransition[index] - maxFreqTransitionStep) {
self.maxFreqTransition[index] -= maxFreqTransitionStep;
}
});
self.legs.forEach((leg, i) => {
let phase = self.phases[i];
leg.forEach((joint, j) => {
if (j > 0) {
const rotationPhase = (phase / self.animationFrames + (j / (leg.length / (self.maxFreqTransition[i] * 3)))) * 2 * Math.PI;
const rotationAmplitude = j / leg.length * 60;
if (i % 2 === 0) {
self.legs[i][j].angle = Math.cos(rotationPhase) * rotationAmplitude;
} else {
self.legs[i][j].angle = Math.sin(rotationPhase) * rotationAmplitude;
}
}
});
});
},
generatePaths(xCenter, yCenter, jointLength, addAngle = 0, zoomFactor = 1, widthMultiplier = 1) {
const self = this;
let result = [];
self.absoluteLegs().forEach((leg) => {
let legResult = {
left: [],
right: []
};
let xCurrent = xCenter;
let yCurrent = yCenter;
leg.forEach((joint) => {
const addedAngle = joint.angle + addAngle;
legResult.left.push({
x: Math.round(xCurrent + Math.cos(degToRad(addedAngle - 90)) * (joint.width / 2 * widthMultiplier) * zoomFactor),
y: Math.round(yCurrent + Math.sin(degToRad(addedAngle - 90)) * (joint.width / 2 * widthMultiplier) * zoomFactor),
});
legResult.right.push({
x: Math.round(xCurrent + Math.cos(degToRad(addedAngle + 90)) * (joint.width / 2 * widthMultiplier) * zoomFactor),
y: Math.round(yCurrent + Math.sin(degToRad(addedAngle + 90)) * (joint.width / 2 * widthMultiplier) * zoomFactor),
});
xCurrent += Math.cos(degToRad(addedAngle)) * (jointLength / 2) * zoomFactor;
yCurrent += Math.sin(degToRad(addedAngle)) * (jointLength / 2) * zoomFactor;
});
let finalLegResult = [];
legResult.left.forEach((point) => {
finalLegResult.push(point);
});
finalLegResult.push({
x: Math.round(xCurrent),
y: Math.round(yCurrent)
});
legResult.right.reverse().forEach((point) => {
finalLegResult.push(point);
});
result.push(finalLegResult);
});
return result;
},
absoluteLegs() {
const self = this;
let result = [];
self.legs.forEach((leg) => {
let absoluteJoints = [];
leg.forEach((joint, i) => {
let absoluteJoint = Object.assign({}, joint);
if (i > 0) {
absoluteJoint.angle = absoluteJoints[i - 1].angle + joint.angle;
}
absoluteJoints.push(absoluteJoint);
});
result.push(absoluteJoints);
});
return result;
}
}