gum-jsx 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +2 -3
  2. package/docs/code/Arc.jsx +2 -2
  3. package/docs/code/Ellipse.jsx +2 -2
  4. package/docs/code/Images.jsx +1 -1
  5. package/docs/code/Line.jsx +1 -1
  6. package/docs/code/Math.jsx +12 -10
  7. package/docs/code/Network.jsx +1 -1
  8. package/docs/code/Points.jsx +4 -1
  9. package/docs/code/Shape.jsx +1 -1
  10. package/docs/code/SymFill.jsx +1 -1
  11. package/docs/code/SymShape.jsx +3 -4
  12. package/docs/code/Tables.jsx +3 -2
  13. package/docs/text/Box.md +1 -1
  14. package/docs/text/Element.md +11 -14
  15. package/docs/text/Gum.md +5 -5
  16. package/docs/text/Math.md +2 -1
  17. package/docs/text/SymFill.md +2 -1
  18. package/docs/text/SymLine.md +2 -1
  19. package/docs/text/SymPoints.md +2 -1
  20. package/docs/text/SymShape.md +2 -1
  21. package/docs/text/SymSpline.md +1 -0
  22. package/gala/code/atomic_orbitals.jsx +1 -2
  23. package/gala/code/complex_plot.jsx +1 -1
  24. package/gala/code/pendulum_physics.jsx +3 -3
  25. package/gala/code/plot_manual.jsx +2 -2
  26. package/gala/code/polygon_slide.jsx +3 -4
  27. package/gala/code/spline_star.jsx +2 -2
  28. package/gala/code/unit_distance.jsx +96 -0
  29. package/gala/text/unit_distance.md +7 -0
  30. package/package.json +1 -1
  31. package/scripts/gum.ts +14 -11
  32. package/scripts/test.ts +0 -69
  33. package/skills/gum-jsx/SKILL.md +12 -15
  34. package/skills/gum-jsx/references/gala/atomic_orbitals.md +1 -2
  35. package/skills/gum-jsx/references/gala/complex_plot.md +1 -1
  36. package/skills/gum-jsx/references/gala/pendulum_physics.md +3 -3
  37. package/skills/gum-jsx/references/gala/plot_manual.md +2 -2
  38. package/skills/gum-jsx/references/gala/polygon_slide.md +3 -4
  39. package/skills/gum-jsx/references/gala/spline_star.md +2 -2
  40. package/skills/gum-jsx/references/geometry.md +6 -6
  41. package/skills/gum-jsx/references/layout.md +5 -2
  42. package/skills/gum-jsx/references/networks.md +1 -1
  43. package/skills/gum-jsx/references/symbolic.md +12 -8
  44. package/skills/gum-jsx/references/utilities.md +18 -14
  45. package/src/elems/core.ts +33 -13
  46. package/src/elems/geometry.ts +37 -14
  47. package/src/elems/layout.ts +5 -7
  48. package/src/elems/math.ts +9 -0
  49. package/src/elems/network.ts +4 -3
  50. package/src/elems/plot.ts +19 -35
  51. package/src/elems/symbolic.ts +30 -24
  52. package/src/eval.ts +13 -3
  53. package/src/gum.ts +9 -9
  54. package/src/lib/const.ts +2 -1
  55. package/src/lib/interp.ts +10 -4
  56. package/src/lib/parse.ts +8 -2
  57. package/src/lib/types.ts +5 -4
  58. package/src/lib/utils.ts +105 -25
  59. package/src/render.ts +7 -27
  60. package/src/types/katex.d.ts +8 -0
package/scripts/test.ts CHANGED
@@ -13,51 +13,6 @@ function loadFile(path: string, encoding: string = 'utf8') {
13
13
  : readFileSync(file, encoding as BufferEncoding)
14
14
  }
15
15
 
16
- function matchNumbers(text: string, pattern: RegExp, message: string): number[] {
17
- const match = text.match(pattern)
18
- if (match == null) throw new Error(message)
19
- return match.slice(1).map(Number)
20
- }
21
-
22
- function rotatePoint([ x, y ]: number[], angle: number, [ cx, cy ]: number[]): number[] {
23
- const theta = angle * Math.PI / 180
24
- const [ dx, dy ] = [ x - cx, y - cy ]
25
- const [ COS, SIN ] = [ Math.cos(theta), Math.sin(theta) ]
26
- return [ cx + dx * COS - dy * SIN, cy + dx * SIN + dy * COS ]
27
- }
28
-
29
- function midpoint(p0: number[], p1: number[]): number[] {
30
- return [ (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2 ]
31
- }
32
-
33
- function dot(p0: number[], p1: number[]): number {
34
- return p0[0] * p1[0] + p0[1] * p1[1]
35
- }
36
-
37
- function assertGraphArrowHeadForward(svg: string) {
38
- const line = matchNumbers(svg, /<line x1="([-\d.]+)" y1="([-\d.]+)" x2="([-\d.]+)" y2="([-\d.]+)"/, 'Missing arrow shaft')
39
- const path = matchNumbers(svg, /<path d="M ([-\d.]+),([-\d.]+) L ([-\d.]+),([-\d.]+) M [-\d.]+,[-\d.]+ L ([-\d.]+),([-\d.]+)"/, 'Missing arrow head')
40
- const rotate = matchNumbers(svg, /transform="rotate\(([-\d.]+), ([-\d.]+), ([-\d.]+)\)"/, 'Missing arrow head transform')
41
-
42
- const [ x1, y1, x2, y2 ] = line
43
- const [ tipx, tipy, base0x, base0y, base1x, base1y ] = path
44
- const [ angle, cx, cy ] = rotate
45
- const center = [ cx, cy ]
46
- const tip = rotatePoint([ tipx, tipy ], angle, center)
47
- const base = midpoint(
48
- rotatePoint([ base0x, base0y ], angle, center),
49
- rotatePoint([ base1x, base1y ], angle, center),
50
- )
51
- const shaft = [ x2 - x1, y2 - y1 ]
52
- const head = [ tip[0] - base[0], tip[1] - base[1] ]
53
- if (dot(shaft, head) <= 0) throw new Error('Arrow head points opposite the shaft')
54
- }
55
-
56
- function assertRoundedLineCornerCircular(svg: string) {
57
- const [ rx, ry ] = matchNumbers(svg, /<path d="[^"]* A ([-\d.]+),([-\d.]+) /, 'Missing rounded corner arc')
58
- if (Math.abs(rx - ry) > 1e-6) throw new Error(`RoundedLine corner arc is skewed: ${rx},${ry}`)
59
- }
60
-
61
16
  const dirs = ['docs/code', 'gala/code']
62
17
  let passed = 0
63
18
  let failed = 0
@@ -80,30 +35,6 @@ for (const dir of dirs) {
80
35
  }
81
36
  }
82
37
 
83
- try {
84
- const code = '<Frame aspect={1.2}><Graph coord={[0, 0, 1, 1]}><Arrow points={[[0.25, 0.25], [0.75, 0.75]]} /></Graph></Frame>'
85
- const elem = evaluateGum(code, { size: 500, theme: 'dark', loadFile })
86
- assertGraphArrowHeadForward(elem.svg())
87
- console.log('PASS regression Graph Arrow wide aspect')
88
- passed++
89
- } catch (e: any) {
90
- const { message = 'Unknown error' } = e
91
- console.error(`FAIL regression Graph Arrow wide aspect: ${message}`)
92
- failed++
93
- }
94
-
95
- try {
96
- const code = '<Frame aspect={4}><RoundedLine points={[[0.1,0.2],[0.5,0.2],[0.5,0.8]]} radius={0.1} /></Frame>'
97
- const elem = evaluateGum(code, { size: 500, theme: 'dark', loadFile })
98
- assertRoundedLineCornerCircular(elem.svg())
99
- console.log('PASS regression RoundedLine circular corners')
100
- passed++
101
- } catch (e: any) {
102
- const { message = 'Unknown error' } = e
103
- console.error(`FAIL regression RoundedLine circular corners: ${message}`)
104
- failed++
105
- }
106
-
107
38
  console.log()
108
39
  console.error(`${passed} passed`)
109
40
  console.error(`${failed} failed`)
@@ -160,30 +160,27 @@ Below is the full documentation for the core `gum.jsx` components: `Element`, `G
160
160
 
161
161
  ## Element
162
162
 
163
- The base class for all `gum.jsx` objects. You will usually not be working with this object directly unless you are implementing your own custom elements. An **Element** has a few methods that can be overriden, each of which takes a **Context** object as an argument. The vast majority of implementations will override only `props` and `inner` (for non-unary elements).
163
+ The base class for all Gum objects. You will usually not be working with this object directly unless you are implementing your own custom elements. However, many of the arguments are common to all elements and are documented here.
164
164
 
165
- The position and size of an element are specified in the internal coordinates (`coord`) of its parent, which defaults to the unit square. Rectangles are always specified in `[left, top, right, bottom]` format. You can also specify the placement by specifying `pos` and `size` or various combinations of `xsize`/`ysize`. When not specified, `rect` defaults to the unit square.
165
+ The position and size of an element are specified in the internal coordinates (`coord`) of its parent, which defaults to the unit square. Rectangles are always specified in `[left, top, right, bottom]` format. Where the element is placed is ultimately determined by its `rect`. However, it is more common to specify the `rect` using one of the convenience parameters below.
166
+
167
+ There are several convenience parameters that can be used to specify the `rect` in a more intuitive way. These include `size`/`xsize`/`ysize` for controlling the extent of the rectangle around `pos` and the analogous `rad`/`xrad`/`yrad` for radial specification. You can also specify the limits in either or both dimensions with `xrect`/`yrect`, where a single number is interpreted as a zero-length limit at that position.
168
+
169
+ The `expand` parameter can be used to control whether the element should be expanded to fully contain its `rect`. This is useful if you have an aspected element with a desired size in one dimension. A very common case is a **Text** element where you specify `pos` and `ysize` and leave the width to be determined by the aspect ratio.
166
170
 
167
171
  Parameters:
168
- - `tag` = `g` — the SVG tag associated with this element
169
- - `unary` = `false` — whether there is inner text for this element
170
172
  - `aspect` = `null` — the width to height ratio for this element
173
+ - `rect` — a fully specified rectangle to place the child in (takes precedence over other parameters)
171
174
  - `pos` — the desired position of the center of the child's rectangle
172
- - `size` ­— the desired size of the child's rectangle (can be single number or pair)
173
- - `xsize`/`ysize` ­— specify the size for a specific dimension (and expand the other)
174
- - `rect` — a fully specified rectangle to place the child in (this will override `pos`/`size`)
175
- - `aspect` — the aspect ratio of the child's rectangle
175
+ - `size`/`xsize`/`ysize` ­— the desired size of the child's rectangle (`size` can be single number or pair)
176
+ - `rad`/`xrad`/`yrad` the desired radius of the child's rectangle (`rad` can be single number or pair)
177
+ - `xrect`/`yrect` — the limits of the child's rectangle in the x and y dimensions (both can be single number or pair)
176
178
  - `expand` — when `true`, instead of embedding the child within `rect`, it will make the child just large enough to fully contain `rect`
177
179
  - `align` — how to align the child when it doesn't fit exactly within `rect`, options are `left`, `right`, `center`, or a fractional position (can set vertical and horizontal separately with a pair)
178
180
  - `rotate` — how much to rotate the child by (degrees counterclockwise)
179
181
  - `spin` — like rotate but will maintain the same size
180
182
  - `flex` ­— override to set `aspect = null`
181
- - `...` = `{}` — additional attributes that are included in `props`
182
-
183
- Methods:
184
- - `props(ctx)` — returns a dictionary of attributes for the SVG element. The default implementation returns the non-null `attr` passed to the constructor
185
- - `inner(ctx)` — returns the inner text of the SVG element (for non-unary). Defaults to returing empty string
186
- - `svg(ctx)` — returns the rendered SVG of the element as a `String`. Default implementation constructs SVG from `tag`, `unary`, `props`, and `inner`
183
+ - `...` = `{}` — additional attributes are applied directly to the resulting SVG
187
184
 
188
185
  **Example**
189
186
 
@@ -234,7 +231,7 @@ Generated code:
234
231
 
235
232
  This is a simple container class allowing you to add padding, margins, and a border to a single **Element**. It's pretty versatile and is often used to set up the outermost positioning of a figure. Mirroring the standard CSS definitions, padding is space inside the border and margin is space outside the border. This has no border by default, but there is a specialized subclass of this called **Frame** that defaults to `border = 1`.
236
233
 
237
- **Box** can be pretty handly in various situations. It is differentiated from **Group** in that it will adopt the `aspect` of the child element. This is useful if you want to do something like shift an element up or down by a certain amount while maintaining its aspect ratio. Simply wrap it in a **Box** and set child's `pos` to the desired offset.
234
+ **Box** can be pretty handly in various situations. It is differentiated from **Group** in that it will adopt the `aspect` of the first child element. This is useful if you want to do something like shift an element up or down by a certain amount while maintaining its aspect ratio. Simply wrap it in a **Box** and set child's `pos` to the desired offset.
238
235
 
239
236
  There are multiple ways to specify padding and margins. If given as a scalar, it is constant across all sides. If two values are given, they correspond to the horizontal and vertical sides. If four values are given, they correspond to `[left, top, right, bottom]`.
240
237
 
@@ -38,8 +38,7 @@ const dz2_r = (t) => 0.28 * abs(3 * cos(t) ** 2 - 1)
38
38
  // plot radial orbital lobes via SymSpline
39
39
  const OrbitalLobe = ({ rfn, t0, t1, sign }) =>
40
40
  <SymSpline N={50} tlim={[t0, t1]}
41
- fx={t => rfn(t) * cos(t)}
42
- fy={t => rfn(t) * sin(t)}
41
+ f={t => polar(t, rfn(t))}
43
42
  stroke={sign ? pos_col : neg_col}
44
43
  fill={sign ? pos_fill : neg_fill}
45
44
  stroke-width={2} opacity={0.6}
@@ -14,7 +14,7 @@ const Curve = ({ fx, stroke }) => <SymLine fx={fx} ylim={ylim} stroke={stroke} s
14
14
  const xlabel = <Latex>x = a + bi</Latex>
15
15
  const ylabel = <Latex>c</Latex>
16
16
  return <Plot aspect={2} margin={0.3} xlim={xlim} ylabel={ylabel} xlabel={xlabel} ylabel-offset={0.075}>
17
- <Mesh2D xlocs={41} ylocs={21} xlim={xlim} ylim={ylim} opacity={0.3} />
17
+ <Mesh2D xlocs={41} ylocs={21} xlim={xlim} ylim={ylim} opacity={0.1} />
18
18
  <HLine loc={0} lim={xlim} opacity={0.3} />
19
19
  <VLine loc={0} lim={ylim} opacity={0.3} />
20
20
  <Curve fx={y => -y + sqrt(maximum(0, y*y-1))} stroke={blue} />
@@ -29,9 +29,9 @@ const arcRad = 0.25
29
29
  // Computed positions
30
30
  const rodRot = 90 - rodAng
31
31
  const eqnY = pivotY + rodLen
32
- const [ bobX, bobY ] = polar(rodRot, rodLen, [pivotX, pivotY])
33
- const [ midX, midY ] = polar(rodRot, 0.50 * rodLen, [pivotX, pivotY])
34
- const [ tenX, tenY ] = polar(rodRot, 0.75 * rodLen, [pivotX, pivotY])
32
+ const [ bobX, bobY ] = polard(rodRot, rodLen, [pivotX, pivotY])
33
+ const [ midX, midY ] = polard(rodRot, 0.50 * rodLen, [pivotX, pivotY])
34
+ const [ tenX, tenY ] = polard(rodRot, 0.75 * rodLen, [pivotX, pivotY])
35
35
 
36
36
  return <Box margin={0.06}>
37
37
  <VStack spacing={0.05}>
@@ -11,8 +11,8 @@ const aspect = 2
11
11
  const ratio = pi / aspect
12
12
  return <Box margin={0.3}>
13
13
  <Group coord={[0, 1, 2*pi, -1]} aspect={aspect}>
14
- <HMesh locs={5} lim={[0, 2*pi]} opacity={0.3} />
15
- <VMesh locs={5} lim={[-1, 1]} opacity={0.3} />
14
+ <HMesh locs={5} opacity={0.15} />
15
+ <VMesh locs={5} opacity={0.15} />
16
16
  <HAxis ticks={5} lim={[0, 2*pi]} pos={[pi, -1]} size={[2*pi, 0.08]} />
17
17
  <VAxis ticks={5} lim={[-1, 1]} pos={[0, 0]} size={[0.08*ratio, 2]} />
18
18
  <SymLine fy={sin} xlim={[0, 2*pi]} />
@@ -16,11 +16,10 @@ const shapes = [
16
16
  ]
17
17
 
18
18
  const RegularPolygon = ({ n, ...args }) =>
19
- <SymShape {...args}
20
- aspect spin={90*(n-2)/n}
19
+ <SymShape {...args} aspect
21
20
  xlim={[-1, 1]} ylim={[-1, 1]}
22
- tvals={linspace(0, 2*pi, n+1)}
23
- fx={cos} fy={sin}
21
+ tvals={linspace(0, 2*pi, n, false)}
22
+ f={t => polar(t+pi/2*(n-2)/n)}
24
23
  />
25
24
 
26
25
  return <Slide title="Simple Regular Polygons" wrap={25}>
@@ -19,8 +19,8 @@ const theta0 = linspace(0, 2 * pi, n, false).map(t => t - pi / 2)
19
19
  const theta1 = theta0.map(t => t + pi / n)
20
20
 
21
21
  // get inner/outer point positions
22
- const points0 = theta0.map(t => polar(t * r2d))
23
- const points1 = theta1.map(t => polar(t * r2d, R))
22
+ const points0 = theta0.map(t => polar(t))
23
+ const points1 = theta1.map(t => polar(t, R))
24
24
  const points = zip(points0, points1).flat()
25
25
 
26
26
  // return full spline
@@ -33,8 +33,8 @@ Prompt: two ellipses, one wider and one taller
33
33
  Generated code:
34
34
  ```jsx
35
35
  <Group>
36
- <Ellipse pos={[0.3, 0.2]} size={[0.4, 0.2]} />
37
- <Ellipse pos={[0.6, 0.6]} size={[0.4, 0.5]} />
36
+ <Ellipse pos={[0.3, 0.2]} rad={[0.2, 0.1]} />
37
+ <Ellipse pos={[0.6, 0.6]} rad={[0.2, 0.25]} />
38
38
  </Group>
39
39
  ```
40
40
 
@@ -57,8 +57,8 @@ Prompt: elliptical and circular arcs using start and end angles
57
57
  Generated code:
58
58
  ```jsx
59
59
  <Group>
60
- <Arc pos={[0.3, 0.5]} size={[0.4, 0.3]} start={-45} end={210} stroke={blue} stroke-width={2} />
61
- <Arc pos={[0.7, 0.5]} size={0.3} start={90} end={-150} stroke={red} stroke-width={2} />
60
+ <Arc pos={[0.3, 0.5]} rad={[0.2, 0.15]} start={-45} end={210} stroke={blue} stroke-width={2} />
61
+ <Arc pos={[0.7, 0.5]} rad={0.15} start={90} end={-150} stroke={red} stroke-width={2} />
62
62
  </Group>
63
63
  ```
64
64
 
@@ -81,7 +81,7 @@ Prompt: draw a piecewise line spiraling outwards (with dots at vertices)
81
81
 
82
82
  Generated code:
83
83
  ```jsx
84
- const spiral = linspace(0, 5, 25).map(t => polar(360 * t, t/5))
84
+ const spiral = linspace(0, 5, 25).map(t => polar(2*pi * t, t/5))
85
85
  return <Box margin>
86
86
  <Graph coord={[-1, -1, 1, 1]}>
87
87
  <Line points={spiral} />
@@ -107,7 +107,7 @@ Prompt: draw a stop sign
107
107
 
108
108
  Generated code:
109
109
  ```jsx
110
- const hexagon = linspace(0, 360, 8, false).map(t => polar(t))
110
+ const hexagon = linspace(0, 2*pi, 8, false).map(t => polar(t))
111
111
  return <Box fill="#bbb" rounded padding margin>
112
112
  <Graph xlim={[-1, 1]} ylim={[-1, 1]} aspect={1}>
113
113
  <Shape points={hexagon} fill="#CC0202" stroke={white} stroke_width={20} spin={180/8} />
@@ -6,7 +6,7 @@
6
6
 
7
7
  This is a simple container class allowing you to add padding, margins, and a border to a single **Element**. It's pretty versatile and is often used to set up the outermost positioning of a figure. Mirroring the standard CSS definitions, padding is space inside the border and margin is space outside the border. This has no border by default, but there is a specialized subclass of this called **Frame** that defaults to `border = 1`.
8
8
 
9
- **Box** can be pretty handly in various situations. It is differentiated from **Group** in that it will adopt the `aspect` of the child element. This is useful if you want to do something like shift an element up or down by a certain amount while maintaining its aspect ratio. Simply wrap it in a **Box** and set child's `pos` to the desired offset.
9
+ **Box** can be pretty handly in various situations. It is differentiated from **Group** in that it will adopt the `aspect` of the first child element. This is useful if you want to do something like shift an element up or down by a certain amount while maintaining its aspect ratio. Simply wrap it in a **Box** and set child's `pos` to the desired offset.
10
10
 
11
11
  There are multiple ways to specify padding and margins. If given as a scalar, it is constant across all sides. If two values are given, they correspond to the horizontal and vertical sides. If four values are given, they correspond to `[left, top, right, bottom]`.
12
12
 
@@ -124,7 +124,10 @@ Prompt: A plot of three different increasing curves of varying steepness and mul
124
124
 
125
125
  Generated code:
126
126
  ```jsx
127
- <Plot xlim={[-1, 1]} ylim={[-1, 1]} grid margin={0.3} aspect xlabel="time (seconds)" ylabel="space (meters)" title="Spacetime Vibes">
127
+ <Plot coord={[-1, -1, 1, 1]} margin={0.2} grid
128
+ xlabel="time (seconds)" ylabel="space (meters)"
129
+ title="Spacetime Vibes"
130
+ >
128
131
  <Points point-size={0.04} points={[
129
132
  [0, 0.5], [0.5, 0], [-0.5, 0], [0, -0.5]
130
133
  ]} />
@@ -103,6 +103,6 @@ Generated code:
103
103
  <Node id="test" pos={[0.75, 0.25]} wrap={6}>This is a test of wrapping capabilities</Node>
104
104
  <Node id="ball" pos={[0.75, 0.75]}><Ellipse aspect={1.5} fill={blue}/></Node>
105
105
  <Edge start="hello" end="test" />
106
- <Edge start="hello" end="ball" start-side="s" curve={3} />
106
+ <Edge start="hello" end="ball" start-side="s" />
107
107
  </Network>
108
108
  ```
@@ -4,11 +4,12 @@
4
4
 
5
5
  *Inherits*: **Group** > **Element**
6
6
 
7
- Flexible interface to generate sets of points symbolically or in combination with fixed inputs. The most common usage is to specify the range for x-values with `xlim` and a function to plot with `fy`. But you can specify the transpose with `ylim`/`fx`, or do a fully parametric path using `tlim`/`fx`/`fy`.
7
+ Flexible interface to generate sets of points symbolically or in combination with fixed inputs. The most common usage is to specify the range for x-values with `xlim` and a function to plot with `fy`. But you can specify the transpose with `ylim`/`fx`, or do a fully parametric path using `tlim` with either `f` or `fx`/`fy`.
8
8
 
9
9
  You can also specify point size functionally with `point-size` and the shape with `point-shape`. Both of these functions take `(x, y, t, i)` values as inputs and return the desired value for each point. As with other groups, the **SymPoints** element itself can still be laid out with the normal `size`/`xsize`/`ysize` element parameters.
10
10
 
11
11
  Parameters:
12
+ - `f` — a function mapping t-values to `[x, y]` points
12
13
  - `fx`/`fy` — a function mapping from x-values, y-values, or t-values
13
14
  - `point-size` = `0.05` — a size or a function mapping from `(x, y, t, i)` values to a size
14
15
  - `point-shape` = `Dot` — a shape or function mapping from `(x, y, t, i)` values to a shape
@@ -35,9 +36,10 @@ return <Plot xlim={[0, 2*pi]} ylim={[-1.5, 1.5]} grid fill={lightgray} margin={[
35
36
 
36
37
  Flexible interface to generate two-dimensional paths symbolically or in combination with fixed inputs. There are variety of acceptable input combinations, but the most common usage is to specify the range to use for x-values with `xlim` and a function to plot with `fy`. To plot a polygon instead of a line, use **SymShape**.
37
38
 
38
- Alternatively, you can specify the transpose with `ylim`/`fx`, or even do a fully parametric path using `tlim`/`fx`/`fy`. In any of these cases, one can either specify limits with `xlim`/`ylim`/`tlim` or specific values with `xvals`/`yvals`/`tvals`.
39
+ Alternatively, you can specify the transpose with `ylim`/`fx`, or do a fully parametric path using `tlim` with either `f` or `fx`/`fy`. In any of these cases, one can either specify limits with `xlim`/`ylim`/`tlim` or specific values with `xvals`/`yvals`/`tvals`.
39
40
 
40
41
  Parameters:
42
+ - `f` — a function mapping t-values to `[x, y]` points
41
43
  - `fx`/`fy` — a function mapping from x-values, y-values, or t-values
42
44
  - `xlim`/`ylim`/`tlim` — a pair of numbers specifying variable limits
43
45
  - `xvals`/`yvals`/`tvals` — a list of x-values, y-values, or t-values to use
@@ -59,9 +61,10 @@ Generated code:
59
61
 
60
62
  *Inherits*: **Shape** > **Pointstring** > **Element**
61
63
 
62
- Flexible interface to generate shapes symbolically or in combination with fixed inputs. Operates similarly to **Shape**, but generates a shape from the points generated by `fx`/`fy`.
64
+ Flexible interface to generate shapes symbolically or in combination with fixed inputs. Operates similarly to **Shape**, but generates a shape from points generated by `f` or `fx`/`fy`.
63
65
 
64
66
  Parameters:
67
+ - `f` — a function mapping t-values to `[x, y]` points
65
68
  - `fx`/`fy` — a function mapping from x-values, y-values, or t-values
66
69
  - `xlim`/`ylim`/`tlim` — a pair of numbers specifying variable limits
67
70
  - `xvals`/`yvals`/`tvals` — a list of x-values, y-values, or t-values to use
@@ -73,12 +76,11 @@ Prompt: Draw a rounded star shape with a blue fill. Wrap it in a rounded frame.
73
76
 
74
77
  Generated code:
75
78
  ```jsx
76
- const rad = t => 1 - 0.3 * cos(2.5 * t)**2
79
+ const rad = t => 1 - 0.2 * cos(5 * (t - pi/2))
77
80
  return <Frame rounded padding margin>
78
81
  <SymShape aspect fill={blue}
79
82
  tlim={[0, 2*pi]} N={200}
80
- fx={t => rad(t) * sin(t)}
81
- fy={t => rad(t) * cos(t)}
83
+ f={t => polar(t, rad(t))}
82
84
  />
83
85
  </Frame>
84
86
  ```
@@ -91,6 +93,7 @@ Flexible interface to generate smooth two-dimensional spline curves symbolically
91
93
 
92
94
 
93
95
  Parameters:
96
+ - `f` — a function mapping t-values to `[x, y]` points
94
97
  - `fx`/`fy` — a function mapping from x-values, y-values, or t-values
95
98
  - `xlim`/`ylim`/`tlim` — a pair of numbers specifying variable limits
96
99
  - `xvals`/`yvals`/`tvals` — a list of x-values, y-values, or t-values to use
@@ -118,9 +121,10 @@ return <Plot xlim={[0, 2*pi]} ylim={[-1, 1]} grid margin={0.15} aspect={phi}>
118
121
 
119
122
  *Inherits*: **Fill** > **Shape** > **Pointstring** > **Element**
120
123
 
121
- Flexible interface to generate filled in paths symbolically or in combination with fixed inputs. This generates a polygon by running through the points generated by `fx1`/`fy1` and then backwards through the points generated by `fx2`/`fy2`. To generate a simple filled curve, pass your function to `fy1` and let `fy2` be `0`.
124
+ Flexible interface to generate filled in paths symbolically or in combination with fixed inputs. This generates a polygon by running through the points generated by `f1` or `fx1`/`fy1` and then backwards through the points generated by `f2` or `fx2`/`fy2`. To generate a simple filled curve, pass your function to `fy1` and let `fy2` be `0`.
122
125
 
123
126
  Parameters:
127
+ - `f1`/`f2` — functions mapping t-values to `[x, y]` points for either bound
124
128
  - `fx1`/`fy1` — a function generating one of the bounds for the fill (or a constant)
125
129
  - `fx2`/`fy2` — a function generating the other bound for the fill (or a constant)
126
130
  - `xlim`/`ylim`/`tlim` — a pair of numbers specifying variable limits
@@ -135,7 +139,7 @@ Generated code:
135
139
  ```jsx
136
140
  const decay = x => exp(-0.1*x) * sin(x)
137
141
  return <Graph xlim={[0, 6*pi]} ylim={[-1, 1]} aspect={phi}>
138
- <SymFill fy1={decay} fy2={0} fill={blue} fill-opacity={0.5} N={250} />
142
+ <SymFill f1={t => [t, decay(t)]} f2={t => [t, 0]} tlim={[0, 6*pi]} fill={blue} fill-opacity={0.5} N={250} />
139
143
  <SymLine fy={decay} N={250} />
140
144
  </Graph>
141
145
  ```
@@ -29,25 +29,28 @@ Here we collect a variety of global mathematical functions and constants. You ca
29
29
  - `round(x)` — the rounding function
30
30
  - `clamp(x, lim=[0, 1])` — clamp `x` to the range `lim`
31
31
  - `rescale(x, lim=[0, 1])` — linearly rescale `x` to the range `lim`
32
- - `polar(angle, radius=1, center=[0, 0])` — convert polar coordinates (`angle` in degrees, `radius` scalar or size vector) to a 2D point around `center`
32
+ - `polar(theta, radius=1, center=[0, 0])` — convert polar coordinates (`theta` in radians, `radius` scalar or size vector) to a 2D point around `center`
33
+ - `polard(angle, radius=1, center=[0, 0])` — same as `polar` but takes `angle` in degrees
33
34
 
34
35
  Angles use gum's usual screen-space convention: `0` points right and `90` points down.
35
36
 
36
37
  **Example**
37
38
 
38
- Prompt: embed a five point star in a circle. place red dots at its vertices.
39
+ Prompt: draw a spirograph in a box for a set of example parameters
39
40
 
40
41
  Generated code:
41
42
  ```jsx
42
- const verts = linspace(0, 360, 10, false).map((t, i) => {
43
- const radius = i % 2 == 0 ? 1 : 0.5
44
- return polar(90 + t, radius)
45
- })
46
- return <Graph aspect coord={[-1, -1, 1, 1]}>
47
- <Circle pos={[0, 0]} size={2} stroke={darkgray} />
48
- <Shape points={verts} stroke={blue} stroke-width={2} />
49
- {verts.map(pos => <Dot pos={pos} size={0.05} fill={red} />)}
50
- </Graph>
43
+ const [R, r, d, k] = [10, 7, 4, 7]
44
+ const fx = t => (R - r) * cos(t) + d * cos(((R - r) / r) * t)
45
+ const fy = t => (R - r) * sin(t) - d * sin(((R - r) / r) * t)
46
+ return <TitleFrame title="Spirograph" padding={0.2} margin rounded>
47
+ <Graph aspect coord={[-R, -R, R, R]}>
48
+ <Circle pos={[0, 0]} rad={R} stroke={darkgray} stroke-dasharray={10} />
49
+ <Circle pos={[0, 0]} rad={R - r} stroke-dasharray={5} />
50
+ <SymSpline fx={fx} fy={fy} tlim={[0, 2*pi*k]} stroke={blue} stroke-width={2} />
51
+ </Graph>
52
+ <Span pos={[0.5, 1.1]} ysize={0.05} font-family={mono}>R = 10 | r = 7 | d = 4</Span>
53
+ </TitleFrame>
51
54
  ```
52
55
 
53
56
  ## Arrays
@@ -146,9 +149,10 @@ Prompt: load "data.csv" and plot each row as a blue dot
146
149
 
147
150
  Generated code:
148
151
  ```jsx
149
- return <Graph xlim={[0, 10]} ylim={[0, 10]}>
152
+ return <Graph aspect coord={[0, 0, 10, 10]}>
153
+ <Mesh2D xlocs={10} ylocs={10} opacity={0.25} />
150
154
  {loadTable('data.csv').map(({ x, y }) =>
151
- <Dot pos={[x, y]} rad={0.1} fill={blue} />
155
+ <Dot pos={[x, y]} size={0.5} fill={blue} />
152
156
  )}
153
157
  </Graph>
154
158
  ```
@@ -171,7 +175,7 @@ Generated code:
171
175
  ```jsx
172
176
  <Box rounded clip>
173
177
  <Group aspect={2}>
174
- <LoadImage id="image.png" xsize={1} />
178
+ <LoadImage id="image.png" xrect={[0, 1]} />
175
179
  </Group>
176
180
  </Box>
177
181
  ```
package/src/elems/core.ts CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { THEME } from '../lib/theme'
4
4
  import { DEFAULTS as D, svgns, sans, light, blue, red, d2r } from '../lib/const'
5
- import { is_scalar, abs, cos, sin, tan, cot, mul2, div2, filter_object, expand_rect, rect_box, cbox_rect, rect_cbox, merge_points, merge_rects, ensure_vector, broadcast_point, rounder, heavisign, abs_min, abs_max, rect_radial, rotate_aspect, remap_rect, rescaler, resizer, rect_size, vector_angle, polar, upright_rect } from '../lib/utils'
5
+ import { is_scalar, abs, cos, sin, tan, cot, mul2, div2, filter_object, expand_rect, rect_box, cbox_rect, rect_cbox, merge_points, merge_rects, join_limits, ensure_pair, rounder, heavisign, abs_min, abs_max, rect_radial, rotate_aspect, remap_rect, rescaler, resizer, rect_size, vector_angle, polard, upright_rect } from '../lib/utils'
6
6
  import { random } from '../lib/rng'
7
7
 
8
- import type { Point, Rect, Size, AlignValue, Align, Side, Attrs, MNumber, MPoint, Spec } from '../lib/types'
8
+ import type { Point, Rect, Size, AlignValue, Align, Side, Attrs, MNumber, MPoint, Spec, Limit } from '../lib/types'
9
9
 
10
10
  //
11
11
  // rect embedding
@@ -118,7 +118,7 @@ function adjust_rotate(rotate: number, prect: Rect, coord: Rect): number {
118
118
  const csize = rect_size(coord)
119
119
  const psize = rect_size(prect)
120
120
  const proj = div2(psize, csize)
121
- const vec = polar(rotate, proj)
121
+ const vec = polard(rotate, proj)
122
122
  return vector_angle(vec)
123
123
  }
124
124
 
@@ -217,7 +217,7 @@ class Context {
217
217
  const transform = rotate1 ? rotate_repr(rotate1, [ x0, y0 ], this.prec) : undefined
218
218
 
219
219
  // broadcast align into [ halign, valign ] components
220
- const [ hafrac, vafrac ] = ensure_vector(align, 2).map(align_frac)
220
+ const [ hafrac, vafrac ] = ensure_pair(align).map(align_frac)
221
221
  const [ x, y ] = [
222
222
  x0 + (0.5 - hafrac) * (w - w0),
223
223
  y0 + (0.5 - vafrac) * (h - h0),
@@ -259,7 +259,7 @@ function props_repr(d: Attrs, prec: number): string {
259
259
 
260
260
  // reserved keys
261
261
  const SPEC_KEYS = [ 'rect', 'coord', 'aspect', 'aspect0', 'expand', 'align', 'upright', 'offset', 'rotate', 'rotate_adjust', 'rotate_invar' ]
262
- const HELP_KEYS = [ 'pos', 'size', 'xsize', 'ysize', 'flex', 'spin', 'orient' ]
262
+ const HELP_KEYS = [ 'pos', 'size', 'xsize', 'ysize', 'rad', 'xrad', 'yrad', 'xrect', 'yrect', 'flex', 'spin', 'orient' ]
263
263
  const EXTR_KEYS = [ 'stack_size' ]
264
264
  const RESERVED_KEYS = [ ...SPEC_KEYS, ...HELP_KEYS, ...EXTR_KEYS ]
265
265
 
@@ -300,6 +300,11 @@ interface ElementArgs extends SpecArgs {
300
300
  size?: number | Size
301
301
  xsize?: number
302
302
  ysize?: number
303
+ rad?: number | Size
304
+ xrad?: number
305
+ yrad?: number
306
+ xrect?: number | Limit
307
+ yrect?: number | Limit
303
308
  flex?: boolean
304
309
  spin?: number
305
310
  orient?: number
@@ -316,7 +321,7 @@ class Element {
316
321
  attr: Attrs
317
322
 
318
323
  constructor(args: ElementArgs = {}) {
319
- const { tag, unary, children, pos, size, xsize, ysize, flex, spin, orient, ...attr0 } = args
324
+ const { tag, unary, children, pos, size: size0, xsize: xsize0, ysize: ysize0, rad, xrad, yrad, xrect, yrect, flex, spin, orient, ...attr0 } = args
320
325
  const [ spec, attr ] = spec_split(attr0, false)
321
326
  this.args = args
322
327
 
@@ -332,12 +337,27 @@ class Element {
332
337
  this.spec = spec
333
338
  this.attr = attr
334
339
 
335
- // handle pos/size conveniences
336
- if (pos != null || size != null || xsize != null || ysize != null) {
337
- const has_xy = xsize != null || ysize != null
338
- const size1 = has_xy ? [ xsize ?? 0, ysize ?? 0 ] as Point : undefined
339
- this.spec.rect ??= cbox_rect([ ...(pos ?? D.pos), ...(broadcast_point(size ?? size1 ?? D.size)) ])
340
- if (has_xy) this.spec.expand = true
340
+ // handle pos/rad/xrad/yrad conveniences
341
+ const [ x, y ] = pos ?? D.pos
342
+ const size = rad != null ? mul2(rad, 2) : size0
343
+ const xsize = xrad != null ? 2 * xrad : xsize0
344
+ const ysize = yrad != null ? 2 * yrad : ysize0
345
+
346
+ // handle rect conveniences
347
+ if (this.spec.rect != null) {
348
+ // already have a rect
349
+ } else if (size != null) {
350
+ const [ w, h ] = ensure_pair(size)
351
+ this.spec.rect = cbox_rect([ x, y, w, h ])
352
+ } else if (xrect != null || yrect != null) {
353
+ const xrect1 = ensure_pair(xrect ?? x)
354
+ const yrect1 = ensure_pair(yrect ?? y)
355
+ this.spec.rect = join_limits({ h: xrect1, v: yrect1 })
356
+ if (xrect == null || yrect == null) this.spec.expand = true
357
+ } else if (xsize != null || ysize != null) {
358
+ const [ w, h ] = ensure_pair([ xsize ?? 0, ysize ?? 0 ])
359
+ this.spec.rect = cbox_rect([ x, y, w, h ])
360
+ if (xsize == null || ysize == null) this.spec.expand = true
341
361
  }
342
362
 
343
363
  // various convenience conversions
@@ -642,7 +662,7 @@ class Svg extends Group {
642
662
  constructor(args: SvgArgs = {}) {
643
663
  const { children: children0, size : size0 = D.svg_size, padding = 1, bare = false, dims = true, filters, aspect: aspect0 = 'auto', view: view0, style, xmlns = svgns, font_family = sans, font_weight = light, prec = D.prec, ...attr } = THEME(args, 'Svg')
644
664
  const children = ensure_children(children0)
645
- const size_base = broadcast_point(size0)
665
+ const size_base = ensure_pair(size0)
646
666
 
647
667
  // precompute aspect info
648
668
  const aspect = aspect0 == 'auto' ? children_aspect(children) : aspect0
@@ -1,9 +1,9 @@
1
1
  // geometry elements
2
2
 
3
3
  import { THEME } from '../lib/theme'
4
- import { DEFAULTS as D, d2r, none, gray } from '../lib/const'
5
- import { is_boolean, is_scalar, is_array, ensure_vector, ensure_point, check_array, upright_limits, rounder, abs, rect_radial, make_mpoint, squeeze_mpoint, merge_points, broadcast_point, sub2m, add2, sub2, mul2, div2, norm, angle_direc, unit_direc, vector_angle, polar, prefix_split} from '../lib/utils'
6
- import { cubic_spline_data } from '../lib/interp'
4
+ import { DEFAULTS as D, none, gray } from '../lib/const'
5
+ import { is_boolean, is_scalar, is_array, ensure_vector, ensure_point, check_array, upright_limits, rounder, abs, rect_radial, make_mpoint, merge_points, ensure_pair, add2, sub2, mul2, div2, norm, angle_direc, unit_direc, vector_angle, polard, prefix_split} from '../lib/utils'
6
+ import { cubic_spline_data, cubic_spline_tangent } from '../lib/interp'
7
7
  import { Context, Element, Group, Rectangle } from './core'
8
8
 
9
9
  import type { Point, Rect, Limit, Grad, Attrs, MPoint, Orient, Rounded, Direc } from '../lib/types'
@@ -126,6 +126,31 @@ class CoordLine extends Line {
126
126
  }
127
127
  }
128
128
 
129
+ interface SegmentsArgs extends ElementArgs {
130
+ edges?: [Point, Point][]
131
+ }
132
+
133
+ class Segments extends Element {
134
+ edges: [Point, Point][]
135
+
136
+ constructor(args: SegmentsArgs = {}) {
137
+ const { edges = [], ...attr } = args
138
+ super({ tag: 'path', unary: true, ...attr })
139
+ this.args = args
140
+ this.edges = edges
141
+ }
142
+
143
+ props(ctx: Context): Attrs {
144
+ const attr = super.props(ctx)
145
+ const d = this.edges.map(([e0, e1]) => {
146
+ const [p0, p1] = ctx.mapPoint(e0)
147
+ const [q0, q1] = ctx.mapPoint(e1)
148
+ return `M ${rounder(p0, ctx.prec)},${rounder(p1, ctx.prec)} L ${rounder(q0, ctx.prec)},${rounder(q1, ctx.prec)}`
149
+ }).join(' ')
150
+ return { d, ...attr }
151
+ }
152
+ }
153
+
129
154
  //
130
155
  // shape classes
131
156
  //
@@ -186,12 +211,11 @@ interface RayArgs extends LineArgs {
186
211
  class Ray extends Line {
187
212
  constructor(args: RayArgs = {}) {
188
213
  const { angle = 0, loc = D.pos, size = 0.5, ...attr } = THEME(args, 'Ray')
189
- const theta = angle * d2r
190
214
  const [ x, y ] = loc
191
215
  const [ rx, ry ] = ensure_vector(size, 2)
192
216
  const points: Point[] = [
193
217
  [ x, y ],
194
- polar(theta, [ rx, ry ], [ x, y ])
218
+ polard(angle, [ rx, ry ], [ x, y ])
195
219
  ]
196
220
  super({ points, ...attr })
197
221
  this.args = args
@@ -553,9 +577,8 @@ class CubicSplineCmd extends Command {
553
577
 
554
578
  args(ctx: Context): string {
555
579
  // use dir if provided, otherwise use tan
556
- const dist = squeeze_mpoint(sub2m(this.pos2, this.pos1)).map(abs) as Point
557
- const tan1 = this.dir1 != null ? mul2(this.dir1, dist) : this.tan1
558
- const tan2 = this.dir2 != null ? mul2(this.dir2, dist) : this.tan2
580
+ const tan1 = cubic_spline_tangent(this.pos1, this.pos2, this.dir1, this.tan1)
581
+ const tan2 = cubic_spline_tangent(this.pos1, this.pos2, this.dir2, this.tan2)
559
582
  if (tan1 == null || tan2 == null) throw new Error('Spline tangent must be defined')
560
583
 
561
584
  // compute scaled tangents
@@ -624,7 +647,7 @@ function parse_rounded(rounded: Rounded): Point[] {
624
647
  const [ rx, ry ] = rounded
625
648
  rounded = [[rx, ry], [rx, ry], [rx, ry], [rx, ry]]
626
649
  }
627
- return rounded.map(broadcast_point)
650
+ return rounded.map(ensure_pair)
628
651
  }
629
652
 
630
653
  interface RoundedRectArgs extends ElementArgs {
@@ -717,9 +740,9 @@ class Arc extends Path {
717
740
  constructor(args: ArcArgs = {}) {
718
741
  const { start, end, upright = true, ...attr } = THEME(args, 'Arc')
719
742
  if (start == null || end == null) throw new Error('Must provide `start` and `end` angles')
720
- const [ theta0, theta1 ] = upright_limits([ start, end ])
721
- const large = (theta1 - theta0) > 180
722
- const [ point0, point1 ] = [ theta0, theta1 ].map(t => polar(t, 0.5, [0.5, 0.5]))
743
+ const [ angle0, angle1 ] = upright_limits([ start, end ])
744
+ const large = (angle1 - angle0) > 180
745
+ const [ point0, point1 ] = [ angle0, angle1 ].map(t => polard(t, 0.5, [0.5, 0.5]))
723
746
  const children = [ new MoveCmd(point0), new ArcCmd(point1, 0.5, true, large) ]
724
747
  super({ children, upright, ...attr })
725
748
  this.args = args
@@ -837,5 +860,5 @@ class Arrow extends Group {
837
860
  // exports
838
861
  //
839
862
 
840
- export { Line, UnitLine, VLine, HLine, CoordLine, Square, Ellipse, Arc, Circle, Dot, Ray, Pointstring, Shape, Triangle, Fill, VFill, HFill, Path, Command, MoveCmd, LineCmd, ArcCmd, CornerCmd, RoundedCornerCmd, CubicSplineCmd, Spline, RoundedRect, RoundedLine, ArrowHead, Arrow }
841
- export type { LineArgs, UnitLineArgs, CoordLineArgs, ArcArgs, DotArgs, RayArgs, SplineArgs, RoundedRectArgs, RoundedLineArgs, ArrowHeadArgs, ArrowArgs, CubicSplineCmdArgs, FillArgs }
863
+ export { Line, UnitLine, VLine, HLine, CoordLine, Segments, Square, Ellipse, Arc, Circle, Dot, Ray, Pointstring, Shape, Triangle, Fill, VFill, HFill, Path, Command, MoveCmd, LineCmd, ArcCmd, CornerCmd, RoundedCornerCmd, CubicSplineCmd, Spline, RoundedRect, RoundedLine, ArrowHead, Arrow }
864
+ export type { LineArgs, UnitLineArgs, CoordLineArgs, SegmentsArgs, ArcArgs, DotArgs, RayArgs, SplineArgs, RoundedRectArgs, RoundedLineArgs, ArrowHeadArgs, ArrowArgs, CubicSplineCmdArgs, FillArgs }