geomotif.core
¶
The engine: value types, sampling, spacing, transforms and the registry.
Everything here is motif-agnostic. Concrete geometry lives in
:mod:geomotif.motifs; this package is what makes writing one cheap.
Modules:
| Name | Description |
|---|---|
motif |
The motif contract: the one thing you implement to extend the library. |
range |
Concise min/max/step metadata for a motif parameter. |
registry |
Motif registration, lookup and introspection. |
sampling |
Arc-length measurement and resampling, generalized to any polyline. |
spacing |
Spacing curves that control the distribution of points along a path. |
style |
color and layers: how a design is drawn, rather than what it is. |
transform |
Affine transforms and the composite operators built on them. |
types |
The geometric value types every motif produces and every tool consumes. |
Classes:
| Name | Description |
|---|---|
Motif |
Convenience base: implement :meth: |
SupportsBuild |
Structural contract -- any object with |
Range |
A min/max/step bound for a parameter, as a field-metadata mapping. |
ArcTable |
Cumulative-length table over a polyline, and the inverse of it. |
CircularSpacing |
Circular (quarter-arc) easing -- abrupt at one end, flat at the other. |
CompositeSpacing |
Chain curves, feeding each one's output into the next. |
CubicSpacing |
Classic cubic easing ( |
ExponentialSpacing |
Exponential easing with adjustable |
LinearSpacing |
Equal spacing between every point (the default, a "0 curve"). |
PowerSpacing |
|
QuadraticSpacing |
Classic quadratic easing ( |
ReversedSpacing |
Mirror any curve: dense where the original was sparse, and vice versa. |
SineSpacing |
Sinusoidal easing -- a gentle, natural-feeling bias. |
SmoothstepSpacing |
Hermite smoothstep |
SpacingCurve |
Base class for point-spacing curves. |
TableSpacing |
A curve drawn by hand, from arbitrary |
Affine |
A 2D affine transform, composable with |
Bounds |
An axis-aligned rectangle enclosing some geometry. |
Design |
The universal result: zero or more strokes plus zero or more loose points. |
Path |
One continuous polyline. |
Functions:
| Name | Description |
|---|---|
densify |
Evaluate |
resample |
Return |
resample_path |
Return |
samples_for_turns |
Return a sensible densification count for a curve spanning |
coerce_spacing |
Normalize anything spacing-shaped into a :class: |
clip_to |
Trim |
fit_to |
Scale and center |
jitter |
Randomly displace every point, for controlled hand-drawn irregularity. |
layer |
Overlay designs into one. Equivalent to repeated |
mirror_axis |
Return |
offset_path |
Return a parallel copy of |
radial_repeat |
Repeat |
snap |
Move every point onto the nearest line of a square grid. |
symmetry_group |
Apply a full cyclic |
tile |
Repeat |
Motif
¶
Bases: ABC
Convenience base: implement :meth:build, inherit everything else.
The ABC exists to hand you :meth:generate and registration, not to
police the type -- see :class:SupportsBuild if you would rather not
inherit at all.
Methods:
| Name | Description |
|---|---|
build |
Return the design at its natural/native resolution. |
generate |
Build, then resample to |
generate
¶
generate(count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, distribute: Distribution = 'length', by: Placement = 'length') -> Design
Build, then resample to count points (or a fixed step distance).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
count
|
int
|
Total number of points to return. Must be >= 2. |
None
|
step
|
float
|
Fixed real distance between consecutive points, letting the count
fall out of the geometry. Mutually exclusive with |
None
|
spacing
|
SpacingCurve or callable
|
Distribution of points along the path. Defaults to equal spacing. |
None
|
distribute
|
('length', 'even', 'per_path')
|
How |
"length"
|
by
|
('length', 'parameter')
|
Place points by real distance along the curve (the default), or by even steps through its parametrization. |
"length"
|
Returns:
| Type | Description |
|---|---|
Design
|
The resampled design, ready to plot or export. |
Source code in src/geomotif/core/motif.py
SupportsBuild
¶
Bases: Protocol
Structural contract -- any object with build() works everywhere.
Following the :class:typing.SupportsInt convention, this is the
structural twin of :class:Motif: anything that can build a design is
accepted wherever a motif is, so nobody is ever forced to inherit.
Methods:
| Name | Description |
|---|---|
build |
Return the design at its natural resolution. |
Range
dataclass
¶
Bases: Mapping[str, float | None]
A min/max/step bound for a parameter, as a field-metadata mapping.
Any of the three may be None to leave that bound unset; a consumer
then falls back to its own heuristic for the missing axis. The common
case is to set all three, and the commonest still is Range(lo, hi,
step=1) for an integer count.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min
|
float
|
Smallest sensible value, inclusive. |
None
|
max
|
float
|
Largest sensible value, inclusive. |
None
|
step
|
float
|
Granularity for an integer or quantized parameter. |
None
|
Examples:
>>> from dataclasses import field
>>> field(default=5, metadata=Range(1, 50, step=1)).metadata.get("min")
1
ArcTable
¶
Cumulative-length table over a polyline, and the inverse of it.
Building the table is O(n). A lone "where is the point at distance d?"
costs a binary search and one linear interpolation; asking for a whole run
of increasing distances -- which is what resampling does -- walks the table
once between them all instead, so the run is linear in the table rather
than n log n. See :meth:points_at.
Methods:
| Name | Description |
|---|---|
point_at |
Return the point |
point_at_fraction |
Return the point at fraction |
points_at |
Return the point at each distance, in the order they were asked for. |
segment |
Return the part of the polyline lying between two distances. |
points_at_fractions |
Return the point at each fraction of the total length. |
Attributes:
| Name | Type | Description |
|---|---|---|
total |
float
|
Total length of the polyline. |
vertices |
tuple[Point, ...]
|
The measured points, including the closing vertex if closed. |
Source code in src/geomotif/core/sampling.py
vertices
property
¶
The measured points, including the closing vertex if closed.
point_at
¶
Return the point distance along the polyline, clamped to its ends.
A zero-length polyline (every vertex coincident) always returns its single location rather than dividing by zero -- the degenerate case should collapse gracefully, not explode.
Source code in src/geomotif/core/sampling.py
point_at_fraction
¶
points_at
¶
Return the point at each distance, in the order they were asked for.
Exactly what calling :meth:point_at on each in turn returns, and
several times faster for the run of lookups that resampling actually
performs. Those arrive in increasing order, so the segment holding one
is at or after the segment that held the last, and the whole run walks
the table once between them instead of binary-searching all of it every
time.
Order is exploited, never assumed: a distance that goes backwards seeks again, so this has no precondition to get wrong and no fast and slow version to keep in agreement.
Source code in src/geomotif/core/sampling.py
segment
¶
Return the part of the polyline lying between two distances.
The ends are exact -- interpolated where they fall inside a segment -- and every vertex between them is kept as it was, so this is a piece of the polyline rather than a resampling of one. That is what an animation drawing itself on needs: the geometry so far, at the resolution it was built at.
Distances outside the polyline clamp to its ends, and a range that collapses to a point returns that one point.
Source code in src/geomotif/core/sampling.py
points_at_fractions
¶
Return the point at each fraction of the total length.
CircularSpacing
¶
Bases: _ModalCurve
Circular (quarter-arc) easing -- abrupt at one end, flat at the other.
Source code in src/geomotif/core/spacing.py
CompositeSpacing
¶
Bases: SpacingCurve
Chain curves, feeding each one's output into the next.
CompositeSpacing(a, b)(t) == b(a(t)) -- written in application order,
so it reads left to right. Composition of monotone [0, 1] -> [0, 1] maps
is itself monotone with fixed endpoints, so the result is always a valid
spacing curve.
Source code in src/geomotif/core/spacing.py
CubicSpacing
¶
ExponentialSpacing
¶
Bases: _ModalCurve
Exponential easing with adjustable strength (dramatic bias).
strength (default 10, the CSS/Penner standard) controls how extreme
the clustering is; higher values pack points ever more tightly at the
slow end. The curve is normalized so it still maps 0 -> 0 and 1 -> 1.
Source code in src/geomotif/core/spacing.py
LinearSpacing
¶
PowerSpacing
¶
Bases: _ModalCurve
t ** exponent -- the general-purpose "by how much" control.
exponent == 1-- equal spacing (identical to :class:LinearSpacing)exponent > 1-- with mode"in", spacing gradually increases; larger exponents exaggerate the effect0 < exponent < 1-- the opposite bias
Combine with mode="out" to flip which end is dense.
Source code in src/geomotif/core/spacing.py
QuadraticSpacing
¶
ReversedSpacing
¶
Bases: SpacingCurve
Mirror any curve: dense where the original was sparse, and vice versa.
ReversedSpacing(f)(t) == 1 - f(1 - t), which is exactly the "out"
of a modal curve's "in" -- but this works on curves that have no mode,
including plain callables and :class:TableSpacing.
Source code in src/geomotif/core/spacing.py
SineSpacing
¶
SmoothstepSpacing
¶
Bases: SpacingCurve
Hermite smoothstep 3t^2 - 2t^3 -- inherently ease-in-out.
Spacing grows toward the middle of the path and shrinks again toward the end, with perfectly smooth acceleration.
SpacingCurve
¶
Bases: ABC
Base class for point-spacing curves.
Subclasses must implement :meth:ease, mapping [0, 1] -> [0, 1]
monotonically with the endpoints fixed.
Methods:
| Name | Description |
|---|---|
ease |
Map a fraction of the way along the curve to a fraction of its length. |
TableSpacing
¶
Bases: SpacingCurve
A curve drawn by hand, from arbitrary (t, eased) control points.
Interpolation between control points is linear. That is a deliberate choice over a smoother spline: linear interpolation of monotone data is exactly monotone, whereas cubic fits can overshoot and hand back a curve that walks backwards -- which shows up as points in the wrong order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
sequence of (float, float)
|
Control points in [0, 1] x [0, 1]. |
required |
Examples:
A curve that spends the first half of its length on the first quarter of the path::
TableSpacing([(0.5, 0.25)])
Source code in src/geomotif/core/spacing.py
Affine
dataclass
¶
Affine(a: float = 1.0, b: float = 0.0, c: float = 0.0, d: float = 1.0, e: float = 0.0, f: float = 0.0)
A 2D affine transform, composable with @ and callable on points.
Defaults to the identity, so Affine() is a no-op you can build on.
Methods:
| Name | Description |
|---|---|
identity |
Return the transform that changes nothing. |
translate |
Move by |
rotate |
Rotate by |
scale |
Scale by |
mirror |
Reflect across the line at |
shear |
Slant by |
inverse |
Return the transform that undoes this one. |
Attributes:
| Name | Type | Description |
|---|---|---|
determinant |
float
|
Signed area scale factor; negative when the transform reflects. |
determinant
property
¶
Signed area scale factor; negative when the transform reflects.
identity
classmethod
¶
translate
classmethod
¶
rotate
classmethod
¶
Rotate by angle radians, counter-clockwise in y-up coordinates.
Source code in src/geomotif/core/transform.py
scale
classmethod
¶
Scale by sx horizontally and sy vertically (sy defaults to sx).
Source code in src/geomotif/core/transform.py
mirror
classmethod
¶
Reflect across the line at angle radians passing through through.
Source code in src/geomotif/core/transform.py
shear
classmethod
¶
inverse
¶
inverse() -> Affine
Return the transform that undoes this one.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the transform is singular (a zero scale factor, say), which collapses the plane onto a line and cannot be undone. |
Source code in src/geomotif/core/transform.py
Bounds
dataclass
¶
An axis-aligned rectangle enclosing some geometry.
Methods:
| Name | Description |
|---|---|
from_points |
Return the tightest bounds containing every point. |
union |
Return the smallest bounds containing both rectangles. |
padded |
Return these bounds grown by |
Attributes:
| Name | Type | Description |
|---|---|---|
width |
float
|
Horizontal extent. |
height |
float
|
Vertical extent. |
center |
Point
|
Midpoint of the rectangle. |
from_points
classmethod
¶
from_points(points: Iterable[Point]) -> Bounds
Return the tightest bounds containing every point.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/geomotif/core/types.py
union
¶
Return the smallest bounds containing both rectangles.
Source code in src/geomotif/core/types.py
Design
dataclass
¶
Design(paths: tuple[Path, ...] = (), points: tuple[Point, ...] = (), meta: Mapping[str, object] = EMPTY_META)
The universal result: zero or more strokes plus zero or more loose points.
meta carries the motif name and its resolved parameters (including any
resolved random seed), which is what makes a design reproducible,
serializable to a spec file, and self-labelling in the gallery.
Methods:
| Name | Description |
|---|---|
transformed |
Return this design with |
flipped_y |
Return this design mirrored about the x-axis. |
resampled |
Return this design resampled to |
snapped |
Return this design with every point moved onto a grid of |
fit |
Return this design scaled and centered inside a |
Attributes:
| Name | Type | Description |
|---|---|---|
bounds |
Bounds
|
Bounds over every point in every path plus the loose points. |
transformed
¶
Return this design with m applied to every point.
Source code in src/geomotif/core/types.py
flipped_y
¶
flipped_y() -> Design
Return this design mirrored about the x-axis.
The y-up/y-down question is a property of the target coordinate space, not of any motif, which is why it lives here rather than as a flag on every builder.
Source code in src/geomotif/core/types.py
resampled
¶
resampled(count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, distribute: Distribution = 'length') -> Design
Return this design resampled to count points (or a fixed step).
See :func:geomotif.core.sampling.resample for the full contract.
Source code in src/geomotif/core/types.py
snapped
¶
snapped(step: float = 1.0, *, mode: SnapMode = 'half-even', drop_duplicates: bool = True) -> Design
Return this design with every point moved onto a grid of step.
Rounding the geometry rather than each file as it is written, so every exporter agrees and a plot shows what the file will hold. Defaults to whole units.
See :func:geomotif.core.transform.snap for the full contract.
Source code in src/geomotif/core/types.py
fit
¶
fit(width: float, height: float, *, padding: float = 0.0, flip_y: bool = False) -> Design
Return this design scaled and centered inside a width x height canvas.
Scaling is uniform, so the design is never distorted; it is centered
in whichever axis has slack. The result sits in [0, width] x
[0, height], with flip_y=True producing y-down (screen/SVG)
coordinates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
width
|
float
|
Canvas size. Both must be positive. |
required |
height
|
float
|
Canvas size. Both must be positive. |
required |
padding
|
float
|
Margin reserved on all four sides. |
0.0
|
flip_y
|
bool
|
Mirror vertically so y increases downward. |
False
|
Returns:
| Type | Description |
|---|---|
Design
|
The fitted design. A design with no extent in either axis (a single point, or a perfectly vertical line) is translated but never scaled, since there is no finite scale that fills a canvas from nothing. |
Source code in src/geomotif/core/types.py
Path
dataclass
¶
One continuous polyline.
closed means the last point connects back to the first. The closing
segment is implied, never stored, so a closed path's points are never
duplicated at the seam.
Points are normalized to a tuple of finite (float, float) pairs at
construction, so any sequence of pairs may be passed in.
Attributes:
| Name | Type | Description |
|---|---|---|
length |
float
|
Total polyline length, including the closing segment if closed. |
bounds |
Bounds
|
Tightest rectangle containing every vertex. |
length
property
¶
Total polyline length, including the closing segment if closed.
A two-point "closed" path is treated as a single open segment: its closing segment retraces the one it already has, and counting that twice reports a length no plotter would ever draw.
densify
¶
densify(fn: Callable[[float], Point], *, samples: int, domain: tuple[float, float] = (0.0, 1.0)) -> tuple[Point, ...]
Evaluate fn at evenly spaced parameters across domain.
Returns samples + 1 points, so both endpoints of the domain are
included and the result contains exactly samples segments.
Source code in src/geomotif/core/sampling.py
resample
¶
resample(design: Design, count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, distribute: Distribution = 'length', by: Placement = 'length') -> Design
Return design resampled across all of its paths.
Loose points are passed through untouched: they are already exactly the points the motif meant, with no curve to redistribute them along.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The design to resample. |
required |
count
|
int
|
Total number of points. How it is split across paths depends on
|
None
|
step
|
float
|
Fixed distance between consecutive points, applied independently to
every path. |
None
|
spacing
|
SpacingCurve or callable
|
Distribution of points along each path. |
None
|
distribute
|
('length', 'even', 'per_path')
|
How a total
|
"length"
|
by
|
('length', 'parameter')
|
Placement mode along each individual path; see :func: |
"length"
|
Returns:
| Type | Description |
|---|---|
Design
|
A new design with the same loose points and metadata. |
Source code in src/geomotif/core/sampling.py
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 | |
resample_path
¶
resample_path(path: Path, count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, by: Placement = 'length') -> Path
Return path resampled to count points, or at a fixed step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
The polyline to resample. |
required |
count
|
int
|
Total number of points to return. Must be >= 2. Mutually exclusive
with |
None
|
step
|
float
|
Fixed real distance between consecutive points; the count falls out
of the geometry. Any remainder shorter than |
None
|
spacing
|
SpacingCurve or callable
|
Distribution of points along the path. Defaults to equal spacing.
Cannot be combined with |
None
|
by
|
('length', 'parameter')
|
|
"length"
|
Returns:
| Type | Description |
|---|---|
Path
|
The resampled path, preserving |
Source code in src/geomotif/core/sampling.py
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
samples_for_turns
¶
Return a sensible densification count for a curve spanning turns.
Sample density has to scale with how much the curve actually bends, or a tightly wound motif is measured by a polyline that cuts every corner. This is the adaptive heuristic the spiral generator used, exposed so every motif can share one answer.
Source code in src/geomotif/core/sampling.py
coerce_spacing
¶
coerce_spacing(spacing: SpacingLike | None) -> SpacingCurve
Normalize anything spacing-shaped into a :class:SpacingCurve.
None means :class:LinearSpacing. This is the single place the
library decides what counts as a spacing curve, so every entry point
accepts exactly the same things and fails the same way.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Source code in src/geomotif/core/spacing.py
clip_to
¶
Trim design to a rectangle, splitting paths that leave and re-enter.
Clipped paths come back open even if they went in closed: a shape whose outline has been cut is no longer a closed loop, and pretending otherwise would draw a chord across the gap. Loose points outside the rectangle are dropped.
Source code in src/geomotif/core/transform.py
fit_to
¶
fit_to(design: Design, width: float, height: float, *, padding: float = 0.0, flip_y: bool = False) -> Design
Scale and center design inside a canvas. See :meth:Design.fit.
Source code in src/geomotif/core/transform.py
jitter
¶
Randomly displace every point, for controlled hand-drawn irregularity.
Each coordinate is offset independently by a uniform value in
[-amount, amount]. The RNG is private to this call -- the global
:mod:random state is never touched -- so a given seed always
reproduces the same result no matter what else the program is doing.
Source code in src/geomotif/core/transform.py
layer
¶
Overlay designs into one. Equivalent to repeated +.
mirror_axis
¶
Return design overlaid with its reflection across a line.
offset_path
¶
Return a parallel copy of path, offset by distance to its left.
Left is relative to the direction of travel in y-up coordinates, so a negative distance offsets to the right. Corners are mitered, with a limit that falls back to a plain bevel on very sharp turns.
This is the "simple parallel stroke" of guilloché and knot outlines, not a CAD offset: self-intersections on tight concave corners are not cleaned up, and the result may cross itself where the offset exceeds the local radius of curvature.
Source code in src/geomotif/core/transform.py
radial_repeat
¶
Repeat design n times evenly around a point -- the mandala workhorse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The unit to repeat. |
required |
n
|
int
|
Number of copies, including the original. Must be >= 1. |
required |
about
|
(float, float)
|
Center of rotation. |
(0.0, 0.0)
|
mirror
|
bool
|
Also emit a reflected copy in each sector, giving dihedral rather than merely cyclic symmetry. |
False
|
Source code in src/geomotif/core/transform.py
snap
¶
snap(design: Design, step: float = 1.0, *, mode: SnapMode = 'half-even', drop_duplicates: bool = True) -> Design
Move every point onto the nearest line of a square grid.
This is rounding applied to the design rather than to each file as it is
written, which is the difference that matters: every exporter then agrees,
and a plot of the result shows what the file will actually contain.
design.snapped() alone rounds to whole units.
Snapping trades this library's exact arc-length spacing for grid alignment. Points that were an equal real distance apart come out equal only to within half a step, so snap after resampling and choose a step well below the spacing if the evenness is what you are there for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to snap. |
required |
step
|
float
|
Grid size, in the design's own units. Must be finite and positive.
|
1.0
|
mode
|
('half-even', 'half-up', 'floor', 'ceil', 'trunc')
|
How a coordinate between two grid lines is resolved. |
"half-even"
|
drop_duplicates
|
bool
|
Remove points that a coarse grid has landed on top of their immediate neighbour, and then any stroke left with fewer than two points. On by default, because those are zero-length segments: ink a plotter cannot draw and a pen-down/pen-up it should not spend the time on. Turn it off to keep the point count exactly as it was, which is what a caller feeding a fixed-size buffer or a per-point parallel array needs. |
True
|
Returns:
| Type | Description |
|---|---|
Design
|
Snapped, with each surviving stroke's style following it across. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from geomotif import Design, Path
>>> square = Design((Path(((0.4, 0.4), (9.6, 0.4), (9.6, 9.6))),))
>>> list(snap(square))
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]
>>> list(snap(square, 0.25))
[(0.5, 0.5), (9.5, 0.5), (9.5, 9.5)]
Source code in src/geomotif/core/transform.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 | |
symmetry_group
¶
Apply a full cyclic Cn or dihedral Dn symmetry group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The fundamental domain to replicate. |
required |
group
|
str
|
|
required |
Source code in src/geomotif/core/transform.py
tile
¶
Repeat design on a rectangular lattice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The unit cell contents. |
required |
cols
|
int
|
Lattice size. Both must be >= 1. |
required |
rows
|
int
|
Lattice size. Both must be >= 1. |
required |
dx
|
float
|
Spacing between columns and rows. |
required |
dy
|
float
|
Spacing between columns and rows. |
required |
stagger
|
float
|
Fraction of |
0.0
|