Many interactive elements share common functionality such as animating a DOM element or listening to a slide gesture. For consistency, browser-compatibility and accessibility, we should use the shared utility methods in the @mathigon/boost library. Refer to its documentation for more information about:
- Element selection
- SVG and Canvas drawing
- Animations
- Event hand gesture handling
- Custom Web Components
Every course is divided into multiple steps, separated by ---
s. Every step has a unique ID which
is provided in the >
block at the beginning of a step, in the content.md
file:
---
> id: my-step-1
{.my-class} Here is a paragraph
---
Note: Specifying an ID for every step is optional, but recommended since the IDs are used to identify student progress in our database. Missing step IDs could lead to discrepancies if we insert new steps into an existing course.
The step IDs correspond to the names of functions exported in the functions.ts
file for the
same course. The function is executed whenever the step is revealed for the first time, and takes
a $step
argument, which is a reference to the custom HTML <x-step>
element that wraps around
the step. Check types.d.ts for the available properties and methods.
export function myStep1($step: Step) {
const $paragraph = $step.$('.my-class');
}
Note: Step IDs are in kebab-case
while function names are in camelCase
.
TODO...
Every step contains an observable object, which can be used to create reactive
export function myStep1($step: Step) {
$step.model.a = 10
$step.model.b = 11
}
Any variables you assign to $step.model
can then be accessed in Markdown. If the model changes,
the template will update automatically.
Here is ${a} and ${b}.
Many built-in interactive elements automatically integrate with the model:
Here is a variable slider ${a}{a|5|0,10,1} and some variable values: a = ${a}, b = ${b}. Here
is a point: (${p.x},${p.y}).
Here is [a button](action:increment(1)) that triggers a function whenever you click it.
// A large horizontal slider that binds to model.b
x-slider(steps=100 :bind="b")
// An interactive geopad component
x-geopad(width=200 height=200): svg
// This is a movable point that binds a Point instance to model.p
circle.move(name="p" cx=10 cy=10)
export function myStep1($step: Step) {
$step.model.click = (n: number) => $step.model.b += 1;
console.log($step.model.p); // Get the current value of model.p.
$step.model.watch(() => {
// This callback is triggered whenever model.p changes.
console.log('point', $step.model.p);
});
}
Note: The model.watch
function is very efficient: when executed for the first time, it tracks
which model properties are accessed within its body. Then it will keep executing the callback any
time these properties change, but not when other properties of model
change.
GeoPad is a reusable component for displaying interactive, two-dimensional geometry diagrams and coordinate systems. It is highly customisable and integrates well with Mathigon's TypeScript libraries.
You can add a simple GeoPad instance in Markdown using this syntax:
x-geopad(width=600 height=200): svg
circle.move.red(x="point(10,10)" name="a")
circle.blue(x="point(20,20)" name="b")
path.green(x="line(a,b)")
Every GeoPad needs to have an <svg>
element as its child. The SVG can contain a number of
different elements: points are <circle>
s and lines, circles, polygons or other paths are
<path>
s. Every element should have an x=
attribute that specifies its value. Elements can
optionally have a name=
attribute with a (unique) ID.
Once you have given elements a name, you can reference them as parameters when creating new
elements, like in the line(a,b)
example above, which creates a line from point a
to point b
.
Elements with a name are also added to the model
of the step they appear in. This means that you
can dynamically change their value in TypeScript, or listen to events when their value changes:
export function myStep($step: Step) {
// Log the current value of point a.
console.log($step.model.a);
// Trigger a callback whenever point a changes.
$step.model.watch(() => console.log($step.model.a));
}
It is often easiest to specify all children of a GeoPad component in HTML. If you want to
dynamically add elements later, you can use the .drawPoint()
and .drawPath()
methods. Here
are some examples:
$geopad.drawPoint(`point(10,10)`, {classes: 'red', name: 'p1'});
$geopad.drawPoint(new Point(10, 10), {classes: 'blue', name: 'p2'});
$geopad.drawPoint(({p1, p2}) => p1.rotate(Math.PI, p2), {classes: 'green', name: 'p3'});
$geopad.drawPath(`polygon(p1,p2, p3)`, {animated: 1000});
The first parameter can be an expression string, a geo element instance, or a function that evaluates to a geo element. Supported options in the second argument are:
classes?: string
– multiple space-separated classes to add to the DOM elementanimated?: boolean
– whether to animate the drawing of this elementname?: string
– which name to give this element, so that its value can be referenced elsewheretarget?: string
– the target attribute for this element, which can be used to highlight it when hovering over certain expressions in the same stepsinteractive?: boolean
– only for points: whether this element is interactive or fixed
When specifying the value of an element using the x=
attribute in HTML, or using a string as the
first argument of the drawPoint/drawPath
functions, you and enter an arbitrary JavaScript
expression. The following special variables and are automatically added to $step.model
:
Expression | Return value |
---|---|
pi |
Math.PI |
point(a,b) |
new Point(a, b) |
angle(a,b,c) |
new Angle(a, b, c) |
line(a,b) |
new Line(a, b) |
ray(a,b) |
new Ray(a, b) |
segment(a,b) |
new Segment(a, b) |
circle(a,b) |
new Circle(a, b) |
arc(a,b,c) |
new Arc(a, b, c) |
sector(a,b,c) |
new Sector(a, b, c) |
polygon(...points) |
new Polygon(...Points) |
polyline(...points) |
new Polyline(...Points) |
triangle(a,b,c) |
new Triangle(a, b, c) |
rectangle(a,w,h) |
new Rectangle(a, w, h) |
distance(a,b) |
Point.distance(a, b) |
round(a) |
Fermat.round(a) |
sqrt(a,b) |
Math.sqrt(a, b) |
floor(a,b) |
Math.floor(a, b) |
ceil(a,b) |
Math.ceil(a, b) |
intersections(...els) |
Fermat.intersections(...els) |
All of these functions are safe, for example, if you call line(a,b)
and either a
or b
is
undefined
, it will also return undefined
(rather than throw an Error, or return an invalid
Line
instance). This is particularly important when dealing with interactive elements: if the
user moves points in a way that removes certain elements (e.g. the intersection of two lines),
all the children of those elements will simply disappear.
If you want to use other functions in your expressions, you can simply add them to $step.model
.
TODO…