---
# System prepended metadata

title: PARTICLE ANIMATION - 29/09

---

# PARTICLE ANIMATION - 29/09

**ParticleAnimation.kt**
```kotlin
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import com.annhienktuit.domain.models.EPointF
import com.annhienktuit.muzikv2.R
import com.annhienktuit.muzikv2.enums.Interpolation
import com.annhienktuit.muzikv2.ui.views.visualizer.nextgen.painters.Painter
import com.annhienktuit.muzikv2.utils.VisualizerHelper
import com.annhienktuit.muzikv2.utils.audiovisualizer.PolyBezierPathUtil
import com.annhienktuit.muzikv2.utils.maths.polynomials.PolynomialSplineFunction
import java.util.*
import kotlin.math.PI

/**
 *Created by Nhien Nguyen on 9/26/2022
 */
class ParticleAnimation(
    var context: Context,
    override var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG),
    private var startHz: Int = 20,
    private var endHz: Int = 2000,
    private var interpolator: Interpolation = Interpolation.SPLINE,
) : Painter() {
    private var upperGravityPoints = Array(0) { GravityModel(0f) }
    private val barWidth =
        context.resources.getDimensionPixelSize(R.dimen.default_visualizer_bar_width).toFloat()
    private val radiusR: Float = .4f
    private val ampR: Float = 1f //The rate of amplification
    private val angleOffset = 2 * PI.toFloat() / numberOfDot
    private var shortest = 0F
    override var discRadius = 0
        set(value) {
            field = value
            shortest = getShortest()
            adjustRadius(shortest)
        }
    override var isPlaying = false
    var firstTimeRunning = true
    private var currentHeight = 0
    private var currentWidth = 0
    lateinit var upperFFT: FloatArray
    var upperCurveUtil: PolynomialSplineFunction? = null

    // Fields for normalization
    private var min = 0f
    private var max = 0f
    private val highFreqDelta = 20
    private var frequencyMap = HashMap<Int, Int>()
    private var random = Random()
    private var actualBarHeight = 0F
    private var scale = 1F

    //Fields for bottom animator
    private var bottomLinePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    lateinit var bottomFFT: FloatArray
    private var polyPathsUtil = PolyBezierPathUtil()
    var bottomCurveUtil: PolynomialSplineFunction? = null
    private var bottomStartHZ = 8000
    private var bottomEndHz = 10000
    private var bottomGravityPoints = Array(0) { GravityModel(0f) }

    //Fields for spears
    private var spearPaint = Paint(Paint.ANTI_ALIAS_FLAG)

    init {
        paint.apply {
            color = Color.parseColor(visualizerColor)
            style = Paint.Style.FILL
            strokeWidth = barWidth
            strokeCap = Paint.Cap.ROUND
            alpha = 200
        }
        bottomLinePaint.apply {
            color = Color.parseColor(visualizerColor)
            style = Paint.Style.STROKE
            strokeWidth = barWidth / 2
            strokeCap = Paint.Cap.ROUND
            alpha = 200
        }
        spearPaint.apply {
            color = Color.parseColor(visualizerColor)
            style = Paint.Style.FILL
            strokeWidth = barWidth / 2
            strokeCap = Paint.Cap.ROUND
            alpha = 30
        }
    }

    override fun calc(helper: VisualizerHelper) {
        calcForUpperAnimator(helper)
        calcForBottomAnimator(helper)
    }

    private fun calcForBottomAnimator(helper: VisualizerHelper) {
        bottomFFT = (helper.getFftMagnitudeRange(bottomStartHZ, bottomEndHz))
        bottomFFT = getCircleFft(bottomFFT)
        if (bottomGravityPoints.size != bottomFFT.size) {
            bottomGravityPoints = Array(bottomFFT.size) {
                GravityModel(0F)
            }
        }
        bottomGravityPoints.forEachIndexed { index, bar ->
            bar.update(bottomFFT[index] * ampR / 1.5F)
        }
        bottomCurveUtil = interpolateFft(bottomGravityPoints, numberOfDot / 5, interpolator)
    }

    private fun calcForUpperAnimator(helper: VisualizerHelper) {
        upperFFT = helper.getFftMagnitudeRange(startHz, endHz)
        upperFFT = getPowerFft(upperFFT)
        upperFFT = if (firstTimeRunning) {
            getCircleFft(upperFFT, true)
        } else {
            getCircleFft(upperFFT)
        }
        minMaxNormalize(upperFFT, visualizerMaxHeight, 0)
        if (upperGravityPoints.size != upperFFT.size) upperGravityPoints = Array(upperFFT.size) {
            GravityModel(
                0f
            )
        }
        upperGravityPoints.forEachIndexed { index, bar -> bar.update(upperFFT[index] * ampR) }
        upperCurveUtil = interpolateFftCircle(upperGravityPoints, numberOfDot, interpolator)
    }

    override fun draw(canvas: Canvas, width: Int, height: Int, drawWithoutCalc: Boolean) {
        if (drawWithoutCalc) {
            drawHelperCircleLine(canvas, .5f, .5f) {
                // Still need to run this function to do the translate, but we don't update data on the first time
                // Otherwise, it will draw in somewhere psychically
            }
            return
        }
        currentWidth = width
        currentHeight = height
        if (firstTimeRunning) {
            shortest = Integer.min(width, height).toFloat()
            adjustRadius(shortest)
        }
        drawHelperCircleLine(canvas, .5f, .5f) {
            if (!firstTimeRunning || !drawWithoutCalc) {
                for (idx in 0 until numberOfDot) {
                    //TODO: Replace these magic numbers
                    upperPoints[idx] = toCartesianEpointF(
                        shortest / 2f * (radiusR + 0.03F) + upperCurveUtil?.value(idx.toDouble())!!
                            .toFloat(), angleOffset * idx
                    )
                    bottomPoints[idx] = toCartesianEpointF(
                        shortest / 2.35f * (radiusR + 0.03F) + bottomCurveUtil?.value(idx.toDouble())!!
                            .toFloat(), angleOffset * idx
                    )
                }
            }
            canvas.drawCircle(0f, 0f, 10f, paint)
        }
        upperPoints[numberOfDot] = upperPoints[0]
        bottomPoints[numberOfDot] = bottomPoints[0] // we do this to connect the begin-end of line
        makeSureViewNotOutOfBound()
        for (idx in upperPoints.indices) {
            canvas.drawCircle(upperPoints[idx].x, upperPoints[idx].y, 4f, paint)
        }
        canvas.drawPath(
            polyPathsUtil.computePathThroughKnots(bottomPoints.asList()), bottomLinePaint
        )
        drawSpears(canvas)
        if (firstTimeRunning) firstTimeRunning = false
    }

    private fun drawSpears(canvas: Canvas) {
        for (idx in bottomPoints.indices) {
            if (upperPoints[idx].distanceFrom(bottomPoints[idx]) > drawSpearThreshold) {
                canvas.drawLine(
                    upperPoints[idx].x,
                    upperPoints[idx].y,
                    (bottomPoints[idx].x + upperPoints[idx].x) / 2,
                    (bottomPoints[idx].y + upperPoints[idx].y) / 2,
                    spearPaint
                )
            }
        }
    }

    /**
     * Check whether the current bar is over [visualizerMaxHeight]
     * If true, scale tp fit [visualizerMaxHeight]
     */
    private fun makeSureViewNotOutOfBound() {
        for (idx in upperPoints.indices) {
            actualBarHeight = (upperPoints[idx].distanceFrom(centerPoint) - discMargin - discRadius)
            if (actualBarHeight > visualizerMaxHeight) {
                scale = visualizerMaxHeight / actualBarHeight
                upperPoints[idx] = upperPoints[idx].scaleBy(scale)
            }
        }
    }

    /**
     * Calculate the new radius between 2 opposite point in circle
     * Then scale [shortest] based on ratio between new radius and old radius
     */
    private fun adjustRadius(minValue: Float) {
        val firstPoint = toCartesian(minValue / 2f * radiusR, angleOffset * 0)
        val secondPoint = toCartesian(minValue / 2f * radiusR, angleOffset * (numberOfDot / 2))
        val currentRadius = (firstPoint.first - secondPoint.first) / 2
        val newRadius: Float
        if (currentRadius < discRadius) {
            newRadius = discRadius.toFloat() + discMargin
            val ratio = (newRadius / currentRadius)
            this.shortest *= ratio
        }
    }

    /**
     * Apply min-max normalization then handle if its value is "high frequency" continuously
     * @see <a href="https://towardsdatascience.com/everything-you-need-to-know-about-min-max-normalization-in-python-b79592732b79">Min-Max Normalization </a> <br>
     * <p> A data is defined to be "high frequency" if its value is in range <br> [[visualizerMaxHeight] - [highFreqDelta] , [visualizerMaxHeight]] </p>
     */
    private fun minMaxNormalize(fftData: FloatArray, maxLimit: Int, minLimit: Int = 0) {
        min = Float.MAX_VALUE
        max = Float.MIN_VALUE
        for (value in fftData) {
            if (value > max) max = value
            if (value < min) min = value
        }
        if (max > visualizerMaxHeight) {
            for (idx in fftData.indices) {
                fftData[idx] = ((fftData[idx] - min) / (max - min)) * maxLimit + minLimit
                if (fftData[idx].isExclusive(
                        visualizerMaxHeight - highFreqDelta, visualizerMaxHeight
                    )
                ) {
                    handleHighFreqValue(idx)
                } else {
                    resetMapValue(idx)
                }
            }
        }
    }

    /**
     * Handle high frequency value to avoid low variability
     * If that data is in range [[visualizerMaxHeight] - [highFreqDelta] , [visualizerMaxHeight]]
     */
    private fun handleHighFreqValue(idx: Int) {
        try {
            if (frequencyMap.containsKey(idx)) {
                frequencyMap[idx] = frequencyMap[idx]!!.plus(1)
            } else {
                frequencyMap[idx] = 1
            }
            if (frequencyMap[idx]!! > 3) {
                frequencyMap[idx] = 0
                upperFFT[idx] =
                    (random.nextInt((visualizerMaxHeight - highFreqDelta) - highFreqDelta + 1) + highFreqDelta).toFloat()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * If the frequency value is not continue to return high frequency, reset it counter in [frequencyMap]
     */
    private fun resetMapValue(idx: Int) {
        if (frequencyMap.containsKey(idx) && frequencyMap[idx]!! > 0) {
            frequencyMap[idx] = 0
        }
    }

    private fun Float.isExclusive(start: Int, stop: Int): Boolean {
        return (this >= start && this <= stop)
    }

    private fun getShortest(): Float = Integer.min(currentWidth, currentHeight).toFloat()

    companion object {
        private const val TAG = "ParticleAnimation"
        private const val numberOfDot: Int = 100
        private const val drawSpearThreshold = 20F
        private const val visualizerColor = "#FFFFFF"
        val upperPoints = Array(numberOfDot + 1) { EPointF() }
        val bottomPoints = Array(numberOfDot + 1) { EPointF() }
        val centerPoint = EPointF(0F, 0F)
    }
}
```