-
Notifications
You must be signed in to change notification settings - Fork 0
Design documentation
This FAQ indicates the rationale for the proposed design of Mathemagical.js. The current page is part of the broader documentation of the Mathemagical prototype. Since the subject of this section is library design and development, rather than library usage, some familiarity with software design practices is assumed.
Will Mathemagical provide a canvas object model, analogous to the Document Object Model (DOM)?
This may make sense, given Mathemagical's overall object-oriented approach. In the long run, we may take inspiration from existing libraries such as Konva.js.Why have separate classes for different types of coordinate systems?
Having separate classes for different types of coordinate systems seems consistent with SOLID development. It also reduces the number of parameters that need to be passed in when creating the windows.Why have a single class for both 2D and 3D Cartesian coordinates?
The 2D and 3D Cartesian windows overlap a lot; also, having them belong to a single class would be consistent with p5’screateCanvas()
and createGraphics()
, which accept an optional 2D or 3D renderer parameter.
Can .background()
and .border()
accept objects as parameters?
Yes. This is consistent with p5’s background()
. Since border()
doesn’t exist in p5, Mathemagical provides a Mathemagical border object, which can be created with .createBorder()
.
Are drawing objects based on any kind of general model?
Currently, yes. The prototype library uses a simple vertex-based model. For example, a square is represented as a sequence of four vertices. This has many benefits:
- We have the option of applying transforms directly to individual objects, rather than the entire canvas.
- We can have separate update and render commands; this works well with the p5 draw loop and allows different updates for animations and interactions to be applied before rendering.
- We can support a variety of other features, including Manim’s methods for placing objects, such as
next_to
; tracing or homotopy animations; interactively applied transforms; and built-in listeners for hover events.
An interesting trade-off is that native p5 shapes are drawn with the usual syntax, but the implementation is different. For example, calling the graph window's square()
method does not actually invoke p5's square()
function. We currently render a square by including its vertices inside the more general beginShape()
/endShape()
commands, in order to account for any transformations to its vertices.
Why create objects to represent existing p5 primitives?
Using the same object-oriented approach for all drawings allows us to provide a consistent interface:createPoint()
and createArrow()
work the same way.
Is it possible to draw Mathemagical shapes by passing in individual arguments instead of objects?
Yes. All drawing commands for p5 shapes and Mathemagical shapes will accept two kinds of input: individual arguments and objects. Individual arguments will be more familiar to most p5 users, and objects will be more flexible. Both will be supported within the same interface, just as p5’s point()
supports individual arguments for the coordinates or a single vector argument.
Also, for each drawing object, the render function and constructor function will accept the same individual parameters (e.g. w.point(x, y)
and w.createPoint(x, y)
). (If individual arguments are passed to a drawing function, then they’ll be passed to an object constructor internally.)
Are there trade-offs to adding object-oriented drawing (square(mySquare)
) to p5's interface (square(x, y, s)
)?
There is one issue we’ve observed. In order to make p5’s interface work in a custom graph window w
, we need to have rendering methods like w.point()
. However, in object-oriented programming, there are benefits to encapsulating the rendering methods with the objects being rendered, with syntax like myPoint.render()
. We could invoke these object-level rendering methods from a single method of the window class, with syntax like w.draw(myPoint)
. But, we want to provide an interface that’s consistent with p5’s.
Our solution is to define .render()
methods in the classes for the individual drawing objects, and then register wrapper functions on the graph window’s prototype. In other words, the user can call w.point()
, but this just tells the library to call myPoint.render()
. This is consistent with p5’s interface, it maintains encapsulation, and it’s extensible. The trade-off is that the codebase needs to include wrapper functions that aren’t necessary with the w.draw(myPoint)
approach.
Why provide a .set()
method with the same parameters as the object constructors?
Animations and interactions may require properties to be modified after the initial object construction. A .set()
method allows them to be modified in the same way that they were originally set. This is also consistent with the .set()
method of p5.Vector
objects. Properties not specified during the initial object construction can be modified with their own setters using p5 syntax (e.g. stroke can be set with .stroke()
).
Would it be better to create objects with syntax like createCircle()
, instead of w.createCircle()
?
We currently plan to support both types of commands, with different meanings attached to each:
- Creating a circle with
createCircle()
means it’s created without specifying a graph window, so it’s a model object. It has mathematical properties like a center and a radius, and methods that compute properties like area and circumference. - Creating a circle with
w.createCircle()
means it’s created in a graph window for drawing, so it’s a drawing object. In addition to mathematical properties, it has physical properties like a color, a thickness, and a position that we can point to.
In this approach, p5 vectors can be neatly classified as model objects, and drawing objects can be analogized to DOM elements created with document.createElement(). Another benefit to the current approach is that we don’t need to pass in a graph window to w.createCircle()
, so it can take the same parameters as p5’s circle()
.
This does leave the question of whether to create animation and interaction objects with syntax like w.createRotation()
, createRotation()
, or both. When animation and interaction objects give updates or inputs to a drawing object, they’ll have access to that drawing object’s graph window, so it may not be necessary to attach these to a particular window when they’re created. For now, we’ve used the w.createRotation()
syntax, in order to provide an interface that’s as uniform as possible across drawing objects, animation objects, and interaction objects.
We'll add any significant questions about the design of animation objects here, as they arise.
How does the proposed event-handling interface compare to the browser’s event-handling APIs?
The event-handling APIs built into the browser have the syntax
eventTarget.addEventListener(type, listener, options)
. Our main interface has the syntax drawingObject.takeInput(controllerObject)
.
We see several advantages to our proposed interface:
- The main components of Mathemagical.js are drawing, animation, and interaction. A consistent, object-oriented approach to all three components improves the user experience.
- The current design accommodates the same type of usage as the browser APIs: replace
eventTarget.addEventListener
withdrawingObject.takeInput
and replacetype, listener, options
with acontrollerObject
that encapsulates those inputs. - Controller objects combine flexibility with simplicity. They can store a lot of information about the interaction, including multiple listener-handler pairs, and they can be created with one command.
For users who want to customize the default event handlers used by Mathemagical controller objects, we provide an interface that's very close to that of eventTarget.addEventListener(type, listener, options)
.