This tutorial will guide you through the basics of making a custom module, which can be done entirely within the web app, without the need of any external development environment.
"Module" is a currently experimental feature of Ray Optics Simulation. It allows the creation of modular combinations of objects with custom parameters, custom control points, and arrays of objects. This feature extends the capability of this simulator by combining, specializing, or reparametrizing objects created by existing tools to make new tools. For example, the CircleSource
module (see Tools -> Other -> Import module) combines an array of point sources created by the existing "Point Source (<360°)" tool along a circle, to make a "circular source" tool which didn't exist in the simulator. The FresnelLens
module specializes the "Glass->Custom equation" tool, so that the equation represents a specific curve of the Fresnel lens parametrized by the number of slices, thus making a specialized "Fresnel lens" tool, which also didn't exist before. In addition to making new tools, this feature can also make some optics demonstrations more interactive. For example, by dragging the third control point of the BeamExpander
module, one can directly see how the position of the common focal point of the two lenses affects the beam width, without needing to adjust the focal lengths of the two lenses individually.
Note that not all custom control points require a module. Some simple cases can be achieved by the "handle" feature (see the "Group, rotate, and scale objects" section in the help popup at the bottom right corner of the simulator). Since making a module is much more complicated than creating a handle, you should first check if your case can be achieved by the "handle" feature before considering making a module. See here for a non-trivial example of a custom control point (moving two plastic bags out of water) without using a module.
This app currently does not have a visual interface for creating modules, so you need to directly edit the JSON of the scene.
You can enable the built-in JSON editor by clicking the "settings" dropdown at the top-right corner of the app, and then check "Show JSON editor". The code editor should appear at the left-hand side of the app, with the JSON code of the current scene. Make sure you have a large enough screen, as this feature does not work well on mobile devices.
As you edit the scene using the usual visual scene editor, the code in the JSON editor will update accordingly, with the changed part highlighted. Conversely, directly editing the code in the JSON editor will update the scene accordingly. If you are not familiar with JSON or any kind of text-based data format, you may wish to play around with it for a while.
In particular, when you add an object to the scene, it is added to the objs
array. And if you modify some of its properties to a non-default value, they appear as key-value pairs in that object.
IMPORTANT: In this tutorial page, if you do not see the JSON code editor in the iframes below, please turn it on and reload this page, as you will need to see the code to understand how it works.
Let's look at our first example of a module.
You should see four lines of texts. By looking at the JSON editor, you will see that the first two are directly in the top-level objs
array as usual, but the last two are in modules.ExampleModule.objs
instead.
The module
is a dictionary where the key is the name of the module (in this case ExampleModule
), and the value is the definition of that module. In particular, the modules.ExampleModule.objs
array describes the (template of) objects within that module, which is different from the top-level objs
which describes the objects in the scene.
To put the objects within the module to the scene, we need a "module object" in the top-level objs
array, which is objs[2]
in this example, whose type is ModuleObj
and whose module
property is the name of the module.
The module definition in the modules
dictionary is not editable by the visual scene editor. So when you click any of the last two texts in this example, you are just selecting the module object, and not the objects in the module. Since the coordinates of the texts in the module definition in this example are absolute coordinates, the last two texts are not draggable. We will learn how to make them draggable by using control points later.
If you select a module object, there is a "Demodulize" button on the object bar. Clicking it will "expand" the module object into its constituent, and objs
will now contain all the four texts. This operation is not reversible (but of course you can click "undo").
The suggested way of creating a module currently is to first create an empty module using the JSON editor, create some objects using the visual scene editor, and then cut and paste the objects from objs
to modules.ModuleName.objs
using the JSON editor.
The objects within the module can be defined by a set of parameters. Let’s look at a simple example:
Here modules.ModuleName.params
is an array of strings "name=start:step:end:default"
defining the name of the variables and the range of the sliders. The sliders appear on the object bar when the module object is selected.
Within the modules.ExampleModule.objs
array, any values can be expressed using those parameters. Within a string (such as the text
property of a TextLabel
), the equations of the variables are enclosed by a pair of backticks. For number parameters (such as the fontSize
property of a TextLabel
), you need to make it a string so that you can use the backtick format in it, so each equation is sandwiched by a pair of backticks and a pair of quotes. The equation are evaluated with math.js (https://mathjs.org/docs/reference/functions/evaluate.html). See there for the available syntax and functions you can use in the equations.
The actual values of the parameters are stored in the params
property of the module object, which, unlike the module definition, can be directly edited by the scene editor using the slider.
To make the module object draggable, we need to parametrize the objects within the module using a set of control points. Let’s look at the example:
Here modules.ModuleName.numPoints
defines the number of control points. The coordinates of the control points are (x_1
, y_1
), (x_2
, y_2
), etc, and are used in the same ways as the parameters within modules.ExampleModule.objs
as described by the previous section. Note that the index starts from 1.
The actual values of the coordinates of the control points are stored in the points
property of the module object, which, unlike the hard-coded coordinates in Example 1, can be edited by the visual scene editor by dragging the control points, each shown as two concentric gray circles in the scene. If you drag elsewhere in the module object (such as dragging the text labels), all the control points will move together.
Since our module object can now move, it is now quite easy to create multiple instances as in usual tools. The name of the module is shown in the Tools -> Other menu, and you can select that and then click two points in the blank space in sequence for the two control points to create another instance of the module. You can also use the “duplicate” button on the object bar.
More complicated module can be built using arrays and conditionals. Let’s look at the example:
Within modules.ExampleModule.objs
, any objects in an array can have two special keys: "for"
and "if"
. The value of the "for"
key is either a string of the format "name=start:step:end"
defining a loop variable, or an array of several strings of this format describing a multidimensional loop. Such an object in the array is duplicated several times according to the loop variables. The value of the "if"
key is a string representing a math.js expression that evaluates to a boolean, and such an object is included in the array if and only if the boolean is true.
To prevent accidental infinite loop, the total number of iteration of each "for"
loop is limited by the maxLoopLength
property of the module definition, whose default value is 1000. You can set this property to a larger value if needed.
For objects that already have custom equation input (such as Mirror -> Custom Equation), the equation property in the JSON is a string representing a LaTeX equation, rather than a math.js expression. To include custom parameters in the equation, you must use the same template syntax as if the LaTeX equation were a regular text. So the part enclosed by the backticks is in math.js expression, while the part outside is in LaTeX. The module parameters can only be accessed in the math.js part, and the independent variables of the custom equation (e.g. \(x\)) can only be accessed in the LaTeX part. Here is an example of generating a mirror with equation \(y=\cos(2\pi x+\phi)\), where \(\phi\) is a module parameter:
In the future, there may be a way to unified the equation input.
For objects that already support different ways to define its shape (currently only Glass -> Spherical lens). There are special JSON syntax for such objects that can be used within the module definition, even if they are always defined by shape in the top level objs
array. Here is an example:
To contribute your module, see Contributing modules.