export class Equalizer { private lastIncrementedIndex: number; private lastTargetIndex: number; private minValue: number; private maxValue: number; private values: number[]; // I initialize the equalizer. constructor( minValue: number, maxValue: number, initialValues: number[] ) { // Validate the possible range of values. if ( minValue >= maxValue ) { throw( new Error( "Min value must be less than Max value." ) ); } // Validate the number of values. if ( initialValues.length === 1 ) { throw( new Error( "Initial values must have a length greater than 1." ) ); } // Validate the initial state of the values. Since the point of the equalizer is // to maintain a total across the distribution, the values must start out as the // summation of the max value. if ( this.sum( initialValues ) !== maxValue ) { throw( new Error( "Initial values don't sum to max value." ) ); } this.minValue = minValue; this.maxValue = maxValue; this.values = initialValues; this.lastIncrementedIndex = -1; this.lastTargetIndex = -1; } // --- // PUBLIC METHODS. // --- // I set the given index to the given value and return the resultant state of the // equalizer values. public setValue( targetIndex: number, newValue: number ) : number[] { // If the target index has changed, let's reset our distribution references. if ( targetIndex !== this.lastTargetIndex ) { this.lastTargetIndex = targetIndex; this.lastIncrementedIndex = targetIndex; } var currentValue = this.values[ targetIndex ]; // Constrain the application of the new value to the target index. var nextValue = this.constrain( newValue ); // Get the portion of the new value that was actually consumed. var delta = ( nextValue - currentValue ); // If no portion of the new value was actually consumed, there's nothing left to // do. if ( ! delta ) { // NOTE: This probably shouldn't happen. Smells like developer-error. return( this.values.slice() ); } // At this point, we've validated the new value against the target value, we can // apply the new value back to the collection. this.values[ targetIndex ] = nextValue; // Now, we have to distribute the INVERSE of the delta to the rest of the values // in the equalizer. We want to distribute the delta equally across all of the // other facets, so let's keep looping and handing out a single step. var deltaToDistribute = Math.abs( delta ); var step = ( delta > 0 ) ? -1 : 1 ; // Since we know that the equalizer values will always maintain a fixed sum, we // know that it is safe to keep looping until the delta has been fully consumed. while ( deltaToDistribute ) { // Increment and constrain the next index. if ( ++this.lastIncrementedIndex >= this.values.length ) { this.lastIncrementedIndex = 0; } // As we distribute the inverse delta, always skip the target index as this // index received the whole of the new value above. if ( this.lastIncrementedIndex === this.lastTargetIndex ) { continue; } var currentValue = this.values[ this.lastIncrementedIndex ]; // Constrain the application of the STEP to the current index. It's possible // that this index has already reached a local maximum and cannot be updated. var nextValue = this.constrain( currentValue + step ); if ( nextValue !== currentValue ) { this.values[ this.lastIncrementedIndex ] = nextValue; deltaToDistribute--; } } return( this.values.slice() ); } // --- // PRIVATE METHODS. // --- // I constrain the given value to be within the min-max range. private constrain( value: number ) : number { value = Math.max( value, this.minValue ); value = Math.min( value, this.maxValue ); return( value ); } // I sum the given collection of numbers. private sum( values: number[] ) : number { var total = values.reduce( ( total, value ) => { return( total + value ); } ); return( total ); } }