65.9K
CodeProject 正在变化。 阅读更多。
Home

原生 JS 中的物理模拟

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2020年9月29日

CPOL

12分钟阅读

viewsIcon

15371

downloadIcon

217

基于 HTML、JavaScript 的 Canvas 物理计算机模型

目录

引言

为了理解物理过程和现象,通常会进行各种实验和演示。软件模拟过程允许您在实验室外观察事件或实验。从最狭义的角度讲,物理计算机模拟是对物理现象真实世界行为的重现。通过这种建模,可以改变许多参数,观察它们如何影响实验,并理解现象的本质。

本文介绍了使用 JavaScript 和 HTML 创建的物理世界模型。这些模型可以在任何平台上的 Web 浏览器中运行。本文涵盖了三个主题:

  1. 物理学“动力学”章节中的“力的合成(按角度)”
  2. “液体压强”章节中的“阿基米德原理”
  3. “机械振动”章节中的“数学摆”

物理主题

力的合成(按角度)

力是物体之间相互作用引起的推或拉。在国际单位制(SI)中,力的单位是牛顿(N)。它是一个矢量量。我们可以对力进行加减。合力是两个或多个力的组合,它产生的加速度与所有这些力产生的加速度相同。

在简单情况下,当一个物体受到两个同方向作用力时,合力的大小等于两个力的大小之和 \(F= F_1 + F_2\)。

Simple forces addition

在图形上,可以通过平行四边形法则找到两个按角度作用的力的合力。

Forces addition

合力为: \(F= \sqrt{F_1^2 + F_2^2 + 2F_1F_2 \cos \alpha }\)

如果力 \(F_1 \) 等于力 \(F_2 \) ,则可以通过以下公式找到它们: \(F_1 = F_2 = \frac{F}{2 \cos( \alpha/2 ) } \)

阿基米德定律

阿基米德原理指出,浸没在流体中的物体所受的浮力等于物体排开的流体的重量。作用在物体上的向下的力是物体的重量。如果浮力大于重力,物体会上浮;如果浮力小于重力,物体会下沉。如果浮力等于重力,则物体保持静止。

浮力可以表示为 \(F_b = \rho G V \),其中 \(\rho\) 是流体密度 \( (kg/m^3) \),g 是重力加速度 \( (9.81m/s^2) \),V 是物体体积 \( (m^3) \)。物体体积由公式 \(V = m/\rho \) 得到,其中 \(\rho\) 是物体密度,m 是物体质量。

Archimedes' Law

数学摆

数学摆是一个质点(小物体),悬挂在一根细的无质量、不可伸长的绳子或无质量的杆上。摆的运动周期是指摆动一次(来回)所需的时间,以秒为单位。

Pendulum

从角度 \(\alpha\) 开始的单摆周期的公式为 \(T= 2\Pi\sqrt{L/g }\),其中 L 是摆杆的长度,g 是重力加速度 \( (9.81m/s^2) \)

摆的频率是指每秒来回摆动的次数,以赫兹(Hz)为单位。频率的公式为 \( f = 1/T \)

HTML、CSS 代码

该项目由几个带有 JavaScript 代码的 HTML 页面组成。主页面是项目根目录下的“index.html”。它在页面左侧包含主菜单。菜单由 DIV 元素组成。左侧放置了一个 IFRAME 元素。用户可以在此操作每个物理主题的参数。物理实验模拟对象的绘制和操作发生在主页面中心的 canvas 对象上。

Main area

BoardArea”文件夹包含加载到主页面 Iframe 元素的 HTML 页面。这些 HTML 页面包含模型的输入控件作为参数。在“css”文件夹中是层叠样式表。文件“controls.css" 应用了与输入元素、按钮、标签相关的样式。“main.css" 包含了“index.html”页面的结构样式,“menu.css" 包含了主页面菜单的构建样式,而“variables.css”文件包含了项目的全局 CSS 变量。所有 JS 代码脚本都位于 "./js" 文件夹中。

Structure

JavaScript 代码设计

该项目的代码根据所需目的被分成几个对象。在这个 JS 设计中,使用了一些 Sergey A Kryukov 作者的文章中的思想。

通用方法

整个应用程序的一些通用方法位于对象: application(负责通用属性和方法)和 contextLayout(用于应用与 canvas 相关的函数和变量)。代码位于文件“./js/appMethods.js”中。

来自 application 的代码片段

      const application = {
            arcPath: (2 * Math.PI),
            degrToRad: (degrees) => (Math.PI / 180) * degrees,
            kgToNewton: (kg) =>  kg * 9.81,
            timeout : ms => new Promise(resolve => setTimeout(resolve, ms)),
            // ...
   }

这是来自 contextLayout 对象中的代码

      const contextLayout = {

            /**
            * clears whole canvas area
            */
            clearCanvas: function (canvas) {
                let context = canvas.getContext("2d");
                context.clearRect(0, 0, canvas.width, canvas.height);
            },// clearCanvas
        
            // ...
   }

Canvas 绘制的舞台元素

物理实验有时需要操作各种辅助元素,如重锤、球、钩子。它们有助于实验和演示。为此目的有一个特殊的对象 StageItem,代码位于“./js/Tools/stageElements.js”。StageItem 是一个 Canvas 绘制的对象,可以派生出额外的项目。

      /**
      * Canvas drawn object for physics experiments like cargo, brick etc.
      */
     function StageItem(options = {}) {
     
       /**
        * default values for an item
        */
       Object.assign(this, {
         x: 0,       // left top start X coordinate of item 
         y: 0,       // left top start Y coordinate of item 
         width: 40,  // width of item in pixels
         height: 40, // height of item in pixels
         fill: "#7aa7ca",      // background color of item
         strokeColor: "black", // stroke color of item
         weight: 1,            // weight of item ( can be kg, g, pound etc.)
         itemText: "item",     // text on item
         lineWidth: 2,         // stroke width of item
         canvas: undefined,
       }, options);
     //...
     }

     //...
   }

StageItem 具有以下属性: x, y 是左上角的坐标,width, height 是项目的宽度和高度,fill 是背景颜色,strokeColor 是边框颜色,weight 是重量(kg、g),text 是绘制在项目上的文本,lineWidth 是绘制对象的边框宽度,canvas 是用于绘制的 canvas。

StageItem 派生出两个附加对象: Brick 用于在 canvas 上绘制重锤,Ball 用于在 canvas 上绘制重锤球。

      /**
      * Canvas drawn brick object
      */
     function Brick(options) {
     
       // inheritance from StageItem
       StageItem.call(this, options);
     
       // default values for Brick
       Object.assign(this, {
         roundRadius: 5, // round radius of corners
       },
         options);
     
     }// Brick
     
     Brick.prototype = Object.create(StageItem.prototype);
     Brick.prototype.constructor = Brick;

     Brick.prototype.draw = function () {
           //...
     }
     
     /**
      * Canvas drawn ball object
      */
     function Ball(options) {
     
       // inheritance from StageItem
       StageItem.call(this, options);
     
       // default values for Ball
       Object.assign(this, options);
     
     }// Ball
     
     Ball.prototype = Object.create(StageItem.prototype);
     Ball.prototype.constructor = Ball;

     Ball.prototype.draw = function () {
            //...
     }

这两个类都有原型方法 draw。下面显示了如何在 canvas 上绘制砖块。

      let myCanvas = document.getElementById("Canvas");

      let brick = new Brick({
            x: 450,
            y: 130,
            width: 40,
            height: 40,
            fill: "LightGray",
            strokeColor: "black",
            itemText: "Brick",
            lineWidth: 2,
            canvas: myCanvas,
        });
        
        brick.draw();

Items of stage

Canvas 上的拖拽

为了与舞台上不同的绘制对象进行交互,即移动它们、悬挂它们,使用了 JS 对象 dragRendering,它位于单独的文件“./js/dragging.js”中。该代码允许捕获一个可以跟随用户鼠标的对象。dragRendering 对象接受参数 canvas 用于交互,以及一个可拖动项数组 dragElements

      const dragRendering = {

      //canvas for dragging
      canvas: undefined,
  
      //elements of stage with id of an element 
      dragElements: [{ 
          id: undefined, // unique id of an item 
          isDraggable : true,// is it draggable element on canvas
          isDragging : false,// is item currently being dragged on canvas 
          elem: undefined    // draggable element (see stageElement.js, Brick in particular) 
      }],

      /**
      * adds dragging elements
      */
      addElements: function (elements) {
            this.dragElements = elements.slice();
            this.dragElements.forEach(element => {  element.isDraggable = true; });
            this.drawElements();
        },

      // ...
      }

数组 dragElements 接受参数: id 是每个可拖动项的唯一 ID,isDraggable 表示项是否可拖动,isDragging 是一个标志,用于检查项当前是否被拖动,elem 是要移动的对象(参见上面“Canvas 绘制的舞台元素”部分)。为了填充数组 dragElements,有一个专门实现的功能 addElements

现在看三个重要函数: canvasMouseDown 在鼠标指针位于元素内部时在该元素上触发;canvasMouseMove 在鼠标指针捕获元素进行拖动时在该元素上触发;canvasMouseUp 在用户释放元素并停止其在 canvas 上的移动时在该元素上触发。

      canvasMouseDown: function (e) {  /* ... */ }
      canvasMouseMove: function (e) {  /* ... */ }
      canvasMouseUp: function (e) {  /* ... */ }

用户可以在拖动过程中调用自定义函数 refresh,而无需重新定义 canvasMouseMove;在鼠标按下事件中调用 startedDragging,而无需重新定义 canvasMouseDown;在鼠标抬起事件中调用 stoppedDragging,而无需重写 canvasMousUp

      /**
      * custom function to determine end of dragging
      * @param {*} elem stopped dragging element
      */
     stoppedDragging: function (elem) { },
 
         /**
      * custom function to determine begin of dragging
      * @param {*} elem started dragging element
      */
     startedDragging: function (elem) { },
 
     /**
     * custom function to draw on canvas during dragging
     */
     refresh: function () { },

总的来说,如何在 canvas 上应用拖动效果如下所示。

      let myCanvas = document.getElementById("Canvas");

      myCanvas.onmousedown = function (e) {
        dragRendering.canvasMouseDown(e);
      };
      myCanvas.onmouseup = function (e) {
        dragRendering.canvasMouseUp(e);
      };
      myCanvas.onmousemove = function (e) {
        dragRendering.canvasMouseMove(e);
      };

      let elementSize = 30;
      // Clears dragging elements for the canvas
      dragRendering.dragElements = [];
      dragRendering.refresh = function () { console.log('dragging');  };
      dragRendering.stoppedDragging = function (elem) { console.log('stopped dragging');  };
      dragRendering.startedDragging = function (elem) { console.log('started dragging'); };

      const bricks = [
          {
              id: "X", elem: new Brick({
                  x: 10,
                  y: 10,
                  height: elementSize,
                  width: elementSize,
                  canvas: myCanvas,
                  itemText: "X"
              })
          },
          {
              id: "y", elem: new Ball({
                  x: 50,
                  y: 50,
                  height: elementSize,
                  width: elementSize,
                  canvas: myCanvas,
                  itemText: "Y"
              })
          },
      ];

      // Adds bricks to the canvas
      dragRendering.addElements(bricks);

主窗口与物理主题之间的通信

每个模拟物理过程的主题对应于一个单独的 const 对象,位于单独的 JS 文件中。这些对象是 forcesbyAngleDemoarchimedesPrincipleDemopendulumDemoappliancesDemo。这些对象位于“./js/topicsRender/”文件夹中。每个对象都接受如 canvas、context 等参数,或者具有执行绘制到 canvas、从 IFrame 元素接收数据等实现的方法和函数。

反过来,文件“./js/main.js”允许构建与物理主题对应的 HTML 菜单,并根据所选菜单项重新绘制 canvas 区域。为此,它有一个特殊的对象 applicationRendering

      const applicationRendering = {
            topics: {  forcesAdditionByAngleId: 1, ArchimedesPrincipleId: 2 , 
              pendulumDemoId : 3, AppliancesDemoId: 4}, // data attributes from the 
                                        // HTML menu corresponding to the physical topics 
            currentTopic: undefined,    // to set current selected topic from HTML the menu
            canvas: application.canvas, // main canvas in the application

            // ...

            /**
            * initial initialization of the application
            */
            initialInit: function () {
                  contextLayout.stretchCanvas(this.canvas, "divCanvas");
              
                  dragRendering.canvas = forcesbyAngleDemo.canvas =
                  archimedesPrincipleDemo.canvas = pendulumDemo.canvas = 
                                                   appliancesDemo.canvas = this.canvas;
                     forcesbyAngleDemo.ctx = archimedesPrincipleDemo.ctx = 
                                     pendulumDemo.ctx = appliancesDemo.ctx = this.context;
              
                  this.renderMenu();
                }, // initialInit

                //...
                /**
                * renders the HTML menu
                */
                renderMenu: function () {
                        //...
                        elementDiv.onclick = function () {
                              //...
                              //renders canvas depending on the current topic
                              switch (applicationRendering.currentTopic) {
                                case applicationRendering.topics.forcesAdditionByAngleId:
                                  forcesbyAngleDemo.init();
                                  break;
                                case applicationRendering.topics.ArchimedesPrincipleId:
                                  archimedesPrincipleDemo.init();
                                  break;
                                  case applicationRendering.topics.pendulumDemoId:
                                    pendulumDemo.init();
                                    break;
                                  case applicationRendering.topics.AppliancesDemoId:
                                    appliancesDemo.init();
                                    break;
                                default:
                                  // no topic provided
                              } // switch
                        } //elementDiv.onclick
                        //...
                  }
                }
            }; // applicationRendering

主窗口与 Iframe 之间的通信

在物理实验中,需要根据不同的公式进行计算。每个物理主题对象对应一个单独的 HTML 页面,其中包含输入元素,允许更改变量并将其传递到所需公式。这些 HTML 页面可以根据所选的物理主题加载到内联 IFrame 中。

./BoardArea/FluidMechanics/ArchimedesPrinciple.html” HTML 页面,包含“阿基米德原理”主题的输入元素和参数。

IFrame fro Archimedes' principle

为了将数据从 IFrame 传递到主窗口,使用了方法 parent.postMessage。要接收数据,主窗口必须有一个事件处理程序 window.addEventListener('message')。来自单独文件“framesRendering.js”的常量对象 frameRender 会捕获参数页面中每个输入元素的 change 事件,并将消息发送到父主窗口。主窗口以 JSON 格式接收此消息,并将其进一步传递给物理主题。

      const frameRender = {
            /**
            * custom function to send message from IFrame to parent 
            */
           passControlsMessage: function () { },
           
            bindEvents: function () {
                  let passMsg = this.passControlsMessage.bind(this);
                  let divElement = document.querySelector('body');
                  // all input elements
                  let inputElements = divElement.querySelectorAll('input');
                  for (let elem of inputElements) {
                        
                   elem.oninput = function () {
                        setRangeLabelValue();
                        passMsg();
              }
            }
      }

      window.onload = function () {
            frameRender.bindEvents();
            frameRender.passControlsMessage();
        };

IFrame 中的“ArchimedesPrinciple.html”页面向父窗口传递数据

      frameRender.passControlsMessage = function () {
            let bodyDensity = document.getElementById("bodyDensityTextBox").value;
            let pass_data = {
                  'bodyDensity': bodyDensity
              };

              parent.postMessage(JSON.stringify(pass_data), "*");
      }

在主窗口中从 IFrame 接收数据

      window.addEventListener(
            "message",
            function (e) {
              var key = e.message ? "message" : "data";
              var data = e[key];
              applicationRendering.topicVariables = JSON.parse(data);
              applicationRendering.receiveData();
            },
            false
          );

反之,要向 IFrame 发送消息,则使用父窗口中的 frameEl.postMessage 方法。要在框架内接收消息,会应用 window.addEventListener('message')

JavaScript 绘制的物理仪器

实验室实验的装置是特殊的设备,允许您收集、分析、计算和处理从特定现象和效应中获得的信息。该项目中有几种这样的 canvas 绘制项,它们被放置在“./js/Tools”文件夹的单独文件中。

箭头测力计

测力计是用于测量力、力矩(扭矩)或功率的设备。有几种类型的测力计:机械测力计(箭头式和弹簧式)以及电子式。

要创建箭头式测力计,使用了两个 canvas 绘制对象 ApplianceDynamometer。这些对象分别放置在文件“./js/Tools/appliance.js”和“./js/Tools/dynamometer.js”中。Appliance 对象模拟了仪表盘的表盘,带有指针。它有两个原型方法。draw 方法绘制表盘,它是一组带有阴影的填充圆。第二个原型方法 drawPointer 绘制具有所需宽度和角度的指针。

      /**
      * Canvas drawn object to create face of gauge tools like clocks, dynamometers, timers
      * @options {obj} destructing parameters
      */
    function Appliance(options = {}) {
    
      // default values for the appliance
      Object.assign(this, {
        centerX: 0,                // X center coordinate 
        centerY: 0,                // Y center coordinate 
        radius: 50,                // radius of the appliance's face
        angle: 0,                  // applies rotation angle around center's point
        ringColor: "#5f5f5f",      // color of outer ring
        innerRingColor: "#e9e9e9", // color of the second outer ring
        backRingColor: "#7e7e7e",  // color of background ring
        pointerColor: "#d95358",   // color of arrow pointer 
        canvas: undefined          // canvas to draw on 
      }, options);

      // ....

      Appliance.prototype = {

            /**
             * Draws appliance
             */
            draw : function () {
                  // ...
            }

            // Draws arrow indicator 
            drawPointer : function (angle = 0, arcWidth = 10) {
                  // ...
            }
      }

Appliance 对象具有以下属性: centerX, centerY 是中心坐标,radius 是仪表盘的半径,angle 应用于围绕设备中心点的旋转角度,ringColor 是外环的颜色,innerRingColor 是第二个外环的颜色,backRingColor 是背景环的颜色,pointerColor 是指针的颜色,canvas 是用于绘制的 canvas。

下面展示了如何在 canvas 上绘制仪表盘的示例。

      let myCanvas = document.getElementById("Canvas");

      let appliance = new Appliance({
            centerX: 600, 
            centerY: 300, 
            radius: 100,
            canvas: myCanvas
        });
        
        appliance.draw();
        appliance.drawPointer(45);     

Appliance

Dynamometer 对象继承自 Appliance 类。它有三个原型方法。draw 方法用于在 canvas 上绘制此测力计。第二个方法是 setStaticValue,它允许绘制给定值的指针。方法 setValue 允许通过动画绘制给定值的指针。方法 setValue 是异步的,并返回一个 Promise。

Dynamometer 具有附加属性: maxValue 是测力计的最大值,value 是测力计的当前值,rotateStep 是指针的旋转速度(即,在时间段内的值变化)。

      /**
      * Canvas drawn dynamometer
      */
     function Dynamometer(options) {
     
       // inheritance from Appliance
       Appliance.call(this, options);
       
       // default values for Dynamometer
       Object.assign(this, {
         maxValue: 10,          // maximum possible value of dynamometer
         value: 0,              // current  value of dynamometer
         rotateStep : undefined // speed of rotation of the arrow 
                                // (i.e. change of value by time period)
       }, options);
       // ... 
      }

      Dynamometer.prototype = {
            /**
             * draws entire Dynamometer
             */
            draw: function () {
         // …
            },
          
            /**
             * sets arrow indicator to value without animation
             */
            setStaticValue: function (valuePointer = 0) {
         // …
            }, // setStaticValue
          
            /**
             * sets arrow indicator to value with animation
             */
            setValue: async function (valuePointer = 0) {
         // …
          }

下面展示了如何在 canvas 上绘制 dynamometer 的示例。

      let myCanvas = document.getElementById("Canvas");

      let dynamometer = new Dynamometer({
            centerX: 250,
            centerY: 300,
            radius: 100,
            ringColor: "red",
            innerRingColor: "yellow",
            canvas: myCanvas,
            rotateStep : 0.1,
            angle: 45,
            maxValue: 20
        });
        
        dynamometer.draw();
        dynamometer.setValue(-12).then(function () {
            alert('Value was set');
        })

 Arrow dynamometer

Spring

弹簧对象是 canvas 绘制的正弦波。弹簧的每个匝都是从 0 到 360 度范围内的正弦曲线。所需匝数可以模拟弹簧的匝数。下面展示了如何绘制弹簧模拟。

      let ctx = document.getElementById("Canvas").getContext("2d");
      let startX = 200; // begin x coord. of spring 
      let startY = 300; // begin y coord. of spring 
      let radius = 50;  // radius of sin wave

      ctx.save();
      ctx.lineCap = "round";
    
      let totalLength = 300; //total length on spring
      let swings = 5;   // number of swings on spring 
      let swingLength = totalLength / swings; // length of each short swing
    
      ctx.beginPath();
      ctx.lineWidth = 1;
      ctx.strokeStyle = 'black';
      let currentSwing = 0;
      
      // drawing of an each swing in cycle
      while (currentSwing < swings) {
        let beginX = startX + swingLength * currentSwing;
        let endX = beginX + swingLength;
        let waveStepX = (endX - beginX) / 360;
        // drawing of particular swing
        for (let x = 0; x <= endX - beginX; x += waveStepX) {
          y = startY - (Math.sin((1 / waveStepX) * (x * Math.PI / 180))) * radius;
            ctx.lineTo(beginX + x, y);
        }
    
        currentSwing++;
      }
      ctx.stroke();

      ctx.restore();

为了创建弹簧,使用了特殊的 Spring 对象及其所需属性。为了绘制对象,有一个特殊的原型方法 draw

      /**
      * Canvas drawn spring object 
      */
      function Spring(options = {}) {
            // default values for the spring
            Object.assign(this, {
              canvas: undefined,
              swingFrontColor: "lightGray", //  color of front swing
              swingBackColor: "gray",       //  color to simulate back swing 
              startX: 0,                    // star X coordinate of spring 
              startY: 0,                    // star Y coordinate of spring 
              length: 100,                  // length of spring
              radius: 50,                   // radius of sine wave 
              swings: 5,                    // count of swings
              angle: 0,                     // rotation angle relatively of upper left corner
              swingWidth: 3                 // width of each swing
            }, options);
          }

          Spring.prototype.draw = function () {
                // ...
          }

Spring 的属性是: canvas 是用于绘制的 canvas,startX, startY 是正弦波的起始坐标,swingFrontColor 是正面匝的颜色,<color>swingBackColor 是背面匝的颜色,length 是弹簧的总长度,radius 是正弦波的半径,swings 是匝数,angle 应用于围绕左上角的旋转角度,swingWidth 是每个匝的像素宽度。

Spring

标尺

Ruler 对象在 canvas 上绘制度量标尺以测量长度。用户可以设置此对象允许的最大值。Ruler 被分成与最大值成比例的刻度线。

      /**
      * Canvas drawn ruler object 
      */
     function Ruler(options = {}) {
       // default values for ruler
       Object.assign(this, {
         canvas: undefined,
         startX: 0,           // left top start X coordinate of ruler 
         startY: 0,           // left top start Y coordinate of ruler 
         length: 200,         // length of ruler
         height: 50,          // height of ruler
         maxValue: 10,        // max ruler value
         backColor: "orange", // background color of ruler
         strokeColor: "black",//  line color 
         showBackground: true,//shows/hides background of ruler
         showBottom: true,    //shows/hides bottom strokes of ruler
         angle: 0,            // rotation angle relatively of upper left corner
       }, options);
     }

     Ruler.prototype.draw = function () {
      // ...
      }

Ruler 具有以下属性: canvas 是用于绘制的 canvas,startX, startY 是左上角的起始坐标,length, height 是标尺的长度和高度,maxValue 是最大值,backColor 是背景颜色,strokeColor 是线条颜色,showBackground 允许显示或隐藏背景,showBottom 允许显示或隐藏标尺的底部刻度,angle 应用于围绕左上角的旋转角度。

下面展示了如何在 canvas 上绘制 ruler

      let myCanvas = document.getElementById("Canvas");

      let ruler = new Ruler({
            startX: 200,
            startY: 50,
            canvas: myCanvas,
            length: 500,
            height: 60,
            strokeColor: "black",
            maxValue: 500
        });

        ruler.draw();

Ruler

弹簧测力计

在弹簧测力计中,力传递到弹簧,弹簧根据力的方向被压缩或拉伸。弹簧变形量与力成正比。

SpringDynamometer 的原型有三个函数: draw 方法用于在 canvas 上绘制此测力计,它绘制整个测力计;setStaticValue 方法用于绘制给定值的指示器;setValue 方法用于通过动画绘制给定值的指示器。方法 setValue 是异步的,并返回一个 Promise。

      function SpringDynamometer(options = {}) {
            // default values for the ruler
            Object.assign(this, {
              canvas: undefined,     // canvas to draw on 
              startX: 0,             // left top start X coordinate of spring dynamometer 
              startY: 0,             // // left top start Y coordinate of spring dynamometer 
              length: 200,           // length of spring dynamometer
              height: 50,            // height of spring dynamometer
              maxValue: 10,          // maximum possible value of spring dynamometer
              backColor: "orange",   // background color of ruler
              strokeColor: "black",  //  line color 
              angle: 0,              // rotation angle relatively of upper left corner
              animatedHolder: false, // are holder and hook animated
              value: 0,              // current  value of spring dynamometer
              animationStep: undefined // speed of animation for spring
            }, options);

            SpringDynamometer.prototype = {
                  /**
                   * draws entire Spring Dynamometer
                   */
                  draw: function () {
          // …
          }

                  /**
                   * sets spring indicator to value without animation
                   */
                  setStaticValue: function (valuePointer = 0) {
      // …
          } 
         
                  /**
                  * sets spring indicator to value with animation
                  */
                  setValue: async function (valuePointer = 0) {
          // …
          }   

SpringDynamometer 的属性是: canvas 是用于绘制的 canvas,startX, startY 是左上角的起始坐标,length, height 是弹簧测力计的长度和高度,maxValue 是最大值,backColor 是背景颜色,strokeColor 是线条颜色,angle 应用于围绕左上角的旋转角度,animatedHolder 决定是否动画显示带有挂钩的支架,value 是测力计的当前值,animationStep 是支架旋转的动画速度(即,在时间段内的值变化)。

下面展示了如何应用弹簧测力计的属性并将其绘制到 canvas 上的示例。

      let myCanvas = document.getElementById("Canvas");

      let springDynamometer = new SpringDynamometer({
            startX: 100,
            startY: 50,
            canvas: myCanvas,
            angle: 90,
            length: 400,
            height: 60,
            strokeColor: "black",
            value: 0,
            animatedHolder: true,
            maxValue: 50
        });
        
        springDynamometer.draw();
        springDynamometer.setValue(30).then(function () {
            springDynamometer.setValue(0);
        });

Spring dynamometer

JavaScript 物理模型

“力的合成(按角度)”模型

文件“forcesbyAngleRender.js”中的 forcesbyAngleDemo 对象负责创建模拟力合成的模型。它应用两个测力计,可以按一定角度放置,以及三个可以拖拽到舞台上并在测力计下方的细线上悬挂的砖块。通过改变砖块的重量和测力计之间的角度等参数,用户可以计算施加到测力计上的总力。

这是来自 forcesbyAngleDemo 的代码片段。

      const forcesbyAngleDemo = {
            dynamometers: undefined,
            canvas: undefined,
            ctx: undefined,
                  // …
        
                  init: function () {
                        this.applySettings();
                        this.dynamometers = forcesbyAngleDemo.initRender();
                
                        dragRendering.refresh = 
                            function () { forcesbyAngleDemo.refreshDrawDrag(); };
                        dragRendering.stoppedDragging = 
                            function (elem) { forcesbyAngleDemo.stoppedDrawDrag(elem); };
                        dragRendering.startedDragging = 
                            function (elem) { forcesbyAngleDemo.startedDrawDrag(elem); };
                  },
        
                  /**
                  * Initial placement of canvas' elements
                  */
                  initRender: function () {
                        //...
        
                        // Draws box and thread on canvas
                        this.drawBox();
                        this.drawRope();
                        // ... 
        
                        // Draws dynamometers
                        dynamLeft.draw();
                        dynamRight.draw();
                  },
        
                  /**
                  * Function to call when started dragging 
                  */
                     startedDrawDrag: function (dragElem) {
                         let el = dragRendering.findElem(dragElem.id).elem;
                        // ...
                     },
        
                     /**
                     * Function to call when stopped dragging 
                     */
                    stoppedDrawDrag: function (dropElem) {
                          // suspends bricks under the dynamometers
                          // ...
                          // sets value for the dynamometers
                          this.setDynamValues();
                          // sends data to the IFrame
                          this.passFrameValue();
                    },
        
                    setDynamValues: function () {
        		            this.dynamometers.dynamLeft.setValue(this.getForce()).then(() => {
            			      this.setDraggingFlags();
            			      this.passFrameValue();
        		            })
			            // ...
                    },                 
        } 

Forces addition demo

“阿基米德定律”模型

常量 archimedesPrincipleDemo 应用了一个弹簧测力计和一个砖块,该砖块可以被拖拽并悬挂在测力计下方的细线上。测力计可以移向装有液体的盒子,以显示施加在它上面的浮力。

这是来自 archimedesPrincipleDemo 的代码片段。

      const archimedesPrincipleDemo = {
            dynamometer: undefined,
            canvas: undefined,
            ctx: undefined,
            cancelTimer: false,

            init: function () {
                  this.applySettings();
                  this.dynamometers = archimedesPrincipleDemo.initRender();
          
                  dragRendering.refresh = 
                      function () { archimedesPrincipleDemo.refreshDrawDrag(); };
                  dragRendering.stoppedDragging = 
                      function (elem) { archimedesPrincipleDemo.stoppedDrawDrag(elem); };
                  dragRendering.startedDragging = 
                      function (elem) { archimedesPrincipleDemo.startedDrawDrag(elem); };
              },

              refreshDrawDrag: function () {
                  this.drawBox();
                  this.drawLiquidBoxBig();
                  this.dynamometers.springDynamometer.draw();
              },

              /**
              * Function to call when stopped dragging 
              */
              stoppedDrawDrag: function (dropElem) {
                  // suspends the brick under the dynamometer
                  // ...
                  // sets value for the dynamometer and moves it 
                  this.dynamometers.springDynamometer.setValue
                        (gravityForce).then(function () {
                        archimedesPrincipleDemo.animatePosition().then(function () {
                              //...
                        })
                  })
                  // sends data to the IFrame
                  this.passFrameValue(volume, buyonantForce, 
                       application.roundTwoDigits(this.getForce()));
              }          
      }

Archimedes law demo

“数学摆”模型

常量对象 pendulumDemo 在 canvas 上绘制两个动画摆,并应用摆的长度及其起始运动角度等参数。setTimeout 函数已用于创建周期性动画,该动画与真实计时器精确同步。

      const pendulumDemo = {
            canvas: undefined,
            ctx: undefined,
            timer: undefined,

            settings: {
                  pendulumLength: 0,
                  pendulumLength2: 0,
                  pendulumCoord: { x: 0, y: 0 }, // pendulum's center coord
              },

            // ...
            
            animatePendulum: function (startAngle, timeInterval, startAngle2, timeInterval2) {
                  let interval = 20; // interval for the timer in ms
                  var expected = Date.now() + interval;
                  this.timer = setTimeout(step, interval);
                  let canvas = this.canvas;

                  // ...
                  function step() {
                        var dt = Date.now() - expected;
                        contextLayout.clearCanvas(pendulumDemo.canvas);
                        expected += interval;

                        //...
                        pendulumDemo.drawPendulum
                                     (currentPosition.x, currentPosition.y, 'red');
                        pendulumDemo.drawPendulum
                                     (currentPosition2.x, currentPosition2.y, 'blue');
                        
                        pendulumDemo.timer = setTimeout(step, Math.max(0, interval - dt)); 
                  }
            }
      }

Pendulum law

在线演示

在线演示可在 此处 找到。

历史

  • 2020年9月28日:初始版本
© . All rights reserved.