
"use strict";

class CProgram {
    constructor( ) {
        // Config.
        this.Dependencies = {
            _Math: new CMath( ),
            _Spirograph: new CSpirograph( ),
            _TimeStep: new CTimeStep( )
            };
        // Data.
        this.DependencyInjected = false;
    }
    DependencyInject( parent ) {
        for ( const key in this.Dependencies ) {
            const value = this.Dependencies[ key ]
            value.DependencyInject( this );
        }
        this.DependencyInjected = true;
    }
    Initialise( ) {
        for ( const key in this.Dependencies ) {
            const value = this.Dependencies[ key ]
            if ( ! value.DependencyInjected )
                throw new Error( key );
            value.Initialise( );
        }
    }
    Execute( ) {
        this.DependencyInject( );
        this.Initialise( );

        this.Dependencies._TimeStep.ExecuteB( document.timeline.currentTime );

        for ( const key in this.Dependencies ) {
            const value = this.Dependencies[ key ]
            if ( ! value.DependencyInjected )
                throw new Error( key );
            value.Execute( );
        }
    }
}
class CMath {
    constructor( ) {
        // Config.
        this.RadToDeg = 180 / Math.PI;
        this.DegToRad = Math.PI / 180;
        // Data.
        this.DependencyInjected = false;
    }
    Initialise( ) {
    }
    DependencyInject( parent ) {
        this.DependencyInjected = true;
    }
    Execute( ) {
    }

    Mod( n, m ) {
        return ( ( n % m ) + m ) % m;
    }
    ModB( startingValue, offset, minimumValue, modulus ) {
        return ( startingValue - minimumValue + (offset % modulus) + modulus) % modulus + minimumValue;

    }
    Clamp( n, min, max ) {
        return Math.min( Math.max( n, min ), max );
    }

    GetRandomInt(min, max) {
        min = Math.ceil(min);
        max = Math.floor(max);
        return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive
    }
    GetRandomIntInclusive(min, max) {
        min = Math.ceil(min);
        max = Math.floor(max);
        return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive
    }
    GetRandomArbitrary( min, max ) {
        return Math.random() * (max - min) + min;
    }
}
class CSpirograph {
    constructor( ) {
        // Config.
        this.FOnKeyDown = this.OnKeyDown.bind( this );
        this.FOnClick = this.OnClick.bind( this );
        this.FOnRequestAnimationFrame = this.OnRequestAnimationFrame.bind( this );

        // Data.
        this.Rotators = [ ];
        this.Iterations = 1;
        this.RotatorIndexCurrent = 0;
        this.PositionReusable = new SPosition( );
        this.RotatorLengthRatioSum = 0;

        this.DependencyInjected = false;
    }
    Initialise( ) {
        this.Stats = new CSpirographStats( this );
        this.Animation = new CSpirographAnimation( this );
        this.RandomiseIncludes = new CSpirographRandomiseIncludes( this );
        this.Canvas = new CSpirographCanvas(  this );
    }
    DependencyInject( parent ) {
        this._Math = parent.Dependencies._Math;
        this._TimeStep = parent.Dependencies._TimeStep;

        this.DependencyInjected = true;
    }
    Execute( ) {
        this.InjectCanvas( );
        this.BindHotkeys( );
        this.AddOriginRotator( );
        this.CreaseRotators( 1 );
        this.CalculateRotatorLengthRatioSum( );
        this._TimeStep.InitialiseStep( document.timeline.currentTime );
        this.OnRequestAnimationFrame( null );
        this.ClearCanvas( );
        this.UpdateConfigs( );
    }

    AddOriginRotator( ) {
        this.Rotators.push( new CRotator( this.Animation.CelerationDivisorLinearDefault, this.Animation.LengthRatioDivisorLinearDefault, this.Canvas.Origin.X, this.Canvas.Origin.Y ) );
    }
    CalculateRotatorLengthRatioSum( ) {
        let a = 0;
        for ( let i = 1; i < this.Rotators.length; i++ )
            a += this.Rotators[ i ].Length.Ratio;
        this.RotatorLengthRatioSum = a;
    }
    ResetRotator( ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ] = new CRotator( this.Animation.CelerationDivisorLinearDefault, this.Animation.LengthRatioDivisorLinearDefault );
        this.UpdateConfigs( );
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    ResetRotators( ) {
        for ( let i = 1; i < this.Rotators.length; i++ )
            this.Rotators[ i ] = new CRotator( this.Animation.CelerationDivisorLinearDefault, this.Animation.LengthRatioDivisorLinearDefault );
        this.UpdateConfigs( );
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }

    CreaseRotatorIndexCurrent( direction ) {
        if ( direction === 0 )
            return;
        this.RotatorIndexCurrent = this._Math.Clamp( this.RotatorIndexCurrent + direction, 1, this.Rotators.length - 1 );
        this.UpdateConfigs( );
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseRotators( direction ) {
        if ( direction === 0 )
            return;
        // Increase of decrease rotators.
        let c = Math.abs( direction );
        if ( direction > 0 ) {
            for ( let i = 0; i < c; i++ )
                this.Rotators.push( new CRotator( this.Animation.CelerationDivisorLinearDefault, this.Animation.LengthRatioDivisorLinearDefault ) );
            this.RotatorIndexCurrent = this.Rotators.length - 1;
        } else if ( direction < 0 ) {
            for ( let i = 0; i < c; i++ ) {
                if ( this.Rotators.length > 2 )
                    this.Rotators.pop( );
            }
            if ( this.RotatorIndexCurrent > this.Rotators.length - 1 )
                this.RotatorIndexCurrent = this.Rotators.length - 1;
        }
        this.Canvas.Clear = true;
        this.UpdateConfigs( );
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseIterations( direction, rotator ) {
        if ( direction === 0 )
            return;
        let v = this.Iterations,
            c = Math.abs( direction );
        if ( direction > 0 ) {
            for ( let i = 0; i < c; i ++ )
                v += v;
            console.log( this.Iterations + ", " + c + ", " + v );
        } else if ( direction < 0 ) {
            for ( let i = 0; i < c; i ++ ) {
                v = v / 2;
                if ( v < 1 ) {
                    v = 1;
                    break;
                }
            }
        }
        this.Iterations = v;
        this.UpdateConfigs( );
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }

    CreaseRotatorLengthRatio( direction, rotator ) {
        if ( direction === 0 )
            return;
        // Increase of decrease rotator length ratio.
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        if ( r.Length.SpeedLinear )
            r.Length.Ratio = this._Math.Mod( r.Length.Ratio + direction / this.Animation.LengthRatioDivisorLinear, 1 );
        else
            r.Length.Ratio = this._Math.Mod( r.Length.Ratio + direction / this.Animation.LengthRatioDivisor, 1 );
        this.CalculateRotatorLengthRatioSum( );
        this.Canvas.Clear = true;
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseRotatorLengthSpeed( direction, rotator ) {
        if ( direction === 0 )
            return;
        // Increase of decrease rotator length ratio speed.
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        if ( r.Length.SpeedLinear )
            r.Length.Speed = ( r.Length.Speed + direction ) % 180;
        else
            r.Length.Speed = ( r.Length.Speed + direction ) % 180;
        this.Canvas.Clear = true;
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseRotatorAngle( direction, rotator ) {
        if ( direction === 0 )
            return;
        // Increase of decrease rotator angle.
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        r.Angle.Degrees = this._Math.Mod( r.Angle.Degrees + direction, 360 );
        this.Canvas.Clear = true;
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseRotatorAngleSpeed( direction, rotator ) {
        if ( direction === 0 )
            return;
        // Increase of decrease rotator angle speed.
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        r.Angle.Speed = ( r.Angle.Speed + direction ) % 180;
        this.Canvas.Clear = true;
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseRotatorAngleCeleration( direction, rotator ) {
        if ( direction === 0 )
            return;
        // Increase of decrease rotator angle celeration.
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        r.Angle.Celeration = ( r.Angle.Celeration + direction ) % 180;
        this.Canvas.Clear = true;
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseRotatorAngleOffset( direction, rotator ) {
        if ( direction === 0 )
            return;
        // Increase of decrease rotator angle.
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        r.Angle.Offset = this._Math.Mod( r.Angle.Offset + direction, 360 );
        this.Canvas.Clear = true;
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseRotatorAngleOffsetSpeed( direction, rotator ) {
        if ( direction === 0 )
            return;
        // Increase of decrease rotator angle speed.
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        r.Angle.OffsetSpeed = ( r.Angle.OffsetSpeed + direction ) % 180;
        this.Canvas.Clear = true;
        this.UpdateStats( );
    }
    CreaseRotatorAngleOffsetCeleration( direction, rotator ) {
        if ( direction === 0 )
            return;
        // Increase of decrease rotator angle celeration.
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        r.Angle.OffsetCeleration = ( r.Angle.OffsetCeleration + direction ) % 180;
        this.Canvas.Clear = true;
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseRotatorAngleOffsetCelerationAngle( direction, rotator ) {
        if ( direction === 0 )
            return;
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        r.Angle.OffsetCelerationAngleDegrees = this._Math.Mod( r.Angle.OffsetCelerationAngleDegrees + direction, 360 );
        this.Canvas.Clear = true;
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }
    CreaseRotatorLengthSpeedAngle( direction, rotator ) {
        if ( direction === 0 )
            return;
        let r = rotator || this.Rotators[ this.RotatorIndexCurrent ];
        r.Length.SpeedAngleDegrees = this._Math.Mod( r.Length.SpeedAngleDegrees + direction, 360 );
        this.Canvas.Clear = true;
        if ( ! this.Stats.Realtime )
            this.UpdateStats( );
    }

    RandomiseRotatorsParametersAll( ) {
        let r;
        for ( let i = 1; i < this.Rotators.length; i++ ) {
            r = this.Rotators[ i ];
            if ( this.RandomiseIncludes.RotatorsAngle )
                this.RandomiseRotatorsAngle( r );
            if ( this.RandomiseIncludes.RotatorsAngleSpeed )
                this.RandomiseRotatorsAngleSpeed( r );
            // if ( this.RandomiseIncludes.RotatorsAngleCeleration )
            //     this.RandomiseRotatorsAngleCeleration( r );

            if ( this.RandomiseIncludes.RotatorsAngleOffsetCelerationLinear )
                this.RandomiseRotatorsAngleOffsetCelerationLinear( r );
            if ( this.RandomiseIncludes.RotatorsAngleOffset )
                this.RandomiseRotatorsAngleOffset( r );
            if ( this.RandomiseIncludes.RotatorsAngleOffsetSpeed )
                this.RandomiseRotatorsAngleOffsetSpeed( r );
            if ( this.RandomiseIncludes.RotatorsAngleOffsetCeleration )
                this.RandomiseRotatorsAngleOffsetCeleration( r );
            if ( this.RandomiseIncludes.RotatorsAngleOffsetCelerationAngle )
                this.RandomiseRotatorsAngleOffsetCelerationAngle( r );

            if ( this.RandomiseIncludes.RotatorsLengthSpeedLinear )
                this.RandomiseRotatorsLengthSpeedLinear( r );
            if ( this.RandomiseIncludes.RotatorsLengthRatio )
                this.RandomiseRotatorsLengthRatio( r );
            if ( this.RandomiseIncludes.RotatorsLengthSpeed )
                this.RandomiseRotatorsLengthSpeed( r );
            if ( this.RandomiseIncludes.RotatorsLengthSpeedAngle )
                this.RandomiseRotatorsLengthSpeedAngle( r );
            
            if ( this.RandomiseIncludes.DrawStartDirectionAwayFromTip )
                this.RandomiseDrawStartDirectionAwayFromTip( r );
            if ( this.RandomiseIncludes.DrawStartRotatorIndexCount )
                this.RandomiseDrawStartRotatorIndexCount( r );
        }
        this.Canvas.Clear = true;
        this.UpdateConfigs( );
        if ( ( ! this.Stats.Realtime )
                && this.Stats.Show )
            this.UpdateStats( );
    }
    RandomiseRotatorsAngle( rotator ) {
        if ( rotator )
            rotator.Angle.Degrees = this._Math.GetRandomInt( 0, 360 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Angle.Degrees = this._Math.GetRandomInt( 0, 360 );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsAngleSpeed( rotator ) {
        if ( rotator )
            rotator.Angle.Speed = this._Math.GetRandomInt( -180, 180 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Angle.Speed = this._Math.GetRandomInt( -180, 180 );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats>Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsAngleCeleration( rotator ) {
        if ( rotator )
            rotator.Angle.Celeration = this._Math.GetRandomInt( -180, 180 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Angle.Celeration = this._Math.GetRandomInt( -180, 180 );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsAngleOffset( rotator ) {
        if ( rotator )
            rotator.Angle.Offset = this._Math.GetRandomArbitrary( 0, 360 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Angle.Offset = this._Math.GetRandomArbitrary( 0, 360 );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsAngleOffsetSpeed( rotator ) {
        if ( rotator )
            rotator.Angle.OffsetSpeed = this._Math.GetRandomArbitrary( -180, 180 + Number.MIN_VALUE );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Angle.OffsetSpeed = this._Math.GetRandomArbitrary( -180, 180 + Number.MIN_VALUE );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsAngleOffsetCeleration( rotator ) {
        if ( rotator )
            rotator.Angle.OffsetCeleration = this._Math.GetRandomArbitrary( -180, 180 + Number.MIN_VALUE );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Angle.OffsetCeleration = this._Math.GetRandomArbitrary( -180, 180 + Number.MIN_VALUE );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsAngleOffsetCelerationAngle( rotator ) {
        if ( rotator )
            rotator.Angle.OffsetCelerationAngleDegrees = this._Math.GetRandomArbitrary( 0, 360 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Angle.OffsetCelerationAngleDegrees = this._Math.GetRandomArbitrary( 0, 360 );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsAngleOffsetCelerationLinear( rotator ) {
        if ( rotator )
            rotator.Angle.OffsetCelerationLinear = this._Math.GetRandomIntInclusive( 0, 1 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Angle.OffsetCelerationLinear = this._Math.GetRandomIntInclusive( 0, 1 );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsLengthRatio( rotator ) {
        if ( rotator )
            rotator.Length.Ratio = this._Math.GetRandomArbitrary( 0, 1 + Number.MIN_VALUE );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Length.Ratio = this._Math.GetRandomArbitrary( 0, 1 + Number.MIN_VALUE );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsLengthSpeed( rotator ) {
        if ( rotator )
            rotator.Length.Speed = this._Math.GetRandomArbitrary( -this.Animation.LengthRatioDivisor, this.Animation.LengthRatioDivisor + Number.MIN_VALUE );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Length.Speed = this._Math.GetRandomArbitrary( -this.Animation.LengthRatioDivisor, this.Animation.LengthRatioDivisor + Number.MIN_VALUE );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsLengthSpeedAngle( rotator ) {
        if ( rotator )
            rotator.Length.SpeedAngleDegrees = this._Math.GetRandomArbitrary( 0, 360 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Length.SpeedAngleDegrees = this._Math.GetRandomArbitrary( 0, 360 );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseRotatorsLengthSpeedLinear( rotator ) {
        if ( rotator )
            rotator.Length.SpeedLinear = this._Math.GetRandomIntInclusive( 0, 1 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Rotators[ i ].Length.SpeedLinear = this._Math.GetRandomIntInclusive( 0, 1 );
            this.Canvas.Clear = true;
            if ( ( ! this.Stats.Realtime )
                    && this.Stats.Show )
                this.UpdateStats( );
        }
    }
    RandomiseDrawStartDirectionAwayFromTip( rotator ) {
        if ( rotator )
            this.Animation.DrawStartDirectionAwayFromTip = this._Math.GetRandomIntInclusive( 0, 1 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Animation.DrawStartDirectionAwayFromTip = this._Math.GetRandomIntInclusive( 0, 1 );
            this.Canvas.Clear = true;
            this.UpdateConfigs( );
        }
    }
    RandomiseDrawStartRotatorIndexCount( rotator ) {
        if ( rotator )
            this.Animation.DrawStartRotatorIndexCount = this._Math.GetRandomIntInclusive( 0, this.Rotators.length - 1 );
        else {
            for ( let i = 1; i < this.Rotators.length; i++ )
                this.Animation.DrawStartRotatorIndexCount = this._Math.GetRandomIntInclusive( 0, this.Rotators.length - 1 );
            this.Canvas.Clear = true;
            this.UpdateConfigs( );
        }
    }

    UpdateStats( ) {
        if ( ! this.Stats.Show )
            return;

        let a,
            b = document.activeElement,
            r = this.Rotators[ this.RotatorIndexCurrent ];

        a = document.getElementById( "IInput_Fps" );
        if ( a !== b )
            a.value = Math.round( this.Stats.Framerate );
        a = document.getElementById( "IInput_RotatorAngle" );
        if ( a !== b )
            a.value = r.Angle.Degrees;
        a = document.getElementById( "IInput_RotatorAngleSpeed" );
        if ( a !== b )
            a.value = r.Angle.Speed;
        /*
        a = document.getElementById( "IInput_RotatorAngleCeleration" );
        if ( a !== b )
            a.value = r.Angle.Celeration;
        */

        a = document.getElementById( "IInput_RotatorAngleOffsetSpeed" );
        if ( a !== b )
            a.value = r.Angle.OffsetSpeed;
        a = document.getElementById( "IInput_RotatorAngleOffsetCeleration" );
        if ( a !== b )
            a.value = r.Angle.OffsetCeleration;
        a = document.getElementById( "ICheckbox_RotatorAngleOffsetCelerationLinear" );
        a.checked = r.Angle.OffsetCelerationLinear;

        a = document.getElementById( "IInput_RotatorLengthSpeed" );
        if ( a !== b )
            a.value = r.Length.Speed;
        a = document.getElementById( "IInput_RotatorLengthSpeedAngle" );
        if ( a !== b )
            a.value = r.Length.SpeedAngleDegrees;
        a = document.getElementById( "ICheckbox_RotatorLengthSpeedLinear" );
        a.checked = r.Length.SpeedLinear;

        if ( r.Angle.OffsetCelerationLinear ) {
            a = document.getElementById( "IInput_RotatorAngleOffset" );
            if ( a !== b )
                a.value = r.Angle.Offset;
            if ( a.disabled )
                a.disabled = false;
            a = document.getElementById( "IInput_RotatorAngleOffsetCelerationAngle" );
            if ( a !== b )
                a.value = r.Angle.OffsetCelerationAngleDegrees;
            if ( ! a.disabled )
                a.disabled = true;
            a = document.getElementById( "IText_RotatorAngleOffset" );
            if ( a.className !== "" )
                a.className = "";
            a = document.getElementById( "IText_RotatorAngleOffsetCelerationAngle" );
            if ( a.className !== "CTextDisabled" )
                a.className = "CTextDisabled";
        } else {
            a = document.getElementById( "IInput_RotatorAngleOffset" );
            if ( a !== b )
                a.value = r.Angle.Offset;
            if ( ! a.disabled )
                a.disabled = true;
            a = document.getElementById( "IInput_RotatorAngleOffsetCelerationAngle" );
            if ( a !== b )
                a.value = r.Angle.OffsetCelerationAngleDegrees;
            if ( a.disabled )
                a.disabled = false;
            a = document.getElementById( "IText_RotatorAngleOffsetCelerationAngle" );
            if ( a.className !== "" )
                a.className = "";
        }

        if ( r.Length.SpeedLinear ) {
            a = document.getElementById( "IInput_RotatorLengthRatio" );
            if ( a !== b )
                a.value = r.Length.Ratio;
            if ( a.disabled )
                a.disabled = false;
            a = document.getElementById( "IInput_RotatorLengthSpeedAngle" );
            if ( a !== b )
                a.value = r.Length.SpeedAngleDegrees;
            if ( ! a.disabled )
                a.disabled = true;
            a = document.getElementById( "IText_RotatorLengthRatio" );
            if ( a.className !== "" )
                a.className = "";
            a = document.getElementById( "IText_RotatorLengthSpeedAngle" );
            if ( a.className !== "CTextDisabled" )
                a.className = "CTextDisabled";
        } else {
            a = document.getElementById( "IInput_RotatorLengthRatio" );
            if ( a !== b )
                a.value = r.Length.Ratio;
            if ( ! a.disabled )
                a.disabled = true;
            a = document.getElementById( "IInput_RotatorLengthSpeedAngle" );
            if ( a !== b )
                a.value = r.Length.SpeedAngleDegrees;
            if ( a.disabled )
                a.disabled = false;
            a = document.getElementById( "IText_RotatorLengthRatio" );
            if ( a.className !== "CTextDisabled" )
                a.className = "CTextDisabled";
            a = document.getElementById( "IText_RotatorLengthSpeedAngle" );
            if ( a.className !== "" )
                a.className = "";
        }
    }
    UpdateConfigs( ) {
        let a,
            b = document.activeElement;

        a = document.getElementById( "IInput_Framerate" );
        if ( a != b )
            a.value = this.Canvas.Framerate;

        a = document.getElementById( "ICheckbox_Play" );
        a.checked = this.Canvas.Play;
        
        a = document.getElementById( "ICheckbox_Redraw" );
        a.checked = this.Canvas.Redraw;
        a = document.getElementById( "ICheckbox_RealtimeStats" );
        a.checked = this.Canvas.Realtime;

        a = document.getElementById( "IInput_Iterations" );
        if ( a != b )
            a.value = this.Iterations;
        a = document.getElementById( "IInput_Rotators" );
        if ( a != b )
            a.value = this.Rotators.length - 1;
        a = document.getElementById( "IInput_RotatorIndex" );
        if ( a != b )
            a.value = this.RotatorIndexCurrent;
        a = document.getElementById( "ICheckbox_DrawStartDirectionAwayFromTip" );
        a.checked = this.Animation.DrawStartDirectionAwayFromTip;
        a = document.getElementById( "ICheckbox_RandomiseDrawStartDirectionAwayFromTipInclude" );
        a.checked = this.RandomiseIncludes.DrawStartDirectionAwayFromTip;
        a = document.getElementById( "IInput_DrawStartRotatorIndexCount" );
        if ( a != b )
            a.value = this.Animation.DrawStartRotatorIndexCount;
        a = document.getElementById( "ICheckbox_RandomiseDrawStartRotatorIndexCountInclude" );
        a.checked = this.RandomiseIncludes.DrawStartRotatorIndexCount;

        a = document.getElementById( "ICheckbox_RandomiseRotatorsAngleInclude" );
        a.checked = this.RandomiseIncludes.RotatorsAngle;
        a = document.getElementById( "ICheckbox_RandomiseRotatorsAngleSpeedInclude" );
        a.checked = this.RandomiseIncludes.RotatorsAngleSpeed;
        // a = document.getElementById( "ICheckbox_RandomiseRandomiseRotatorsAngleCelerationInclude" );
        // a.checked = this.RandomiseIncludes.RandomiseRotatorsAngleCeleration;
        a = document.getElementById( "ICheckbox_RandomiseRotatorsAngleOffsetCelerationLinearInclude" );
        a.checked = this.RandomiseIncludes.RotatorsAngleOffsetCelerationLinear;
        a = document.getElementById( "ICheckbox_RandomiseRotatorsAngleOffsetInclude" );
        a.checked = this.RandomiseIncludes.RotatorsAngleOffset;
        a = document.getElementById( "ICheckbox_RandomiseRotatorsAngleOffsetSpeedInclude" );
        a.checked = this.RandomiseIncludes.RotatorsAngleOffsetSpeed;
        a = document.getElementById( "ICheckbox_RandomiseRotatorsAngleOffsetCelerationInclude" );
        a.checked = this.RandomiseIncludes.RotatorsAngleOffsetCeleration;
        a = document.getElementById( "ICheckbox_RandomiseRotatorsAngleOffsetCelerationAngleInclude" );
        a.checked = this.RandomiseIncludes.RotatorsAngleOffsetCelerationAngle;
        a = document.getElementById( "ICheckbox_RandomiseRotatorsLengthSpeedLinearInclude" );
        a.checked = this.RandomiseIncludes.RotatorsLengthSpeedLinear;
        a = document.getElementById( "ICheckbox_RandomiseRotatorsLengthRatioInclude" );
        a.checked = this.RandomiseIncludes.RotatorsLengthRatio
        a = document.getElementById( "ICheckbox_RandomiseRotatorsLengthSpeedInclude" );
        a.checked = this.RandomiseIncludes.RotatorsLengthSpeed
        a = document.getElementById( "ICheckbox_RandomiseRotatorsLengthSpeedAngleInclude" );
        a.checked = this.RandomiseIncludes.RotatorsLengthSpeedAngle
        
        a = document.getElementById( "IInput_AnimationSpeedDivisor" );
        if ( a != b )
            a.value = this.Animation.SpeedDivisor;
        a = document.getElementById( "IInput_AnimationCelerationDivisor" );
        if ( a != b )
            a.value = this.Animation.CelerationDivisor;
        a = document.getElementById( "IInput_AnimationCelerationDivisorLinear" );
        if ( a != b )
            a.value = this.Animation.CelerationDivisorLinear;
        a = document.getElementById( "ICheckbox_AnimationCelerationDivisorLinearDefault" );
        a.checked = this.Animation.CelerationDivisorLinearDefault;
        
        a = document.getElementById( "IInput_AnimationLengthRatioDivisor" );
        if ( a != b )
            a.value = this.Animation.LengthRatioDivisor;
        a = document.getElementById( "IInput_AnimationLengthRatioDivisorLinear" );
        if ( a != b )
            a.value = this.Animation.LengthRatioDivisorLinear;
        a = document.getElementById( "ICheckbox_AnimationLengthRatioDivisorLinearDefault" );
        a.checked = this.Animation.LengthRatioDivisorLinearDefault;
        a = document.getElementById( "IInput_LineThickness" );
        if ( a != b )
            a.value = this.Canvas.LineWidth;

    }
    InjectCanvas( ) {
        let size = Math.min( window.innerWidth, window.innerHeight ),
            a = document.getElementById( "ICentre" ), // document.createElement( "div" ),
            b = document.createElement( "canvas" );
        b.id = "ICanvas";
        b.width = size;
        b.height = size;
        b.tabIndex = 1;

        this.Canvas.Element = b;
        this.Canvas.Context = b.getContext( "2d" );
        this.Canvas.Context.lineWidth = this.Canvas.LineWidth;
        this.Canvas.Context.strokeStyle = this.Canvas.StrokeStyle;
        this.Canvas.Context.fillStyle = this.Canvas.FillStyle;
        this.Canvas.Context.translate( this.Canvas.Translate.X, this.Canvas.Translate.Y );
        this.Canvas.Origin.X = size / 2;
        this.Canvas.Origin.Y = this.Canvas.Origin.X;
        a.prepend( b );
        // document.body.prepend( a );
    }
    ClearCanvas( ) {
        this.Canvas.Context.clearRect( 0, 0, this.Canvas.Element.width, this.Canvas.Element.height);
    }
    ClearCanvasB( ) {
        if ( this.Canvas.Redraw )
            this.ClearCanvas( );
        else if ( this.Canvas.Clear ) {
            if ( this.Canvas.Clear === true )
                this.Canvas.Clear = 2;
            else
                this.Canvas.Clear += 1;
            if ( this.Canvas.Clear > 3 )
                this.Canvas.Clear = false;
            this.ClearCanvas( );
        }
    }
    BindHotkeys( ) {
        this.Canvas.Element.addEventListener( "keydown", this.FOnKeyDown, true );
        this.Canvas.Element.addEventListener( "mouseup", this.FOnClick, true );
    }

    SetShowStats( value ) {
        this.Stats.Show = value;
        let a = document.getElementById( "IStats" );
        if ( this.Stats.Show ) {
            a.style.display = "block";
            this.UpdateStats( );
            return;
        }
        a.style.display = "none";
    }
    SetCanvasPlay( value ) {
        this.Canvas.Play = value;
        if ( this.Canvas.Play )
            this.OnRequestAnimationFrame( null );
        this.UpdateConfigs( );
    }

    DrawAtTip( ) {
        let c = this.Canvas.Context,
            s = this.Canvas.Scale,
            w = this.Canvas.Element.width,
            h = this.Canvas.Element.height,
            degToRad = this._Math.DegToRad,
            a, // Angle.
            r; // Rotator.
        this.ClearCanvasB( );
        for ( let j = 0; j < this.Iterations; j++ ) {
            this.PositionReusable.SetB( this.Rotators[ 0 ].Position );
            for ( let i = 1; i < this.Rotators.length; i++ ) {
                r = this.Rotators[ i ];
                a = r.Angle.Degrees + r.Angle.Offset;
                this.PositionReusable.X = this.PositionReusable.X + Math.cos( a * degToRad ) * ( w * s.X / ( 2 * this.RotatorLengthRatioSum ) ) * r.Length.Ratio;
                this.PositionReusable.Y = this.PositionReusable.Y - Math.sin( a * degToRad ) * ( h * s.Y / ( 2 * this.RotatorLengthRatioSum ) ) * r.Length.Ratio;
                r.PositionPrevious.SetB( r.Position );
                r.Position.SetB( this.PositionReusable );
                r.Angle.Rotate( );
                // r.Angle.OscillateCeleration( );
            }
            c.beginPath( );
            c.moveTo( r.PositionPrevious.X, r.PositionPrevious.Y );
            c.lineTo( r.Position.X, r.Position.Y );
            c.stroke( );
        }
        this.RotatorLengthRatioSum = 0;
        for ( let i = 1; i < this.Rotators.length; i++ ) {
            r = this.Rotators[ i ];
            r.Angle.RotateOffset( this.Animation.SpeedDivisor );
            if ( r.Angle.OffsetCelerationLinear )
                r.Angle.OscillateCelerationOffsetLinear( this.Animation.CelerationDivisor * this.Animation.CelerationDivisorLinear, degToRad );
            else
                r.Angle.OscillateCelerationOffset( this.Animation.CelerationDivisor, degToRad );
            if ( r.Length.SpeedLinear )
                r.Length.OscillateLinear( this.Animation.LengthRatioDivisor * this.Animation.LengthRatioDivisorLinear, degToRad );
            else
                r.Length.Oscillate( this.Animation.LengthRatioDivisor, degToRad );
            this.RotatorLengthRatioSum += r.Length.Ratio;
        }
    }
    DrawAwayFromTip( ) {
        let c = this.Canvas.Context,
            s = this.Canvas.Scale,
            w = this.Canvas.Element.width,
            h = this.Canvas.Element.height,
            degToRad = this._Math.DegToRad,
            a, // Angle.
            r,
            rB = this.Rotators[ this.Rotators.length - 1 - Math.min( this.Rotators.length - 1, this.Animation.DrawStartRotatorIndexCount ) ]; // Rotator.
        this.ClearCanvasB( );
        for ( let j = 0; j < this.Iterations; j++ ) {
            this.PositionReusable.SetB( this.Rotators[ 0 ].Position );
            for ( let i = 1; i < this.Rotators.length; i++ ) {
                r = this.Rotators[ i ];
                a = r.Angle.Degrees + r.Angle.Offset;
                this.PositionReusable.X = this.PositionReusable.X + Math.cos( a * degToRad ) * ( w * s.X / ( 2 * this.RotatorLengthRatioSum ) ) * r.Length.Ratio;
                this.PositionReusable.Y = this.PositionReusable.Y - Math.sin( a * degToRad ) * ( h * s.Y / ( 2 * this.RotatorLengthRatioSum ) ) * r.Length.Ratio;
                r.PositionPrevious.SetB( r.Position );
                r.Position.SetB( this.PositionReusable );
                r.Angle.Rotate( );
                // r.Angle.OscillateCeleration( );
            }
            c.beginPath( );
            c.moveTo( rB.Position.X, rB.Position.Y );
            c.lineTo( r.Position.X, r.Position.Y );
            c.stroke( );
        }
        this.RotatorLengthRatioSum = 0;
        for ( let i = 1; i < this.Rotators.length; i++ ) {
            r = this.Rotators[ i ];
            r.Angle.RotateOffset( this.Animation.SpeedDivisor );
            if ( r.Angle.OffsetCelerationLinear )
                r.Angle.OscillateCelerationOffsetLinear( this.Animation.CelerationDivisor * this.Animation.CelerationDivisorLinear, degToRad );
            else
                r.Angle.OscillateCelerationOffset( this.Animation.CelerationDivisor, degToRad );
            if ( r.Length.SpeedLinear )
                r.Length.OscillateLinear( this.Animation.LengthRatioDivisor * this.Animation.LengthRatioDivisorLinear, degToRad );
            else
                r.Length.Oscillate( this.Animation.LengthRatioDivisor, degToRad );
            this.RotatorLengthRatioSum += r.Length.Ratio;
        }
    }
    DrawTowardsTip( ) {
        let c = this.Canvas.Context,
            s = this.Canvas.Scale,
            w = this.Canvas.Element.width,
            h = this.Canvas.Element.height,
            degToRad = this._Math.DegToRad,
            a, // Angle.
            r,
            rB = this.Rotators[ this.Animation.DrawStartRotatorIndexCount ]; // Rotator.
        this.ClearCanvasB( );
        for ( let j = 0; j < this.Iterations; j++ ) {
            this.PositionReusable.SetB( this.Rotators[ 0 ].Position );
            for ( let i = 1; i < this.Rotators.length; i++ ) {
                r = this.Rotators[ i ];
                a = r.Angle.Degrees + r.Angle.Offset;
                this.PositionReusable.X = this.PositionReusable.X + Math.cos( a * degToRad ) * ( w * s.X / ( 2 * this.RotatorLengthRatioSum ) ) * r.Length.Ratio;
                this.PositionReusable.Y = this.PositionReusable.Y - Math.sin( a * degToRad ) * ( h * s.Y / ( 2 * this.RotatorLengthRatioSum ) ) * r.Length.Ratio;
                r.PositionPrevious.SetB( r.Position );
                r.Position.SetB( this.PositionReusable );
                r.Angle.Rotate( );
                // r.Angle.OscillateCeleration( );
            }
            c.beginPath( );
            c.moveTo( rB.Position.X, rB.Position.Y );
            c.lineTo( r.Position.X, r.Position.Y );
            c.stroke( );
        }
        this.RotatorLengthRatioSum = 0;
        for ( let i = 1; i < this.Rotators.length; i++ ) {
            r = this.Rotators[ i ];
            r.Angle.RotateOffset( this.Animation.SpeedDivisor );
            if ( r.Angle.OffsetCelerationLinear )
                r.Angle.OscillateCelerationOffsetLinear( this.Animation.CelerationDivisor * this.Animation.CelerationDivisorLinear, degToRad );
            else
                r.Angle.OscillateCelerationOffset( this.Animation.CelerationDivisor, degToRad );
            if ( r.Length.SpeedLinear )
                r.Length.OscillateLinear( this.Animation.LengthRatioDivisor * this.Animation.LengthRatioDivisorLinear, degToRad );
            else
                r.Length.Oscillate( this.Animation.LengthRatioDivisor, degToRad );
            this.RotatorLengthRatioSum += r.Length.Ratio;
        }
    }

    OnRequestAnimationFrame( e ) {
        this._TimeStep.Step( e );
        this.Canvas.CounterFramerateMs.Step( this._TimeStep.Difference );

        while ( this.Canvas.CounterFramerateMs.ThresholdReached ) {
            this.Canvas.CounterFramerateMs.StepDelta( ); // document.timeline.currentTime.

            if ( this.Animation.DrawStartDirectionAwayFromTip ) {
                if ( this.Animation.DrawStartRotatorIndexCount === 0 )
                    this.DrawAtTip( );
                else
                    this.DrawAwayFromTip( );
            } else {
                if ( this.Animation.DrawStartRotatorIndexCount > this.Rotators.length - 2 )
                    this.DrawAtTip( );
                else
                    this.DrawTowardsTip( );
            }

            if ( this.Stats.Realtime )
                this.UpdateStats( );
        }

        this.Stats.Counter1S.Step( this._TimeStep.Difference );
        if ( this.Stats.Counter1S.ThresholdReached ) {
            this.Stats.Counter1S.StepDelta( );
            if ( this.Canvas.CounterFramerateMs.Difference !== 0 )
                this.Stats.Framerate += ( 1000 / this._TimeStep.Difference - this.Stats.Framerate ) * 0.5;
        }

        if ( this.Canvas.Play )
            window.requestAnimationFrame( this.FOnRequestAnimationFrame );
    }

    OnCheckbox_Play( e ) {
        this.SetCanvasPlay( e.currentTarget.checked );
    }
    OnCheckbox_Redraw( e ) {
        this.Canvas.Redraw = e.currentTarget.checked;
    }
    OnCheckbox_Stats( e ) {
        this.SetShowStats( e.currentTarget.checked );
    }
    OnCheckbox_RealtimeStats( e ) {
        this.Stats.Realtime = e.currentTarget.checked;
    }
    OnCheckbox_RotatorLengthSpeedLinear( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        r.Length.SpeedLinear = e.currentTarget.checked;
        this.Canvas.Clear = true;
    }

    OnCheckbox_RotatorAngleOffsetCelerationLinear( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        r.Angle.OffsetCelerationLinear = e.currentTarget.checked;
        this.Canvas.Clear = true;
    }
    OnCheckbox_DrawStartDirectionAwayFromTip( e ) {
        this.Animation.DrawStartDirectionAwayFromTip = e.currentTarget.checked;
        this.Canvas.Clear = true;
    }

    OnChangeInput_Iterations( e ) {
        let v = Number( e.currentTarget.value ),
            d = v - this.Iterations,
            c = Math.abs( d );
        if ( c > 2 ) {
            if ( v < 1 )
                v = 1;
            this.Iterations = v;
        } else
            this.CreaseIterations( d );
        e.currentTarget.value = this.Iterations;
    }
    OnChangeInput_Rotators( e ) {
        this.CreaseRotators( Number( e.currentTarget.value ) + 1 - this.Rotators.length );
        e.currentTarget.value = this.Rotators.length - 1;
    }
    OnChangeInput_RotatorIndex( e ) {
        this.CreaseRotatorIndexCurrent( Number( e.currentTarget.value ) - this.RotatorIndexCurrent );
        e.currentTarget.value = this.RotatorIndexCurrent;
    }
    OnChangeInput_DrawStartCount( e ) {
        this.Animation.DrawStartRotatorIndexCount = Math.max( 0, Number( e.currentTarget.value ) );
        e.currentTarget.value = this.Animation.DrawStartRotatorIndexCount;
        this.Canvas.Clear = true;
    }

    OnChangeInput_RotatorAngle( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorAngle( Number( e.currentTarget.value ) - r.Angle.Degrees, r );
        e.currentTarget.value = r.Angle.Degrees;
    }
    OnChangeInput_RotatorAngleSpeed( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorAngleSpeed( Number( e.currentTarget.value ) - r.Angle.Speed, r );
        e.currentTarget.value = r.Angle.Speed;
    }
    OnChangeInput_RotatorAngleCeleration( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorAngleCeleration( Number( e.currentTarget.value ) - r.Angle.Celeration, r );
        e.currentTarget.value = r.Angle.Celeration;
    }
    OnChangeInput_RotatorAngleOffset( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorAngleOffset( Number( e.currentTarget.value ) - r.Angle.Offset, r );
        e.currentTarget.value = r.Angle.Offset;
    }
    OnChangeInput_RotatorAngleOffsetSpeed( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorAngleOffsetSpeed( Number( e.currentTarget.value ) - r.Angle.OffsetSpeed, r );
        e.currentTarget.value = r.Angle.OffsetSpeed;
    }
    OnChangeInput_RotatorAngleOffsetCeleration( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorAngleOffsetCeleration( Number( e.currentTarget.value ) - r.Angle.OffsetCeleration, r );
        e.currentTarget.value = r.Angle.OffsetCeleration;
    }
    OnChangeInput_RotatorAngleOffsetCelerationAngle( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorAngleOffsetCelerationAngle( Number( e.currentTarget.value ) - r.Angle.OffsetCelerationAngleDegrees, r );
        e.currentTarget.value = r.Angle.OffsetCelerationAngleDegrees;
    }
    OnChangeInput_RotatorLengthRatio( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorLengthRatio( Number( e.currentTarget.value ) - r.Length.Ratio, r );
        e.currentTarget.value = r.Length.Ratio;
    }
    OnChangeInput_RotatorLengthSpeed( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorLengthSpeed( Number( e.currentTarget.value ) - r.Length.Speed, r );
        e.currentTarget.value = r.Length.Speed;
    }
    OnChangeInput_RotatorLengthSpeedAngle( e ) {
        let r = this.Rotators[ this.RotatorIndexCurrent ];
        this.CreaseRotatorLengthSpeedAngle( Number( e.currentTarget.value ) - r.Length.SpeedAngleDegrees, r );
        e.currentTarget.value = r.Length.SpeedAngleDegrees;
    }

    OnChangeInput_LineThickness( e ) {
        this.Canvas.LineWidth = Math.max( 0, Number( e.currentTarget.value ) );
        this.Canvas.Context.lineWidth = this.Canvas.LineWidth;
        e.currentTarget.value = this.Canvas.LineWidth;
        this.Canvas.Clear = true;
        this.UpdateConfigs( );
    }
    OnChangeInput_Framerate( e ) {
        this.Canvas.Framerate = Math.max( 0, Number( e.currentTarget.value ) );
        this.Canvas.FramerateMs = 1000 / this.Canvas.Framerate;
        this.Canvas.CounterFramerateMs.SetThreshold( this.Canvas.FramerateMs, this.Canvas.Framerate );
        e.currentTarget.value = this.Canvas.Framerate;
        this.Canvas.Clear = true;
        this.UpdateConfigs( );
    }

    OnChangeInput_AnimationSpeedDivisor( e ) {
        this.Animation.SpeedDivisor = Math.max( 1, Number( e.currentTarget.value ) );
        e.currentTarget.value = this.Animation.SpeedDivisor;
        this.Canvas.Clear = true;
    }
    OnChangeInput_AnimationCelerationDivisor( e ) {
        this.Animation.CelerationDivisor = Math.max( 1, Number( e.currentTarget.value ) );
        e.currentTarget.value = this.Animation.CelerationDivisor;
        this.Canvas.Clear = true;
    }
    OnChangeInput_AnimationCelerationDivisorLinear( e ) {
        this.Animation.CelerationDivisorLinear = Math.max( 1, Number( e.currentTarget.value ) );
        e.currentTarget.value = this.Animation.CelerationDivisorLinear;
        this.Canvas.Clear = true;
    }
    OnCheckbox_AnimationCelerationDivisorLinearDefault( e ) {
        this.Animation.CelerationDivisorLinearDefault = e.currentTarget.checked;
    }
    OnChangeInput_AnimationLengthRatioDivisor( e ) {
        this.Animation.LengthRatioDivisor = Math.max( 1, Number( e.currentTarget.value ) );
        e.currentTarget.value = this.Animation.LengthRatioDivisor;
        this.Canvas.Clear = true;
    }
    OnChangeInput_AnimationLengthRatioDivisorLinear( e ) {
        this.Animation.LengthRatioDivisorLinear = Math.max( 1, Number( e.currentTarget.value ) );
        e.currentTarget.value = this.Animation.LengthRatioDivisorLinear;
        this.Canvas.Clear = true;
    }
    OnCheckbox_AnimationLengthRatioDivisorLinearDefault( e ) {
        this.Animation.LengthRatioDivisorLinearDefault = e.currentTarget.checked;
    }
    
    OnCheckbox_RandomiseRotatorsAngleInclude( e ) {
        this.RandomiseIncludes.RotatorsAngle = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsAngleSpeedInclude( e ) {
        this.RandomiseIncludes.RotatorsAngleSpeed = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsAngleCelerationInclude( e ) {
        this.RandomiseIncludes.RotatorsAngleCeleration = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsAngleOffsetCelerationLinearInclude( e ) {
        this.RandomiseIncludes.RotatorsAngleOffsetCelerationLinear = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsAngleOffsetInclude( e ) {
        this.RandomiseIncludes.RotatorsAngleOffset = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsAngleOffsetSpeedInclude( e ) {
        this.RandomiseIncludes.RotatorsAngleOffsetSpeed = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsAngleOffsetCelerationInclude( e ) {
        this.RandomiseIncludes.RotatorsAngleOffsetCeleration = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsAngleOffsetCelerationAngleInclude( e ) {
        this.RandomiseIncludes.RotatorsAngleOffsetCelerationAngle = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsLengthSpeedLinearInclude( e ) {
        this.RandomiseIncludes.RotatorsLengthSpeedLinear = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsLengthRatioInclude( e ) {
        this.RandomiseIncludes.RotatorsLengthRatio = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsLengthSpeedInclude( e ) {
        this.RandomiseIncludes.RotatorsLengthSpeed = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseRotatorsLengthSpeedAngleInclude( e ) {
        this.RandomiseIncludes.RotatorsLengthSpeedAngle = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseDrawStartDirectionAwayFromTipInclude( e ) {
        this.RandomiseIncludes.DrawStartDirectionAwayFromTip = e.currentTarget.checked;
    }
    OnCheckbox_RandomiseDrawStartRotatorIndexCountInclude( e ) {
        this.RandomiseIncludes.DrawStartRotatorIndexCount = e.currentTarget.checked;
    }

    OnKeyDown( e ) {
        let test = false,
            r = this.Rotators[ this.RotatorIndexCurrent ];
        switch ( e.key ) {
            case " ": this.SetCanvasPlay( ! this.Canvas.Play ); break;
            case "1": this.CreaseRotatorIndexCurrent( -1 ); break;
            case "2": this.CreaseRotatorIndexCurrent( 1 ); break;

            case "[": this.CreaseIterations( -1 ); break;
            case "]": this.CreaseIterations( 1 ); break;
            case "-": this.CreaseRotators( -1 ); break;
            case "=": this.CreaseRotators( 1 ); break;

            case ";": this.CreaseRotatorAngleOffsetCelerationAngle( -1 ); break;
            case "'": this.CreaseRotatorAngleOffsetCelerationAngle( 1 ); break;
            case ",": this.CreaseRotatorLengthSpeedAngle( -1 ); break;
            case ".": this.CreaseRotatorLengthSpeedAngle( 1 ); break;

            case "q": this.CreaseRotatorAngleOffset( -1 ); break;
            case "w": this.CreaseRotatorAngleOffset( 1 ); break;
            case "a": this.CreaseRotatorAngleOffsetSpeed( -1 ); break;
            case "s": this.CreaseRotatorAngleOffsetSpeed( 1 ); break;
            case "z": this.CreaseRotatorAngleOffsetCeleration( -1 ); break;
            case "x": this.CreaseRotatorAngleOffsetCeleration( 1 ); break;

            case "o": this.CreaseRotatorAngleSpeed( -1 ); break;
            case "p": this.CreaseRotatorAngleSpeed( 1 ); break;
            case "k": this.CreaseRotatorAngleCeleration( -1 ); break;
            case "l": this.CreaseRotatorAngleCeleration( 1 ); break;

            case "n": this.CreaseRotatorLengthSpeed( -1 ); break;
            case "m": this.CreaseRotatorLengthSpeed( 1 ); break;

            default:
                test = true;
                break;
        }
        if ( test )
            return;

        e.preventDefault( );
    }
    OnClick( e ) {
        if ( e.button === 0 )
            this.SetShowStats( ! this.Stats.Show );
    }
}
class CTimeStep {
    constructor( ) {
        // Config.
        // Data.
        this.TickCount = 0;
        this.Difference = 0;
        this.Previous = 0;

        this.DependencyInjected = false;
    }
    Initialise( ) {
    }
    DependencyInject( parent ) {
        this.DependencyInjected = true;
    }
    Execute( ) {
    }
    ExecuteB( time ) {
        this.InitialiseStep( time );
    }

    Step( time ) {
        this.TickCount = time;
        this.Difference = this.TickCount - this.Previous;
        this.Previous = this.TickCount;
    }
    InitialiseStep( time ) {
        this.TickCount = time
        this.Previous = this.TickCount
    }
}

// Main data classes.
class CSpirographStats {
    constructor( parent ) {
        // Data.
        this.Realtime = true;
        this.Show = true;

        this.Framerate = 0;
        this.Counter1S = new CTimeStepCounter( parent._TimeStep, 1000 );
    }
}
class CSpirographCanvas {
    constructor( parent ) {
        // Config.
        this.Scale = new SPosition( 0.99, 0.99 );
        this.Translate = new SPosition( 0.5, 0.5 );
        // Data.
        this.Framerate = 60; // Hz.
        this.FramerateMs = 1000 / this.Framerate;
        this.CounterFramerateMs = new CTimeStepCounter( parent._TimeStep, this.FramerateMs, 0, this.Framerate );
        this.Element = null;
        this.Context = null;
        this.Origin = new SPosition( );
        this.Redraw = false;
        this.Play = true;
        this.Clear = false;
        this.StrokeStyle = "#FFFFFF";
        this.FillStyle = "#FFFFFF";
        this.LineWidth = 1;
    }
}
class CSpirographAnimation {
    constructor( parent ) {
        // Data.
        this.SpeedDivisor = 360;
        this.CelerationDivisor = 360;
        this.CelerationDivisorLinear = 1;
        this.CelerationDivisorLinearDefault = true;
        this.LengthRatioDivisor = 360;
        this.LengthRatioDivisorLinear = 360;
        this.LengthRatioDivisorLinearDefault = true;
        this.DrawStartDirectionAwayFromTip = true;
        this.DrawStartRotatorIndexCount = 0;
    }
}
class CSpirographRandomiseIncludes {
    constructor( parent ) {
        // Data.
        this.RotatorsAngle = true;
        this.RotatorsAngleSpeed = true;
        // this.RotatorsAngleCeleration
        this.RotatorsAngleOffsetCelerationLinear = true;
        this.RotatorsAngleOffset = true;
        this.RotatorsAngleOffsetSpeed = true;
        this.RotatorsAngleOffsetCeleration = true;
        this.RotatorsAngleOffsetCelerationAngle = true;
        this.RotatorsLengthSpeedLinear = true;
        this.RotatorsLengthRatio = true;
        this.RotatorsLengthSpeed = true;
        this.RotatorsLengthSpeedAngle = true;
        this.DrawStartDirectionAwayFromTip = true;
        this.DrawStartRotatorIndexCount = true;
    }
}

// Data classes.
class CRotator {
    constructor( AngleOffsetCelerationLinear, lengthSpeedLinear, positionX = 0, positionY = 0 ) {
        // Data.
        this.Angle = new CRotatorAngle( AngleOffsetCelerationLinear );
        this.Length = new CRotatorLength( lengthSpeedLinear );
        this.Position = new SPosition( positionX, positionY );
        this.PositionPrevious = new SPosition( positionX, positionY );
    }
}
 class CRotatorLength {
    constructor( speedLinear ) {
        // Data.
        this.Ratio = 1;

        this.SpeedLinear = speedLinear;
        this.SpeedAngleDegrees = 0;
        this.Speed = 0;
        this.SpeedDirection = 1;
    }
    OscillateLinear( divisor = 1, degToRad ) {
        this.Ratio = this.Ratio + this.Speed * this.SpeedDirection / divisor;
        if ( this.Ratio > 1 ) {
            this.Ratio = 1;
            this.SpeedDirection *= -1;
        } else if ( this.Ratio < 0 ) {
            this.Ratio = 0;
            this.SpeedDirection *= -1;
        }
    }
    Oscillate( divisor = 1, degToRad ) {
    if ( this.Speed === 0 )
        return;
    this.SpeedAngleDegrees = ( ( ( this.SpeedAngleDegrees + this.Speed / divisor ) % 360 ) + 360 ) % 360;
    this.Ratio = ( 1 + Math.sin( this.SpeedAngleDegrees * degToRad ) ) / 2;
    }
}
 class CRotatorAngle {
    constructor( offsetCelerationLinear ) {
        // Data.
        this.Degrees = 0;
        this.Speed = 1; // Speed of rotation.
        this.Celeration = 0; // Speed of acceleration or deceleration.
        this.CelerationDirection = 1;

        this.Offset = 0;
        this.OffsetCeleration = 0;
        this.OffsetSpeed = 0;

        this.OffsetCelerationLinear = offsetCelerationLinear;
        this.OffsetCelerationAngleDegrees = 0;
        this.OffsetCelerationDirection = 1;
    }
    Rotate( divisor = 1 ) {
        this.Degrees = ( ( ( this.Degrees + this.Speed / divisor ) % 360 ) + 360 ) % 360;
    }
    RotateOffset( divisor = 1 ) {
        this.Offset = ( ( ( this.Offset + this.OffsetSpeed / divisor ) % 360 ) + 360 ) % 360;
    }
    OscillateCeleration( divisor = 1 ) {
        // Accelerate or decelerate.
        this.Speed = this.Speed + this.Celeration * this.CelerationDirection / divisor;
        if ( this.Speed > 180 ) {
            this.Speed = 180;
            this.CelerationDirection *= -1;
        }
        else if ( this.Speed < -180 ) {
            this.Speed = -180;
            this.CelerationDirection *= -1;
        }
    }
    OscillateCelerationOffsetLinear( divisor, degToRad ) {
        this.OffsetSpeed = this.OffsetSpeed + this.OffsetCeleration * this.OffsetCelerationDirection / divisor;
        if ( this.OffsetSpeed > 180 ) {
            this.OffsetSpeed = 180;
            this.OffsetCelerationDirection *= -1;
        } else if ( this.OffsetSpeed < -180 ) {
            this.OffsetSpeed = -180;
            this.OffsetCelerationDirection *= -1;
        }
    }
    OscillateCelerationOffset( divisor, degToRad ) {
        // Accelerate or decelerate.
        if ( this.OffsetSpeed === 0 )
            return;
        this.OffsetCelerationAngleDegrees = ( ( ( this.OffsetCelerationAngleDegrees + this.OffsetCeleration / divisor ) % 360 ) + 360 ) % 360;
        this.OffsetSpeed = Math.sin( this.OffsetCelerationAngleDegrees * degToRad ) * 180;
    }
}

class CTimeStepCounter{
    constructor( _timeStep, threshold = 0, value = 0, thresholdResetMultipler = 1 ) {
        this._TimeStep = _timeStep;
        this.Value = value;
        this.Threshold = threshold;
        if ( thresholdResetMultipler < 1 )
            thresholdResetMultipler = 1;
        this.ThresholdReset = threshold * thresholdResetMultipler; // For step delta.
        this.ThresholdReached = false;

        this.TickCount = this._TimeStep.TickCount;
        this.Difference = 0;
        this.Previous = this.TickCount;
    }
    Step( difference ) { // Begin.
        this.Value += difference;
        if ( this.Value >= this.Threshold )
            this.ThresholdReached = true;
    }
    StepDelta( ) { // End.
        this.Value -= this.Threshold;
        if ( ( this.Value < 0 )
                || ( this.Value >= this.ThresholdReset ) )
            this.Value = 0;
        if ( this.Value < this.Threshold )
            this.ThresholdReached = false;

        this.TickCount = this._TimeStep.TickCount;
        this.Difference = this.TickCount - this.Previous;
        this.Previous = this.TickCount;
    }
    Reset( differenceTotal = 0 ) {
        this.Value = differenceTotal;
    }
    SetThreshold( threshold, thresholdResetMultipler = 1 ) {
        this.Threshold = threshold;
        if ( thresholdResetMultipler < 1 )
            thresholdResetMultipler = 1;
        this.ThresholdReset = threshold * thresholdResetMultipler;
    }
}

// Data structs.
class SPosition {
    constructor( x = 0, y = 0 ) {
        this.X = x;
        this.Y = y;
    }
    Set( x, y ) {
        this.X = x;
        this.Y = y;
    }
    SetB( position ) {
        this.X = position.X;
        this.Y = position.Y;
    }
    Clone( ) {
        return new SPosition( this.X, this.Y );
    }
}
