Groovy lists: quick reference for JMathAnim scripts
Recipes for the list operations that show up while writing scenes: slicing, zipping, sliding windows, building maps, and the JMathAnim specific bits (coordinates, groups, formulas, animation lists).
Every snippet was checked against Groovy 5.0.0, the version used by
jmathanim-core. Unless a recipe says otherwise, the methods return a new list and leave the original untouched.
1. Creating and reading
def L = [10, 20, 30, 40] // a list
def empty = [] // an empty one
def r = 0..5 // a range: 0,1,2,3,4,5 (0..<5 stops at 4)
def nums = (0..<5).toList() // a range turned into a real list
| Recipe | Code | Result |
|---|---|---|
| First element | L.first() or L.head() or L[0] |
10 |
| Last element | L.last() or L[-1] |
40 |
| n-th from the end | L[-2] |
30 |
| Size | L.size() |
4 |
| Is it empty | L.isEmpty() or !L |
false |
| Position of a value | L.indexOf(30) |
2 |
| Position by condition | L.findIndexOf { it > 15 } |
1 |
| As text | L.join(", ") |
"10, 20, 30, 40" |
A range unpacks into separate arguments with the spread operator *:
def all = [*0..3, 99] // [0, 1, 2, 3, 99]
2. Removing the first or the last element
The whole point: tail/init return a copy, removeAt modifies the list.
| Want | Code | Result on [10,20,30,40] |
|---|---|---|
| Without the first | L.tail() |
[20, 30, 40] |
| Without the last | L.init() |
[10, 20, 30] |
| Without the first n | L.drop(2) |
[30, 40] |
| Without the last n | L.dropRight(2) |
[10, 20] |
| Only the first n | L.take(2) |
[10, 20] |
| Only the last n | L.takeRight(2) |
[30, 40] |
| Without the first (slice) | L[1..-1] |
[20, 30, 40] |
| Without the last (slice) | L[0..-2] |
[10, 20, 30] |
| Remove in place | L.removeAt(0) |
L becomes [20, 30, 40] |
| Remove last in place | L.removeLast() |
L becomes [10, 20, 30] |
def inner = pts.tail().init() // drop both ends
def rest = objs.drop(1) // everything but the first object
Trap. The
-operator removes by value, every match, it is not an index:[1,2,2,3] - 2gives[1,3]. To remove a position useremoveAtor a slice.Trap.
tail(),init()andfirst()throw on an empty list. Guard withif (L)or useL.take(1)/L.drop(1), which are always safe.
3. Slices and sublists
L[1..2] // [20, 30] a range of positions
L[0, 2] // [10, 30] pick positions one by one
L[1..-1] // [20, 30, 40] negative index counts from the end
L[-2..-1] // [30, 40]
L.reverse() // [40, 30, 20, 10]
L.unique(false) // removes duplicates without touching L
unique() and sort() modify the list; pass false to get a copy instead:
def sorted = L.sort(false) // L stays as it was
def byName = objs.sort(false) { it.getCenter().x }
4. Combining two lists element by element
transpose() is the workhorse: it pairs the n-th of each list.
def a = [1, 2, 3]
def b = ["x", "y", "z"]
[a, b].transpose() // [[1,x], [2,y], [3,z]]
a.zip(b) // the same, only for exactly two lists
To do something with each pair, name the two parts in the closure:
def labels = [a, b].transpose().collect { n, s -> "$s$n" } // [x1, y2, z3]
[a, b].transpose().each { n, s -> println "$s -> $n" }
More than two lists work the same way:
[a, b, [true, false, true]].transpose() // [[1,x,true], [2,y,false], [3,z,true]]
The index-based version, when you also need the position:
def out = (0..<a.size()).collect { i -> a[i] + b[i] }
Trap. If the lists have different lengths,
transpose()truncates to the shortest one, silently. Checka.size() == b.size()when it matters.
All pairs instead of matching pairs (cartesian product, a JMathAnim extension on lists):
def grid = [0, 1] * [0, 1, 2] // [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2]]
The first list is the outer one. Plain Groovy has [a, b].combinations(), which
takes any number of lists but varies the first one fastest.
5. Each element with the next one (sliding window)
collate(size, step, keepPartial) is the general tool. For consecutive pairs:
L.collate(2, 1, false) // [[10,20], [20,30], [30,40]]
| Want | Code | Result on [10,20,30,40] |
|---|---|---|
| Consecutive pairs | L.collate(2, 1, false) |
[[10,20],[20,30],[30,40]] |
| Consecutive triples | L.collate(3, 1, false) |
[[10,20,30],[20,30,40]] |
| Disjoint blocks of 2 | L.collate(2) |
[[10,20],[30,40]] |
| Cyclic pairs (closing the loop) | (L + [L.first()]).collate(2, 1, false) |
[[10,20],[20,30],[30,40],[40,10]] |
| Same, with transpose | [L.init(), L.tail()].transpose() |
[[10,20],[20,30],[30,40]] |
Then work on the pairs:
def steps = L.collate(2, 1, false).collect { x, y -> y - x } // [10, 10, 10]
Trap. The third argument matters.
L.collate(2, 1)defaults tokeepPartial = trueand leaves a trailing incomplete group[40], which breaks any closure expecting two parameters. Always passfalsefor windows.Groovy has no
eachCons.collate(n, 1, false)is the equivalent, and it returns[]for a list shorter than the window, which is the safe behaviour.
6. Building a map from two lists
def keys = ["a", "b", "c"]
def values = [1, 2, 3]
def m = [keys, values].transpose().collectEntries() // [a:1, b:2, c:3]
collectEntries() with no argument reads a list of [key, value] pairs. With a
closure you decide what the key and the value are:
[keys, values].transpose().collectEntries { k, v -> [(v): k] } // [1:a, 2:b, 3:c]
The parentheses around (v) are required: without them Groovy takes v as
the literal key name.
Other map recipes:
keys.collectEntries { [(it): it.toUpperCase()] } // key list -> computed values
[a:1, b:2].collectEntries { k, v -> [(v): k] } // swap keys and values
[a:1, b:2, c:3].subMap(["a", "c"]) // [a:1, c:3]
[a:1, b:2].keySet() as List // [a, b]
[a:1, b:2].values() as List // [1, 2]
[a:1, b:2].collect { k, v -> "$k=$v" } // [a=1, b=2]
[a:1, b:2].each { k, v -> println "$k $v" }
[a:1, b:2].find { k, v -> v > 1 }?.key // b
Maps keep insertion order, so collectEntries over an ordered list gives an
ordered map. That is what group(obj: map) relies on for naming elements.
7. Transforming, filtering, folding
| Operation | Groovy | Alias added by JMathAnim |
|---|---|---|
| Transform each element | collect { } |
map { } |
| Keep those matching | findAll { } |
filter { } |
| Transform and flatten | collectMany { } |
flatMap { } |
| Fold into one value | inject(init) { a, b -> } |
reduce(init) { a, b -> } |
def doubled = [1, 2, 3].collect { it * 2 } // [2, 4, 6]
def evens = (1..10).findAll { it % 2 == 0 } // [2, 4, 6, 8, 10]
def flat = [[1,2],[3]].collectMany { it } // [1, 2, 3]
def total = [1, 2, 3].inject(0) { acc, x -> acc + x } // 6
Shortcuts worth knowing:
objs*.getCenter() // spread-dot: calls the method on each element
[1, 2, 3].sum() // 6
[1, 2, 3].sum { it * it } // 14, sum of a computed value
[1, 2, 3].average() // 2
[[1,2],[3,[4]]].flatten() // [1, 2, 3, 4]
With the index. withIndex() pairs each element with its position:
objs.withIndex().collect { obj, i -> animShift(obj: obj, dx: i * 0.5, run: false) }
objs.withIndex(1).each { obj, n -> println "object number $n" } // start at 1
objs.eachWithIndex { obj, i -> obj.shift(i * 0.5, 0) } // for side effects
Generating a list instead of consuming one:
def n = 6
def angles = (0..<n).collect { 2 * PI * it / n } // n evenly spaced angles
def ts = (0..<n).collect { it / (n - 1d) } // n values from 0 to 1
def xs = (0..10).step(2) // [0, 2, 4, 6, 8, 10]
def copies = (0..<n).collect { proto.copy() }
Note the 1d in the second line: it / (n - 1) with two integers is still
integer division in some contexts, so force a double when in doubt.
8. Order, grouping, searching
L.max() // 40
L.min() // 10
objs.max { it.getWidth() } // element with the largest computed value
L.find { it > 15 } // 20, the first match (null if none)
L.findAll { it > 15 } // [20, 30, 40], all matches
L.any { it > 35 } // true
L.every { it > 5 } // true
L.count { it > 15 } // 3
L.split { it > 25 } // [[30,40], [10,20]] matching first, then the rest
(1..6).groupBy { it % 3 } // [1:[1,4], 2:[2,5], 0:[3,6]]
[1,2,3].intersect([2,3,4]) // [2, 3]
([1,2] + [2,3]).unique(false) // [1, 2, 3], the union
9. Lists in JMathAnim
Coordinates
Anywhere a Vec is expected, a plain list of 2 or 3 numbers works:
def v = [1, 2] as Vec
def P = [1, 2] as Point
def s = shape(type: "segment", from: [0, 0], to: [1, 1])
Scaling a list scales every element, and the result is still a valid coordinate:
def half = 0.5 * [1, 3] // [0.5, 1.5]
def back = [1, 3] / 2 // [0.5, 1.5]
Trap, and an important one. The scaling
*only works with the number on the left.[1, 3] * 2is Groovy's own list repetition and gives[1, 3, 1, 3]. Write2 * [1, 3], or[1, 3] / 0.5if you prefer the object on the left.Trap.
+between lists concatenates:[1, 2] + [3, 4]is[1, 2, 3, 4], not a vector sum. The DSL depends on this to join lists of objects. To add vectors, convert first:([1, 3] as Vec) + [1, 0].
Groups and formulas are iterables
MathObjectGroup, LatexMathObject and the other containers implement
Iterable, so every method in this document works on them directly:
g.each { it.fillColor("orange") }
g.collect { it.getCenter() }
g.collate(2, 1, false) // consecutive pairs of elements
formula.findAll { it.getWidth() > 0.2 }
What you get back is always a plain list, never a group. Rebuild one when you need it:
def sub = [g[0], g[2]] as MathObjectGroup
def all = g as List // explicit conversion, rarely needed
Indexing is JMathAnim's own: g[0] is the n-th element, g[-1] the last,
g[0..2] a list of the first three, and g["title"] the element stored under
that name.
Adding to the scene
<< accepts a single object, a list, or a map of named parts:
scene << circle
scene << [circle, square, dot]
scene << (0..<5).collect { dot + [it * 0.5, 0] } // + returns a shifted copy
group << [a, b]
group << [head: a, body: b] // named elements
Recipe: segments joining consecutive points
The classic use of the sliding window.
def pts = [[0, 0], [1, 0.5], [2, -0.3], [3, 0.8]]
def segs = pts.collate(2, 1, false).collect { p, q ->
shape(type: "segment", from: p, to: q, style: [drawColor: "steelblue"])
}
scene << segs
Closing the polygon is one + away:
def closed = (pts + [pts.first()]).collate(2, 1, false)
Recipe: pairing objects with styles
def colors = ["red", "green", "blue"]
[g, colors].transpose().each { obj, c -> obj.fillColor(c) }
Same idea against a formula, one colour per glyph:
[formula, colorList].transpose().each { glyph, c -> glyph.color(c) }
Recipe: a named group from two lists
A map in obj: names the elements, and the keys keep their writing order, so two
parallel lists become a group whose pieces are reachable by name.
def names = ["title", "body", "footer"]
def objs = [latex(text: r"$E=mc^2$"), shape(type: "circle"), text(text: "bottom")]
def g = group(obj: [names, objs].transpose().collectEntries(),
layout: "lower", gap: 0.2)
g["body"].fillColor("gold")
Recipe: a list of animations built in a loop
Children of a container must be built with run: false, otherwise each one plays
on its own while the argument list is evaluated.
def anims = objs.withIndex().collect { obj, i ->
animShift(obj: obj, dx: 0.5 * (i + 1), run: false)
}
animGroup(anims: anims, delay: 0.3) // staggered, all together
animJoin(anims: anims, gap: 0.2) // one after another
The shortest form, when no extra parameter is needed:
[a1, a2] as AnimationGroup // played together
[a1, a2] as JoinAnimation // played one after another
Recipe: a grid without nested loops
def dot = point()
scene << ((0..<5) * (0..<4)).collect { dot + it } // 20 points, one per [i,j] pair
Each pair is a [i, j] list, so it feeds straight into the shifting +.
Recipe: polygon points computed on the fly
def n = 7
def verts = (0..<n).collect { [Math.cos(2 * PI * it / n), Math.sin(2 * PI * it / n)] }
def poly = shape(type: "polygon", points: verts)
Raw lists of LaTeX strings
The r prefix applies to a whole list, nested lists and maps included, which
saves escaping every backslash:
def texts = r["$\alpha$", "$\beta$", "$\gamma$"]
It needs two elements or more; a one element list is written [r"$\pi$"].
10. Trap summary
| Looks like | Actually does | Do instead |
|---|---|---|
[1, 3] * 2 |
Repeats the list, [1,3,1,3] |
2 * [1, 3] to scale |
[1, 2] + [3, 4] |
Concatenates | ([1,2] as Vec) + [3,4] to add |
L - 2 |
Removes every element equal to 2 | L.removeAt(i) or a slice |
L.collate(2, 1) |
Leaves a trailing partial group | L.collate(2, 1, false) |
[a, b].transpose() with different sizes |
Truncates to the shortest, silently | Check the sizes first |
L.sort(), L.unique() |
Modify L |
L.sort(false), L.unique(false) |
L.first() on an empty list |
Throws | if (L), or L.take(1) |
g.collect { } on a group |
Returns a plain list | as MathObjectGroup to rebuild |
collectEntries { [it: v] } |
Key is the literal string "it" |
collectEntries { [(it): v] } |
it / (n - 1) with ints |
Integer division | it / (n - 1d) |
See also
- A Groovy survival kit, the user facing introduction, with the full list of operator shortcuts.
- DSL cheatsheet, for the parameters of every DSL block.
ListExtensions.groovyinjmathanim-core, where the list extensions (as Vec,as MathObjectGroup, scaling, cartesian product, themap/filter/reducealiases) are defined.