home back

Basic Objects

This chapter covers the fundamental building blocks of JMathAnim animations. All drawable objects inherit from the MathObject class (or implement the Drawable interface), which provides common methods for transformations like scale, rotate, and shift.

Table of Contents

The Vec Class

The Vec class represents a 2D vector (with an additional z-coordinate for future 3D support). It's the fundamental class for representing coordinates in the math view.

Creating Vectors

def v = Vec.to(4, 5)        // Creates vector (4, 5)
def w = Vec.to(2, -1)       // Creates vector (2, -1)
def origin = Vec.origin()   // Creates vector (0, 0)

Accessing Components

def x = v.x                 // Get x-coordinate
def y = v.y                 // Get y-coordinate
def norm = v.norm()         // Get Euclidean norm (length)

Vector Operations

JMathAnim provides two styles of vector operations:

  1. Immutable operations - Return a new vector, leaving the original unchanged:
def sum = v.add(w)          // Returns v + w (new vector)
def scaled = v.mult(3)      // Returns 3v (new vector)
def diff = v.to(w)          // Returns w - v (new vector)
  1. In-place operations - Modify the original vector:
v.scale(3)                  // Multiplies v by 3 (modifies v)
v.shift(w)                  // Adds w to v (modifies v)
v.rotate(45 * DEGREES)      // Rotates v by 45° (modifies v)

Additional Methods

def dot = v.dot(w)          // Dot product of v and w
def angle = v.getAngle()    // Angle in radians (0 to 2π)

Note: The DEGREES constant converts degrees to radians. Use it for all angle operations: angle * DEGREES.


The Point Class

The Point class is the most basic MathObject, representing a single point in space. Every drawable object in JMathAnim has a position defined by points. The main difference between a Point and a Vec class is that the Vec is a lightweight object to hold coordinates, while Point is a MathObject that can be drawn on screen.

Creating Points

def p = Point.at(1, 1)      // Point at coordinates (1, 1)
def q = Point.origin()      // Point at (0, 0)
def r = Point.random()      // Point at random screen location

Accessing Coordinates

Points contain a Vec object storing their position:

def v = p.getVec()          // Get position vector
def x = p.getVec().x        // Get x-coordinate
def d = p.norm()            // Distance from origin
def AB = A.to(B)            // Vector from point A to point B

Point Styles

Points can be displayed in various styles using the DotStyle enum:

def A = Point.at(-1.75, 0).dotStyle(DotStyle.CIRCLE)
def B = Point.at(-1.25, 0).dotStyle(DotStyle.CROSS)
def C = Point.at(-0.75, 0).dotStyle(DotStyle.PLUS)
def D = Point.at(-0.25, 0).dotStyle(DotStyle.RING)
def E = Point.at(0.25, 0).dotStyle(DotStyle.TRIANGLE_UP_HOLLOW)
def F = Point.at(0.75, 0).dotStyle(DotStyle.TRIANGLE_DOWN_HOLLOW)
def G = Point.at(1.25, 0).dotStyle(DotStyle.TRIANGLE_UP_FILLED)
def H = Point.at(1.75, 0).dotStyle(DotStyle.TRIANGLE_DOWN_FILLED)

scene.add(A, B, C, D, E, F, G, H)

// Add reference axis
scene.add(
    Line.XAxis()
        .thickness(2)
        .drawColor("gray")
        .layer(-1)  // Draw behind points
)

dots

Note: Points require higher thickness values (typically 20-40) to be clearly visible, as the thickness represents the point's diameter.


The Shape Class

The Shape class is one of the most important classes in JMathAnim. It represents curves (closed or open) and provides extensive functionality for creating and manipulating 2D shapes.

Basic Shape Constructors

JMathAnim provides convenient static methods for common shapes:

// Circle with radius 1, centered at origin
def circ = Shape.circle()

// Unit square with lower-left corner at origin
def sq = Shape.square()

// Regular pentagon with first two vertices at (0,0) and (1,0)
def reg = Shape.regularPolygon(5)

// Triangle from specified vertices
// Accepts Vec, Point, or any Coordinates object
def poly = Shape.polygon(
    Vec.to(0.25, -0.5),
    Vec.to(1.25, -0.5),
    Vec.to(0.25, 0.5)
)

// Axis-aligned rectangle
// Parameters: lower-left corner, upper-right corner
def rect = Shape.rectangle(Vec.to(1, 2), Vec.to(3, 5))

// Line segment between two points
def seg = Shape.segment(Vec.to(-1, -1), Vec.to(-0.5, 1.5))

// Circular arc
// Parameters: arc length in radians
def arc = Shape.arc(PI / 4)

scene.add(circ, sq, reg, poly, rect, seg, arc)

basicShapes

Creating Shapes with LOGO Commands

Shapes can also be created using LOGO turtle graphics commands, which is useful for complex or iterative patterns:

// CLO (or CLOSE) is a non-standard command to close the path
def logoCmd = "REPEAT 12 [FD .5 RT 150 FD .5 LT 120] CLO"
def logoShape = Shape.logo(logoCmd)

scene.add(
    logoShape
        .center()
        .style("solidblue")
)

Example of Shape created with LOGO commands

Supported LOGO Commands

Command Aliases Description
FORWARD n FD n Move forward n units
BACKWARD n BK n Move backward n units
RIGHT angle RT angle Turn right by angle degrees
LEFT angle LT angle Turn left by angle degrees
REPEAT n [commands] Execute commands n times
PENUP PU Lift pen (move without drawing)
PENDOWN PD Lower pen (resume drawing)
CLOSE CLO Close the path (JMathAnim extension)

Commands are case-insensitive. Full reference available at Logo Commands Reference.

Working with Shape Paths

Each Shape contains a JMPath object that manages its path. Points can be accessed using zero-based circular indexing:

def pentagon = Shape.regularPolygon(5)

def p0 = pentagon.getPoint(0)    // First vertex
def p1 = pentagon.getPoint(1)    // Second vertex
def p5 = pentagon.getPoint(5)    // Wraps around to first vertex (same as p0)

Important: Shape points use circular array indexing, so getPoint(n) and getPoint(n + vertexCount) return the same point.

Shape Centers

Shapes provide two methods for finding their center:

def bbox_center = shape.getCenter()     // Center of bounding box
def geometric_center = shape.getCentroid()  // Average of all vertices

Note: For regular polygons, .getCentroid() returns the true geometric center, while .getCenter() returns the center of the bounding box, which may differ for rotated shapes.


The LatexMathObject Class

The LatexMathObject class renders mathematical expressions using LaTeX, allowing you to display formulas and text with professional typesetting.

Creating LaTeX Objects

def text = LatexMathObject.make("Look mom, I'm \\LaTeX!")
scene.add(text)

LaTeX 1

Handling Special Characters

LaTeX uses $ and \ as special characters. In Groovy, these must be escaped:

// INCORRECT - Groovy will interpret $ and \ as escape sequences
LatexMathObject.make("$e^{i\pi}+1=0$")  // ERROR!

// CORRECT - Escape the special characters
LatexMathObject.make("\$e^{i\\pi}+1=0\$")  // Works!

// BETTER - Use raw strings (prefix with 'r')
LatexMathObject.make(r"$e^{i\pi}+1=0$")  // Easiest!

Tip: Raw strings (prefixed with r) ignore escape sequences. Use either single or double quotes:

LatexMathObject.make(r"$e^{i\pi}+1=0$")  // Both work
LatexMathObject.make(r'$e^{i\pi}+1=0$')  // identically

Compilation Modes

JMathAnim offers two LaTeX compilation methods:

  1. Default (JLaTeXMath) - Fast, built-in renderer for most formulas:
def formula = LatexMathObject.make(r'$e^{i\pi}+1=0$')
  1. External LaTeX - Uses system LaTeX for advanced features:
def formula = LatexMathObject.make(
    r'$e^{i\pi}+1=0$',
    CompileMode.CompileFile
)

The external compiler: - Requires a LaTeX distribution installed on your system - Compiles to .dvi, converts to .svg, imports as MultiShapeObject - Caches .svg files for reuse in subsequent runs - Supports commands not available in JLaTeXMath (e.g., \begin{verbatim})

Recommendation: Use the default mode unless you need advanced LaTeX features.


The Line Class

The Line class represents infinite mathematical lines. In practice, it draws the visible portion within the current view.

Creating Lines

def A = Point.at(1, 1)
def B = Point.at(0, 1)
def v = Vec.to(1, 0.2)

// Line through points A and B
def line1 = Line.make(A, B)
    .drawColor("red")
    .thickness(20)

// Line through A in direction v
def line2 = Line.make(A, A.add(v))
    .drawColor("blue")
    .thickness(10)

// Predefined axes
def line3 = Line.XAxis().drawColor("darkorange")        // y = 0
def line4 = Line.YAxis().drawColor("darkmagenta")       // x = 0
def line5 = Line.XYBisector().drawColor("darkgreen")    // y = x

scene.add(line1, line2, line3, line4, line5)
scene.play.shift(5, -1, -1.5, A)  // Animate point A

Line01

Note: Lines use direction vectors only at construction time. Moving a point after creating the line won't update the line's direction (unlike constraints, which are covered in advanced topics).

The Ray Class

Similar to Line, the Ray class represents half-infinite rays with syntax identical to Line constructors.


The Axes Class

The Axes class creates Cartesian coordinate axes with customizable ticks and labels. It's essentially a container managing multiple Line, Shape, and LatexMathObject instances.

Creating Axes

def axes = Axes.make()

// Add primary ticks
axes.generatePrimaryXTicks(-2, 2, 0.5)  // x: -2, -1.5, -1, ..., 2
axes.generatePrimaryYTicks(-2, 2, 0.5)  // y: -2, -1.5, -1, ..., 2

// Add custom ticks
axes.addXTicksLegend(0.75, TickType.PRIMARY)  // Tick at x = 0.75
axes.addYTicksLegend(r"$\pi/4$", PI / 4, TickType.PRIMARY)  // Custom label

scene.add(
    axes,
    Shape.circle()
        .scale(0.5)
        .drawColor("darkblue")
)

axes01

Note: By default, axes are created without ticks. Use generatePrimaryXTicks and generatePrimaryYTicks to add them.


The CartesianGrid Class

Creates a grid with primary and secondary divisions, useful for reference and measurements.

// Grid centered at origin with subdivisions
def grid = CartesianGrid.make(
    0, 0,       // Center point (usually origin)
    2,          // Horizontal subdivisions for secondary grid
    2           // Vertical subdivisions for secondary grid
)

scene.add(
    grid,
    Shape.circle()
        .drawColor("blue")
        .fillColor("blue")
        .fillAlpha(0.5)
)

Cartesian grid example

Styling: Grids use the gridPrimaryDefault and gridSecondaryDefault styles defined in your config files. See the Styling chapter for customization details.


The Arrow Class

The Arrow class (introduced in v0.9.10-SNAPSHOT) provides flexible arrow creation with various arrowhead styles and curvature options.

Basic Arrow Types

def A = Vec.to(0, 0)
def B = Vec.to(1, 0)

// Single-headed arrows with different styles
def ar1 = Arrow.make(A, B, ArrowType.ARROW1)
    .color("midnightblue")
def ar2 = Arrow.make(A.copy(), B.copy(), ArrowType.ARROW2)
    .color("firebrick")
def ar3 = Arrow.make(A.copy(), B.copy(), ArrowType.ARROW3)
    .color("darkgreen")

// Double-headed arrows
def ar4 = Arrow.makeDouble(
    A.copy(), B.copy(),
    ArrowType.ARROW2,  // Start arrowhead
    ArrowType.ARROW3   // End arrowhead
).color("goldenrod")

def ar5 = Arrow.makeDouble(
    A.copy(), B.copy(),
    ArrowType.SQUARE,
    ArrowType.SQUARE
).color("slategray")

// Arrange arrows vertically
def arrows = MathObjectGroup.make(ar1, ar2, ar3, ar4, ar5)
arrows.setLayout(LayoutType.LOWER, 0.1, 0.1)

scene.add(arrows)
camera.zoomToAllObjects()

Arrow types example

Curved Arrows

Arrows can be curved by setting a curvature parameter:

def A = Point.at(0, 0)
def B = Vec.to(1, 0)

def arrow = Arrow.make(A, B, ArrowType.ARROW1)
    .drawColor("firebrick4")
    .fillColor("firebrick3")
    .setArrowCurvature(60 * DEGREES)

scene.add(arrow)
camera.zoomToAllObjects()

Curved arrow example

Advanced Styling

Control the scale of individual arrowhead components:

def A = Point.at(0, 0).layer(1).drawColor("red")
def B = Point.at(2, 0).layer(1).drawColor("red")

def arrow = Arrow.make(A, B, ArrowType.ARROW2)
    .setArrowThickness(100)      // Base thickness
    .setCurvature(-45 * DEGREES)
    .setStartScale(5)            // Start arrowhead 5× larger than end
    .style("solidblue")

scene.add(arrow, A, B)
camera.zoomToAllObjects()

Arrow with scaled heads


The Delimiter Class

The Delimiter class creates extensible symbols (brackets, braces, parentheses) that automatically adjust to fit between two control points.

Creating Delimiters

def sq = Shape.square().center().style("solidBlue")

// Red brace with label
def delim1 = Delimiter.make(
    sq.getPoint(2),
    sq.getPoint(1),
    DelimiterType.BRACE,
    0.05  // Gap between control point and delimiter
)
delim1.addLabelTip(r"$x$")
delim1.color("#d35d6e")

// Green bracket with custom label (rotates with delimiter)
def delim2 = Delimiter.make(
    sq.getPoint(1),
    sq.getPoint(0),
    DelimiterType.BRACKET,
    0.05
)
delim2.addLabelTip(Shape.regularPolygon(3).scale(0.1))
delim2.color("#5aa469")
delim2.getLabelTip()
    .fillColor("yellow")
    .thickness(6)

// Control label rotation
delim2.setRotationType(RotationType.FIXED)  // Don't rotate label

// Orange parenthesis with scaled size
def delim3 = Delimiter.make(
    sq.getPoint(0),
    sq.getPoint(3),
    DelimiterType.PARENTHESIS,
    0.05
)
delim3.style("solidorange")
delim3.setDelimiterScale(3)  // 3× larger
delim3.addLabelTip("3")

scene.add(delim1, delim2, delim3, sq)

// Animate to show automatic adjustment
play.scale(3, 0.75, 1.8, sq)
play.scale(3, 2, 0.25, sq)
play.rotate(3, 60 * DEGREES, sq)
play.shrinkOut(sq)

delimiter01

Key Features: - Delimiters automatically recalculate every frame - Always draw to the "left" when looking from point A to point B - Labels can be set to FIXED (don't rotate), ROTATE (rotate fully), or SMART (avoid upside-down, default)

Advanced Delimiter Methods

// Set label with automatic LatexMathObject creation
delimiter.setLabel("x", 0.1)  // String creates LaTeX object

// Add label showing delimiter length (auto-updates)
delimiter.addLengthLabel()

// New delimiter types (v0.9.4+)
def lengthArrow = Delimiter.make(A, B, DelimiterType.LENGTH_ARROW, gap)
def lengthBracket = Delimiter.make(A, B, DelimiterType.LENGTH_BRACKET, gap)

The TippableObject Class

TippableObject marks specific points on shapes with customizable tips (symbols, text, arrows). A specialized subclass LabelTip is available for LaTeX labels.

Creating Tips

def hexagon = Shape.regularPolygon(6).center().style("solidblue")

// Arrow head at midpoint of one side
def tip1 = TippableObject.arrowHead(hexagon, 0.5 / 6, ArrowType.ARROW1)
    .setSlopeDirection(SlopeDirectionType.POSITIVE)
    .layer(1)

// Rotating LaTeX label
def tip2 = LabelTip.make(hexagon, 1.5 / 6, r"$a+b$")
    .color("darkslateblue")
    .layer(1)

// Non-rotating LaTeX label
def tip3 = LabelTip.make(hexagon, 2.5 / 6, r"$x$")
    .setRotationType(RotationType.FIXED)
    .color("firebrick")
    .layer(1)

// Equal length marks (2 marks)
def tip4 = TippableObject.equalLengthTip(hexagon, 3.5 / 6, 2)
    .layer(1)

// Custom shape tip aligned with contour
def tip5 = TippableObject.make(
    hexagon, 4.5 / 6,
    SlopeDirectionType.POSITIVE,
    Shape.circle().scale(0.05).style("solidOrange")
).layer(1)

// Custom shape tip aligned away from contour
def tip6 = TippableObject.make(
    hexagon, 5.5 / 6,
    SlopeDirectionType.NEGATIVE,
    Shape.square().scale(0.1).style("solidRed")
)
tip6.setAnchor(AnchorType.LEFT).layer(1)

scene.add(hexagon, tip1, tip2, tip3, tip4, tip5, tip6)
play.rotate(5, PI, hexagon)

tippable01

Understanding the Position Parameter: - Position is specified as a fraction of the shape's perimeter - 0.5 / 6 means "halfway along the first side" of a 6-sided shape - 1.5 / 6 means "halfway along the second side" - Positions wrap around (circular indexing)

Slope Direction: - POSITIVE: Tip points to the left of the contour direction - NEGATIVE: Tip points to the right of the contour direction


Importing Images

JMathAnim supports both bitmap and vector images. Images are loaded from the resources/images/ folder by default (configurable in styling settings).

Bitmap Images

Import any bitmap format supported by JavaFX (PNG, JPG, etc.):

def img = JMImage.make("euler.jpg")
    .center()
    .rotate(-5 * DEGREES)

def text = LatexMathObject.make("All hail the great Euler!")
    .stack()
    .withGaps(0.1, 0.1)
    .withDestinyAnchor(AnchorType.LOWER)
    .toObject(img)

scene.play(
    Commands.moveIn(2, ScreenAnchor.LEFT, img),
    ShowCreation.make(2, text)
)
scene.waitSeconds(2)

image01

SVG Images

Import and animate SVG vector graphics:

def svg = SVGImport.load("donaldKnuth.svg")
svg.setHeight(2).center()

play.showCreation(svg)  // Animates drawing the SVG
scene.waitSeconds(2)

svgCreation

File Location: Place SVG files in <project_root>/resources/images/

SVG Import Details: - Creates a MultiShape object containing multiple Shape instances - Each SVG element becomes a separate Shape with a JMPath - Supports standard transformations and animations - Limitation: Some elements (e.g., gradients) may not import with 100% accuracy


The MathObjectGroup Class

MathObjectGroup manages collections of MathObject instances as a single entity, enabling batch operations and layouts.

Creating Groups

def square = Shape.square()
def triangle = Shape.regularPolygon(3)
def circle = Shape.circle()

def group = MathObjectGroup.make(square, triangle, circle)

Group Operations

Groups support all standard MathObject operations:

group.shift(1, 1)              // Move entire group
group.rotate(45 * DEGREES)      // Rotate all members
group.scale(2)                  // Scale all members
group.setFillColor("cadetblue") // Apply to all members

Adding Groups to Scene

Adding a group is equivalent to adding all its members:

scene.add(group)  // Same as: scene.add(square, triangle, circle)

Note: You don't need to add the group itself to display its members. Add objects individually for more control over visibility.

Why Use Groups?

Groups excel at: - Batch transformations: Transform multiple objects together - Layout management: Arrange objects automatically (see TransformingObjects chapter) - Animation coordination: Animate multiple objects as one unit

Example using layouts:

def group = MathObjectGroup.make(square, triangle, circle)
group.setLayout(LayoutType.LOWER, 0.1, 0.1)  // Arrange vertically
scene.add(group)

The Rect Class

The Rect class represents axis-aligned bounding boxes. While not directly drawable, it's essential for positioning and collision detection.

Getting Bounding Boxes

Every MathObject (and anything implementing Boxable) has a bounding box:

def ellipse = Shape.circle()
    .scale(1, 0.5)
    .rotate(45 * DEGREES)
    .style("solidorange")

def bbox = ellipse.getBoundingBox()  // Returns Rect
def rect = Shape.rectangle(bbox).drawColor("blue")

scene.add(ellipse, rect)

Bounding box example

Rect Properties and Methods

// Corner coordinates
def xMin = bbox.xmin     // Left edge
def xMax = bbox.xmax     // Right edge  
def yMin = bbox.ymin     // Bottom edge
def yMax = bbox.ymax     // Top edge

// Dimensions
def width = bbox.getWidth()
def height = bbox.getHeight()

// Corner points
def center = bbox.getCenter()
def upperLeft = bbox.getUpperLeft()
def upperRight = bbox.getUpperRight()
def lowerLeft = bbox.getLowerLeft()
def lowerRight = bbox.getLowerRight()

// Relative positioning
def point = bbox.getRelCoordinates(0.25, 0.75)  // 25% across, 75% up
def centerAlt = bbox.getRelCoordinates(0.5, 0.5)  // Same as getCenter()

// Transformations
def expanded = bbox.addGap(0.1, 0.2)  // Add 0.1 horizontal, 0.2 vertical gap
bbox.centerAt(destination)  // Move center to destination
def rotated = bbox.getRotatedRect(45 * DEGREES)  // Smallest rect containing rotated bbox

Working with the Math View

The math view (visible screen area) is represented as a Rect:

def mathView = scene.getMathView()

// Create points at key screen positions
def center = Point.at(mathView.getCenter())
    .dotStyle(DotStyle.PLUS)

def corners = [
    Point.at(mathView.getUpperLeft()),
    Point.at(mathView.getUpperRight()),
    Point.at(mathView.getLowerLeft()),
    Point.at(mathView.getLowerRight())
]

// Points at 25% and 75% positions
def Q1 = Point.at(mathView.getRelCoordinates(0.25, 0.25))
    .drawColor("red")
def Q2 = Point.at(mathView.getRelCoordinates(0.75, 0.25))
    .drawColor("green")
def Q3 = Point.at(mathView.getRelCoordinates(0.75, 0.75))
    .drawColor("yellow")
def Q4 = Point.at(mathView.getRelCoordinates(0.25, 0.75))
    .drawColor("blue")

scene.add(center, *corners, Q1, Q2, Q3, Q4)
scene.add(
    Shape.rectangle(mathView)
        .scale(0.9)  // 90% of screen size
)

Math view example

Default Math View Coordinates

At initialization (16:9 aspect ratio): - Center: (0, 0) - X range: -2 to 2 - Y range: -1.125 to 1.125

Note: Y boundaries adjust automatically based on aspect ratio while maintaining the center and X boundaries.


Next Steps

Now that you understand the basic objects, explore: - Styling: Customize colors, thickness, and visual properties - Transforming Objects: Advanced positioning, scaling, and animation

home back