WaBis

walter.bislins.ch

JavaScript: Gravitational Acceleration and Potential Field of a Cylinder

This is the JavaScript from the page Gravitational Acceleration and Potential Field of a Cylinder.

#INCLUDE Async.inc         // for asynchron computing to keep the browser responsive
#INCLUDE JsGraph.inc       // for graphic output
#INCLUDE ControlPanel.inc  // for sliders and input fields
#INCLUDE JsgVectMat3.inc   // some 3D vector functions

<jscript>

function Format( num, format, digits ) {
  return NumFormatter.Format( num, format, digits );
}

function IntersectLines( P, r, Q, s ) {
  // line1 = P + lambda1 * r
  // line2 = Q + lambda2 * s
  // r and s must be normalized (length = 1)
  // returns intersection point O of line1 with line2 = [ Ox, Oy ]
  // returns null if lines do not intersect or are identical
  var PQx = Q[0] - P[0];
  var PQy = Q[1] - P[1];
  var rx = r[0];
  var ry = r[1];
  var rxt = -ry;
  var ryt = rx;
  var qx = PQx * rx + PQy * ry;
  var qy = PQx * rxt + PQy * ryt;
  var sx = s[0] * rx + s[1] * ry;
  var sy = s[0] * rxt + s[1] * ryt;
  // if lines are identical or do not cross...
  if (sy == 0) return null;
  var a = qx - qy * sx / sy;
  return [ P[0] + a * rx, P[1] + a * ry ];
}

//////////////////////////////////////////////////////////
// support mouse wheel events
//
// usage: AddWheelListener( domObject, function(event){ var scroll = event.deltaY; ... } );

(function(window,document) {

  var prefix = "", addEventName, wheelEventName;

  // detect event model
  if ( window.addEventListener ) {
    addEventName = "addEventListener";
  } else {
    addEventName = "attachEvent";
    prefix = "on";
  }

  // detect available wheel event
  wheelEventName =
    "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel"
    document.onmousewheel !== undefined ? "mousewheel" :   // Webkit and IE support at least "mousewheel"
    "DOMMouseScroll";                                      // let's assume that remaining browsers are older Firefox

  window.AddWheelListener = function( elem, callback, useCapture ) {
    registerWheelCallback( elem, wheelEventName, callback, useCapture );

    // handle MozMousePixelScroll in older Firefox
    if( wheelEventName == "DOMMouseScroll" ) {
      registerWheelCallback( elem, "MozMousePixelScroll", callback, useCapture );
    }
  };

  function registerWheelCallback( elem, eventName, callback, useCapture ) {
    elem[ addEventName ]( prefix + eventName, wheelEventName == "wheel" ? callback : function( originalEvent ) {
      !originalEvent && ( originalEvent = window.event );

      // create a normalized event object
      var event = {
        // keep a ref to the original event object
        originalEvent: originalEvent,
        target: originalEvent.target || originalEvent.srcElement,
        type: "wheel",
        deltaMode: originalEvent.type == "MozMousePixelScroll" ? 0 : 1,
        deltaX: 0,
        deltaY: 0,
        deltaZ: 0,
        shiftKey: originalEvent.shiftKey,
        altKey: originalEvent.altKey,
        ctrlKey: originalEvent.ctrlKey,
        preventDefault: function() {
          originalEvent.preventDefault ? originalEvent.preventDefault() : originalEvent.returnValue = false;
        }
      };

      // calculate deltaY (and deltaX) according to the event
      if ( wheelEventName == "mousewheel" ) {
        event.deltaY = - 1/40 * originalEvent.wheelDelta;
        // Webkit also supports wheelDeltaX
        originalEvent.wheelDeltaX && ( event.deltaX = - 1/40 * originalEvent.wheelDeltaX );
      } else {
        event.deltaY = originalEvent.deltaY || originalEvent.detail;
      }

      // it's time to fire the callback
      return callback( event );

    }, useCapture || false );
  }

})(window,document);

////////////////////////////////////////////////////////////

var Model = {

  r: 5,  // m
  h: 1,  // m
  EleSize: 0.2, // m
  GridSize: 0.25,
  FieldResolution: 4,
  Density: 11340, // kg/m^3

  x: 4,
  y: 1.5,

  yInput: 1.5,
  yZero: 1,   // 0 -> at surface, 1 -> at center

  ShowCursor: true,
  ShowVectors: true,
  ShowField: true,
  ShowFullField: true,
  ShowCenterLine: true,
  FieldType: 0,    // 0 -> acceleration, 1 -> gravitational potential

  Zoom: 1,
  WindowWidth: 20,
  WindowHeight: 10,
  WindowRatio: 2,    // width / height

  xSlider: 0,
  ySlider: 0,

  graph: null,
  asyncCursor: null,
  asyncFields: null,
  accelVectAtCursor: null,
  accelValueAtCursor: 0,
  potentialAtCursor: 0,
  fieldCells: [],
  nElements: 0,
  elementCount: 0,
  nFieldCells: 0,
  cursorProgressInterval: 100,
  cursorProgressStartTime: 0,
  fieldProgressInterval: 300,
  fieldProgressStartTime: 0,
  isComputingCursor: false,
  isComputingFields: false,

  Init: function() {
    this.asyncCursor = new CAsync();
    this.asyncCursor.RunTime = 100;
    this.asyncFields = new CAsync();
    this.asyncFields.RunTime = 200;

    var me = this;
    this.graph.AddEventHandler( 'click', function(e){ me.OnClick(e); } );
    AddWheelListener( xElement('Graph'), function(e){ me.OnWheel(e); } );

    this.Update();
  },

  OnClick: function( event ) {

    // stop computing field on click of progress text
    if (this.isComputingFields) {
      if (event.offsetY < 32 && event.offsetX > this.graph.VpInnerWidth-200) {
        this.asyncFields.Stop();
        this.isComputingFields = false;
        this.Update( 'stop' );
        return;
      }
    }

    this.x = this.graph.TransCnvsWinX( event.offsetX );
    this.yInput = this.graph.TransCnvsWinY( event.offsetY ) + this.yZero * this.h / 2;
    this.ShowCursor = true;
    this.Update( 'x,yInput,ShowCursor' );
  },

  OnWheel: function( event ) {

    // zoom on wheel rotation, use ctrl key to enhance zoom
    var factor = 1.1;
    if (event.ctrlKey) factor = 1.6;

    if (event.deltaY < 0) {
      this.Zoom *= factor;
    } else {
      this.Zoom /= factor;
    }
    this.Update( 'Zoom' );

    event.preventDefault();
    return true;
  },

  ClearFields: function() {
    this.fieldCells = [];
  },

  CompElementCenterOfMassRadius: function( r, dr, phi ) {
    // distance from center of cylinder to center of mass (CoM) of element
    var rmin = r - dr;
    var CoM = Math.sqrt( (r*r + rmin*rmin) * phi / (4 * Math.tan(phi/2)) );
    return CoM;
  },

  CompNumElements: function( length, eleSize, nMin ) {
    var n = Math.floor( length / eleSize );
    if (n < nMin) n = nMin;
    return n;
  },

  CompElementHeight: function( ) {
    return this.h / this.CompNumElements( this.h, this.EleSize, 1 );
  },

  CompElementRadialSize: function( ) {
    return this.r / this.CompNumElements( this.r, this.EleSize, 1 );
  },

  CompElementAngularSize: function( r ) {
    return Math.PI / this.CompNumElements( Math.PI*r, this.EleSize, 4 );
  },

  CompGridElementHeight: function( ) {
    return this.h / (2 * this.CompNumElements( this.h/2, this.GridSize, 1 ));
  },

  CompGridElementWidth: function( ) {
    return this.r / this.CompNumElements( this.r, this.GridSize, 1 );
  },

  CompNumCylinderElements: function( r, h, eleSize ) {
    var nh = this.CompNumElements( h, eleSize, 1 );
    var nr = this.CompNumElements( r, eleSize, 1 );
    var dr = r / nr;
    var rmax = r + dr/2;
    var nphi = 0;
    for (var er = dr; er < rmax; er += dr) {
      nphi += 2 * this.CompNumElements( Math.PI*er, eleSize, 4 );
    }
    return nh * nphi;
  },

  CreateAsyncState: function( x, y, asyncObj, startTimer, showProgress ) {
    // Due to the asynchrone calculations
    // the tate of the calculations has to be stored in this separate object,
    // that is passed to the asynchrone functions on each call.
    // This way the execution of loops in this functions can be interrupted any time
    // and continued after the system had time to do handle events. This is handled
    // by the Async objects.

    var state = {
      async: asyncObj,
      startTimer: startTimer,
      showProgress: showProgress,
      acceleration: [ 0, 0, 0 ],
      potential: 0,
      pos: [ x, 0, y ],
      h: 0,
      dh: 0,
      hmax: 0,
      r: 0,
      dr: 0,
      rmax: 0,
      phi: 0,
      dphi: 0,
      phimax: 0,
    };
    return state;
  },

  CompValuesAtPosition: function( ) {
    // This function sets up an async handler that is called repeatedly by the asyncCursor object.
    // This function returns immediately without executing the calculation yet. The calculation function is
    // called asynchron by the asyncCursor object repeatedly, until it is finished (returns false).

    this.elementCount = 0;
    this.isComputingCursor = true;
    this.cursorProgressStartTime = Date.now();
    var state = this.CreateAsyncState( this.x, this.y, this.asyncCursor, true, true );
    var me = this;
    this.asyncCursor.CallObj( me, this.DoCompValuesAtPosition, state, null,
      function(){ // finally do
        me.accelVectAtCursor = state.acceleration;
        me.accelValueAtCursor = JsgVect3.Length( state.acceleration );
        me.potentialAtCursor = state.potential;
        me.isComputingCursor = false;
        me.Draw( me.graph );
      }
    );
  },

  DoCompValuesAtPosition: function( state ) {
    // This function is called repeatedly by the state.async object. After some execution time has elapsed
    // it returns to get the system time to handle events. If the calculation is not yet
    // finished, the function returns true, else false. If it returns true, the function is called again
    // by the state.async object to continue the calculation. The current state of all loop variables
    // is maintained in state, so this function can reenter and continue where it leaved.

    function PieArea( r, phi ) {
      return 0.5 * r * r * phi;
    }

    if (state.startTimer) {
      state.async.StartTimer();
    }

    if (state.dh == 0) {
      state.dh = this.CompElementHeight();
      state.hmax = this.h + state.dh/2;
      state.h = state.dh;
      state.dr = 0; // init inner loop
    }
    while (state.h < state.hmax) {

      var hCoM = -state.h + state.dh/2;

      if (state.dr == 0) {
        state.dr = this.CompElementRadialSize();
        state.rmax = this.r + state.dr/2;
        state.r = state.dr;
        state.dphi = 0; // init inner loop
      }
      while (state.r < state.rmax) {

        if (state.dphi == 0) {
          state.dphi = this.CompElementAngularSize( state.r );
          state.phimax = 2 * Math.PI + state.dphi/2;
          state.phi = state.dphi;
        }

        var rCoM = this.CompElementCenterOfMassRadius( state.r, state.dr, state.dphi );
        var ringArea = PieArea( state.r, state.dphi ) - PieArea( state.r-state.dr, state.dphi );
        var eleVolume = ringArea * state.dh;
        var eleMass = eleVolume * this.Density;

        while (state.phi < state.phimax) {

          // center of mass
          var com = [ rCoM * Math.cos( state.phi-state.dphi/2 ), rCoM * Math.sin( state.phi-state.dphi/2 ), hCoM ];

          // acceleration
          var rVect = JsgVect3.Sub( com, state.pos );
          var rDist = JsgVect3.Length( rVect );
          JsgVect3.NormTo( rVect );

          var pot = -(6.674e-11 * eleMass / rDist);
          var a = -pot / rDist;
          var aVect = JsgVect3.Scale( rVect, a );

          JsgVect3.AddTo( state.acceleration, aVect );
          state.potential += pot;
          this.elementCount++;

          state.phi += state.dphi;
          if (state.phi >= state.phimax) {
            state.dphi = 0; // restart loop
          }
        }
        state.r += state.dr;
        if (state.r >= state.rmax) {

          state.dr = 0; // restart loop

        } else if (state.async.IsTimerExpired()) {

          if (state.showProgress) {
            this.ShowProgress();
          }

          return true;
        }
      }
      state.h += state.dh;
      if (state.h >= state.hmax) {
        state.dh = 0; // restart loop
      }
    }
    return false;
  },

  CompFields: function() {
    // setup asynchrone computation of field

    var fieldState = {
      initx: true,
      x: 0,
      dx: 0,
      xmax: 0,
      xlimit: 0,
      inity: true,
      y: 0,
      dy: 0,
      ymin: 0,
      ymax: 0,
      gridBoxWidth: 0,
      gridBoxHeight: 0,
      cellState: null,
    };

    // calculate number of field elements to compute
    var dx = this.CompGridElementWidth();
    var dy = this.CompGridElementHeight();
    var nx = Math.floor((this.WindowWidth/2 + 1.5*dx) / dx);
    var ch = this.h / 2;
    if (ch > this.WindowHeight/2) ch = this.WindowHeight/2;
    var ny = Math.floor((this.WindowHeight/2 + ch + 1.5*dy) / dy);
    this.nFieldCells = nx * ny;
    // subtract elements hidden by cylinder
    if (this.r > this.WindowWidth/2) {
      nx = Math.floor((this.WindowWidth/2 + dx/2) / dx);
    } else {
      nx = Math.floor((this.r + dx/2) / dx);
    }
    ny = Math.floor((ch + 1.5*dy) / dy);
    this.nFieldCells -= nx * ny;

    var ymin = this.WindowHeight / 2 + dy;
    ymin = -dy * Math.floor( ymin / dy );
    fieldState.dx = dx;
    fieldState.dy = dy;
    fieldState.xmax = this.WindowWidth / 2 + dx;
    fieldState.ymin = ymin;
    fieldState.ymax = this.WindowHeight / 2 + dy;

    var me = this;
    this.asyncFields.CallObj( me, this.DoCompField, fieldState, null,
      function(){ // finally do
        me.isComputingFields = false;
        me.Draw( me.graph );
      }
    );
  },

  CompFieldsAtPosition: function( fieldState ) {

    if (!fieldState.cellState) {
      fieldState.cellState = this.CreateAsyncState( fieldState.x, fieldState.y, this.asyncFields, false, false );
    }

    if (this.DoCompValuesAtPosition( fieldState.cellState )) {
      // if acceleration computation not finished, return and reenter loop again
      return true;
    }

    // acceleration computation finished, save result in this.fieldCells
    var accelVect = fieldState.cellState.acceleration;
    var fieldCell = {
      pos: [ fieldState.x, fieldState.y ],
      accelDir: JsgVect2.Norm( [ accelVect[0], accelVect[2] ] ),
      accelValue: JsgVect3.Length( accelVect ),
      potential: fieldState.cellState.potential,
    }
    this.fieldCells.push( fieldCell );

    fieldState.cellState = null; // restart acceleration computation
    return false; // => acceleration computation finished
  },

  DoCompField: function( state ) {
    // This function is called repeatedly by this.asyncFields object. After some execution time has elapsed
    // it returns to get the system time to handle events. If the calculation is not yet
    // finished, the function returns true, else false. If it returns true, the function is called again
    // by the this.asyncFields object to continue the calculation. The current state of all loop variables
    // is maintained in state, so this function can reenter and continue where it leaved.

    function min( a, b ) { return a < b ? a : b; }

    // if computation of acceleration at cursor is running, pause this thread
    if (this.isComputingCursor) return true;

    this.asyncFields.StartTimer();
    this.isComputingFields = true;

    if (state.gridBoxWidth == 0) {
      state.gridBoxWidth = this.r + state.dx;
      state.gridBoxHeight = state.dy;
    }

    while (state.gridBoxWidth < (this.WindowWidth/2)+state.dx || state.gridBoxHeight < (this.WindowHeight/2)+state.dy) {

      if (state.initx) {
        if (state.gridBoxHeight > (this.WindowHeight/2)+state.dy) {
          state.x = state.gridBoxWidth - state.dx / 2;
        } else {
          state.x = state.dx / 2;
        }
        state.initx = false;
        state.inity = true;
      }

      while (state.x < state.gridBoxWidth) {

        if (state.x < state.xmax) {

          if (state.inity) {
            if (state.x < state.gridBoxWidth - state.dx) {
              state.y = state.gridBoxHeight - state.dy/2;
            } else {
              state.y = -(this.h/2) + state.dy/2;
              if (state.y < state.ymin) state.y = state.ymin + state.dy / 2;
            }
            state.inity = false;
          }

          if (state.y < state.ymax) {
            var ylimit = min( state.ymax, state.gridBoxHeight );
            while (state.y < ylimit) {

              if (this.CompFieldsAtPosition( state )) {
                // continue calculation loop
                return true;
              }

              state.y += state.dy;
              if (state.y < ylimit) {
                if (this.ShowFieldProgress()) {
                  return true;
                }
              }
            }
          }

        }
        state.x += state.dx;
        state.inity = true;
        if (state.x < state.gridBoxWidth) {
          if (this.ShowFieldProgress()) {
            return true;
          }
        }
      }

      state.gridBoxWidth += state.dx;
      state.gridBoxHeight += state.dy;
      state.initx = true;

    }

    return false;  // computation of fields finished
  },

  Update: function( controller ) {
    // This function is called on every change of a model property. It validates the values,
    // updates the ControlPanels and starts the recomputation and redraw of the model.

    function min( x, y ) { return x < y ? x : y; }
    function max( x, y ) { return x > y ? x : y; }

    var fieldsUpdateNeeded = false;
    var fieldsTurnedOn = this.ShowVectors || this.ShowField;

    if (this.FieldResolution < 0.1) this.FieldResolution = 0.1;
    if (this.FieldResolution > 100) this.FieldResolution = 100;

    if (ControlPanels.MatchesField( controller, 'Zoom,r,h,EleSize,GridSize,Density,FieldResolution' )) {
      fieldsUpdateNeeded = true;
    }

    // stop cursor value calculation
    this.asyncCursor.Stop();
    this.isComputingCursor = false;
    this.accelVectAtCursor = null;

    // interrupt field calculation and invalidate field by clearing field
    if (fieldsUpdateNeeded || (this.isComputingFields && !fieldsTurnedOn)) {
      this.asyncFields.Stop();
      this.isComputingFields = false;
      this.ClearFields();
    }
    var fieldsAreCalculated = (this.fieldCells.length > 0) && !this.isComputingFields;

    // validate and correct changes on model properties
    var minSize = max( 2*this.r, this.h ) / 1000;
    if (this.EleSize < minSize) this.EleSize = minSize;
    var minSize = min( this.EleSize, min( this.r, this.h ) ) /100;
    if (this.GridSize < minSize) this.GridSize = minSize;
    if (this.Zoom < 0.1) this.Zoom = 0.1;
    if (this.Zoom > 1000) this.Zoom = 1000;

    // calculate some basic settings
    this.CompWindowSize();
    this.nElements = this.CompNumCylinderElements( this.r, this.h, this.EleSize );

    // synchronize x and y sliders with x and y values
    var yOffset = - this.yZero * this.h / 2;
    if (ControlPanels.MatchesField( controller, 'x,r,h,Zoom' )) {
      this.xSlider = this.x / (this.WindowWidth / 2);
    } else if (ControlPanels.MatchesField( controller, 'xSlider' )) {
      this.x = this.xSlider * (this.WindowWidth / 2);
    }
    if (ControlPanels.MatchesField( controller, 'yInput,yZero,r,h,Zoom' )) {
      this.ySlider = (this.yInput + yOffset) / (this.WindowHeight / 2);
    } else if (ControlPanels.MatchesField( controller, 'ySlider' )) {
      this.yInput = this.ySlider * (this.WindowHeight / 2) - yOffset;
    }
    this.y = this.yInput + yOffset;

    // update control panels with current model values
    ControlPanels.Update( [ 'CylinderGeometryPanel', 'CursorSliderPanel' ] );

    if (this.ShowCursor) {
      this.CompValuesAtPosition();
    }

    if (fieldsTurnedOn && !this.isComputingFields && (!fieldsAreCalculated || fieldsUpdateNeeded)) {
      this.CompFields();
    }

    this.Draw( this.graph );
  },

  CompWindowSize: function( ) {
    this.WindowWidth = 20;
    this.WindowHeight = this.WindowWidth / this.WindowRatio;
    if (2.2 * this.h > this.WindowHeight) {
      this.WindowHeight = 2.2 * this.h;
      this.WindowWidth = this.WindowRatio * this.WindowHeight;
    }
    if (2.5 * this.r > this.WindowWidth) {
      this.WindowWidth = 2.5 * this.r;
      this.WindowHeight = this.WindowWidth / this.WindowRatio;
    }
    this.WindowWidth /= this.Zoom;
    this.WindowHeight /= this.Zoom;
  },

  ShowProgress: function() {
    // show progress text from time to time
    var now = Date.now();
    if (now - this.cursorProgressStartTime > this.cursorProgressInterval) {
      this.Draw( this.graph );
      this.cursorProgressStartTime = now;
    }
  },

  ShowFieldProgress: function() {
    // show progress text from time to time
    var now = Date.now();
    if (now - this.fieldProgressStartTime > this.fieldProgressInterval) {
      this.Draw( this.graph );
      this.fieldProgressStartTime = now;
      return true;
    }
    return false;
  },

  SetTextAttrs: function( g, params ) {
    g.SetTextAttrS( params.font, params.size, params.color, params.align, params.padding );
  },

  GetLineHeight: function( g, params ) {
    // text attributes and transformation must be defined in params prior to this call
    this.SetTextAttrs( g, params );
    var oldTrans = g.SelectTrans( params.trans );
    var lineHeight = g.GetTextSize('T').h;
    g.SelectTrans( oldTrans );
    return lineHeight;
  },

  FormatText: function( params ) {
    // replaces # sign in params.text with params.num applying params formatting rules
    var text = params.text;
    if (text.indexOf('#') >= 0) {
      params.text = text.replace( '#', Format( params.num, params.format, params.digits ) );
    }
  },

  DrawText: function( g, params ) {
    // Draws params.text and text box according to params rules and attributes
    this.SetTextAttrs( g, params );
    this.FormatText( params );
    var oldTrans = g.SelectTrans( params.trans );
    if (params.boxMode > 0) {
      g.SetAreaAttr( params.boxBgColor, params.boxBorderColor, params.boxBorderWidth );
      g.TextBox( params.text, params.pos, params.boxMode );
    }
    g.Text( params.text, params.pos );
    g.SelectTrans( oldTrans );
  },

  Draw: function( g ) {

    function sqr(x) { return x*x; }
    function max( x, y ) { return x > y ? x : y; }

    g.Reset();
    g.SetAngleMeasure( 'deg' );

    g.MapWindow( 0, 0, 1.01 * this.WindowWidth, 1.01 * this.WindowHeight, 0 );

    // fields and vectors
    if (this.fieldCells.length > 0 && (this.ShowVectors || this.ShowField)) {
      var dx = this.CompGridElementWidth();
      var dy = this.CompGridElementHeight();
      var dx2 = dx / 2;
      var dy2 = dy / 2;
      var len = dx/3;  // vector line length
      var log10 = Math.log(10);
      var time = Date.now();
      g.SetAlpha( 0.2 );

      for (var i = this.fieldCells.length-1; i >= 0; i--) {

        var fieldCell = this.fieldCells[i];

        // acceleration and potential field
        if (this.ShowField) {

          // generate a color from the field value
          var fieldValue = this.FieldType == 1 ? -fieldCell.potential : fieldCell.accelValue;
          var colval = (this.FieldResolution * Math.abs(Math.log(fieldValue) / log10)) % 1.0;
          g.SetBgColor( JsgColor.HL( colval, 0.5 ) );

          // draw field cells
          var x = fieldCell.pos[0];
          var y = fieldCell.pos[1];
          g.RectWH( x-dx2, y-dy2, dx, dy, 2 );
          g.RectWH( -x-dx2, y-dy2, dx, dy, 2 );
          y = fieldCell.pos[1] + this.h;
          if (this.ShowFullField && (-y+dy2 > -this.WindowHeight/2)) {
            g.RectWH( x-dx2, -y-dy2, dx, dy, 2 );
            g.RectWH( -x-dx2, -y-dy2, dx, dy, 2 );
          }
        }

        // vector field
        if (this.ShowVectors) {

          var x = fieldCell.pos[0];
          var y = fieldCell.pos[1];
          var pstart = JsgVect2.Add( fieldCell.pos, JsgVect2.Scale( fieldCell.accelDir, -len ) );
          var pend = JsgVect2.Add( fieldCell.pos, JsgVect2.Scale( fieldCell.accelDir, len ) );
          g.Line( pstart, pend );
          pstart[0] *= -1;
          pend[0] *= -1;
          g.Line( pstart, pend );
          if (this.ShowFullField && (-y+dy2 > -this.WindowHeight/2)) {
            pstart[1] += this.h;
            pend[1] += this.h;
            pstart[1] *= -1;
            pend[1] *= -1;
            g.Line( pstart, pend );
            pstart[0] *= -1;
            pend[0] *= -1;
            g.Line( pstart, pend );
          }
        }

        // if drawing the field takes too long, skip the rest of drawing
        if (this.isComputingFields && (Date.now() - time > 100)) {
          break;
        }

      }
      g.SetAlpha( 1 );
    }

    // cylinder
    g.SetAreaAttr( '#eee', 'black', 2 );
    g.Rect( -this.r, 0, this.r, -this.h, 3 );
    g.SetAlpha( 0.2 );
    g.EllipseArc( 0, 0, this.r, 0.1*this.r, 0, 0, 180, 3 );
    g.SetAlpha( 1 );

    // some cylinder elements
    var dr = this.CompElementRadialSize();
    var dh = this.CompElementHeight();
    g.SetAlpha( 0.1 );
    g.SetAreaAttr( 'white', 'black', 1 );
    var rmax = this.r + dr/2;
    for (var r = dr; r < rmax; r += dr) {
      g.Rect( r-dr, -dh, r, 0, 3 );
    }
    var hmax = this.h + dh/2;
    for (h = 2*dh; h < hmax; h += dh) {
      g.Rect( this.r-dr, -h, this.r, -h+dh, 3 );
    }
    g.SetAlpha( 1 );

    // axes
    var yOffset = -this.yZero * this.h / 2;
    g.SetLineAttr( 'gray', 1 );
    g.Axes( 0, yOffset );

    // lines to cursor
    if (this.ShowCursor) {
      if (!xArray(this.accelVectAtCursor)) {
        g.SetMarkerAttr( 'Cross', 5, 'red', 'red', 1 );
        g.Marker( this.x, this.y );
      }
      g.SetLineAttr( 'red', 1 );
      g.SetAlpha( 0.5 );
      g.Line( this.x, yOffset, this.x, this.y );
      g.Line( 0, this.y, this.x, this.y );
      g.SetAlpha( 1 );
    }

    // center of mass
    g.SetMarkerAttr( 'Plus', 8, 'black', 'black', 1 );
    g.Marker( 0, -this.h/2 );

    var textParams = {
      font: 'Arial',
      size: 14,
      color: 'black',
      align: '',
      padding: 4,
      text: '',
      num: 0,
      format: 'std',
      digits: 3,
      pos: [0,0],
      trans: 'viewport',
      boxMode: 0,  // 0 -> no box
      boxBgColor: 'white',
      boxBorderColor: 'black',
      boxBorderWidth: 1,
    };

    // total number of elements
    if (this.nElements > 0) {
      textParams.align = 'SW';
      textParams.trans = 'viewport';
      textParams.text = 'Number of Cylinder Elements = #';
      textParams.num = this.nElements;
      textParams.format = 'fix';
      textParams.digits = 0;
      textParams.boxMode = 2;
      textParams.pos = [ 0, g.VpInnerHeight - g.ScalePix(3) ];
      this.DrawText( g, textParams );
    }

    // cursor progress
    if (this.ShowCursor && this.isComputingCursor && this.elementCount > 0) {
      var progress = 100 * this.elementCount / this.nElements;
      textParams.align = 'NW';
      textParams.trans = 'viewport';
      textParams.text = 'Computing Value at Cursor: # %';
      textParams.num = progress;
      textParams.format = 'fix0';
      textParams.digits = 1;
      textParams.boxMode = 2;
      textParams.pos = [ 0, 0 ];
      this.DrawText( g, textParams );
    }

    // field progress
    if (this.isComputingFields && this.fieldCells.length > 0) {
      var progress = 100 * this.fieldCells.length / this.nFieldCells;
      if (progress > 100) progress = 100;
      textParams.align = 'NE';
      textParams.trans = 'viewport';
      textParams.text = 'Computing Field: # %';
      textParams.num = progress;
      textParams.format = 'fix0';
      textParams.digits = 2;
      textParams.boxMode = 2;
      textParams.pos = [ g.VpInnerWidth, 0 ];
      this.DrawText( g, textParams );
    }

    // acceleration
    if (this.ShowCursor && xArray(this.accelVectAtCursor)) {

      // line parallel to acceleration at cursor direction center of cylinder
      var rVect = JsgVect2.Norm( [ this.accelVectAtCursor[0], this.accelVectAtCursor[2] ] );
      var pStart = [ this.x, this.y ];
      var pEnd;
      if (this.ShowCenterLine && this.FieldType == 0 && Math.abs(this.x) >= 1e-6) {
        pEnd = IntersectLines( pStart, rVect, [0,0], [0,-1] );
        if (pEnd) {
          g.SetLineAttr( 'green', 1 );
          g.Line( pStart, pEnd );
          g.SetMarkerAttr( 'Circle', 8, 'green', 'white', 1 );
          g.Marker( pEnd );
        }
      }

      if (this.FieldType == 0) {
        // arrow at cursor
        var vLength = g.PixToWinX( 30 );
        pEnd = JsgVect2.Add( pStart, JsgVect2.Scale( rVect, vLength ) );
        g.SetMarkerAttr( 'Arrow1', 8, 'red', 'red', 1.5 );
        g.Arrow( pStart, pEnd );
      } else {
        // circle at cursor
        g.SetMarkerAttr( 'Circle', 6, 'red', 'white', 1 );
        g.Marker( pStart );
      }

      // Analytical acceleration along the axis of a cylinder
      // http://astrowww.phys.uvic.ca/~tatum/celmechs/celm5.pdf
      var accel;
      var z = this.y;
      if (z < -this.h/2) z = -(z + this.h); // mirror at plane through middle of cylinder
      var k = 2 * Math.PI * 6.674e-11 * this.Density;
      if (z >= 0) {
        // outside cylinder
        var accel = k * (this.h - Math.sqrt( sqr(this.h+z) + sqr(this.r) ) + Math.sqrt( sqr(z) + sqr(this.r) ));
      } else {
        // inside cyliner; note z is negative, pointing from surface into cylinder
        var accel = k * (this.h + 2*z - Math.sqrt( sqr(this.h+z) + sqr(this.r) ) + Math.sqrt( sqr(z) + sqr(this.r) ));
      }

      textParams.trans = 'viewport';  // to get the line height in viewport coordinates
      var lineHeight = this.GetLineHeight( g, textParams ) + g.ScalePix(4);

      textParams.align = 'SE';
      textParams.text = 'Analytical g(0,y) = # m/s^2';
      textParams.num = accel;
      textParams.format = 'sci';
      textParams.digits = 3;
      textParams.pos = [ g.VpInnerWidth, g.VpInnerHeight - g.ScalePix(3) ];
      textParams.boxMode = 2;
      textParams.boxBgColor = 'white';
      this.DrawText( g, textParams );

      // acceleration for point mass according Newton
      if (Math.abs(this.x) >= this.r || this.y > 0 || this.y < -this.h) {
        var cylVolume = Math.PI * sqr(this.r) * this.h;
        var cylMass = cylVolume * this.Density;
        accel = 6.674e-11 * cylMass / ( sqr(this.x) + sqr(this.y+this.h/2) );

        textParams.text = 'Pointmass g = # m/s^2';
        textParams.num = accel;
        textParams.pos[1] -= lineHeight;
        this.DrawText( g, textParams );
      }

      // field values from cursor
      textParams.trans = 'viewport';
      textParams.align = 'NW';
      var lineHeight = this.GetLineHeight( g, textParams ) + g.ScalePix(4);

      textParams.format = 'sci';
      textParams.digits = 3;
      textParams.text = 'g   = # m/s^2';
      textParams.num = this.accelValueAtCursor;
      this.FormatText( textParams );
      var text1 = textParams.text;

      textParams.text = 'gx  = # m/s^2';
      textParams.num = this.accelVectAtCursor[0];
      this.FormatText( textParams );
      var text2 = textParams.text;

      textParams.text = 'gy  = # m/s^2';
      textParams.num = this.accelVectAtCursor[2];
      this.FormatText( textParams );
      var text3 = textParams.text;

      textParams.text = 'pot = # J/kg';
      textParams.num = this.potentialAtCursor;
      this.FormatText( textParams );
      var text4 = textParams.text;

      var oldTrans = g.SelectTrans( 'viewport' );
      var box = g.GetTextSize( text1 );
      var w = box.w;
      box = g.GetTextSize( text2 );
      w = max( box.w, w );
      box = g.GetTextSize( text3 );
      w = max( box.w, w );
      box = g.GetTextSize( text4 );
      w = max( box.w, w );
      w += g.ScalePix(12);
      h = 4 * lineHeight + g.ScalePix(8);

      // draw text box
      var tpos = [ g.ScalePix(2), g.ScalePix(2) ];
      g.SetAreaAttr( 'yellow', 'black', 1 );
      g.RectWH( 0, 0, w, h, 3 );

      // draw text
      g.Text( text1, tpos );
      tpos[1] += lineHeight;
      g.Text( text2, tpos );
      tpos[1] += lineHeight;
      g.Text( text3, tpos );
      tpos[1] += lineHeight;
      g.Text( text4, tpos );
      g.SelectTrans( oldTrans );

    }

  },

};

Model.graph = NewGraph2D( {
  Id: 'Graph',
  Width: '100%',
  Height: '50%',
  DrawFunc: function(g){ Model.Draw(g); },
  AutoScalePix: true,
} );

ControlPanels.NewSliderPanel( {
  Name: 'CursorSliderPanel',
  ModelRef: 'Model',
  NCols: 1,
  OnModelChange: function(controller){ Model.Update(controller); },
  Format: 'std',
  Digits: 3,
  ReadOnly: false,
  PanelFormat: 'InputShortWidth'

} ).AddValueSliderField( {
  Name: 'x',
  SliderValueRef: 'xSlider',
  Min: -1,
  Max: 1,
  Inc: 0.1,
  Units: 'm',
  Color: 'green',
  EnabledRef: 'ShowCursor',

} ).AddValueSliderField( {
  Name: 'yInput',
  Label: 'y',
  SliderValueRef: 'ySlider',
  Min: -1,
  Max: 1,
  Inc: 0.1,
  Units: 'm',
  Color: 'blue',
  EnabledRef: 'ShowCursor',

} ).AddValueSliderField( {
  Name: 'Zoom',
  Min: 0.5,
  Max: 5,
  Inc: 0.1,
  Color: 'black',

} ).Render();

ControlPanels.NewPanel( {
  Name: 'CylinderGeometryPanel',
  ModelRef: 'Model',
  NCols: 2,
  OnModelChange: function(controller){ Model.Update(controller); },
  Format: 'std',
  Digits: 6,
  ReadOnly: false,
  PanelFormat: 'InputNormalWidth'

} ).AddTextField( {
  Name: 'r',
  Label: 'Radius',
  Inc: 0.1,
  Units: 'm',

} ).AddTextField( {
  Name: 'h',
  Label: 'Height',
  Inc: 0.1,
  Units: 'm',

} ).AddTextField( {
  Name: 'EleSize',
  Label: 'Element Size',
  Inc: 0.1,
  Units: 'm',

} ).AddTextField( {
  Name: 'GridSize',
  Label: 'Grid Size',
  Inc: 0.1,
  Units: 'm',

} ).AddTextField( {
  Name: 'Density',
  Inc: 1000,
  Units: 'kg/m<sup>3</sup>',

} ).AddTextField( {
  Name: 'FieldResolution',
  Label: 'Field Resolution',
  Inc: 1,

} ).AddRadiobuttonField( {
  Name: 'FieldType',
  Label: 'Field Type',
  ValueType: 'int',
  Items: [
    {
      Name: 'Acceleration',
      Value: 0
    }, {
      Name: 'Potential',
      Value: 1
    }
  ]

} ).AddRadiobuttonField( {
  Name: 'yZero',
  Label: 'y = 0 at',
  ValueType: 'int',
  Items: [
    {
      Name: 'surface',
      Value: 0
    }, {
      Name: 'center',
      Value: 1
    }
  ]

} ).AddCheckboxField( {
  Name: 'FieldOptions',
  Label: 'Display',
  ColSpan: 3,
  Items: [
    {
      Name: 'ShowCursor',
      Text: 'Cursor',
    }, {
      Name: 'ShowVectors',
      Text: 'Vectors',
    }, {
      Name: 'ShowField',
      Text: 'Field',
    }, {
      Name: 'ShowFullField',
      Text: 'Full Field',
    }, {
      Name: 'ShowCenterLine',
      Text: 'Center of Gravity Line',
      EnabledRef: 'ShowCursor',
    },
  ]

} ).Render();

xOnLoad( function(){ Model.Init(); } );
</jscript>

More Page Infos / Sitemap
Created Monday, November 18, 2019
Scroll to Top of Page
Changed Saturday, November 23, 2019