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;
    }
}