Skip to content

geomotif.explore

An explorable gallery: one page, sliders, and every picture already drawn.

The catalog's parameters are the interesting part and a static gallery cannot show them. What k does to a rose, what depth does to a dragon and what factor does to a times-table circle are all things you learn by dragging a slider and watching, and never by reading a default value.

So: a page you can drag. It has no server, no build step and no JavaScript library -- every frame is rendered ahead of time by geomotif's own SVG writer and embedded in the document, and the sliders only choose which one is showing. That means the page works from a file:// URL, inside a zip, and with nothing installed and no network::

geomotif explore rose fractal.dragon --out explore.html
geomotif explore --family spiral --out spirals.html

One parameter moves at a time. Rendering every combination of five parameters would be a combinatorial explosion and a hundred-megabyte file, so each slider sweeps its own parameter with the others left at the motif's example values. The page says so; it is the honest limit of pre-rendering, and in exchange one motif is a few hundred kilobytes rather than the product of every slider's length. --samples trims a dense one further.

Numbers and booleans get sliders. A parameter that is a point, a set of coordinates or another motif does not -- there is no single axis to drag it along -- and is listed on the page as fixed, exactly as the command line reports the same parameters as not settable.

Classes:

Name Description
Sweep

One parameter, the values it was drawn at, and the drawings.

Functions:

Name Description
to_html

Render an explorable page for one or more registered motifs.

save_html

Write an explorable page and return the path written.

sweeps_for

Render one sweep per parameter of a motif that a slider can move.

Sweep dataclass

Sweep(parameter: str, values: tuple[object, ...], images: tuple[str, ...], start: int)

One parameter, the values it was drawn at, and the drawings.

to_html

to_html(names: Sequence[str], *, steps: int = DEFAULT_STEPS, size: int = DEFAULT_SIZE, samples: int | None = None, title: str = 'geomotif') -> str

Render an explorable page for one or more registered motifs.

Parameters:

Name Type Description Default
names sequence of str

Registered motif names. Several become a picker beside the sliders.

required
steps int

Values per slider. Must be >= 2; odd values put the motif's own example in the middle.

DEFAULT_STEPS
size int

Canvas for each frame, in SVG user units.

DEFAULT_SIZE
samples int

Resample every frame to this many points. Worth setting for a dense motif: the page holds steps frames per parameter, and it is the vertices that make it large.

None
title str

The document's title.

'geomotif'

Returns:

Type Description
str

A complete, self-contained HTML document.

Raises:

Type Description
ValueError

If no names are given, or none of them could be drawn.

KeyError

If a name is not registered.

Source code in src/geomotif/explore.py
def to_html(
    names: Sequence[str],
    *,
    steps: int = DEFAULT_STEPS,
    size: int = DEFAULT_SIZE,
    samples: int | None = None,
    title: str = "geomotif",
) -> str:
    """Render an explorable page for one or more registered motifs.

    Parameters
    ----------
    names : sequence of str
        Registered motif names. Several become a picker beside the sliders.
    steps : int
        Values per slider. Must be >= 2; odd values put the motif's own
        example in the middle.
    size : int
        Canvas for each frame, in SVG user units.
    samples : int, optional
        Resample every frame to this many points. Worth setting for a dense
        motif: the page holds ``steps`` frames per parameter, and it is the
        vertices that make it large.
    title : str
        The document's title.

    Returns
    -------
    str
        A complete, self-contained HTML document.

    Raises
    ------
    ValueError
        If no names are given, or none of them could be drawn.
    KeyError
        If a name is not registered.
    """
    if not names:
        raise ValueError("cannot explore nothing: name at least one motif")
    if steps < 2:
        raise ValueError(f"steps must be >= 2, got {steps}")

    panels = [
        (info, sweeps_for(info, steps=steps, size=size, samples=samples))
        for info in (registry.describe(name) for name in names)
        if info.available
    ]
    drawable = [(info, sweeps) for info, sweeps in panels if sweeps]
    if not drawable:
        raise ValueError(
            f"none of {list(names)} has a parameter that can be swept; every one of "
            f"them is defined by values a slider cannot move along"
        )
    return _document(drawable, title=title, size=size)

save_html

save_html(names: Sequence[str], path: str | PathLike[str], **kwargs: Any) -> Path

Write an explorable page and return the path written.

Keyword arguments are passed straight through to :func:to_html.

Source code in src/geomotif/explore.py
def save_html(names: Sequence[str], path: str | PathLike[str], **kwargs: Any) -> pathlib.Path:
    """Write an explorable page and return the path written.

    Keyword arguments are passed straight through to :func:`to_html`.
    """
    target = pathlib.Path(path)
    target.write_text(to_html(names, **kwargs), encoding="utf-8")
    return target

sweeps_for

sweeps_for(info: MotifInfo, *, steps: int = DEFAULT_STEPS, size: int = DEFAULT_SIZE, samples: int | None = None) -> tuple[Sweep, ...]

Render one sweep per parameter of a motif that a slider can move.

A value the motif refuses -- a modulus of one, a depth of zero -- is dropped rather than reported: the sweep is generated from a range rather than chosen from one, so hitting the edge of what a motif accepts is expected, and the parameter simply offers the values that worked.

Source code in src/geomotif/explore.py
def sweeps_for(
    info: MotifInfo,
    *,
    steps: int = DEFAULT_STEPS,
    size: int = DEFAULT_SIZE,
    samples: int | None = None,
) -> tuple[Sweep, ...]:
    """Render one sweep per parameter of a motif that a slider can move.

    A value the motif refuses -- a modulus of one, a depth of zero -- is
    dropped rather than reported: the sweep is generated from a range rather
    than chosen from one, so hitting the edge of what a motif accepts is
    expected, and the parameter simply offers the values that worked.
    """
    sweeps: list[Sweep] = []
    for param in info.params:
        values = _values_for(param, info, steps)
        if not values:
            continue
        drawn = [(value, _draw(info, param.name, value, size, samples)) for value in values]
        kept = [(value, image) for value, image in drawn if image is not None]
        if len(kept) < 2:
            continue
        base = _default_for(param, info)
        sweeps.append(
            Sweep(
                parameter=param.name,
                values=tuple(value for value, _ in kept),
                images=tuple(image for _, image in kept),
                start=_nearest(base, [value for value, _ in kept]),
            )
        )
    return tuple(sweeps)