Repository: computational-psychology/stimupy (language: python)

Issue:
Remove deprecated `resolve_circular_params`
since circular gratings now also use the generic `sine_wave` and `resolve_grating_params`
Reorganized (and updated) existing demos to fit new organization
Still need demos for following `stimuli`:

- [ ] `sbcs`
- [ ] `todorovics`
- [ ] `whites`

And for all of the `noises`


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 889a2aac..b8d14eb8 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,8 @@
 
 Contains submodules for
 - drawing basic visual stimulus components ([components](stimupy/components/))
-- creating different (brightness) illusions ([illusions](stimupy/illusions/))
-- replicating illusions in certain published papers  ([papers](stimupy/papers/))
+- creating different paramaterized stimuli ([stimuli](stimupy/stimuli/))
+- replicating stimuli in certain published papers  ([papers](stimupy/papers/))
 converting pixel values to degrees of visual angle ([utils](stimupy/utils/))
 
 `Stimupy` is a pure-Python package
@@ -21,8 +21,8 @@ so that they are reproducible, flexible, and easy.
   - basic visual stimulus [components](stimupy/components/),
     such as basic shapes, gratings, Gaussians, Gabors
   - visual [noise](stimupy/noises/) textures, of different kinds,
-  - and many different stimuli commonly referred to as [illusions](stimupy/illusions/)
-    with some special regions of interest,
+  - and many different parameterized stimuli [stimuli](stimupy/stimuli/)
+    most with some special regions of interest,
     such as Simultaneous Brightness Contrast, White's illusion,
     but also Hermann Grids, checkerboards, Ponzo illusion, etc.
 
@@ -33,7 +33,7 @@ so that they are reproducible, flexible, and easy.
     which might reveal relations between formerly unconnected stimuli
 
 - Stimuli are also composable/composed:
-`illusions` tend to be composed from several `components`.
+`stimuli` tend to be composed from several `components`.
 
 - Generated stimuli are output as a Python `dict`ionary,
 containing the stimulus-image as a NumPy-array,
diff --git a/docs/_templates/autosummary/illusions.rst b/docs/_templates/autosummary/stimuli.rst
similarity index 96%
rename from docs/_templates/autosummary/illusions.rst
rename to docs/_templates/autosummary/stimuli.rst
index e2d79e42..f623d50c 100644
--- a/docs/_templates/autosummary/illusions.rst
+++ b/docs/_templates/autosummary/stimuli.rst
@@ -58,7 +58,7 @@
 
    {% block functions %}
    {% if functions %}
-   :doc:`Demos <../../demos/illusions/{{name}}>`
+   :doc:`Demos <../../demos/stimuli/{{name}}>`
    ------------------------------------------------------------------------------
 
    Functions
diff --git a/docs/reference/api.md b/docs/reference/api.md
index 064fd024..e7313c86 100644
--- a/docs/reference/api.md
+++ b/docs/reference/api.md
@@ -5,7 +5,7 @@
    :recursive:
 
    stimupy.components
-   stimupy.illusions
+   stimupy.stimuli
    stimupy.noises
    stimupy.utils
    stimupy.papers
diff --git a/docs/reference/demos/components/circulars.md b/docs/reference/demos/components/circulars.md
deleted file mode 100644
index 4aa1d2d6..00000000
--- a/docs/reference/demos/components/circulars.md
+++ /dev/null
@@ -1,532 +0,0 @@
----
-jupytext:
-  formats: md:myst
-  text_representation:
-    extension: .md
-    format_name: myst
-    format_version: 0.13
-    jupytext_version: 1.14.5
-kernelspec:
-  display_name: Python 3 (ipykernel)
-  language: python
-  name: python3
----
-
-```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/circulars.md)
- to get interactivity
-```
-
-# Components - Circulars
-{py:mod}`stimupy.components.circulars`
-
-```{code-cell} ipython3
-:tags: [remove-cell]
-
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
-
-## Disc
-{py:func}`stimupy.components.circulars.disc`
-
-```{code-cell} ipython3
-from stimupy.components.circulars import disc
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-w_radius = iw.FloatSlider(value=3, min=1, max=6, description="radius [deg]")
-
-w_int = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity disc")
-w_int_back = iw.FloatSlider(value=0., min=0, max=1, description="intensity background")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd, w_radius])
-b_intensities = iw.HBox([w_int, w_int_back])
-b_add = iw.HBox([w_ori, w_mask])
-ui = iw.VBox([b_im_size, b_intensities, b_add])
-
-# Function for showing stim
-def show_disc(
-    height=None,
-    width=None,
-    ppd=None,
-    radius=None,
-    intensity_disc=None,
-    intensity_background=None,
-    origin=None,
-    add_mask=False,
-):
-    stim = disc(
-        visual_size=(height, width),
-        ppd=ppd,
-        radius=radius,
-        intensity_disc=intensity_disc,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_disc,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "radius": w_radius,
-        "intensity_disc": w_int,
-        "intensity_background": w_int_back,
-        "origin": w_ori,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Annulus
-{py:func}`stimupy.components.circulars.annulus`
-
-```{code-cell} ipython3
-from stimupy.components.circulars import annulus
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_radius1 = iw.FloatSlider(value=2, min=1, max=4, description="radius1 [deg]")
-w_radius2 = iw.FloatSlider(value=4, min=3, max=6, description="radius2 [deg]")
-
-w_int = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity ring")
-w_int_back = iw.FloatSlider(value=0., min=0, max=1, description="intensity background")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_radius1, w_radius2])
-b_intensities = iw.HBox([w_int, w_int_back])
-b_add = iw.HBox([w_ori, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
-
-# Function for showing stim
-def show_annulus(
-    height=None,
-    width=None,
-    ppd=None,
-    radius1=None,
-    radius2=None,
-    intensity_ring=None,
-    intensity_background=None,
-    origin=None,
-    add_mask=False,
-):
-    stim = annulus(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2),
-        intensity_ring=intensity_ring,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_annulus,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "radius1": w_radius1,
-        "radius2": w_radius2,
-        "intensity_ring": w_int,
-        "intensity_background": w_int_back,
-        "origin": w_ori,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Ring
-{py:func}`stimupy.components.circulars.ring`
-
-```{code-cell} ipython3
-from stimupy.components.circulars import ring
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_radius1 = iw.FloatSlider(value=2, min=1, max=4, description="radius1 [deg]")
-w_radius2 = iw.FloatSlider(value=4, min=3, max=6, description="radius2 [deg]")
-
-w_int = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity ring")
-w_int_back = iw.FloatSlider(value=0., min=0, max=1, description="intensity background")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_radius1, w_radius2])
-b_intensities = iw.HBox([w_int, w_int_back])
-b_add = iw.HBox([w_ori, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
-
-# Function for showing stim
-def show_ring(
-    height=None,
-    width=None,
-    ppd=None,
-    radius1=None,
-    radius2=None,
-    intensity_ring=None,
-    intensity_background=None,
-    origin=None,
-    add_mask=False,
-):
-    stim = annulus(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2),
-        intensity_ring=intensity_ring,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_ring,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "radius1": w_radius1,
-        "radius2": w_radius2,
-        "intensity_ring": w_int,
-        "intensity_background": w_int_back,
-        "origin": w_ori,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Disc and rings
-{py:func}`stimupy.components.circulars.disc_and_rings`
-
-```{code-cell} ipython3
-from stimupy.components.circulars import disc_and_rings
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_radius1 = iw.FloatSlider(value=1, min=0, max=2, description="radius1 [deg]")
-w_radius2 = iw.FloatSlider(value=2, min=1, max=3, description="radius2 [deg]")
-w_radius3 = iw.FloatSlider(value=3, min=2, max=4, description="radius2 [deg]")
-
-w_int1 = iw.FloatSlider(value=0.8, min=0, max=1, description="int-ring1")
-w_int2 = iw.FloatSlider(value=0.5, min=0, max=1, description="int-ring2")
-w_int3 = iw.FloatSlider(value=0.3, min=0, max=1, description="int-ring3")
-w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_radius1, w_radius2, w_radius3])
-b_intensities = iw.HBox([w_int1, w_int2, w_int3, w_int_back])
-b_add = iw.HBox([w_ori, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
-
-# Function for showing stim
-def show_disc_and_rings(
-    height=None,
-    width=None,
-    ppd=None,
-    radius1=None,
-    radius2=None,
-    radius3=None,
-    int1=None,
-    int2=None,
-    int3=None,
-    intensity_background=None,
-    origin=None,
-    add_mask=False,
-):
-    stim = disc_and_rings(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2, radius3),
-        intensity_rings=(int1, int2, int3),
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_disc_and_rings,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "radius1": w_radius1,
-        "radius2": w_radius2,
-        "radius3": w_radius3,
-        "int1": w_int1,
-        "int2": w_int2,
-        "int3": w_int3,
-        "intensity_background": w_int_back,
-        "origin": w_ori,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Sine-wave
-{py:func}`stimupy.components.circulars.sine_wave`
-
-```{code-cell} ipython3
-from stimupy.components.circulars import sine_wave
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
-w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
-
-w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
-w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
-w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_clip = iw.ToggleButton(value=False, disabled=False, description="clip")
-w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_phase])
-b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
-b_add = iw.HBox([w_ori, w_clip, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
-
-# Function for showing stim
-def show_sine_wave(
-    height=None,
-    width=None,
-    ppd=None,
-    frequency=None,
-    phase_shift=None,
-    int1=None,
-    int2=None,
-    intensity_background=None,
-    origin=None,
-    clip=False,
-    add_mask=False,
-):
-    stim = sine_wave(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_sine_wave,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "frequency": w_freq,
-        "phase_shift": w_phase,
-        "int1": w_int1,
-        "int2": w_int2,
-        "intensity_background": w_int_back,
-        "origin": w_ori,
-        "clip": w_clip,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Square-wave
-{py:func}`stimupy.components.circulars.square_wave`
-
-```{code-cell} ipython3
-from stimupy.components.circulars import square_wave
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
-w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
-
-w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
-w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
-w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_clip = iw.ToggleButton(value=False, disabled=False, description="clip")
-w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_phase])
-b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
-b_add = iw.HBox([w_ori, w_clip, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
-
-# Function for showing stim
-def show_square_wave(
-    height=None,
-    width=None,
-    ppd=None,
-    frequency=None,
-    phase_shift=None,
-    int1=None,
-    int2=None,
-    intensity_background=None,
-    origin=None,
-    clip=False,
-    add_mask=False,
-):
-    stim = square_wave(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_square_wave,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "frequency": w_freq,
-        "phase_shift": w_phase,
-        "int1": w_int1,
-        "int2": w_int2,
-        "intensity_background": w_int_back,
-        "origin": w_ori,
-        "clip": w_clip,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Bessel
-{py:func}`stimupy.components.circulars.bessel`
-
-```{code-cell} ipython3
-from stimupy.components.circulars import bessel
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
-w_order = iw.IntSlider(value=0, min=0, max=5, description="order")
-
-w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
-w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_order])
-b_intensities = iw.HBox([w_int1, w_int2])
-b_add = iw.HBox([w_ori, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
-
-# Function for showing stim
-def show_bessel(
-    height=None,
-    width=None,
-    ppd=None,
-    frequency=None,
-    order=None,
-    int1=None,
-    int2=None,
-    origin=None,
-    add_mask=False,
-):
-    stim = bessel(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        order=order,
-        intensity_rings=(int1, int2),
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_bessel,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "frequency": w_freq,
-        "order": w_order,
-        "int1": w_int1,
-        "int2": w_int2,
-        "origin": w_ori,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
diff --git a/docs/reference/demos/components/frames.md b/docs/reference/demos/components/frames.md
index 3c37fb32..99abc0a9 100644
--- a/docs/reference/demos/components/frames.md
+++ b/docs/reference/demos/components/frames.md
@@ -105,157 +105,3 @@ out = iw.interactive_output(
 # Show
 display(ui, out)
 ```
-
-## Sine-wave
-{py:func}`stimupy.components.frames.sine_wave`
-
-```{code-cell} ipython3
-from stimupy.components.frames import sine_wave
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
-w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
-
-w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
-w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
-w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_clip = iw.ToggleButton(value=False, disabled=False, description="clip")
-w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_phase])
-b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
-b_add = iw.HBox([w_ori, w_clip, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
-
-# Function for showing stim
-def show_sine_wave(
-    height=None,
-    width=None,
-    ppd=None,
-    frequency=None,
-    phase_shift=None,
-    int1=None,
-    int2=None,
-    intensity_background=None,
-    origin=None,
-    clip=False,
-    add_mask=False,
-):
-    stim = sine_wave(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_sine_wave,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "frequency": w_freq,
-        "phase_shift": w_phase,
-        "int1": w_int1,
-        "int2": w_int2,
-        "intensity_background": w_int_back,
-        "origin": w_ori,
-        "clip": w_clip,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Square-wave
-{py:func}`stimupy.components.frames.square_wave`
-
-```{code-cell} ipython3
-from stimupy.components.frames import square_wave
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
-w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
-
-w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
-w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
-w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_clip = iw.ToggleButton(value=False, disabled=False, description="clip")
-w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_phase])
-b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
-b_add = iw.HBox([w_ori, w_clip, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
-
-# Function for showing stim
-def show_square_wave(
-    height=None,
-    width=None,
-    ppd=None,
-    frequency=None,
-    phase_shift=None,
-    int1=None,
-    int2=None,
-    intensity_background=None,
-    origin=None,
-    clip=False,
-    add_mask=False,
-):
-    stim = square_wave(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_square_wave,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "frequency": w_freq,
-        "phase_shift": w_phase,
-        "int1": w_int1,
-        "int2": w_int2,
-        "intensity_background": w_int_back,
-        "origin": w_ori,
-        "clip": w_clip,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
diff --git a/docs/reference/demos/components/radials.md b/docs/reference/demos/components/radials.md
new file mode 100644
index 00000000..0a1d6b2c
--- /dev/null
+++ b/docs/reference/demos/components/radials.md
@@ -0,0 +1,241 @@
+---
+jupytext:
+  formats: md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+    jupytext_version: 1.14.5
+kernelspec:
+  display_name: Python 3 (ipykernel)
+  language: python
+  name: python3
+---
+
+```{important}
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/radials.md)
+ to get interactivity
+```
+
+# Components - Radials
+{py:mod}`stimupy.components.radials`
+
+```{code-cell} ipython3
+:tags: [remove-cell]
+
+import IPython
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+```
+
+## Disc
+{py:func}`stimupy.components.radials.disc`
+
+```{code-cell} ipython3
+from stimupy.components.radials import disc
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
+w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
+w_radius = iw.FloatSlider(value=3, min=1, max=6, description="radius [deg]")
+
+w_int = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity disc")
+w_int_back = iw.FloatSlider(value=0., min=0, max=1, description="intensity background")
+
+w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
+w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd, w_radius])
+b_intensities = iw.HBox([w_int, w_int_back])
+b_add = iw.HBox([w_ori, w_mask])
+ui = iw.VBox([b_im_size, b_intensities, b_add])
+
+# Function for showing stim
+def show_disc(
+    height=None,
+    width=None,
+    ppd=None,
+    radius=None,
+    intensity_disc=None,
+    intensity_background=None,
+    origin=None,
+    add_mask=False,
+):
+    stim = disc(
+        visual_size=(height, width),
+        ppd=ppd,
+        radius=radius,
+        intensity_disc=intensity_disc,
+        intensity_background=intensity_background,
+        origin=origin,
+    )
+    plot_stim(stim, mask=add_mask)
+
+# Set interactivity
+out = iw.interactive_output(
+    show_disc,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "radius": w_radius,
+        "intensity_disc": w_int,
+        "intensity_background": w_int_back,
+        "origin": w_ori,
+        "add_mask": w_mask,
+    },
+)
+
+# Show
+display(ui, out)
+```
+
+## Annulus (/ Ring)
+{py:func}`stimupy.components.radials.annulus`
+
+```{code-cell} ipython3
+from stimupy.components.radials import annulus
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
+w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
+
+w_radius1 = iw.FloatSlider(value=2, min=1, max=4, description="radius1 [deg]")
+w_radius2 = iw.FloatSlider(value=4, min=3, max=6, description="radius2 [deg]")
+
+w_int = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity ring")
+w_int_back = iw.FloatSlider(value=0., min=0, max=1, description="intensity background")
+
+w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
+w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd])
+b_geometry = iw.HBox([w_radius1, w_radius2])
+b_intensities = iw.HBox([w_int, w_int_back])
+b_add = iw.HBox([w_ori, w_mask])
+ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
+
+# Function for showing stim
+def show_annulus(
+    height=None,
+    width=None,
+    ppd=None,
+    radius1=None,
+    radius2=None,
+    intensity_ring=None,
+    intensity_background=None,
+    origin=None,
+    add_mask=False,
+):
+    stim = annulus(
+        visual_size=(height, width),
+        ppd=ppd,
+        radii=(radius1, radius2),
+        intensity_ring=intensity_ring,
+        intensity_background=intensity_background,
+        origin=origin,
+    )
+    plot_stim(stim, mask=add_mask)
+
+# Set interactivity
+out = iw.interactive_output(
+    show_annulus,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "radius1": w_radius1,
+        "radius2": w_radius2,
+        "intensity_ring": w_int,
+        "intensity_background": w_int_back,
+        "origin": w_ori,
+        "add_mask": w_mask,
+    },
+)
+
+# Show
+display(ui, out)
+```
+
+
+## Rings
+{py:func}`stimupy.components.radials.rings`
+
+```{code-cell} ipython3
+from stimupy.components.radials import rings
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
+w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
+
+w_radius1 = iw.FloatSlider(value=1, min=0, max=2, description="radius1 [deg]")
+w_radius2 = iw.FloatSlider(value=2, min=1, max=3, description="radius2 [deg]")
+w_radius3 = iw.FloatSlider(value=3, min=2, max=4, description="radius2 [deg]")
+
+w_int1 = iw.FloatSlider(value=0.8, min=0, max=1, description="int-ring1")
+w_int2 = iw.FloatSlider(value=0.5, min=0, max=1, description="int-ring2")
+w_int3 = iw.FloatSlider(value=0.3, min=0, max=1, description="int-ring3")
+w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
+
+w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
+w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd])
+b_geometry = iw.HBox([w_radius1, w_radius2, w_radius3])
+b_intensities = iw.HBox([w_int1, w_int2, w_int3, w_int_back])
+b_add = iw.HBox([w_ori, w_mask])
+ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
+
+# Function for showing stim
+def show_rings(
+    height=None,
+    width=None,
+    ppd=None,
+    radius1=None,
+    radius2=None,
+    radius3=None,
+    int1=None,
+    int2=None,
+    int3=None,
+    intensity_background=None,
+    origin=None,
+    add_mask=False,
+):
+    stim = rings(
+        visual_size=(height, width),
+        ppd=ppd,
+        radii=(radius1, radius2, radius3),
+        intensity_rings=(int1, int2, int3),
+        intensity_background=intensity_background,
+        origin=origin,
+    )
+    plot_stim(stim, mask=add_mask)
+
+# Set interactivity
+out = iw.interactive_output(
+    show_rings,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "radius1": w_radius1,
+        "radius2": w_radius2,
+        "radius3": w_radius3,
+        "int1": w_int1,
+        "int2": w_int2,
+        "int3": w_int3,
+        "intensity_background": w_int_back,
+        "origin": w_ori,
+        "add_mask": w_mask,
+    },
+)
+
+# Show
+display(ui, out)
+```
diff --git a/docs/reference/demos/components/gratings.md b/docs/reference/demos/components/waves.md
similarity index 59%
rename from docs/reference/demos/components/gratings.md
rename to docs/reference/demos/components/waves.md
index 6e1519cb..58994029 100644
--- a/docs/reference/demos/components/gratings.md
+++ b/docs/reference/demos/components/waves.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/gratings.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/waves.md)
  to get interactivity
 ```
 
-# Components - Gratings
-{py:mod}`stimupy.components.gratings`
+# Components - Waves
+{py:mod}`stimupy.components.waves`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -28,17 +28,18 @@ import ipywidgets as iw
 from stimupy.utils import plot_stim
 ```
 
-## Sine-wave
-{py:func}`stimupy.components.gratings.sine_wave`
+## Sinewave
+{py:func}`stimupy.components.waves.sine`
 
 ```{code-cell} ipython3
-from stimupy.components.gratings import sine_wave
+from stimupy.components.waves import sine
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
 w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
 w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
 
+w_base_type = iw.Dropdown(value="horizontal", options=['horizontal','vertical','rotated','radial','cityblock','angular'], description="base_type")
 w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
 w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
 w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
@@ -53,16 +54,17 @@ w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_phase, w_rot])
+b_geometry = iw.HBox([w_base_type, w_freq, w_phase, w_rot])
 b_intensities = iw.HBox([w_int1, w_int2])
 b_add = iw.HBox([w_ori, w_period, w_round, w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
 
 # Function for showing stim
-def show_sine_wave(
+def show_sine(
     height=None,
     width=None,
     ppd=None,
+    base_type=None,
     rotation=None,
     frequency=None,
     phase_shift=None,
@@ -73,13 +75,14 @@ def show_sine_wave(
     period=None,
     add_mask=False,
 ):
-    stim = sine_wave(
+    stim = sine(
         visual_size=(height, width),
         ppd=ppd,
+        base_type=base_type,
         rotation=rotation,
         frequency=frequency,
         phase_shift=phase_shift,
-        intensity_bars=(int1, int2),
+        intensities=(int1, int2),
         origin=origin,
         round_phase_width=round_phase_width,
         period=period,
@@ -88,11 +91,12 @@ def show_sine_wave(
 
 # Set interactivity
 out = iw.interactive_output(
-    show_sine_wave,
+    show_sine,
     {
         "height": w_height,
         "width": w_width,
         "ppd": w_ppd,
+        "base_type": w_base_type,
         "rotation": w_rot,
         "frequency": w_freq,
         "phase_shift": w_phase,
@@ -109,17 +113,18 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Square-wave
-{py:func}`stimupy.components.gratings.square_wave`
+## Squarewave
+{py:func}`stimupy.components.waves.square`
 
 ```{code-cell} ipython3
-from stimupy.components.gratings import square_wave
+from stimupy.components.waves import square
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
 w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
 w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
 
+w_base_type = iw.Dropdown(value="horizontal", options=['horizontal','vertical','rotated','radial','cityblock','angular'], description="base_type")
 w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
 w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
 w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
@@ -134,16 +139,17 @@ w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_phase, w_rot])
+b_geometry = iw.HBox([w_base_type, w_freq, w_phase, w_rot])
 b_intensities = iw.HBox([w_int1, w_int2])
 b_add = iw.HBox([w_ori, w_period, w_round, w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
 
 # Function for showing stim
-def show_square_wave(
+def show_square(
     height=None,
     width=None,
     ppd=None,
+    base_type=None,
     rotation=None,
     frequency=None,
     phase_shift=None,
@@ -154,13 +160,14 @@ def show_square_wave(
     period=None,
     add_mask=False,
 ):
-    stim = square_wave(
+    stim = square(
         visual_size=(height, width),
         ppd=ppd,
+        base_type=base_type,
         rotation=rotation,
         frequency=frequency,
         phase_shift=phase_shift,
-        intensity_bars=(int1, int2),
+        intensities=(int1, int2),
         origin=origin,
         round_phase_width=round_phase_width,
         period=period,
@@ -169,11 +176,12 @@ def show_square_wave(
 
 # Set interactivity
 out = iw.interactive_output(
-    show_square_wave,
+    show_square,
     {
         "height": w_height,
         "width": w_width,
         "ppd": w_ppd,
+        "base_type": w_base_type,
         "rotation": w_rot,
         "frequency": w_freq,
         "phase_shift": w_phase,
@@ -191,31 +199,34 @@ display(ui, out)
 ```
 
 ## Staircase
-{py:func}`stimupy.components.gratings.staircase`
+{py:func}`stimupy.components.waves.staircase`
 
 ```{code-cell} ipython3
-from stimupy.components.gratings import staircase
+from stimupy.components.waves import staircase
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
 w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
 w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
 
+w_base_type = iw.Dropdown(value="horizontal", options=['horizontal','vertical','rotated','radial','cityblock','angular'], description="base_type")
 w_freq = iw.FloatSlider(value=0.5, min=0, max=2, description="frequency [cpd]")
 w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
+w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
 
 w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int1")
 w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int2")
 
+w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
 w_period = iw.Dropdown(value="ignore", options=['ignore', 'even', 'odd', 'either'], description="period")
 w_round = iw.ToggleButton(value=False, disabled=False, description="round phase")
 w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_rot])
+b_geometry = iw.HBox([w_base_type, w_freq, w_phase, w_rot])
 b_intensities = iw.HBox([w_int1, w_int2])
-b_add = iw.HBox([w_period, w_round, w_mask])
+b_add = iw.HBox([w_ori, w_period, w_round, w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
 
 # Function for showing stim
@@ -223,22 +234,28 @@ def show_staircase(
     height=None,
     width=None,
     ppd=None,
+    base_type=None,
     rotation=None,
     frequency=None,
+    phase_shift=None,
     int1=None,
     int2=None,
     round_phase_width=False,
     period=None,
     add_mask=False,
+    origin=None,
 ):
     stim = staircase(
         visual_size=(height, width),
         ppd=ppd,
+        base_type=base_type,
         rotation=rotation,
         frequency=frequency,
-        intensity_bars=(int1, int2),
+        phase_shift=phase_shift,
+        intensities=(int1, int2),
         round_phase_width=round_phase_width,
         period=period,
+        origin=origin,
     )
     plot_stim(stim, mask=add_mask)
 
@@ -249,6 +266,7 @@ out = iw.interactive_output(
         "height": w_height,
         "width": w_width,
         "ppd": w_ppd,
+        "base_type": w_base_type,
         "rotation": w_rot,
         "frequency": w_freq,
         "int1": w_int1,
@@ -256,6 +274,8 @@ out = iw.interactive_output(
         "round_phase_width": w_round,
         "period": w_period,
         "add_mask": w_mask,
+        "phase_shift": w_phase,
+        "origin": w_ori,
     },
 )
 
@@ -263,11 +283,12 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Gabor
-{py:func}`stimupy.components.gratings.gabor`
+
+## Bessel
+{py:func}`stimupy.components.waves.bessel`
 
 ```{code-cell} ipython3
-from stimupy.components.gratings import gabor
+from stimupy.components.waves import bessel
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -275,203 +296,55 @@ w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
 w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
 
 w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
-w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
-w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
-w_sigma = iw.FloatSlider(value=2, min=0, max=4, description="sigma [deg]")
+w_order = iw.IntSlider(value=0, min=0, max=5, description="order")
 
-w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int1")
-w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int2")
+w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
+w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
 
 w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_period = iw.Dropdown(value="ignore", options=['ignore', 'even', 'odd', 'either'], description="period")
 w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_phase, w_rot, w_sigma])
+b_geometry = iw.HBox([w_freq, w_order])
 b_intensities = iw.HBox([w_int1, w_int2])
-b_add = iw.HBox([w_ori, w_period, w_mask])
+b_add = iw.HBox([w_ori, w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
 
 # Function for showing stim
-def show_gabor(
+def show_bessel(
     height=None,
     width=None,
     ppd=None,
-    rotation=None,
     frequency=None,
-    phase_shift=None,
-    sigma=None,
+    order=None,
     int1=None,
     int2=None,
     origin=None,
-    period=None,
     add_mask=False,
 ):
-    stim = gabor(
+    stim = bessel(
         visual_size=(height, width),
         ppd=ppd,
-        rotation=rotation,
         frequency=frequency,
-        phase_shift=phase_shift,
-        sigma=sigma,
-        intensity_bars=(int1, int2),
+        order=order,
+        intensities=(int1, int2),
         origin=origin,
-        period=period,
     )
     plot_stim(stim, mask=add_mask)
 
 # Set interactivity
 out = iw.interactive_output(
-    show_gabor,
+    show_bessel,
     {
         "height": w_height,
         "width": w_width,
         "ppd": w_ppd,
-        "rotation": w_rot,
         "frequency": w_freq,
-        "phase_shift": w_phase,
-        "sigma": w_sigma,
+        "order": w_order,
         "int1": w_int1,
         "int2": w_int2,
         "origin": w_ori,
-        "period": w_period,
-        "add_mask": w_mask,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Plaid
-{py:func}`stimupy.components.gratings.plaid`
-
-```{code-cell} ipython3
-from stimupy.components.gratings import plaid
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_period = iw.Dropdown(value="ignore", options=['ignore', 'even', 'odd', 'either'], description="period")
-w_round = iw.ToggleButton(value=False, disabled=False, description="round phase")
-w_mask = iw.Dropdown(value=False, options=[False, 'gaussian_mask', 'grating_mask', 'grating_mask2'], description="add mask")
-
-w_sigma = iw.FloatSlider(value=2, min=0, max=4, description="sigma [deg]")
-w_weight1 = iw.FloatSlider(value=1, min=0, max=1, description="weight1")
-w_weight2 = iw.FloatSlider(value=1, min=0, max=1, description="weight2")
-
-# Grating 1
-w_freq1 = iw.FloatSlider(value=1, min=0, max=2, description="frequency1 [cpd]")
-w_phase1 = iw.FloatSlider(value=0, min=0, max=360, description="phase shift1 [deg]")
-w_rot1 = iw.FloatSlider(value=0, min=0, max=360, description="rotation1 [deg]")
-
-w_int11 = iw.FloatSlider(value=1, min=0, max=1, description="int1-1")
-w_int12 = iw.FloatSlider(value=0, min=0, max=1, description="int1_2")
-
-# Grating 2
-w_freq2 = iw.FloatSlider(value=1, min=0, max=2, description="frequency2 [cpd]")
-w_phase2 = iw.FloatSlider(value=0, min=0, max=360, description="phase shift2 [deg]")
-w_rot2 = iw.FloatSlider(value=90, min=0, max=360, description="rotation2 [deg]")
-
-w_int21 = iw.FloatSlider(value=1, min=0, max=1, description="int2-1")
-w_int22 = iw.FloatSlider(value=0, min=0, max=1, description="int2_2")
-
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_weight = iw.HBox([w_sigma, w_weight1, w_weight2])
-b_add = iw.HBox([w_ori, w_period, w_round, w_mask])
-
-b_geometry1 = iw.HBox([w_freq1, w_phase1, w_rot1])
-b_intensities1 = iw.HBox([w_int11, w_int12])
-
-b_geometry2 = iw.HBox([w_freq2, w_phase2, w_rot2])
-b_intensities2 = iw.HBox([w_int21, w_int22])
-
-ui = iw.VBox([b_im_size, b_weight, b_add, b_geometry1, b_intensities1, b_geometry2, b_intensities2])
-
-# Function for showing stim
-def show_plaid(
-    height=None,
-    width=None,
-    ppd=None,
-    sigma=None,
-    weight1=None,
-    weight2=None,
-    rotation1=None,
-    frequency1=None,
-    phase_shift1=None,
-    int11=None,
-    int12=None,
-    rotation2=None,
-    frequency2=None,
-    phase_shift2=None,
-    int21=None,
-    int22=None,
-    origin=None,
-    round_phase_width=False,
-    period=None,
-    add_mask=False,
-):
-    p_common = {
-        "visual_size": (height, width),
-        "ppd": ppd,
-        "origin": origin,
-        "round_phase_width": round_phase_width,
-        "period": period,
-        
-    }
-    
-    p_grating1 = {
-        "frequency": frequency1,
-        "rotation": rotation1,
-        "phase_shift": phase_shift1,
-        "intensity_bars": (int11, int12),
-    }
-    
-    p_grating2 = {
-        "frequency": frequency2,
-        "rotation": rotation2,
-        "phase_shift": phase_shift2,
-        "intensity_bars": (int21, int22),
-    }
-
-    stim = plaid(
-        grating_parameters1={**p_common, **p_grating1},
-        grating_parameters2={**p_common, **p_grating2},
-        weight1=weight1,
-        weight2=weight2,
-        sigma=sigma,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_plaid,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "sigma": w_sigma,
-        "weight1": w_weight1,
-        "weight2": w_weight2,
-        "rotation1": w_rot1,
-        "frequency1": w_freq1,
-        "phase_shift1": w_phase1,
-        "int11": w_int11,
-        "int12": w_int12,
-        "rotation2": w_rot2,
-        "frequency2": w_freq2,
-        "phase_shift2": w_phase2,
-        "int21": w_int21,
-        "int22": w_int22,
-        "origin": w_ori,
-        "round_phase_width": w_round,
-        "period": w_period,
         "add_mask": w_mask,
     },
 )
diff --git a/docs/reference/demos/illusions/mondrians.md b/docs/reference/demos/illusions/mondrians.md
deleted file mode 100644
index d9a82977..00000000
--- a/docs/reference/demos/illusions/mondrians.md
+++ /dev/null
@@ -1,108 +0,0 @@
----
-jupytext:
-  formats: md:myst
-  text_representation:
-    extension: .md
-    format_name: myst
-    format_version: 0.13
-    jupytext_version: 1.14.5
-kernelspec:
-  display_name: Python 3 (ipykernel)
-  language: python
-  name: python3
----
-
-```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/mondrians.md)
- to get interactivity
-```
-
-# Illusions - Mondrians
-{py:mod}`stimupy.illusions.mondrians`
-
-```{code-cell} ipython3
-:tags: [remove-cell]
-
-import numpy as np
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
-
-## Corrugated Mondrians
-{py:func}`stimupy.illusions.mondrians.corrugated_mondrians`
-
-```{code-cell} ipython3
-from stimupy.illusions.mondrians import corrugated_mondrians
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=30, min=1, max=60, description="ppd")
-
-w_mdepth1 = iw.FloatSlider(value=-2., min=-2, max=2, description="mondrian depth 1")
-w_mdepth2 = iw.FloatSlider(value=0., min=-2, max=2, description="mondrian depth 2")
-w_mdepth3 = iw.FloatSlider(value=2., min=-2, max=2, description="mondrian depth 3")
-
-w_intback = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity_background")
-w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'mondrian_mask'], description="add mask")
-
-w_tidx1 = iw.IntSlider(value=1, min=0, max=10, description="target idx1")
-w_tidx2 = iw.IntSlider(value=1, min=0, max=10, description="target idx2")
-w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_depths = iw.HBox([w_mdepth1, w_mdepth2, w_mdepth3])
-b_target = iw.HBox([w_tidx1, w_tidx2, w_tint])
-b_add = iw.HBox([w_intback, w_mask])
-ui = iw.VBox([b_im_size, b_depths, b_target, b_add])
-
-# Function for showing stim
-def show_corrugated_mondrians(
-    height=None,
-    width=None,
-    ppd=None,
-    intensity_background=None,
-    add_mask=False,
-    depth1=None,
-    depth2=None,
-    depth3=None,
-    target_idx1=None,
-    target_idx2=None,
-    intensity_target=None,
-):
-    intensities = np.random.rand(3, 4)
-
-    stim = corrugated_mondrians(
-        visual_size=(height, width),
-        ppd=ppd,
-        mondrian_depths=(depth1, depth2, depth3),
-        mondrian_intensities=intensities,
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-        target_indices=((target_idx1, target_idx2),),
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_corrugated_mondrians,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "intensity_background": w_intback,
-        "add_mask": w_mask,
-        "depth1": w_mdepth1,
-        "depth2": w_mdepth2,
-        "depth3": w_mdepth3,
-        "target_idx1": w_tidx1,
-        "target_idx2": w_tidx2,
-        "intensity_target": w_tint,
-    },
-)
-
-# Show
-display(ui, out)
-```
diff --git a/docs/reference/demos/illusions/benarys.md b/docs/reference/demos/stimuli/benarys.md
similarity index 94%
rename from docs/reference/demos/illusions/benarys.md
rename to docs/reference/demos/stimuli/benarys.md
index 0ed1c949..9eae4062 100644
--- a/docs/reference/demos/illusions/benarys.md
+++ b/docs/reference/demos/stimuli/benarys.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/benarys.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/benarys.md)
  to get interactivity
 ```
 
-# Illusions - Benarys
-{py:mod}`stimupy.illusions.benarys`
+# Stimuli - Benary's cross
+{py:mod}`stimupy.stimuli.benarys`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -28,11 +28,11 @@ import ipywidgets as iw
 from stimupy.utils import plot_stim
 ```
 
-## Cross generalized
-{py:func}`stimupy.illusions.benarys.cross_generalized`
+## Cross, generalized
+{py:func}`stimupy.stimuli.benarys.cross_generalized`
 
 ```{code-cell} ipython3
-from stimupy.illusions.benarys import cross_generalized
+from stimupy.stimuli.benarys import cross_generalized
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -121,11 +121,11 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Cross rectangles
-{py:func}`stimupy.illusions.benarys.cross_rectangles`
+## Cross, rectangles
+{py:func}`stimupy.stimuli.benarys.cross_rectangles`
 
 ```{code-cell} ipython3
-from stimupy.illusions.benarys import cross_rectangles
+from stimupy.stimuli.benarys import cross_rectangles
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -197,10 +197,10 @@ display(ui, out)
 ```
 
 ## Cross triangles
-{py:func}`stimupy.illusions.benarys.cross_triangles`
+{py:func}`stimupy.stimuli.benarys.cross_triangles`
 
 ```{code-cell} ipython3
-from stimupy.illusions.benarys import cross_triangles
+from stimupy.stimuli.benarys import cross_triangles
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -269,10 +269,10 @@ display(ui, out)
 ```
 
 ## Todorovic generalized
-{py:func}`stimupy.illusions.benarys.todorovic_generalized`
+{py:func}`stimupy.stimuli.benarys.todorovic_generalized`
 
 ```{code-cell} ipython3
-from stimupy.illusions.benarys import todorovic_generalized
+from stimupy.stimuli.benarys import todorovic_generalized
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -362,10 +362,10 @@ display(ui, out)
 ```
 
 ## Todorovic rectangles
-{py:func}`stimupy.illusions.benarys.todorovic_rectangles`
+{py:func}`stimupy.stimuli.benarys.todorovic_rectangles`
 
 ```{code-cell} ipython3
-from stimupy.illusions.benarys import todorovic_rectangles
+from stimupy.stimuli.benarys import todorovic_rectangles
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -437,10 +437,10 @@ display(ui, out)
 ```
 
 ## Todorovic triangles
-{py:func}`stimupy.illusions.benarys.todorovic_triangles`
+{py:func}`stimupy.stimuli.benarys.todorovic_triangles`
 
 ```{code-cell} ipython3
-from stimupy.illusions.benarys import todorovic_triangles
+from stimupy.stimuli.benarys import todorovic_triangles
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/illusions/circulars.md b/docs/reference/demos/stimuli/bullseyes.md
similarity index 70%
rename from docs/reference/demos/illusions/circulars.md
rename to docs/reference/demos/stimuli/bullseyes.md
index 02333020..b3ed0928 100644
--- a/docs/reference/demos/illusions/circulars.md
+++ b/docs/reference/demos/stimuli/bullseyes.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/circulars.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/bullseyes.md)
  to get interactivity
 ```
 
-# Illusions - Circulars
-{py:mod}`stimupy.illusions.circulars`
+# Stimuli - Bullseyes
+{py:mod}`stimupy.stimuli.bullseyes`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -28,11 +28,11 @@ import ipywidgets as iw
 from stimupy.utils import plot_stim
 ```
 
-## Rings
-{py:func}`stimupy.illusions.circulars.rings`
+## Circular
+{py:func}`stimupy.stimuli.bullseyes.circular`
 
 ```{code-cell} ipython3
-from stimupy.illusions.circulars import rings
+from stimupy.stimuli.bullseyes import circular
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -50,7 +50,6 @@ w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], descript
 w_clip = iw.ToggleButton(value=False, disabled=False, description="clip")
 w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'ring_mask'], description="add mask")
 
-w_tidx = iw.IntSlider(value=0, min=0, max=10, description="target idx")
 w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 
 # Layout
@@ -58,11 +57,11 @@ b_im_size = iw.HBox([w_height, w_width, w_ppd])
 b_geometry = iw.HBox([w_freq, w_phase])
 b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
 b_add = iw.HBox([w_ori, w_clip, w_mask])
-b_target = iw.HBox([w_tidx, w_tint])
+b_target = iw.HBox([w_tint])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_rings(
+def show_bullseye(
     height=None,
     width=None,
     ppd=None,
@@ -74,10 +73,9 @@ def show_rings(
     origin=None,
     clip=False,
     add_mask=False,
-    target_indices=None,
     intensity_target=None,
 ):
-    stim = rings(
+    stim = circular(
         visual_size=(height, width),
         ppd=ppd,
         frequency=frequency,
@@ -86,14 +84,13 @@ def show_rings(
         intensity_background=intensity_background,
         origin=origin,
         clip=clip,
-        target_indices=target_indices,
         intensity_target=intensity_target,
     )
     plot_stim(stim, mask=add_mask)
 
 # Set interactivity
 out = iw.interactive_output(
-    show_rings,
+    show_bullseye,
     {
         "height": w_height,
         "width": w_width,
@@ -106,7 +103,6 @@ out = iw.interactive_output(
         "origin": w_ori,
         "clip": w_clip,
         "add_mask": w_mask,
-        "target_indices": w_tidx,
         "intensity_target": w_tint,
     },
 )
@@ -115,11 +111,12 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Two-sided rings
-{py:func}`stimupy.illusions.circulars.two_sided_rings`
+
+## Circular, Two-sided
+{py:func}`stimupy.stimuli.bullseyes.circular_two_sided`
 
 ```{code-cell} ipython3
-from stimupy.illusions.circulars import two_sided_rings
+from stimupy.stimuli.bullseyes import circular_two_sided
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -136,7 +133,6 @@ w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="int background
 w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
 w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'ring_mask'], description="add mask")
 
-w_tidx = iw.IntSlider(value=0, min=0, max=10, description="target idx")
 w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 
 # Layout
@@ -144,11 +140,11 @@ b_im_size = iw.HBox([w_height, w_width, w_ppd])
 b_geometry = iw.HBox([w_freq, w_phase])
 b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
 b_add = iw.HBox([w_ori, w_mask])
-b_target = iw.HBox([w_tidx, w_tint])
+b_target = iw.HBox([w_tint])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_two_sided_rings(
+def show_two_sided_bullseye(
     height=None,
     width=None,
     ppd=None,
@@ -159,10 +155,9 @@ def show_two_sided_rings(
     intensity_background=None,
     origin=None,
     add_mask=False,
-    target_indices=None,
     intensity_target=None,
 ):
-    stim = two_sided_rings(
+    stim = circular_two_sided(
         visual_size=(height, width),
         ppd=ppd,
         frequency=frequency,
@@ -170,14 +165,13 @@ def show_two_sided_rings(
         intensity_rings=(int1, int2),
         intensity_background=intensity_background,
         origin=origin,
-        target_indices=target_indices,
         intensity_target=intensity_target,
     )
     plot_stim(stim, mask=add_mask)
 
 # Set interactivity
 out = iw.interactive_output(
-    show_two_sided_rings,
+    show_two_sided_bullseye,
     {
         "height": w_height,
         "width": w_width,
@@ -189,7 +183,6 @@ out = iw.interactive_output(
         "intensity_background": w_int_back,
         "origin": w_ori,
         "add_mask": w_mask,
-        "target_indices": w_tidx,
         "intensity_target": w_tint,
     },
 )
@@ -198,11 +191,11 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Bullseye
-{py:func}`stimupy.illusions.circulars.bullseye`
+## Rectangular
+{py:func}`stimupy.stimuli.bullseyes.rectangular`
 
 ```{code-cell} ipython3
-from stimupy.illusions.circulars import bullseye
+from stimupy.stimuli.bullseyes import rectangular
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -214,11 +207,11 @@ w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]
 
 w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
 w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
-w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="int background")
+w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
 
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
+w_ori = iw.Dropdown(value="center", options=['mean', 'corner', 'center'], description="origin")
 w_clip = iw.ToggleButton(value=False, disabled=False, description="clip")
-w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'ring_mask'], description="add mask")
+w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
 
 w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 
@@ -226,12 +219,12 @@ w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
 b_geometry = iw.HBox([w_freq, w_phase])
 b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
-b_add = iw.HBox([w_ori, w_clip, w_mask])
 b_target = iw.HBox([w_tint])
+b_add = iw.HBox([w_ori, w_clip, w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_bullseye(
+def show_rectangular(
     height=None,
     width=None,
     ppd=None,
@@ -245,12 +238,12 @@ def show_bullseye(
     add_mask=False,
     intensity_target=None,
 ):
-    stim = bullseye(
+    stim = rectangular(
         visual_size=(height, width),
         ppd=ppd,
         frequency=frequency,
         phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
+        intensity_frames=(int1, int2),
         intensity_background=intensity_background,
         origin=origin,
         clip=clip,
@@ -260,7 +253,7 @@ def show_bullseye(
 
 # Set interactivity
 out = iw.interactive_output(
-    show_bullseye,
+    show_rectangular,
     {
         "height": w_height,
         "width": w_width,
@@ -281,26 +274,109 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Two-sided rings
-{py:func}`stimupy.illusions.circulars.two_sided_bullseye`
+## Bullseye-generalized
+{py:func}`stimupy.stimuli.bullseyes.rectangular_generalized`
 
 ```{code-cell} ipython3
-from stimupy.illusions.circulars import two_sided_bullseye
+from stimupy.stimuli.bullseyes import rectangular_generalized
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
 w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
 w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
 
+w_radius1 = iw.FloatSlider(value=1, min=0, max=2, description="radius1 [deg]")
+w_radius2 = iw.FloatSlider(value=2, min=1, max=3, description="radius2 [deg]")
+w_radius3 = iw.FloatSlider(value=3, min=2, max=4, description="radius2 [deg]")
+
+w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
+w_int2 = iw.FloatSlider(value=0.3, min=0, max=1, description="int-ring2")
+w_int3 = iw.FloatSlider(value=0.8, min=0, max=1, description="int-ring3")
+w_int_back = iw.FloatSlider(value=0., min=0, max=1, description="intensity background")
+
+w_ori = iw.Dropdown(value="center", options=['mean', 'corner', 'center'], description="origin")
+w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
+
+w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd])
+b_geometry = iw.HBox([w_radius1, w_radius2, w_radius3])
+b_intensities = iw.HBox([w_int1, w_int2, w_int3, w_int_back])
+b_target = iw.HBox([w_tint])
+b_add = iw.HBox([w_ori, w_mask])
+ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
+
+# Function for showing stim
+def show_rectangular_generalized(
+    height=None,
+    width=None,
+    ppd=None,
+    radius1=None,
+    radius2=None,
+    radius3=None,
+    int1=None,
+    int2=None,
+    int3=None,
+    intensity_background=None,
+    origin=None,
+    add_mask=False,
+    intensity_target=None,
+):
+    stim = rectangular_generalized(
+        visual_size=(height, width),
+        ppd=ppd,
+        radii=(radius1, radius2, radius3),
+        intensity_frames=(int1, int2, int3),
+        intensity_background=intensity_background,
+        origin=origin,
+        intensity_target=intensity_target,
+    )
+    plot_stim(stim, mask=add_mask)
+
+# Set interactivity
+out = iw.interactive_output(
+    show_rectangular_generalized,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "radius1": w_radius1,
+        "radius2": w_radius2,
+        "radius3": w_radius3,
+        "int1": w_int1,
+        "int2": w_int2,
+        "int3": w_int3,
+        "intensity_background": w_int_back,
+        "origin": w_ori,
+        "add_mask": w_mask,
+        "intensity_target": w_tint,
+    },
+)
+
+# Show
+display(ui, out)
+```
+
+## Two-sided-bullseye
+{py:func}`stimupy.stimuli.bullseyes.rectangular_two_sided`
+
+```{code-cell} ipython3
+from stimupy.stimuli.bullseyes import rectangular_two_sided
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=20, min=1, max=40, description="width [deg]")
+w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
+
 w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
 w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
 
 w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
 w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
-w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="int background")
+w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
 
-w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
-w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'ring_mask'], description="add mask")
+w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
 
 w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 
@@ -308,12 +384,12 @@ w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
 b_geometry = iw.HBox([w_freq, w_phase])
 b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
-b_add = iw.HBox([w_ori, w_mask])
 b_target = iw.HBox([w_tint])
+b_add = iw.HBox([w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_two_sided_bullseye(
+def show_rectangular_two_sided(
     height=None,
     width=None,
     ppd=None,
@@ -322,25 +398,23 @@ def show_two_sided_bullseye(
     int1=None,
     int2=None,
     intensity_background=None,
-    origin=None,
     add_mask=False,
     intensity_target=None,
 ):
-    stim = two_sided_bullseye(
+    stim = rectangular_two_sided(
         visual_size=(height, width),
         ppd=ppd,
         frequency=frequency,
         phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
+        intensity_frames=(int1, int2),
         intensity_background=intensity_background,
-        origin=origin,
         intensity_target=intensity_target,
     )
     plot_stim(stim, mask=add_mask)
 
 # Set interactivity
 out = iw.interactive_output(
-    show_two_sided_bullseye,
+    show_rectangular_two_sided,
     {
         "height": w_height,
         "width": w_width,
@@ -350,7 +424,6 @@ out = iw.interactive_output(
         "int1": w_int1,
         "int2": w_int2,
         "intensity_background": w_int_back,
-        "origin": w_ori,
         "add_mask": w_mask,
         "intensity_target": w_tint,
     },
diff --git a/docs/reference/demos/illusions/checkerboards.md b/docs/reference/demos/stimuli/checkerboards.md
similarity index 94%
rename from docs/reference/demos/illusions/checkerboards.md
rename to docs/reference/demos/stimuli/checkerboards.md
index ca984f1f..2a65af36 100644
--- a/docs/reference/demos/illusions/checkerboards.md
+++ b/docs/reference/demos/stimuli/checkerboards.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/checkerboards.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/checkerboards.md)
  to get interactivity
 ```
 
-# Illusions - Checkerboards
-{py:mod}`stimupy.illusions.checkerboards`
+# Stimuli - Checkerboards
+{py:mod}`stimupy.stimuli.checkerboards`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Checkerboard
-{py:func}`stimupy.illusions.checkerboards.checkerboard`
+{py:func}`stimupy.stimuli.checkerboards.checkerboard`
 
 ```{code-cell} ipython3
-from stimupy.illusions.checkerboards import checkerboard
+from stimupy.stimuli.checkerboards import checkerboard
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -123,10 +123,10 @@ display(ui, out)
 ```
 
 ## Contrast-contrast
-{py:func}`stimupy.illusions.checkerboards.contrast_contrast`
+{py:func}`stimupy.stimuli.checkerboards.contrast_contrast`
 
 ```{code-cell} ipython3
-from stimupy.illusions.checkerboards import contrast_contrast
+from stimupy.stimuli.checkerboards import contrast_contrast
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/illusions/cornsweets.md b/docs/reference/demos/stimuli/cornsweets.md
similarity index 92%
rename from docs/reference/demos/illusions/cornsweets.md
rename to docs/reference/demos/stimuli/cornsweets.md
index be03c17e..7da850ce 100644
--- a/docs/reference/demos/illusions/cornsweets.md
+++ b/docs/reference/demos/stimuli/cornsweets.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/cornsweets.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/cornsweets.md)
  to get interactivity
 ```
 
-# Illusions - Cornsweets
-{py:mod}`stimupy.illusions.cornsweets`
+# Stimuli - Cornsweets
+{py:mod}`stimupy.stimuli.cornsweets`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Cornsweet
-{py:func}`stimupy.illusions.cornsweets.cornsweet`
+{py:func}`stimupy.stimuli.cornsweets.cornsweet`
 
 ```{code-cell} ipython3
-from stimupy.illusions.cornsweets import cornsweet
+from stimupy.stimuli.cornsweets import cornsweet
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/illusions/cubes.md b/docs/reference/demos/stimuli/cubes.md
similarity index 94%
rename from docs/reference/demos/illusions/cubes.md
rename to docs/reference/demos/stimuli/cubes.md
index 1be29dcc..c0530573 100644
--- a/docs/reference/demos/illusions/cubes.md
+++ b/docs/reference/demos/stimuli/cubes.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/cubes.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/cubes.md)
  to get interactivity
 ```
 
-# Illusions - Cubes
-{py:mod}`stimupy.illusions.cubes`
+# Stimuli - Cubes
+{py:mod}`stimupy.stimuli.cubes`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Varying cells
-{py:func}`stimupy.illusions.cubes.varying_cells`
+{py:func}`stimupy.stimuli.cubes.varying_cells`
 
 ```{code-cell} ipython3
-from stimupy.illusions.cubes import varying_cells
+from stimupy.stimuli.cubes import varying_cells
 
 # Define widgets
 w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
@@ -113,10 +113,10 @@ display(ui, out)
 ```
 
 ## Cube
-{py:func}`stimupy.illusions.cubes.cube`
+{py:func}`stimupy.stimuli.cubes.cube`
 
 ```{code-cell} ipython3
-from stimupy.illusions.cubes import cube
+from stimupy.stimuli.cubes import cube
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/illusions/delboeufs.md b/docs/reference/demos/stimuli/delboeufs.md
similarity index 94%
rename from docs/reference/demos/illusions/delboeufs.md
rename to docs/reference/demos/stimuli/delboeufs.md
index 8c1a0d83..2f0f10aa 100644
--- a/docs/reference/demos/illusions/delboeufs.md
+++ b/docs/reference/demos/stimuli/delboeufs.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/delboeufs.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/delboeufs.md)
  to get interactivity
 ```
 
-# Illusions - Delboeufs
-{py:mod}`stimupy.illusions.delboeufs`
+# Stimuli - Delboeufs
+{py:mod}`stimupy.stimuli.delboeufs`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Delboeuf
-{py:func}`stimupy.illusions.delboeufs.delboeuf`
+{py:func}`stimupy.stimuli.delboeufs.delboeuf`
 
 ```{code-cell} ipython3
-from stimupy.illusions.delboeufs import delboeuf
+from stimupy.stimuli.delboeufs import delboeuf
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -105,10 +105,10 @@ display(ui, out)
 ```
 
 ## Two-sided
-{py:func}`stimupy.illusions.delboeufs.two_sided`
+{py:func}`stimupy.stimuli.delboeufs.two_sided`
 
 ```{code-cell} ipython3
-from stimupy.illusions.delboeufs import two_sided
+from stimupy.stimuli.delboeufs import two_sided
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/illusions/dungeons.md b/docs/reference/demos/stimuli/dungeons.md
similarity index 92%
rename from docs/reference/demos/illusions/dungeons.md
rename to docs/reference/demos/stimuli/dungeons.md
index ffa05978..facf3914 100644
--- a/docs/reference/demos/illusions/dungeons.md
+++ b/docs/reference/demos/stimuli/dungeons.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/dungeons.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/dungeons.md)
  to get interactivity
 ```
 
-# Illusions - Dungeons
-{py:mod}`stimupy.illusions.dungeons`
+# Stimuli - Dungeons
+{py:mod}`stimupy.stimuli.dungeons`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Dungeon
-{py:func}`stimupy.illusions.dungeons.dungeon`
+{py:func}`stimupy.stimuli.dungeons.dungeon`
 
 ```{code-cell} ipython3
-from stimupy.illusions.dungeons import dungeon
+from stimupy.stimuli.dungeons import dungeon
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/components/checkerboards.md b/docs/reference/demos/stimuli/gabors.md
similarity index 55%
rename from docs/reference/demos/components/checkerboards.md
rename to docs/reference/demos/stimuli/gabors.md
index d827f513..d8d325f6 100644
--- a/docs/reference/demos/components/checkerboards.md
+++ b/docs/reference/demos/stimuli/gabors.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/checkerboards.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/gabors.md)
  to get interactivity
 ```
 
-# Components - Checkerboards
-{py:mod}`stimupy.components.checkerboards`
+# Stimuli - Gabors
+{py:mod}`stimupy.stimuli.gabors`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -28,79 +28,84 @@ import ipywidgets as iw
 from stimupy.utils import plot_stim
 ```
 
-## Checkerboard
-{py:func}`stimupy.components.checkerboards.checkerboard`
+
+## Gabor
+{py:func}`stimupy.stimuli.gabors.gabor`
 
 ```{code-cell} ipython3
-from stimupy.components.checkerboards import checkerboard
+from stimupy.stimuli.gabors import gabor
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
 w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
 w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
 
-w_freq1 = iw.FloatSlider(value=1, min=0, max=3, description="frequency1 [cpd]")
-w_freq2 = iw.FloatSlider(value=1, min=0, max=3, description="frequency2 [cpd]")
-w_rot = iw.IntSlider(value=0, min=0, max=360, description="rotation [deg]")
+w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
+w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
+w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
+w_sigma = iw.FloatSlider(value=2, min=0, max=4, description="sigma [deg]")
 
-w_int1 = iw.FloatSlider(value=1., min=0, max=1, description="intensity1")
-w_int2 = iw.FloatSlider(value=0., min=0, max=1, description="intensity2")
+w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int1")
+w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int2")
 
+w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
 w_period = iw.Dropdown(value="ignore", options=['ignore', 'even', 'odd', 'either'], description="period")
-w_round = iw.ToggleButton(value=False, disabled=False, description="round check width")
 w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq1, w_freq2, w_rot])
+b_geometry = iw.HBox([w_freq, w_phase, w_rot, w_sigma])
 b_intensities = iw.HBox([w_int1, w_int2])
-b_add = iw.HBox([w_period, w_round, w_mask])
+b_add = iw.HBox([w_ori, w_period, w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_add])
 
 # Function for showing stim
-def show_checkerboard(
+def show_gabor(
     height=None,
     width=None,
     ppd=None,
-    rotation=0,
-    frequency1=None,
-    frequency2=None,
+    rotation=None,
+    frequency=None,
+    phase_shift=None,
+    sigma=None,
+    int1=None,
+    int2=None,
+    origin=None,
     period=None,
-    intensity1=None,
-    intensity2=None,
     add_mask=False,
-    round_phase_width=False,
 ):
-
-    stim = checkerboard(
+    stim = gabor(
         visual_size=(height, width),
         ppd=ppd,
-        frequency=(frequency1, frequency2),
-        period=period,
         rotation=rotation,
-        intensity_checks=(intensity1, intensity2),
-        round_phase_width=round_phase_width,
+        frequency=frequency,
+        phase_shift=phase_shift,
+        sigma=sigma,
+        intensities=(int1, int2),
+        origin=origin,
+        period=period,
     )
     plot_stim(stim, mask=add_mask)
 
 # Set interactivity
 out = iw.interactive_output(
-    show_checkerboard,
+    show_gabor,
     {
         "height": w_height,
         "width": w_width,
         "ppd": w_ppd,
         "rotation": w_rot,
-        "frequency1": w_freq1,
-        "frequency2": w_freq2,
+        "frequency": w_freq,
+        "phase_shift": w_phase,
+        "sigma": w_sigma,
+        "int1": w_int1,
+        "int2": w_int2,
+        "origin": w_ori,
         "period": w_period,
-        "intensity1": w_int1,
-        "intensity2": w_int2,
-        "round_phase_width": w_round,
         "add_mask": w_mask,
     },
 )
 
 # Show
 display(ui, out)
-```
+```
\ No newline at end of file
diff --git a/docs/reference/demos/illusions/gratings.md b/docs/reference/demos/stimuli/gratings.md
similarity index 54%
rename from docs/reference/demos/illusions/gratings.md
rename to docs/reference/demos/stimuli/gratings.md
index b6cfd260..49635d74 100644
--- a/docs/reference/demos/illusions/gratings.md
+++ b/docs/reference/demos/stimuli/gratings.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/gratings.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/gratings.md)
  to get interactivity
 ```
 
-# Illusions - Gratings
-{py:mod}`stimupy.illusions.gratings`
+# Stimuli - Gratings
+{py:mod}`stimupy.stimuli.gratings`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -28,102 +28,12 @@ import ipywidgets as iw
 from stimupy.utils import plot_stim
 ```
 
-## Square-wave
-{py:func}`stimupy.illusions.gratings.square_wave`
 
-```{code-cell} ipython3
-from stimupy.illusions.gratings import square_wave
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
-w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
-w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
-
-w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int1")
-w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int2")
-
-w_ori = iw.Dropdown(value="corner", options=['mean', 'corner', 'center'], description="origin")
-w_period = iw.Dropdown(value="ignore", options=['ignore', 'even', 'odd', 'either'], description="period")
-w_round = iw.ToggleButton(value=False, disabled=False, description="round phase")
-w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'grating_mask'], description="add mask")
-
-w_tidx = iw.IntSlider(value=1, min=1, max=10, description="target idx")
-w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_freq, w_phase, w_rot])
-b_intensities = iw.HBox([w_int1, w_int2])
-b_target = iw.HBox([w_tidx, w_tint])
-b_add = iw.HBox([w_ori, w_period, w_round, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
-
-# Function for showing stim
-def show_square_wave(
-    height=None,
-    width=None,
-    ppd=None,
-    rotation=None,
-    frequency=None,
-    phase_shift=None,
-    int1=None,
-    int2=None,
-    origin=None,
-    round_phase_width=False,
-    period=None,
-    add_mask=False,
-    target_indices=None,
-    intensity_target=None,
-):
-    stim = square_wave(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_bars=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_square_wave,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "rotation": w_rot,
-        "frequency": w_freq,
-        "phase_shift": w_phase,
-        "int1": w_int1,
-        "int2": w_int2,
-        "origin": w_ori,
-        "round_phase_width": w_round,
-        "period": w_period,
-        "add_mask": w_mask,
-        "target_indices": w_tidx,
-        "intensity_target": w_tint,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Uniform
-{py:func}`stimupy.illusions.gratings.uniform`
+## On uniform background
+{py:func}`stimupy.stimuli.gratings.on_uniform`
 
 ```{code-cell} ipython3
-from stimupy.illusions.gratings import uniform
+from stimupy.stimuli.gratings import on_uniform
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -159,7 +69,7 @@ b_add = iw.HBox([w_ori, w_period, w_round, w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_uniform(
+def show_on_uniform(
     height=None,
     width=None,
     ppd=None,
@@ -178,7 +88,7 @@ def show_uniform(
     grating_height=None,
     grating_width=None,
 ):
-    stim = uniform(
+    stim = on_uniform(
         visual_size=(height, width),
         ppd=ppd,
         rotation=rotation,
@@ -197,7 +107,7 @@ def show_uniform(
 
 # Set interactivity
 out = iw.interactive_output(
-    show_uniform,
+    show_on_uniform,
     {
         "height": w_height,
         "width": w_width,
@@ -222,7 +132,3 @@ out = iw.interactive_output(
 # Show
 display(ui, out)
 ```
-
-```{code-cell} ipython3
-
-```
diff --git a/docs/reference/demos/illusions/hermanns.md b/docs/reference/demos/stimuli/hermanns.md
similarity index 91%
rename from docs/reference/demos/illusions/hermanns.md
rename to docs/reference/demos/stimuli/hermanns.md
index f164eed1..5c6a877d 100644
--- a/docs/reference/demos/illusions/hermanns.md
+++ b/docs/reference/demos/stimuli/hermanns.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/hermanns.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/hermanns.md)
  to get interactivity
 ```
 
-# Illusions - Hermanns
-{py:mod}`stimupy.illusions.hermanns`
+# Stimuli - Hermanns
+{py:mod}`stimupy.stimuli.hermanns`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Grid
-{py:func}`stimupy.illusions.hermanns.grid`
+{py:func}`stimupy.stimuli.hermanns.grid`
 
 ```{code-cell} ipython3
-from stimupy.illusions.hermanns import grid
+from stimupy.stimuli.hermanns import grid
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/components/mondrians.md b/docs/reference/demos/stimuli/mondrians.md
similarity index 56%
rename from docs/reference/demos/components/mondrians.md
rename to docs/reference/demos/stimuli/mondrians.md
index 3600b1ed..32cabb2c 100644
--- a/docs/reference/demos/components/mondrians.md
+++ b/docs/reference/demos/stimuli/mondrians.md
@@ -13,26 +13,27 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/mondrians.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/mondrians.md)
  to get interactivity
 ```
 
-# Components - Mondrians
-{py:mod}`stimupy.components.mondrians`
+# Stimuli - Mondrians
+{py:mod}`stimupy.stimuli.mondrians`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
 
+import numpy as np
 import IPython
 import ipywidgets as iw
 from stimupy.utils import plot_stim
 ```
 
-## Mondrians
-{py:func}`stimupy.components.mondrians.mondrians`
+## Mondrian
+{py:func}`stimupy.stimuli.mondrians.mondrian`
 
 ```{code-cell} ipython3
-from stimupy.components.mondrians import mondrians
+from stimupy.stimuli.mondrians import mondrian
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -74,7 +75,7 @@ b_add = iw.HBox([w_intback, w_mask])
 ui = iw.VBox([b_im_size, b_x, b_y, b_h, b_w, b_int, b_add])
 
 # Function for showing stim
-def show_mondrians(
+def show_mondrian(
     height=None,
     width=None,
     ppd=None,
@@ -97,7 +98,7 @@ def show_mondrians(
     add_mask=False,
 ):
 
-    stim = mondrians(
+    stim = mondrian(
         visual_size=(height, width),
         ppd=ppd,
         mondrian_positions=((y1, x1), (y2, x2), (y3, x3)),
@@ -109,7 +110,7 @@ def show_mondrians(
 
 # Set interactivity
 out = iw.interactive_output(
-    show_mondrians,
+    show_mondrian,
     {
         "height": w_height,
         "width": w_width,
@@ -137,3 +138,82 @@ out = iw.interactive_output(
 # Show
 display(ui, out)
 ```
+
+
+## Corrugated Mondrian
+{py:func}`stimupy.stimuli.mondrians.corrugated`
+
+```{code-cell} ipython3
+from stimupy.stimuli.mondrians import corrugated_mondrian
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
+w_ppd = iw.IntSlider(value=30, min=1, max=60, description="ppd")
+
+w_mdepth1 = iw.FloatSlider(value=-2., min=-2, max=2, description="mondrian depth 1")
+w_mdepth2 = iw.FloatSlider(value=0., min=-2, max=2, description="mondrian depth 2")
+w_mdepth3 = iw.FloatSlider(value=2., min=-2, max=2, description="mondrian depth 3")
+
+w_intback = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity_background")
+w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'mondrian_mask'], description="add mask")
+
+w_tidx1 = iw.IntSlider(value=1, min=0, max=10, description="target idx1")
+w_tidx2 = iw.IntSlider(value=1, min=0, max=10, description="target idx2")
+w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd])
+b_depths = iw.HBox([w_mdepth1, w_mdepth2, w_mdepth3])
+b_target = iw.HBox([w_tidx1, w_tidx2, w_tint])
+b_add = iw.HBox([w_intback, w_mask])
+ui = iw.VBox([b_im_size, b_depths, b_target, b_add])
+
+# Function for showing stim
+def show_corrugated_mondrian(
+    height=None,
+    width=None,
+    ppd=None,
+    intensity_background=None,
+    add_mask=False,
+    depth1=None,
+    depth2=None,
+    depth3=None,
+    target_idx1=None,
+    target_idx2=None,
+    intensity_target=None,
+):
+    intensities = np.random.rand(3, 4)
+
+    stim = corrugated_mondrian(
+        visual_size=(height, width),
+        ppd=ppd,
+        mondrian_depths=(depth1, depth2, depth3),
+        mondrian_intensities=intensities,
+        intensity_background=intensity_background,
+        intensity_target=intensity_target,
+        target_indices=((target_idx1, target_idx2),),
+    )
+    plot_stim(stim, mask=add_mask)
+
+# Set interactivity
+out = iw.interactive_output(
+    show_corrugated_mondrian,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "intensity_background": w_intback,
+        "add_mask": w_mask,
+        "depth1": w_mdepth1,
+        "depth2": w_mdepth2,
+        "depth3": w_mdepth3,
+        "target_idx1": w_tidx1,
+        "target_idx2": w_tidx2,
+        "intensity_target": w_tint,
+    },
+)
+
+# Show
+display(ui, out)
+```
diff --git a/docs/reference/demos/illusions/mueller_lyers.md b/docs/reference/demos/stimuli/mueller_lyers.md
similarity index 94%
rename from docs/reference/demos/illusions/mueller_lyers.md
rename to docs/reference/demos/stimuli/mueller_lyers.md
index bd97de9b..4146c370 100644
--- a/docs/reference/demos/illusions/mueller_lyers.md
+++ b/docs/reference/demos/stimuli/mueller_lyers.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/mueller_lyers.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/mueller_lyers.md)
  to get interactivity
 ```
 
-# Illusions - Mueller-Lyers
-{py:mod}`stimupy.illusions.mueller_lyers`
+# Stimuli - Mueller-Lyers
+{py:mod}`stimupy.stimuli.mueller_lyers`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Mueller-Lyer
-{py:func}`stimupy.illusions.mueller_lyers.mueller_lyer`
+{py:func}`stimupy.stimuli.mueller_lyers.mueller_lyer`
 
 ```{code-cell} ipython3
-from stimupy.illusions.mueller_lyers import mueller_lyer
+from stimupy.stimuli.mueller_lyers import mueller_lyer
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -109,10 +109,10 @@ display(ui, out)
 ```
 
 ## Two-sided
-{py:func}`stimupy.illusions.delboeufs.two_sided`
+{py:func}`stimupy.stimuli.delboeufs.two_sided`
 
 ```{code-cell} ipython3
-from stimupy.illusions.mueller_lyers import two_sided
+from stimupy.stimuli.mueller_lyers import two_sided
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/illusions/angulars.md b/docs/reference/demos/stimuli/pinwheels.md
similarity index 94%
rename from docs/reference/demos/illusions/angulars.md
rename to docs/reference/demos/stimuli/pinwheels.md
index 9974da52..3fa56e2c 100644
--- a/docs/reference/demos/illusions/angulars.md
+++ b/docs/reference/demos/stimuli/pinwheels.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/angulars.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/angulars.md)
  to get interactivity
 ```
 
-# Illusions - Angulars
-{py:mod}`stimupy.illusions.angulars`
+# Stimuli - Pinwheels
+{py:mod}`stimupy.stimuli.pinwheels`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Pinwheel
-{py:func}`stimupy.illusions.angulars.pinwheel`
+{py:func}`stimupy.stimuli.pinwheels.pinwheel`
 
 ```{code-cell} ipython3
-from stimupy.illusions.angulars import pinwheel
+from stimupy.stimuli.pinwheels import pinwheel
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/stimuli/plaids.md b/docs/reference/demos/stimuli/plaids.md
new file mode 100644
index 00000000..ffddf352
--- /dev/null
+++ b/docs/reference/demos/stimuli/plaids.md
@@ -0,0 +1,165 @@
+---
+jupytext:
+  formats: md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+    jupytext_version: 1.14.5
+kernelspec:
+  display_name: Python 3 (ipykernel)
+  language: python
+  name: python3
+---
+
+```{important}
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/plaids.md)
+ to get interactivity
+```
+
+# Stimuli - Plaids
+{py:mod}`stimupy.stimuli.plaids`
+
+```{code-cell} ipython3
+:tags: [remove-cell]
+
+import IPython
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+```
+
+## Plaid
+{py:func}`stimupy.stimuli.plaids.plaid`
+
+```{code-cell} ipython3
+from stimupy.stimuli.plaids import plaid
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
+w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
+
+w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
+w_period = iw.Dropdown(value="ignore", options=['ignore', 'even', 'odd', 'either'], description="period")
+w_round = iw.ToggleButton(value=False, disabled=False, description="round phase")
+w_mask = iw.Dropdown(value=False, options=[False, 'gaussian_mask', 'grating_mask', 'grating_mask2'], description="add mask")
+
+w_sigma = iw.FloatSlider(value=2, min=0, max=4, description="sigma [deg]")
+w_weight1 = iw.FloatSlider(value=1, min=0, max=1, description="weight1")
+w_weight2 = iw.FloatSlider(value=1, min=0, max=1, description="weight2")
+
+# Grating 1
+w_freq1 = iw.FloatSlider(value=1, min=0, max=2, description="frequency1 [cpd]")
+w_phase1 = iw.FloatSlider(value=0, min=0, max=360, description="phase shift1 [deg]")
+w_rot1 = iw.FloatSlider(value=0, min=0, max=360, description="rotation1 [deg]")
+
+w_int11 = iw.FloatSlider(value=1, min=0, max=1, description="int1-1")
+w_int12 = iw.FloatSlider(value=0, min=0, max=1, description="int1_2")
+
+# Grating 2
+w_freq2 = iw.FloatSlider(value=1, min=0, max=2, description="frequency2 [cpd]")
+w_phase2 = iw.FloatSlider(value=0, min=0, max=360, description="phase shift2 [deg]")
+w_rot2 = iw.FloatSlider(value=90, min=0, max=360, description="rotation2 [deg]")
+
+w_int21 = iw.FloatSlider(value=1, min=0, max=1, description="int2-1")
+w_int22 = iw.FloatSlider(value=0, min=0, max=1, description="int2_2")
+
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd])
+b_weight = iw.HBox([w_sigma, w_weight1, w_weight2])
+b_add = iw.HBox([w_ori, w_period, w_round, w_mask])
+
+b_geometry1 = iw.HBox([w_freq1, w_phase1, w_rot1])
+b_intensities1 = iw.HBox([w_int11, w_int12])
+
+b_geometry2 = iw.HBox([w_freq2, w_phase2, w_rot2])
+b_intensities2 = iw.HBox([w_int21, w_int22])
+
+ui = iw.VBox([b_im_size, b_weight, b_add, b_geometry1, b_intensities1, b_geometry2, b_intensities2])
+
+# Function for showing stim
+def show_plaid(
+    height=None,
+    width=None,
+    ppd=None,
+    sigma=None,
+    weight1=None,
+    weight2=None,
+    rotation1=None,
+    frequency1=None,
+    phase_shift1=None,
+    int11=None,
+    int12=None,
+    rotation2=None,
+    frequency2=None,
+    phase_shift2=None,
+    int21=None,
+    int22=None,
+    origin=None,
+    round_phase_width=False,
+    period=None,
+    add_mask=False,
+):
+    p_common = {
+        "visual_size": (height, width),
+        "ppd": ppd,
+        "origin": origin,
+        "round_phase_width": round_phase_width,
+        "period": period,
+        "base_type": "rotated",
+    }
+    
+    p_grating1 = {
+        "frequency": frequency1,
+        "rotation": rotation1,
+        "phase_shift": phase_shift1,
+        "intensities": (int11, int12),
+    }
+    
+    p_grating2 = {
+        "frequency": frequency2,
+        "rotation": rotation2,
+        "phase_shift": phase_shift2,
+        "intensities": (int21, int22),
+    }
+
+    stim = plaid(
+        grating_parameters1={**p_common, **p_grating1},
+        grating_parameters2={**p_common, **p_grating2},
+        weight1=weight1,
+        weight2=weight2,
+        sigma=sigma,
+    )
+    plot_stim(stim, mask=add_mask)
+
+# Set interactivity
+out = iw.interactive_output(
+    show_plaid,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "sigma": w_sigma,
+        "weight1": w_weight1,
+        "weight2": w_weight2,
+        "rotation1": w_rot1,
+        "frequency1": w_freq1,
+        "phase_shift1": w_phase1,
+        "int11": w_int11,
+        "int12": w_int12,
+        "rotation2": w_rot2,
+        "frequency2": w_freq2,
+        "phase_shift2": w_phase2,
+        "int21": w_int21,
+        "int22": w_int22,
+        "origin": w_ori,
+        "round_phase_width": w_round,
+        "period": w_period,
+        "add_mask": w_mask,
+    },
+)
+
+# Show
+display(ui, out)
+```
\ No newline at end of file
diff --git a/docs/reference/demos/illusions/ponzos.md b/docs/reference/demos/stimuli/ponzos.md
similarity index 94%
rename from docs/reference/demos/illusions/ponzos.md
rename to docs/reference/demos/stimuli/ponzos.md
index 818de7e4..2362df36 100644
--- a/docs/reference/demos/illusions/ponzos.md
+++ b/docs/reference/demos/stimuli/ponzos.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/ponzos.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/ponzos.md)
  to get interactivity
 ```
 
-# Illusions - Ponzos
-{py:mod}`stimupy.illusions.ponzos`
+# Stimuli - Ponzos
+{py:mod}`stimupy.stimuli.ponzos`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Ponzo
-{py:func}`stimupy.illusions.ponzos.ponzo`
+{py:func}`stimupy.stimuli.ponzos.ponzo`
 
 ```{code-cell} ipython3
-from stimupy.illusions.ponzos import ponzo
+from stimupy.stimuli.ponzos import ponzo
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/docs/reference/demos/illusions/frames.md b/docs/reference/demos/stimuli/rings.md
similarity index 74%
rename from docs/reference/demos/illusions/frames.md
rename to docs/reference/demos/stimuli/rings.md
index d97bfbc4..df335cdb 100644
--- a/docs/reference/demos/illusions/frames.md
+++ b/docs/reference/demos/stimuli/rings.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/frames.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/rings.md)
  to get interactivity
 ```
 
-# Illusions - Frames
-{py:mod}`stimupy.illusions.frames`
+# Stimuli - Rings
+{py:mod}`stimupy.stimuli.rings`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -28,11 +28,11 @@ import ipywidgets as iw
 from stimupy.utils import plot_stim
 ```
 
-## Rings
-{py:func}`stimupy.illusions.frames.rings`
+## Circular
+{py:func}`stimupy.stimuli.rings.circular`
 
 ```{code-cell} ipython3
-from stimupy.illusions.frames import rings
+from stimupy.stimuli.rings import circular
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -44,25 +44,25 @@ w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]
 
 w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
 w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
-w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
+w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="int background")
 
-w_ori = iw.Dropdown(value="center", options=['mean', 'corner', 'center'], description="origin")
+w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
 w_clip = iw.ToggleButton(value=False, disabled=False, description="clip")
-w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
+w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'ring_mask'], description="add mask")
 
-w_tidx = iw.IntSlider(value=1, min=0, max=10, description="target idx")
+w_tidx = iw.IntSlider(value=2, min=0, max=10, description="target idx")
 w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
 b_geometry = iw.HBox([w_freq, w_phase])
 b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
-b_target = iw.HBox([w_tidx, w_tint])
 b_add = iw.HBox([w_ori, w_clip, w_mask])
+b_target = iw.HBox([w_tidx, w_tint])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_rings(
+def show_circular(
     height=None,
     width=None,
     ppd=None,
@@ -77,12 +77,12 @@ def show_rings(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = rings(
+    stim = circular(
         visual_size=(height, width),
         ppd=ppd,
         frequency=frequency,
         phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
+        intensity_rings=(int1, int2),
         intensity_background=intensity_background,
         origin=origin,
         clip=clip,
@@ -93,7 +93,7 @@ def show_rings(
 
 # Set interactivity
 out = iw.interactive_output(
-    show_rings,
+    show_circular,
     {
         "height": w_height,
         "width": w_width,
@@ -106,94 +106,6 @@ out = iw.interactive_output(
         "origin": w_ori,
         "clip": w_clip,
         "add_mask": w_mask,
-        "intensity_target": w_tint,
-        "target_indices": w_tidx,
-    },
-)
-
-# Show
-display(ui, out)
-```
-
-## Rings-generalized
-{py:func}`stimupy.illusions.frames.rings_generalized`
-
-```{code-cell} ipython3
-from stimupy.illusions.frames import rings_generalized
-
-# Define widgets
-w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
-w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
-
-w_radius1 = iw.FloatSlider(value=1, min=0, max=2, description="radius1 [deg]")
-w_radius2 = iw.FloatSlider(value=2, min=1, max=3, description="radius2 [deg]")
-w_radius3 = iw.FloatSlider(value=3, min=2, max=4, description="radius2 [deg]")
-
-w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
-w_int2 = iw.FloatSlider(value=0.3, min=0, max=1, description="int-ring2")
-w_int3 = iw.FloatSlider(value=0.8, min=0, max=1, description="int-ring3")
-w_int_back = iw.FloatSlider(value=0., min=0, max=1, description="intensity background")
-
-w_ori = iw.Dropdown(value="center", options=['mean', 'corner', 'center'], description="origin")
-w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
-
-w_tidx = iw.IntSlider(value=1, min=1, max=4, description="target idx")
-w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
-
-# Layout
-b_im_size = iw.HBox([w_height, w_width, w_ppd])
-b_geometry = iw.HBox([w_radius1, w_radius2, w_radius3])
-b_intensities = iw.HBox([w_int1, w_int2, w_int3, w_int_back])
-b_target = iw.HBox([w_tidx, w_tint])
-b_add = iw.HBox([w_ori, w_mask])
-ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
-
-# Function for showing stim
-def show_rings_generalized(
-    height=None,
-    width=None,
-    ppd=None,
-    radius1=None,
-    radius2=None,
-    radius3=None,
-    int1=None,
-    int2=None,
-    int3=None,
-    intensity_background=None,
-    origin=None,
-    add_mask=False,
-    intensity_target=None,
-    target_indices=None,
-):
-    stim = rings_generalized(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2, radius3),
-        intensity_frames=(int1, int2, int3),
-        intensity_background=intensity_background,
-        origin=origin,
-        intensity_target=intensity_target,
-        target_indices=target_indices,
-    )
-    plot_stim(stim, mask=add_mask)
-
-# Set interactivity
-out = iw.interactive_output(
-    show_rings_generalized,
-    {
-        "height": w_height,
-        "width": w_width,
-        "ppd": w_ppd,
-        "radius1": w_radius1,
-        "radius2": w_radius2,
-        "radius3": w_radius3,
-        "int1": w_int1,
-        "int2": w_int2,
-        "int3": w_int3,
-        "intensity_background": w_int_back,
-        "origin": w_ori,
-        "add_mask": w_mask,
         "target_indices": w_tidx,
         "intensity_target": w_tint,
     },
@@ -203,15 +115,15 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Two-sided-rings
-{py:func}`stimupy.illusions.frames.two_sided_rings`
+## Two-sided rings
+{py:func}`stimupy.stimuli.rings.circular_two_sided`
 
 ```{code-cell} ipython3
-from stimupy.illusions.frames import two_sided_rings
+from stimupy.stimuli.rings import circular_two_sided
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
-w_width = iw.IntSlider(value=20, min=1, max=40, description="width [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
 w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
 
 w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
@@ -219,23 +131,24 @@ w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]
 
 w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
 w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
-w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
+w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="int background")
 
-w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
+w_ori = iw.Dropdown(value="mean", options=['mean', 'corner', 'center'], description="origin")
+w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'ring_mask'], description="add mask")
 
-w_tidx = iw.IntSlider(value=1, min=0, max=10, description="target idx")
+w_tidx = iw.IntSlider(value=2, min=0, max=10, description="target idx")
 w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
 b_geometry = iw.HBox([w_freq, w_phase])
 b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
+b_add = iw.HBox([w_ori, w_mask])
 b_target = iw.HBox([w_tidx, w_tint])
-b_add = iw.HBox([w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_two_sided_rings(
+def show_circular_two_sided(
     height=None,
     width=None,
     ppd=None,
@@ -244,17 +157,19 @@ def show_two_sided_rings(
     int1=None,
     int2=None,
     intensity_background=None,
+    origin=None,
     add_mask=False,
     target_indices=None,
     intensity_target=None,
 ):
-    stim = two_sided_rings(
+    stim = circular_two_sided(
         visual_size=(height, width),
         ppd=ppd,
         frequency=frequency,
         phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
+        intensity_rings=(int1, int2),
         intensity_background=intensity_background,
+        origin=origin,
         target_indices=target_indices,
         intensity_target=intensity_target,
     )
@@ -262,7 +177,7 @@ def show_two_sided_rings(
 
 # Set interactivity
 out = iw.interactive_output(
-    show_two_sided_rings,
+    show_circular_two_sided,
     {
         "height": w_height,
         "width": w_width,
@@ -272,9 +187,10 @@ out = iw.interactive_output(
         "int1": w_int1,
         "int2": w_int2,
         "intensity_background": w_int_back,
+        "origin": w_ori,
         "add_mask": w_mask,
-        "intensity_target": w_tint,
         "target_indices": w_tidx,
+        "intensity_target": w_tint,
     },
 )
 
@@ -282,11 +198,11 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Bullseye
-{py:func}`stimupy.illusions.frames.bullseye`
+## Rectangular
+{py:func}`stimupy.stimuli.rings.rectangular`
 
 ```{code-cell} ipython3
-from stimupy.illusions.frames import bullseye
+from stimupy.stimuli.rings import rectangular
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -304,18 +220,19 @@ w_ori = iw.Dropdown(value="center", options=['mean', 'corner', 'center'], descri
 w_clip = iw.ToggleButton(value=False, disabled=False, description="clip")
 w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
 
+w_tidx = iw.IntSlider(value=1, min=0, max=10, description="target idx")
 w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
 b_geometry = iw.HBox([w_freq, w_phase])
 b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
-b_target = iw.HBox([w_tint])
+b_target = iw.HBox([w_tidx, w_tint])
 b_add = iw.HBox([w_ori, w_clip, w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_bullseye(
+def show_rectangular(
     height=None,
     width=None,
     ppd=None,
@@ -327,9 +244,10 @@ def show_bullseye(
     origin=None,
     clip=False,
     add_mask=False,
+    target_indices=None,
     intensity_target=None,
 ):
-    stim = bullseye(
+    stim = rectangular(
         visual_size=(height, width),
         ppd=ppd,
         frequency=frequency,
@@ -338,13 +256,14 @@ def show_bullseye(
         intensity_background=intensity_background,
         origin=origin,
         clip=clip,
+        target_indices=target_indices,
         intensity_target=intensity_target,
     )
     plot_stim(stim, mask=add_mask)
 
 # Set interactivity
 out = iw.interactive_output(
-    show_bullseye,
+    show_rectangular,
     {
         "height": w_height,
         "width": w_width,
@@ -358,6 +277,7 @@ out = iw.interactive_output(
         "clip": w_clip,
         "add_mask": w_mask,
         "intensity_target": w_tint,
+        "target_indices": w_tidx,
     },
 )
 
@@ -365,11 +285,11 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Bullseye-generalized
-{py:func}`stimupy.illusions.frames.bullseye_generalized`
+## Rectangular, generalized
+{py:func}`stimupy.stimuli.rings.rectangular_generalized`
 
 ```{code-cell} ipython3
-from stimupy.illusions.frames import bullseye_generalized
+from stimupy.stimuli.rings import rectangular_generalized
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -388,18 +308,19 @@ w_int_back = iw.FloatSlider(value=0., min=0, max=1, description="intensity backg
 w_ori = iw.Dropdown(value="center", options=['mean', 'corner', 'center'], description="origin")
 w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
 
+w_tidx = iw.IntSlider(value=1, min=1, max=4, description="target idx")
 w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
 b_geometry = iw.HBox([w_radius1, w_radius2, w_radius3])
 b_intensities = iw.HBox([w_int1, w_int2, w_int3, w_int_back])
-b_target = iw.HBox([w_tint])
+b_target = iw.HBox([w_tidx, w_tint])
 b_add = iw.HBox([w_ori, w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_bullseye_generalized(
+def show_rectangular_generalized(
     height=None,
     width=None,
     ppd=None,
@@ -413,8 +334,9 @@ def show_bullseye_generalized(
     origin=None,
     add_mask=False,
     intensity_target=None,
+    target_indices=None,
 ):
-    stim = bullseye_generalized(
+    stim = rectangular_generalized(
         visual_size=(height, width),
         ppd=ppd,
         radii=(radius1, radius2, radius3),
@@ -422,12 +344,13 @@ def show_bullseye_generalized(
         intensity_background=intensity_background,
         origin=origin,
         intensity_target=intensity_target,
+        target_indices=target_indices,
     )
     plot_stim(stim, mask=add_mask)
 
 # Set interactivity
 out = iw.interactive_output(
-    show_bullseye_generalized,
+    show_rectangular_generalized,
     {
         "height": w_height,
         "width": w_width,
@@ -441,6 +364,7 @@ out = iw.interactive_output(
         "intensity_background": w_int_back,
         "origin": w_ori,
         "add_mask": w_mask,
+        "target_indices": w_tidx,
         "intensity_target": w_tint,
     },
 )
@@ -449,11 +373,11 @@ out = iw.interactive_output(
 display(ui, out)
 ```
 
-## Two-sided-bullseye
-{py:func}`stimupy.illusions.frames.two_sided_bullseye`
+## Two-sided-rings
+{py:func}`stimupy.stimuli.rings.rectangular_two_sided`
 
 ```{code-cell} ipython3
-from stimupy.illusions.frames import two_sided_bullseye
+from stimupy.stimuli.rings import rectangular_two_sided
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
@@ -469,18 +393,19 @@ w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity back
 
 w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
 
+w_tidx = iw.IntSlider(value=1, min=0, max=10, description="target idx")
 w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
 
 # Layout
 b_im_size = iw.HBox([w_height, w_width, w_ppd])
 b_geometry = iw.HBox([w_freq, w_phase])
 b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
-b_target = iw.HBox([w_tint])
+b_target = iw.HBox([w_tidx, w_tint])
 b_add = iw.HBox([w_mask])
 ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
 
 # Function for showing stim
-def show_two_sided_bullseye(
+def show_rectangular_two_sided(
     height=None,
     width=None,
     ppd=None,
@@ -490,22 +415,24 @@ def show_two_sided_bullseye(
     int2=None,
     intensity_background=None,
     add_mask=False,
+    target_indices=None,
     intensity_target=None,
 ):
-    stim = two_sided_bullseye(
+    stim = rectangular_two_sided(
         visual_size=(height, width),
         ppd=ppd,
         frequency=frequency,
         phase_shift=phase_shift,
         intensity_frames=(int1, int2),
         intensity_background=intensity_background,
+        target_indices=target_indices,
         intensity_target=intensity_target,
     )
     plot_stim(stim, mask=add_mask)
 
 # Set interactivity
 out = iw.interactive_output(
-    show_two_sided_bullseye,
+    show_rectangular_two_sided,
     {
         "height": w_height,
         "width": w_width,
@@ -517,9 +444,10 @@ out = iw.interactive_output(
         "intensity_background": w_int_back,
         "add_mask": w_mask,
         "intensity_target": w_tint,
+        "target_indices": w_tidx,
     },
 )
 
 # Show
 display(ui, out)
-```
+```
\ No newline at end of file
diff --git a/docs/reference/demos/stimuli/waves.md b/docs/reference/demos/stimuli/waves.md
new file mode 100644
index 00000000..6b5b7e9d
--- /dev/null
+++ b/docs/reference/demos/stimuli/waves.md
@@ -0,0 +1,135 @@
+---
+jupytext:
+  formats: md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+    jupytext_version: 1.14.5
+kernelspec:
+  display_name: Python 3 (ipykernel)
+  language: python
+  name: python3
+---
+
+```{important}
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/waves.md)
+ to get interactivity
+```
+
+# Stimuli - Waves
+{py:mod}`stimupy.stimuli.waves`
+
+```{code-cell} ipython3
+:tags: [remove-cell]
+
+import numpy as np
+import IPython
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+```
+
+## Sine, linear
+
+## Square, linear
+{py:func}`stimupy.stimuli.waves.square_linear`
+
+```{code-cell} ipython3
+from stimupy.stimuli.waves import square_linear
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
+w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
+
+w_freq = iw.FloatSlider(value=1, min=0, max=2, description="frequency [cpd]")
+w_phase = iw.FloatSlider(value=0, min=0, max=360, description="phase shift [deg]")
+w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
+
+w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int1")
+w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int2")
+
+w_ori = iw.Dropdown(value="corner", options=['mean', 'corner', 'center'], description="origin")
+w_period = iw.Dropdown(value="ignore", options=['ignore', 'even', 'odd', 'either'], description="period")
+w_round = iw.ToggleButton(value=False, disabled=False, description="round phase")
+w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'grating_mask'], description="add mask")
+
+w_tidx = iw.IntSlider(value=1, min=1, max=10, description="target idx")
+w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd])
+b_geometry = iw.HBox([w_freq, w_phase, w_rot])
+b_intensities = iw.HBox([w_int1, w_int2])
+b_target = iw.HBox([w_tidx, w_tint])
+b_add = iw.HBox([w_ori, w_period, w_round, w_mask])
+ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
+
+# Function for showing stim
+def show_square_linear(
+    height=None,
+    width=None,
+    ppd=None,
+    rotation=None,
+    frequency=None,
+    phase_shift=None,
+    int1=None,
+    int2=None,
+    origin=None,
+    round_phase_width=False,
+    period=None,
+    add_mask=False,
+    target_indices=None,
+    intensity_target=None,
+):
+    stim = square_linear(
+        visual_size=(height, width),
+        ppd=ppd,
+        rotation=rotation,
+        frequency=frequency,
+        phase_shift=phase_shift,
+        intensity_bars=(int1, int2),
+        origin=origin,
+        round_phase_width=round_phase_width,
+        period=period,
+        target_indices=target_indices,
+        intensity_target=intensity_target,
+    )
+    plot_stim(stim, mask=add_mask)
+
+# Set interactivity
+out = iw.interactive_output(
+    show_square_linear,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "rotation": w_rot,
+        "frequency": w_freq,
+        "phase_shift": w_phase,
+        "int1": w_int1,
+        "int2": w_int2,
+        "origin": w_ori,
+        "round_phase_width": w_round,
+        "period": w_period,
+        "add_mask": w_mask,
+        "target_indices": w_tidx,
+        "intensity_target": w_tint,
+    },
+)
+
+# Show
+display(ui, out)
+```
+
+## Sine, radial
+
+## Square, radial
+
+## Sine, cityblock
+
+## Square, cityblock
+
+## Sine, angular
+
+## Square, angular
\ No newline at end of file
diff --git a/docs/reference/demos/illusions/wedding_cakes.md b/docs/reference/demos/stimuli/wedding_cakes.md
similarity index 93%
rename from docs/reference/demos/illusions/wedding_cakes.md
rename to docs/reference/demos/stimuli/wedding_cakes.md
index 82a9114e..359057f8 100644
--- a/docs/reference/demos/illusions/wedding_cakes.md
+++ b/docs/reference/demos/stimuli/wedding_cakes.md
@@ -13,12 +13,12 @@ kernelspec:
 ---
 
 ```{important}
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/illusions/wedding_cakes.md)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/wedding_cakes.md)
  to get interactivity
 ```
 
-# Illusions - Wedding cakes
-{py:mod}`stimupy.illusions.wedding_cakes`
+# Stimuli - Wedding cakes
+{py:mod}`stimupy.stimuli.wedding_cakes`
 
 ```{code-cell} ipython3
 :tags: [remove-cell]
@@ -29,10 +29,10 @@ from stimupy.utils import plot_stim
 ```
 
 ## Wedding cake
-{py:func}`stimupy.illusions.wedding_cakes.wedding_cake`
+{py:func}`stimupy.stimuli.wedding_cakes.wedding_cake`
 
 ```{code-cell} ipython3
-from stimupy.illusions.wedding_cakes import wedding_cake
+from stimupy.stimuli.wedding_cakes import wedding_cake
 
 # Define widgets
 w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
diff --git a/stimupy/__init__.py b/stimupy/__init__.py
index 09c4864d..937a67fe 100644
--- a/stimupy/__init__.py
+++ b/stimupy/__init__.py
@@ -1,3 +1,6 @@
 __version__ = "0.99.0"
 
-from . import components, illusions, noises, utils
+from stimupy import components, noises, stimuli, utils
+from stimupy.stimuli import *
+
+__all__ = ["components", "noises", "utils", *stimuli.__all__]
diff --git a/stimupy/components/__init__.py b/stimupy/components/__init__.py
index 60fa12f5..3773973c 100644
--- a/stimupy/components/__init__.py
+++ b/stimupy/components/__init__.py
@@ -1,11 +1,8 @@
 import itertools
-import warnings
-from copy import deepcopy
+
 import numpy as np
 
-from stimupy.utils import int_factorize, resolution
-from stimupy.utils.contrast_conversions import adapt_intensity_range
-from stimupy.utils.utils import round_to_vals
+from stimupy.utils import resolution
 
 __all__ = [
     "image_base",
@@ -13,15 +10,13 @@
     "mask_elements",
     "overview",
     "angulars",
-    "checkerboards",
-    "circulars",
+    "radials",
     "edges",
     "frames",
     "gaussians",
-    "gratings",
     "lines",
-    "mondrians",
     "shapes",
+    "waves",
 ]
 
 
@@ -179,190 +174,6 @@ def mask_elements(
     }
 
 
-def resolve_grating_params(
-    length=None,
-    visual_angle=None,
-    ppd=None,
-    frequency=None,
-    n_phases=None,
-    phase_width=None,
-    period="ignore",
-    round_phase_width=True,
-):
-    """Resolve (if possible) spatial parameters for a grating
-
-    Gratings take the regular resolution parameters (shape, ppd, visual_size).
-    In addition, there has to be an additional specification
-    of the frequency, number and with of phases (e.g., bars).
-    This can be done in two ways:
-    a phase_width (in degrees) and n_phases,
-    and/or by specifying the spatial frequency of the grating (in cycles per degree)
-
-    The total shape (in pixels) and visual size (in degrees) has to match the
-    specification of the phases and their widths.
-    Thus, not all 6 parameters have to be specified, as long as the both the resolution
-    and the distribution of phases can be resolved.
-
-    Note: all phases in a grating have the same width
-
-    Parameters
-    ----------
-    length : Number, or None (default)
-        lenght of grating, in pixels
-    visual_angle :Number, or None (default)
-        visual angle of grating, in degrees
-    ppd : Number, or None (default)
-        pixels per degree along the axis of grating
-    frequency : Number, or None (default)
-        frequency of grating, in cycles per degree
-    n_phases : int, or None (default)
-        number of phases (e.g., bars), i.e., half the number of full periods
-    phase_width : Number, or None (default)
-        extend of a single phase (e.g., bar), in degrees
-    period : "even", "odd", "either", "ignore" (default)
-        whether to ensure the grating has "even" number of phases,
-        "odd" number of phases, or "either" even/odd,
-        or no guarantees ("ignore")
-    round_phase_width : bool,
-        whether to round phase_width to an integer number of pixels, by default True
-
-
-    Returns
-    -------
-    dict[str, Any]
-        dictionary with all six resolution & size parameters resolved.
-    """
-    old_frequency = deepcopy(frequency)
-    old_n_phases = deepcopy(n_phases)
-    old_phase_width = deepcopy(phase_width)
-
-    if period not in ["ignore", "even", "odd", "either"]:
-        raise TypeError(f"period not understood: {period}")
-
-    # Try to resolve resolution
-    try:
-        length, visual_angle, ppd = resolution.resolve_1D(
-            length=length, visual_angle=visual_angle, ppd=ppd
-        )
-    except resolution.TooManyUnknownsError:
-        ppd = ppd
-        length = length
-        visual_angle = visual_angle
-
-    # Try to resolve number and width(s) of phases:
-    # Logic here is that phase_width expresses "degrees per phase",
-    # which we can convert to "phases_per_degree"
-    # phase_width = degrees_per_phase = 1 / phases_per_degree = 1 / (2*frequency)
-    if phase_width is not None:
-        phases_pd = 1 / phase_width
-        if frequency is not None and phases_pd != 2 * frequency:
-            raise ValueError(f"phase_width {phase_width} and frequency {frequency} don't match")
-    elif frequency is not None:
-        phases_pd = 2 * frequency
-    else:  # both are None:
-        phases_pd = None
-
-    # Logic here is that phase_width expresses "degrees per phase",
-    # which we can invert to phases_per_degree, analogous to ppd:
-    # n_phases = phases_per_degree * n_degrees
-    # is analogous to
-    # pix = ppd * n_degrees
-    # Thus we can resolve the number and spacing of phases also as a resolution
-    try:
-        n_phases, min_angle, phases_pd = resolution.resolve_1D(
-            length=n_phases,
-            visual_angle=visual_angle,
-            ppd=phases_pd,
-            round=False,
-        )
-    except Exception as e:
-        raise Exception("Could not resolve grating frequency, phase_width, n_phases") from e
-
-    # Now resolve resolution
-    visual_angle = min_angle if visual_angle is None else visual_angle
-    length, visual_angle, ppd = resolution.resolve_1D(
-        length=length, visual_angle=visual_angle, ppd=ppd
-    )
-
-    # Ensure n phases fit in length?
-    if period != "ignore":
-        n_phases = round_n_phases(n_phases=n_phases, length=length, period=period)
-
-        # Calculate other params again:
-        n_phases, min_angle, phases_pd = resolution.resolve_1D(
-            length=n_phases,
-            visual_angle=visual_angle,
-            ppd=None,
-            round=False,
-        )
-
-    # Ensure each phase consists of integer number of pixels?
-    phase_width = 1 / phases_pd
-    if round_phase_width:
-        phase_width = np.round(phase_width * ppd) / ppd
-        n_phases = length / np.round(phase_width * ppd)
-
-    # Calculate frequency
-    frequency = 1 / (2 * phase_width)
-
-    # Check & warn if we changed some params
-    if period == "ignore":
-        if old_n_phases is not None and n_phases != old_n_phases:
-            warnings.warn(
-                f"Adjusted n_phases={old_n_phases} -> {n_phases} because of poor resolution"
-            )
-
-        if old_phase_width is not None and phase_width != old_phase_width:
-            warnings.warn(
-                f"Adjusted phase_width={old_phase_width} -> {phase_width} because of poor"
-                " resolution"
-            )
-
-        if old_frequency is not None and frequency != old_frequency:
-            warnings.warn(
-                f"Adjusted frequency={old_frequency} -> {frequency} because of poor resolution"
-            )
-    else:
-        if old_n_phases is not None and n_phases != old_n_phases:
-            warnings.warn(
-                f"Adjusted n_phases={old_n_phases} -> {n_phases} because of period={period}"
-            )
-
-        if old_phase_width is not None and phase_width != old_phase_width:
-            warnings.warn(
-                f"Adjusted phase_width={old_phase_width} -> {phase_width} because of"
-                f" period={period}"
-            )
-
-        if old_frequency is not None and frequency != old_frequency:
-            warnings.warn(
-                f"Adjusted frequency={old_frequency} -> {frequency} because of period={period}"
-            )
-
-    # Check that frequency does not exceed Nyquist limit:
-    if frequency > (ppd / 2):
-        raise ValueError(
-            f"Grating frequency ({frequency}) should not exceed Nyquist limit {ppd/2} (ppd/2)"
-        )
-
-    # Accumulate edges of phases (rounding to avoid accumulation of
-    # floating point imprecisions)
-    edges = [*itertools.accumulate(itertools.repeat(phase_width, int(np.ceil(n_phases))))]
-    edges = np.round(np.array(edges), 8)
-    edges = list(edges)
-
-    return {
-        "length": length,
-        "visual_angle": visual_angle,
-        "ppd": ppd,
-        "frequency": frequency,
-        "phase_width": phase_width,
-        "n_phases": n_phases,
-        "edges": edges,
-        "period": period,
-    }
-
-
 def draw_regions(mask, intensities, intensity_background=0.5):
     """Draw image with intensities for components in mask
 
@@ -397,273 +208,7 @@ def draw_regions(mask, intensities, intensity_background=0.5):
     return img
 
 
-def round_n_phases(n_phases, length, period="either"):
-    """Round n_phases of grating to integer, even, or odd -- taking into account pixels
-
-    Finds the nearest integer (optionally limited to even or odd) n_phases
-    that length (in pixels) can be divided into.
-    Note that this maybe be quite far away from the input.
-
-
-    Parameters
-    ----------
-    n_phases : int
-        number of phases (e.g., bars), i.e., half the number of full periods
-    length : Number
-        lenght of grating, in pixels
-    period : "even", "odd", "either" (default)
-        whether to ensure the grating has "even" number of phases,
-        "odd" number of phases, or "either" even/odd
-
-    Returns
-    -------
-    n_phases : int
-        rounded n_phases
-    """
-
-    # n_phases has to integer-divide length
-    possible_phase_pix = np.array(list(int_factorize(length)))
-    possible_n_phases = length / possible_phase_pix
-
-    if period == "even":
-        # only look at possible_n_phases that are even
-        possible_n_phases = possible_n_phases[possible_n_phases % 2 == 0]
-    elif period == "odd":
-        # only look at possible_n_phases that are odd
-        possible_n_phases = possible_n_phases[possible_n_phases % 2 != 0]
-
-    if len(possible_n_phases) == 0:
-        raise ValueError(f"Cannot fit {period} number of phases into {length} px")
-
-    closest = possible_n_phases[np.argmin(np.abs(possible_n_phases - n_phases))]
-
-    return int(closest)
-
-
-def draw_sine_wave(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_phases=None,
-    phase_width=None,
-    period="ignore",
-    rotation=None,
-    phase_shift=None,
-    intensities=None,
-    origin=None,
-    base_type=None,
-    round_phase_width=None,
-):
-    """Draw a sine-wave grating given a certain base_type
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_phases : int, or None (default)
-        number of phases in the grating
-    phase_width : Number, or None (default)
-        width of a single phase, in degrees visual angle
-    period : "even", "odd", "either", "ignore" (default)
-        ensure whether the grating has "even" number of phases, "odd"
-        number of phases, either or whether not to round the number of
-        phases ("ignore")
-    rotation : float or None (default)
-        rotation of grating in degrees
-    phase_shift : float or None (default)
-        phase shift of grating in degrees
-    intensities : Sequence[float, float] or None (default)
-        min and max intensity of sine-wave
-    origin : "corner", "mean", "center" or None (default)
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center
-        if "center": set origin to real center (closest existing value to mean)
-    base_type : str or None
-        if "horizontal", use distance from origin in x-direction,
-        if "vertical", use distance from origin in x-direction;
-        if "rotated", use combined and rotated distance from origin in x-y;
-        if "radial", use radial distance from origin,
-        if "angular", use angular distance from origin,
-        if "cityblock", use cityblock distance from origin
-    round_phase_width : Bool or None (default)
-        if True, round width of bars given resolution
-
-    Returns
-    ----------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each bar (key: "grating_mask"),
-        and additional keys containing stimulus parameters
-    """
-    if rotation is None:
-        raise ValueError("draw_sine_wave() missing argument 'rotation' which is not 'None'")
-    if phase_shift is None:
-        raise ValueError("draw_sine_wave() missing argument 'phase_shift' which is not 'None'")
-    if intensities is None:
-        raise ValueError("draw_sine_wave() missing argument 'intensities' which is not 'None'")
-    if origin is None:
-        raise ValueError("draw_sine_wave() missing argument 'origin' which is not 'None'")
-    if round_phase_width is None:
-        raise ValueError(
-            "draw_sine_wave() missing argument 'round_phase_width' which is not 'None'"
-        )
-    if period is None:
-        period = "ignore"
-
-    base_types = ["horizontal", "vertical", "rotated", "radial", "angular", "cityblock"]
-    if base_type not in base_types:
-        raise ValueError(f"base_type needs to be one of {base_types}")
-
-    lst = [visual_size, ppd, shape, frequency, n_phases, phase_width]
-    if len([x for x in lst if x is not None]) < 3:
-        raise ValueError(
-            "'grating()' needs 3 non-None arguments for resolving from 'visual_size', "
-            "'ppd', 'shape', 'frequency', 'n_phases', 'phase_width'"
-        )
-
-    # Try to resolve resolution
-    try:
-        shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
-    except ValueError:
-        ppd = resolution.validate_ppd(ppd)
-        shape = resolution.validate_shape(shape)
-        visual_size = resolution.validate_visual_size(visual_size)
-
-    alpha = [np.abs(np.cos(np.deg2rad(rotation))), np.abs(np.sin(np.deg2rad(rotation)))]
-
-    if shape.width is not None:
-        length = np.round(alpha[0] * shape.width + alpha[1] * shape.height)
-    else:
-        length = None
-
-    if visual_size.width is not None:
-        visual_angle = alpha[0] * visual_size.width + alpha[1] * visual_size.height
-    else:
-        visual_angle = None
-
-    if ppd.horizontal is not None:
-        ppd_1D = ppd.horizontal
-    else:
-        ppd_1D = None
-
-    if rotation % 90 != 0 and round_phase_width:
-        round_phase_width = False
-        warnings.warn("Rounding phase width is turned off for oblique gratings")
-
-    if rotation % 90 != 0 and period != "ignore":
-        period = "ignore"
-        warnings.warn("Period ignored for oblique gratings")
-
-    # Resolve params
-    params = resolve_grating_params(
-        length=length,
-        visual_angle=visual_angle,
-        n_phases=n_phases,
-        phase_width=phase_width,
-        ppd=ppd_1D,
-        frequency=frequency,
-        period=period,
-        round_phase_width=round_phase_width,
-    )
-    length = params["length"]
-    ppd_1D = params["ppd"]
-    visual_angle = params["visual_angle"]
-    frequency = params["frequency"]
-    phase_width = params["phase_width"]
-    n_phases = params["n_phases"]
-
-    # Determine size/shape of whole image
-    if None in shape:
-        shape = [length * alpha[1], length * alpha[0]]
-        if np.round(alpha[1], 5) == 0:
-            shape[0] = shape[1]
-        if np.round(alpha[0], 5) == 0:
-            shape[1] = shape[0]
-
-    if None in ppd:
-        ppd = (ppd_1D, ppd_1D)
-
-    if None in visual_size:
-        visual_size = resolution.visual_size_from_shape_ppd(shape=shape, ppd=ppd)
-
-    shape = resolution.validate_shape(shape)
-    visual_size = resolution.validate_visual_size(visual_size)
-    ppd = resolution.validate_ppd(ppd)
-
-    # Set up coordinates
-    base = image_base(
-        shape=shape, visual_size=visual_size, ppd=ppd, rotation=rotation, origin=origin
-    )
-    distances = base[base_type]
-    distances = np.round(distances, 6)
-
-    # Shift distances minimally to ensure proper behavior
-    if origin == "corner":
-        distances = adapt_intensity_range(distances, 1e-03, distances.max() - 1e-03)
-    else:
-        distances = adapt_intensity_range(
-            distances, distances.min() - 1e-05, distances.max() - 1e-05
-        )
-
-    # Draw image
-    img = np.sin(frequency * 2 * np.pi * distances + np.deg2rad(phase_shift))
-    img = adapt_intensity_range(img, intensities[0], intensities[1])
-
-    # Create mask
-    if origin == "corner" or base_type == "radial" or base_type == "cityblock":
-        vals = np.arange(
-            distances.min() + phase_width / 2, distances.max() + phase_width * 2, phase_width
-        )
-
-        if origin == "mean":
-            vals -= distances.min()
-    else:
-        dmin = distances.min()
-        dmax = distances.max() + phase_width * 2
-        vals1 = np.arange(0 + phase_width / 2, dmax, phase_width)
-        vals2 = -np.arange(-phase_width / 2, -dmin + phase_width, phase_width)
-        vals = np.unique(np.append(vals2[::-1], vals1))
-
-    phase_shift_ = (phase_shift % 360) / 180 * phase_width
-    mask = round_to_vals(
-        distances - distances.min(), np.round(vals - phase_shift_, 6) - distances.min()
-    )
-
-    for i, val in enumerate(np.unique(mask)):
-        mask = np.where(mask == val, i + 1, mask)
-
-    stim = {
-        "img": img,
-        "mask": mask.astype(int),
-        "visual_size": visual_size,
-        "ppd": ppd,
-        "shape": shape,
-        "frequency": frequency,
-        "n_phases": n_phases,
-        "phase_width": phase_width,
-    }
-    return stim
-
-
-from . import (
-    angulars,
-    checkerboards,
-    circulars,
-    edges,
-    frames,
-    gaussians,
-    gratings,
-    lines,
-    mondrians,
-    shapes,
-)
+from . import angulars, edges, frames, gaussians, lines, radials, shapes, waves
 
 
 def create_overview():
@@ -681,56 +226,29 @@ def create_overview():
         "ppd": 20,
     }
 
-    p_mondrians = {
-        "mondrian_positions": ((0, 0), (0, 5), (1, 3), (4, 6), (6, 1)),
-        "mondrian_sizes": 3,
-        "mondrian_intensities": np.random.rand(5),
-    }
-
     # fmt: off
     stims = {
         # angulars
         "wedge": angulars.wedge(**p, width=30, radius=3),
         "angular_grating": angulars.grating(**p, n_segments=8),
         "pinwheel": angulars.pinwheel(**p, n_segments=8, radius=3),
-        # checkerboards
-        "checkerboard_v1": checkerboards.checkerboard(**p, board_shape=(10, 10)),
-        "checkerboard_v2": checkerboards.checkerboard(**p, board_shape=(10, 10), rotation=45),
-        "checkerboard_v3": checkerboards.checkerboard(**p, frequency=1),
-        "checkerboard_v4": checkerboards.checkerboard(**p, frequency=1, rotation=45),
         # circulars
-        "disc_and_rings": circulars.disc_and_rings(**p, radii=[1, 2, 3]),
-        "disc": circulars.disc(**p, radius=3),
-        "ring": circulars.ring(**p, radii=(1, 3)),
-        "annulus (=ring)": circulars.annulus(**p, radii=(1, 3)),
-        "bessel": circulars.bessel(**p, frequency=1),
-        "circular_sine_wave": circulars.sine_wave(**p, frequency=0.5),
-        "circular_square_wave": circulars.square_wave(**p, frequency=0.5),
+        "rings (generalized)": radials.rings(**p, radii=[1, 2, 3]),
+        "disc": radials.disc(**p, radius=3),
+        "ring": radials.ring(**p, radii=(1, 3)),
+        "annulus (=ring)": radials.annulus(**p, radii=(1, 3)),
         # edges
         "step_edge": edges.step_edge(**p),
         "gaussian_edge": edges.gaussian_edge(**p, sigma=1.5),
         "cornsweet_edge": edges.cornsweet_edge(**p, ramp_width=3),
         # frames
         "frames": frames.frames(**p, radii=(1, 2, 3)),
-        "frames_sine_wave": frames.sine_wave(**p, frequency=0.5),
-        "frames_square_wave": frames.square_wave(**p, frequency=0.5),
         # gaussians
         "gaussian": gaussians.gaussian(**p, sigma=(1, 2)),
-        # gratings
-        "square_wave": gratings.square_wave(**p, frequency=1),
-        "square_wave2": gratings.square_wave(**p, frequency=1, rotation=45),
-        "sine_wave": gratings.sine_wave(**p, frequency=1),
-        "gabor": gratings.gabor(**p, frequency=1, sigma=2),
-        "staircase": gratings.staircase(**p, n_bars=8),
-        "plaid": gratings.plaid(grating_parameters1={**p, "frequency": 1},
-                                grating_parameters2={**p, "frequency": 1, "rotation": 90},
-                                sigma=2),
         # lines
         "line": lines.line(**p, line_length=3),
         "dipole": lines.dipole(**p, line_length=3, line_gap=0.5),
         "line_circle": lines.circle(**p, radius=3),
-        # mondrians
-        "mondrians": mondrians.mondrians(**p, **p_mondrians),
         # shapes
         "rectangle": shapes.rectangle(**p, rectangle_size=3),
         "triangle": shapes.triangle(**p, triangle_size=3),
diff --git a/stimupy/components/angulars.py b/stimupy/components/angulars.py
index 2fa35bc8..e4b1355d 100644
--- a/stimupy/components/angulars.py
+++ b/stimupy/components/angulars.py
@@ -1,7 +1,7 @@
 import numpy as np
 
-from stimupy.components import draw_regions, mask_elements, resolve_grating_params
-from stimupy.components.circulars import ring
+from stimupy.components import draw_regions, mask_elements, waves
+from stimupy.components.radials import ring
 from stimupy.utils import resolution
 
 __all__ = [
@@ -310,7 +310,7 @@ def grating(
         raise ValueError("'frequency' in angular grating must be smaller than 0.5")
 
     # Resolve grating
-    params = resolve_grating_params(
+    params = waves.resolve_grating_params(
         visual_angle=360,
         ppd=1,
         frequency=frequency,
diff --git a/stimupy/components/checkerboards.py b/stimupy/components/checkerboards.py
deleted file mode 100644
index 43e908f7..00000000
--- a/stimupy/components/checkerboards.py
+++ /dev/null
@@ -1,167 +0,0 @@
-import warnings
-import numpy as np
-
-from stimupy.components.gratings import square_wave
-
-__all__ = [
-    "checkerboard",
-]
-
-
-def checkerboard(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    board_shape=None,
-    check_visual_size=None,
-    period="ignore",
-    rotation=0,
-    intensity_checks=(1.0, 0.0),
-    round_phase_width=True,
-):
-    """Draws a checkerboard with given specifications
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size of the total board [height, width] in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] in pixels
-    frequency : Sequence[Number, Number], Number, or None (default)
-        frequency of checkerboard in [y, x] in cpd
-    board_shape : Sequence[Number, Number], Number, or None (default)
-        number of checks in [height, width] of checkerboard
-    check_visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size of a single check [height, width] in degrees
-    period : "even", "odd", "either" or "ignore" (default)
-        ensure whether the grating has "even" number of phases, "odd"
-        number of phases, either or whether not to round the number of
-        phases ("ignore")
-    rotation : Number
-        rotation of grating in degrees (default: 0 = horizontal)
-    intensity_checks : Sequence[float, float]
-        intensity values of checks, by default (1.0, 0.0)
-    round_phase_width : Bool
-        if True, round width of bars given resolution (default: True)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each check (key: "checker_mask"),
-        and additional keys containing stimulus parameters
-    """
-    lst = [visual_size, ppd, shape, frequency, board_shape, check_visual_size]
-    if len([x for x in lst if x is not None]) < 3:
-        raise ValueError(
-            "'checkerboard()' needs 3 non-None arguments for resolving from 'visual_size', "
-            "'ppd', 'shape', 'frequency', 'board_shape', 'check_visual_size'"
-        )
-
-    if isinstance(frequency, (float, int)) or frequency is None:
-        frequency = (frequency, frequency)
-    if isinstance(board_shape, (float, int)) or board_shape is None:
-        board_shape = (board_shape, board_shape)
-    if isinstance(check_visual_size, (float, int)) or check_visual_size is None:
-        check_visual_size = (check_visual_size, check_visual_size)
-
-    create_twice = visual_size is None and shape is None
-
-    # Create checkerboard by treating it as a plaid
-    sw1 = square_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency[0],
-        n_bars=board_shape[0],
-        bar_width=check_visual_size[0],
-        period=period,
-        rotation=rotation,
-        phase_shift=0,
-        intensity_bars=intensity_checks,
-        origin="corner",
-        round_phase_width=round_phase_width,
-    )
-
-    sw2 = square_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency[1],
-        n_bars=board_shape[1],
-        bar_width=check_visual_size[1],
-        period=period,
-        rotation=rotation + 90,
-        phase_shift=0,
-        intensity_bars=intensity_checks,
-        origin="corner",
-        round_phase_width=round_phase_width,
-    )
-
-    # If neither a visual_size nor a shape was given, each square wave
-    # grating is always a square. An easy solution is to just recreate
-    # both gratings with the resolved parameters
-    if create_twice:
-        warnings.filterwarnings("ignore")
-        sw1 = square_wave(
-            visual_size=(sw1["visual_size"][0], sw2["visual_size"][1]),
-            ppd=sw1["ppd"],
-            shape=None,
-            frequency=frequency[0],
-            n_bars=board_shape[0],
-            bar_width=check_visual_size[0],
-            period=period,
-            rotation=rotation,
-            phase_shift=0,
-            intensity_bars=intensity_checks,
-            origin="corner",
-            round_phase_width=round_phase_width,
-        )
-
-        sw2 = square_wave(
-            visual_size=(sw1["visual_size"][0], sw2["visual_size"][1]),
-            ppd=sw1["ppd"],
-            shape=None,
-            frequency=frequency[1],
-            n_bars=board_shape[1],
-            bar_width=check_visual_size[1],
-            period=period,
-            rotation=rotation + 90,
-            phase_shift=0,
-            intensity_bars=intensity_checks,
-            origin="corner",
-            round_phase_width=round_phase_width,
-        )
-        warnings.filterwarnings("default")
-
-    # Add the two square-wave gratings into a checkerboard
-    img = sw1["img"] + sw2["img"]
-    img = np.where(
-        img == intensity_checks[0] + intensity_checks[1], intensity_checks[1], intensity_checks[0]
-    )
-
-    # Create a mask with target indices for each check
-    mask = sw1["grating_mask"] + sw2["grating_mask"] * sw1["grating_mask"].max() * 10
-    unique_vals = np.unique(mask)
-    for v in range(len(unique_vals)):
-        mask[mask == unique_vals[v]] = v + 1
-
-    stim = {
-        "img": img,
-        "checker_mask": mask.astype(int),
-        "grating_mask": sw1["grating_mask"],
-        "grating_mask2": sw2["grating_mask"],
-        "visual_size": sw1["visual_size"],
-        "ppd": sw1["ppd"],
-        "shape": sw1["shape"],
-        "frequency": (sw2["frequency"], sw1["frequency"]),
-        "board_shape": (sw2["n_bars"], sw1["n_bars"]),
-        "check_visual_size": (sw2["bar_width"], sw1["bar_width"]),
-        "period": period,
-        "rotation": rotation,
-        "intensity_checks": intensity_checks,
-    }
-    return stim
diff --git a/stimupy/components/circulars.py b/stimupy/components/circulars.py
deleted file mode 100644
index d70b0d48..00000000
--- a/stimupy/components/circulars.py
+++ /dev/null
@@ -1,634 +0,0 @@
-import itertools
-
-import numpy as np
-
-from stimupy.components import image_base, mask_elements, resolve_grating_params, draw_sine_wave
-from stimupy.utils import resolution
-from stimupy.utils.utils import apply_bessel, round_to_vals
-
-
-__all__ = [
-    "disc_and_rings",
-    "disc",
-    "ring",
-    "annulus",
-    "bessel",
-    "sine_wave",
-    "square_wave",
-]
-
-
-def resolve_circular_params(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_rings=None,
-    ring_width=None,
-):
-    """Resolve (if possible) spatial parameters for circular grating, i.e., set of rings
-
-    Circular grating / rings component takes the regular resolution parameters
-    (shape, ppd, visual_size). In addition, there has to be an additional specification
-    of the number of rings, and their width. This can be done in two ways:
-    a ring_width (in degrees) and n_rings, and/or by specifying the spatial frequency
-    of a circular grating (in cycles per degree)
-
-    The total shape (in pixels) and visual size (in degrees) has to match the
-    specification of the rings and their widths.
-    Thus, not all 6 parameters have to be specified, as long as the both the resolution
-    and the distribution of rings can be resolved.
-
-    Note: all rings in a grating have the same width -- if more control is required
-    see disc_and_rings
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of circular grating, in cycles per degree
-    n_rings : int, or None (default)
-        number of rings
-    ring_width : Number, or None (default)
-        width of a single ring, in degrees
-
-    Returns
-    -------
-    dict[str, Any]
-        dictionary with all six resolution & size parameters resolved.
-    """
-
-    # Resolve params
-    shape = resolution.validate_shape(shape)
-    ppd = resolution.validate_ppd(ppd)
-    visual_size = resolution.validate_visual_size(visual_size)
-
-    length = np.array(shape).min() / 2 if None not in shape else None
-    ppd_1D = np.array(ppd).min() if None not in ppd else None
-    visual_angle = np.array(visual_size).min() / 2 if None not in visual_size else None
-    params = resolve_grating_params(
-        length=length,
-        visual_angle=visual_angle,
-        n_phases=n_rings,
-        phase_width=ring_width,
-        ppd=ppd_1D,
-        frequency=frequency,
-        period="ignore",
-    )
-
-    # Remove too large radii:
-    if visual_angle is not None:
-        if params["edges"][-1] > visual_angle:
-            params["edges"][-1] = visual_angle
-
-    shape = resolution.validate_shape(params["length"] * 2)
-    visual_size = resolution.validate_visual_size(params["visual_angle"] * 2)
-    ppd = resolution.validate_ppd(params["ppd"])
-
-    return {
-        "visual_size": visual_size,
-        "ppd": ppd,
-        "shape": shape,
-        "frequency": params["frequency"],
-        "ring_width": params["phase_width"],
-        "n_rings": params["n_phases"],
-        "radii": params["edges"],
-    }
-
-
-def mask_rings(
-    radii,
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    origin="mean",
-):
-    """Generate mask with integer indices for rings
-
-    Parameters
-    ----------
-    radii : Sequence[Number]
-        outer radii of rings (& disc) in degree visual angle
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each ring (key: "ring_mask"),
-        and additional keys containing stimulus parameters
-
-    Raises
-    ------
-    ValueError
-        if largest radius does not fit in visual size
-    """
-
-    # no axes are None; check if fits
-    if visual_size.height < np.max(radii) * 2 or visual_size.width < np.max(radii) * 2:
-        raise ValueError(
-            f"Largest radius {np.max(radii)} does not fit in visual size {visual_size}"
-        )
-
-    # Mark elements with integer idx-value
-    stim = mask_elements(
-        orientation="radial",
-        edges=radii,
-        rotation=0.0,
-        shape=shape,
-        visual_size=visual_size,
-        ppd=ppd,
-        origin=origin,
-    )
-    stim["ring_mask"] = stim["mask"]
-    del stim["mask"]
-    return stim
-
-
-def disc_and_rings(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    radii=None,
-    intensity_rings=(0, 1),
-    intensity_background=0.5,
-    origin="mean",
-):
-    """Draw a central solid disc with zero or more solid rings (annuli)
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    radii : Sequence[Number]
-        outer radii of rings (& disc) in degree visual angle
-    intensity_rings : Sequence[Number, ...]
-        intensity value for each ring, from inside to out.
-        If fewer intensities are passed than number of radii, cycles through intensities
-    intensity_background : float (optional)
-        value of background, by default 0.5
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each ring (key: "ring_mask"),
-        and additional keys containing stimulus parameters
-    """
-    if radii is None:
-        raise ValueError("disc_and_rings() missing argument 'radii' which is not 'None'")
-
-    # Try to resolve resolution;
-    try:
-        shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
-    except resolution.TooManyUnknownsError:
-        # Check visual_size
-        visual_size = resolution.validate_visual_size(visual_size)
-
-    if visual_size is None or visual_size == (None, None):
-        # Two axes are None; make image large enought to fit
-        visual_size = resolution.validate_visual_size(np.max(radii) * 2)
-    elif None in visual_size:
-        # one axis is None; make square
-        visual_size = [x for x in visual_size if x is not None]
-        visual_size = resolution.validate_visual_size(visual_size)
-
-    if np.diff(radii).min() < 0:
-        raise ValueError("radii need to monotonically increase")
-
-    # Get masks for rings
-    params = mask_rings(radii=radii, shape=shape, visual_size=visual_size, ppd=ppd, origin=origin)
-    shape = params["shape"]
-
-    # Draw rings
-    base = image_base(shape=shape, visual_size=visual_size, origin=origin)
-    distances = base["radial"]
-
-    img = np.ones(shape) * intensity_background
-    ints = [*itertools.islice(itertools.cycle(intensity_rings), len(radii))]
-    for radius, intensity in zip(reversed(radii), reversed(ints)):
-        img[distances < radius] = intensity
-
-    # Assemble output
-    params["intensity_rings"] = intensity_rings
-    return {"img": img, **params}
-
-
-def disc(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    radius=None,
-    intensity_disc=1.0,
-    intensity_background=0.5,
-    origin="mean",
-):
-    """Draw a central disc
-
-    Essentially, `dics(radius)` is an alias for `ring(radii=[0, radius])`
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    radius : Number
-        outer radius of disc in degree visual angle
-    intensity_disc : Number
-        intensity value of disc, by default 1.0
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each ring (key: "ring_mask"),
-        and additional keys containing stimulus parameters
-    """
-    if radius is None:
-        raise ValueError("disc() missing argument 'radius' which is not 'None'")
-
-    radius = np.array(radius)
-    intensity = np.array(intensity_disc)
-
-    if radius.size != 1:
-        raise ValueError("Can only pass 1 radius")
-    if intensity.size != 1:
-        raise ValueError("Can only pass 1 intensity")
-
-    stim = ring(
-        radii=np.insert(radius, 0, 0),
-        intensity_ring=intensity,
-        visual_size=visual_size,
-        ppd=ppd,
-        intensity_background=intensity_background,
-        shape=shape,
-        origin=origin,
-    )
-    return stim
-
-
-def ring(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    radii=None,
-    intensity_ring=1.0,
-    intensity_background=0.5,
-    origin="mean",
-):
-    """Draw a ring (annulus)
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    radii : Sequence[Number, Number]
-        inner and outer radius of ring in degree visual angle
-    intensity_ring : Number
-        intensity value of ring, by default 1.0
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each ring (key: "ring_mask"),
-        and additional keys containing stimulus parameters
-
-    Raises
-    ------
-    ValueError
-        if passed in less/more than 2 radii (inner, outer)
-    ValueError
-        if passed in less/more than 1 intensity
-    """
-    if radii is None:
-        raise ValueError("ring() missing argument 'radii' which is not 'None'")
-    if len(radii) != 2:
-        raise ValueError("Can only pass exactly 2 radii")
-    if len([intensity_ring]) != 1:
-        raise ValueError("Can only pass 1 intensity")
-
-    if radii[1] is None:
-        shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
-        radii[1] = np.max(visual_size) / 2
-
-    if radii[1] < radii[0]:
-        raise ValueError("first radius needs to be smaller than second radius")
-
-    stim = disc_and_rings(
-        radii=radii,
-        intensity_rings=[intensity_background, intensity_ring],
-        shape=shape,
-        visual_size=visual_size,
-        ppd=ppd,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    stim["ring_mask"] = np.where(stim["ring_mask"] == 2, 1, 0)
-    return stim
-
-
-annulus = ring
-
-
-def bessel(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    order=0,
-    intensity_rings=(1.0, 0.0),
-    origin="mean",
-):
-    """Draw a Bessel stimulus, i.e. draw circular rings following an nth order
-    Bessel function of a given frequency.
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of circular grating, in cycles per degree
-    order : int
-        n-th order Bessel function
-    intensity_rings : (float, float)
-        intensity values of rings, first value indicating center intensity
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-
-    Returns
-    ----------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        empty mask (key: "ring_mask"),
-        and additional keys containing stimulus parameters
-    """
-
-    base = image_base(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        rotation=0,
-        origin=origin,
-    )
-
-    arr = base["radial"] * frequency * 2 * np.pi
-    img = apply_bessel(arr, order=order)
-    img = (img - img.min()) / (img.max() - img.min())
-    img = img * (intensity_rings[0] - intensity_rings[1]) + intensity_rings[1]
-
-    stim = {
-        "img": img,
-        "ring_mask": np.zeros(base["shape"]).astype(int),
-        "visual_size": base["visual_size"],
-        "ppd": base["ppd"],
-        "shape": base["shape"],
-        "order": order,
-        "frequency": frequency,
-        "intensity_rings": intensity_rings,
-    }
-    return stim
-
-
-def sine_wave(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_rings=None,
-    ring_width=None,
-    phase_shift=0,
-    intensity_rings=(0.0, 1.0),
-    intensity_background=0.5,
-    origin="mean",
-    clip=False,
-):
-    """Draw a circular sine-wave grating over the whole image
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of circular grating, in cycles per degree
-    n_rings : int, or None (default)
-        number of rings
-    ring_width : Number, or None (default)
-        width of a single ring, in degrees
-    phase_shift : float
-        phase shift of grating in degrees
-    intensity_rings : Sequence[float, float]
-        min and max intensity of sine-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-    clip : Bool
-        if True, clip stimulus to image size (default: False)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each ring (key: "ring_mask"),
-        and additional keys containing stimulus parameters
-    """
-    lst = [visual_size, ppd, shape, frequency, n_rings, ring_width]
-    if len([x for x in lst if x is not None]) < 3:
-        raise ValueError(
-            "'grating()' needs 3 non-None arguments for resolving from 'visual_size', "
-            "'ppd', 'shape', 'frequency', 'n_rings', 'ring_width'"
-        )
-
-    sw = draw_sine_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency,
-        n_phases=n_rings,
-        phase_width=ring_width,
-        period="ignore",
-        rotation=0,
-        phase_shift=phase_shift,
-        intensities=intensity_rings,
-        origin=origin,
-        round_phase_width=False,
-        base_type="radial",
-    )
-
-    if clip:
-        csize = min(sw["visual_size"]) / 2.0
-        circle = disc(
-            visual_size=sw["visual_size"],
-            ppd=sw["ppd"],
-            radius=csize,
-            origin=origin,
-        )
-        sw["img"] = np.where(circle["ring_mask"], sw["img"], intensity_background)
-        sw["mask"] = np.where(circle["ring_mask"], sw["mask"], 0)
-
-    # Create stimulus dict
-    stim = {
-        "img": sw["img"],
-        "ring_mask": sw["mask"].astype(int),
-        "visual_size": sw["visual_size"],
-        "ppd": sw["ppd"],
-        "shape": sw["shape"],
-        "origin": origin,
-        "frequency": sw["frequency"],
-        "frame_width": sw["phase_width"],
-        "n_frames": sw["n_phases"],
-        "intensity_rings": intensity_rings,
-    }
-    return stim
-
-
-def square_wave(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_rings=None,
-    ring_width=None,
-    phase_shift=0,
-    intensity_rings=(0.0, 1.0),
-    intensity_background=0.5,
-    origin="mean",
-    clip=False,
-):
-    """Draw a circular square-wave grating over the whole image
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of circular grating, in cycles per degree
-    n_rings : int, or None (default)
-        number of rings
-    ring_width : Number, or None (default)
-        width of a single ring, in degrees
-    phase_shift : float
-        phase shift of grating in degrees
-    intensity_rings : Sequence[float, float]
-        min and max intensity of square-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-    clip : Bool
-        if True, clip stimulus to image size (default: False)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each ring (key: "ring_mask"),
-        and additional keys containing stimulus parameters
-    """
-    stim = sine_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency,
-        n_rings=n_rings,
-        ring_width=ring_width,
-        phase_shift=phase_shift,
-        intensity_rings=intensity_rings,
-        origin=origin,
-        clip=False,
-    )
-
-    # Round sine-wave to create square wave
-    stim["img"] = round_to_vals(stim["img"], intensity_rings)
-
-    if clip:
-        csize = min(stim["visual_size"]) / 2.0
-        circle = disc(
-            visual_size=stim["visual_size"],
-            ppd=stim["ppd"],
-            radius=csize,
-            origin=origin,
-        )
-        stim["img"] = np.where(circle["ring_mask"], stim["img"], intensity_background)
-        stim["ring_mask"] = np.where(circle["ring_mask"], stim["ring_mask"], 0).astype(int)
-    return stim
-
-
-if __name__ == "__main__":
-    from stimupy.utils.plotting import plot_stimuli
-
-    p = {
-        "visual_size": (10, 20),
-        "ppd": 50,
-    }
-
-    stims = {
-        "disc": disc(**p, radius=3),
-        "disc_and_rings": disc_and_rings(**p, radii=(1, 2, 3)),
-        "ring": ring(**p, radii=(1, 2)),
-        "bessel": bessel(**p, frequency=0.5),
-        "sine_wave": sine_wave(**p, frequency=0.5),
-        "square_wave": square_wave(**p, frequency=0.5),
-    }
-
-    plot_stimuli(stims, mask=True)
diff --git a/stimupy/components/frames.py b/stimupy/components/frames.py
index 6acb7e32..c5bca028 100644
--- a/stimupy/components/frames.py
+++ b/stimupy/components/frames.py
@@ -1,14 +1,9 @@
 import numpy as np
 
-from stimupy.components import draw_regions, mask_elements, draw_sine_wave
-from stimupy.utils.utils import round_to_vals
-from stimupy.components.shapes import rectangle
-
+from stimupy.components import draw_regions, mask_elements
 
 __all__ = [
     "frames",
-    "sine_wave",
-    "square_wave",
 ]
 
 
@@ -117,218 +112,31 @@ def frames(
     return stim
 
 
-def sine_wave(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_frames=None,
-    frame_width=None,
-    phase_shift=0,
-    period="ignore",
-    intensity_frames=(0.0, 1.0),
-    intensity_background=0.5,
-    origin="mean",
-    clip=False,
-):
-    """Draw a sine-wave using cityblock distances over the whole image
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_frames : int, or None (default)
-        number of frames in the grating
-    frame_width : Number, or None (default)
-        width of a single frame, in degrees visual angle
-    phase_shift : float
-        phase shift of grating in degrees
-    period : "full", "half", "ignore" (default)
-        whether to ensure the grating only has "full" periods,
-        half "periods", or no guarantees ("ignore")
-    intensity_frames : Sequence[float, float]
-        min and max intensity of sine-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-    clip : Bool
-        if True, clip stimulus to image size (default: False)
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
     Returns
     -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each frame (key: "frame_mask"),
-        and additional keys containing stimulus parameters
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
     """
-    lst = [visual_size, ppd, shape, frequency, n_frames, frame_width]
-    if len([x for x in lst if x is not None]) < 3:
-        raise ValueError(
-            "'grating()' needs 3 non-None arguments for resolving from 'visual_size', "
-            "'ppd', 'shape', 'frequency', 'n_frames', 'frame_width'"
-        )
-
-    sw = draw_sine_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency,
-        n_phases=n_frames,
-        phase_width=frame_width,
-        period=period,
-        rotation=0,
-        phase_shift=phase_shift,
-        intensities=intensity_frames,
-        origin=origin,
-        round_phase_width=False,
-        base_type="cityblock",
-    )
-
-    if clip:
-        if origin == "corner":
-            rsize = min(sw["visual_size"]) / 2
-            rect = rectangle(
-                visual_size=sw["visual_size"],
-                ppd=sw["ppd"],
-                rectangle_size=rsize,
-                rectangle_position=(0, 0),
-            )
-        else:
-            rsize = min(sw["visual_size"])
-            rect = rectangle(
-                visual_size=sw["visual_size"],
-                ppd=sw["ppd"],
-                rectangle_size=rsize,
-            )
-        sw["img"] = np.where(rect["shape_mask"], sw["img"], intensity_background)
-        sw["mask"] = np.where(rect["shape_mask"], sw["mask"], 0)
-
-    # Create stimulus dict
-    stim = {
-        "img": sw["img"],
-        "frame_mask": sw["mask"].astype(int),
-        "visual_size": sw["visual_size"],
-        "ppd": sw["ppd"],
-        "shape": sw["shape"],
-        "origin": origin,
-        "frequency": sw["frequency"],
-        "frame_width": sw["phase_width"],
-        "n_frames": sw["n_phases"],
-        "period": period,
-        "intensity_frames": intensity_frames,
+    default_params = {
+        "visual_size": (10, 10),
+        "ppd": 30,
     }
-    return stim
-
-
-def square_wave(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_frames=None,
-    frame_width=None,
-    phase_shift=0,
-    period="ignore",
-    intensity_frames=(0.0, 1.0),
-    intensity_background=0.5,
-    origin="mean",
-    clip=False,
-):
-    """Draw a square-wave using cityblock distances over the whole image
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_frames : int, or None (default)
-        number of frames in the grating
-    frame_width : Number, or None (default)
-        width of a single frame, in degrees visual angle
-    phase_shift : float
-        phase shift of grating in degrees
-    period : "full", "half", "ignore" (default)
-        whether to ensure the grating only has "full" periods,
-        half "periods", or no guarantees ("ignore")
-    intensity_frames : Sequence[float, float]
-        min and max intensity of square-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-    clip : Bool
-        if True, clip stimulus to image size (default: False)
+    default_params.update(kwargs)
 
-    Returns
-    ----------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each frame (key: "frame_mask"),
-        and additional keys containing stimulus parameters
-    """
-
-    stim = sine_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency,
-        n_frames=n_frames,
-        frame_width=frame_width,
-        phase_shift=phase_shift,
-        period=period,
-        intensity_frames=intensity_frames,
-        origin=origin,
-        clip=False,
-    )
-
-    # Round sine-wave to create square wave
-    stim["img"] = round_to_vals(stim["img"], intensity_frames)
+    # fmt: off
+    stimuli = {
+        "frames": frames(**default_params, radii=(1, 2, 3)),
+    }
+    # fmt: on
 
-    if clip:
-        if origin == "corner":
-            rsize = min(stim["visual_size"]) / 2
-            rect = rectangle(
-                visual_size=stim["visual_size"],
-                ppd=stim["ppd"],
-                rectangle_size=rsize,
-                rectangle_position=(0, 0),
-            )
-        else:
-            rsize = min(stim["visual_size"])
-            rect = rectangle(
-                visual_size=stim["visual_size"],
-                ppd=stim["ppd"],
-                rectangle_size=rsize,
-            )
-        stim["img"] = np.where(rect["shape_mask"], stim["img"], intensity_background)
-        stim["frame_mask"] = np.where(rect["shape_mask"], stim["frame_mask"], 0)
-    return stim
+    return stimuli
 
 
 if __name__ == "__main__":
     from stimupy.utils import plot_stimuli
 
-    stims = {
-        "frames": frames(visual_size=(8, 16), frame_radii=(1, 2, 3), ppd=32),
-        # "grating": grating(visual_size=(8, 16), ppd=32, frequency=1.0),
-        "sine_wave": sine_wave(visual_size=(8, 16), ppd=32, frequency=0.5),
-        "square_wave": square_wave(visual_size=(8, 16), ppd=32, frequency=0.5),
-    }
-
-    plot_stimuli(stims, mask=False, save=None)
+    stims = overview()
+    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/components/gratings.py b/stimupy/components/gratings.py
deleted file mode 100644
index c6e32442..00000000
--- a/stimupy/components/gratings.py
+++ /dev/null
@@ -1,489 +0,0 @@
-import numpy as np
-
-# import warnings
-
-from stimupy.components import draw_sine_wave, draw_regions
-from stimupy.components.gaussians import gaussian
-from stimupy.utils.utils import round_to_vals
-
-__all__ = [
-    "sine_wave",
-    "square_wave",
-    "staircase",
-    "gabor",
-    "plaid",
-]
-
-
-def sine_wave(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_bars=None,
-    bar_width=None,
-    period="ignore",
-    rotation=0,
-    phase_shift=0,
-    intensity_bars=(0.0, 1.0),
-    origin="center",
-    round_phase_width=False,
-):
-    """Draw sine-wave grating (set of bars) of given spatial frequency
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_bars : int, or None (default)
-        number of bars in the grating
-    bar_width : Number, or None (default)
-        width of a single bar, in degrees visual angle
-    period : "even", "odd", "either" or "ignore" (default)
-        ensure whether the grating has "even" number of phases, "odd"
-        number of phases, either or whether not to round the number of
-        phases ("ignore")
-    rotation : float
-        rotation of grating in degrees (default: 0 = horizontal)
-    phase_shift : float
-        phase shift of grating in degrees
-    intensity_bars : Sequence[float, float]
-        min and max intensity of sine-wave, by default (0.0, 1.0)
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner (default)
-        if "mean": set origin to hypothetical image center
-        if "center": set origin to real center (closest existing value to mean)
-    round_phase_width : Bool
-        if True, round width of bars given resolution
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each bar (key: "grating_mask"),
-        and additional keys containing stimulus parameters
-    """
-
-    if len(intensity_bars) != 2:
-        raise ValueError("intensity_bars should be [float, float]")
-
-    lst = [visual_size, ppd, shape, frequency, n_bars, bar_width]
-    if len([x for x in lst if x is not None]) < 3:
-        raise ValueError(
-            "'sine_wave()' needs 3 non-None arguments for resolving from 'visual_size', "
-            "'ppd', 'shape', 'frequency', 'n_bars', 'bar_width'"
-        )
-
-    sw = draw_sine_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency,
-        n_phases=n_bars,
-        phase_width=bar_width,
-        period=period,
-        rotation=rotation,
-        phase_shift=phase_shift,
-        intensities=intensity_bars,
-        origin=origin,
-        round_phase_width=round_phase_width,
-        base_type="rotated",
-    )
-
-    # Create stimulus dict
-    stim = {
-        "img": sw["img"],
-        "grating_mask": sw["mask"].astype(int),
-        "visual_size": sw["visual_size"],
-        "ppd": sw["ppd"],
-        "shape": sw["shape"],
-        "rotation": rotation,
-        "origin": origin,
-        "frequency": sw["frequency"],
-        "bar_width": sw["phase_width"],
-        "n_bars": sw["n_phases"],
-        "period": period,
-        "intensity_bars": intensity_bars,
-        "phase_shift": phase_shift,
-    }
-    return stim
-
-
-def square_wave(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_bars=None,
-    bar_width=None,
-    period="ignore",
-    rotation=0,
-    phase_shift=0,
-    intensity_bars=(0.0, 1.0),
-    origin="corner",
-    round_phase_width=True,
-):
-    """Draw square-wave grating (set of bars) of given spatial frequency
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_bars : int, or None (default)
-        number of bars in the grating
-    bar_width : Number, or None (default)
-        width of a single bar, in degrees visual angle
-    period : "even", "odd", "either" or "ignore" (default)
-        ensure whether the grating has "even" number of phases, "odd"
-        number of phases, either or whether not to round the number of
-        phases ("ignore")
-    rotation : float
-        rotation of grating in degrees (default: 0 = horizontal)
-    phase_shift : float
-        phase shift of grating in degrees
-    intensity_bars : Sequence[float, float]
-        min and max intensity of square-wave, by default (0.0, 1.0)
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner (default)
-        if "mean": set origin to hypothetical image center
-        if "center": set origin to real center (closest existing value to mean)
-    round_phase_width : Bool
-        if True, round width of bars given resolution
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each bar (key: "grating_mask"),
-        and additional keys containing stimulus parameters
-    """
-
-    stim = sine_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency,
-        n_bars=n_bars,
-        bar_width=bar_width,
-        period=period,
-        rotation=rotation,
-        phase_shift=phase_shift,
-        intensity_bars=intensity_bars,
-        origin=origin,
-        round_phase_width=round_phase_width,
-    )
-
-    # Round sine-wave to create square wave
-    stim["img"] = round_to_vals(stim["img"], intensity_bars)
-    return stim
-
-
-def staircase(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_bars=None,
-    bar_width=None,
-    period="either",
-    rotation=0,
-    intensity_bars=(0.0, 1.0),
-    round_phase_width=True,
-):
-    """Draw a luminance staircase
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_bars : int, or None (default)
-        number of bars in the grating
-    bar_width : Number, or None (default)
-        width of a single bar, in degrees visual angle
-    period : "even", "odd", "either" or "ignore" (default)
-        ensure whether the grating has "even" number of phases, "odd"
-        number of phases, either or whether not to round the number of
-        phases ("ignore")
-    rotation : float
-        rotation of grating in degrees (default: 0 = horizontal)
-    intensity_bars : Sequence[float, ...]
-        if len(intensity_bars)==2, intensity range of staircase (default 0.0, 1.0);
-        if len(intensity_bars)>2, intensity value for each bar.
-        Can specify as many intensities as n_bars.
-        If fewer intensities are passed than n_bars, cycles through intensities.
-    round_phase_width : Bool
-        if True, round width of bars given resolution
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each bar (key: "grating_mask"),
-        and additional keys containing stimulus parameters
-    """
-
-    stim = square_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency,
-        n_bars=n_bars,
-        bar_width=bar_width,
-        period=period,
-        rotation=rotation,
-        phase_shift=0,
-        intensity_bars=(0, 1),
-        origin="corner",
-        round_phase_width=round_phase_width,
-    )
-
-    if len(intensity_bars) == 2:
-        intensity_bars = np.linspace(
-            intensity_bars[0], intensity_bars[1], int(np.ceil(stim["n_bars"]))
-        )
-
-    # Use grating_mask to draw staircase
-    stim["img"] = draw_regions(mask=stim["grating_mask"], intensities=intensity_bars)
-    stim["intensity_bars"] = intensity_bars
-    return stim
-
-
-def gabor(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    bar_width=None,
-    sigma=None,
-    period="ignore",
-    rotation=0,
-    phase_shift=0,
-    intensity_bars=(0.0, 1.0),
-    origin="center",
-):
-    """Draw sine-wave grating (set of bars) of given spatial frequency
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    bar_width : Number, or None (default)
-        width of a single bar, in degrees visual angle
-    sigma : float or (float, float)
-        sigma of Gaussian in degree visual angle (y, x)
-    period : "even", "odd", "either" or "ignore" (default)
-        ensure whether the grating has "even" number of phases, "odd"
-        number of phases, either or whether not to round the number of
-        phases ("ignore")
-    rotation : float
-        rotation of grating in degrees (default: 0 = horizontal)
-    phase_shift : float
-        phase shift of grating in degrees
-    intensity_bars : Sequence[float, ...]
-        maximal intensity value for each bar, by default (0.0, 1.0).
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each bar (key: "grating_mask"),
-        and additional keys containing stimulus parameters
-    """
-    if sigma is None:
-        raise ValueError("gabor() missing argument 'sigma' which is not 'None'")
-
-    stim = sine_wave(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency,
-        bar_width=bar_width,
-        period=period,
-        rotation=rotation,
-        phase_shift=phase_shift,
-        intensity_bars=intensity_bars,
-        origin=origin,
-    )
-
-    gaussian_window = gaussian(
-        visual_size=visual_size,
-        ppd=ppd,
-        sigma=sigma,
-        origin=origin,
-    )
-    mean_int = (intensity_bars[0] + intensity_bars[1]) / 2
-    stim["img"] = (stim["img"] - mean_int) * gaussian_window["img"] + mean_int
-
-    return {
-        **stim,
-        "sigma": sigma,
-        "gaussian_mask": gaussian_window["gaussian_mask"],
-    }
-
-
-def plaid(
-    grating_parameters1,
-    grating_parameters2,
-    weight1=1,
-    weight2=1,
-    sigma=None,
-):
-    """Draw plaid consisting of two sine-wave gratings
-
-    Parameters
-    ----------
-    grating_parameters1 : dict
-        kwargs to generate first sine-wave grating
-    grating_parameters2 : dict
-        kwargs to generate second sine-wave grating
-    weight1 : float
-        weight of first grating (default: 1)
-    weight2 : float
-        weight of second grating (default: 1)
-    sigma : float or (float, float)
-        sigma of Gaussian window in degree visual angle (y, x)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each bar (key: "grating_mask"),
-        and additional keys containing stimulus parameters
-    """
-    if sigma is None:
-        raise ValueError("plaid() missing argument 'sigma' which is not 'None'")
-
-    # Create sine-wave gratings
-    grating1 = sine_wave(**grating_parameters1)
-    grating2 = sine_wave(**grating_parameters2)
-
-    if grating1["shape"] != grating2["shape"]:
-        raise ValueError("Gratings must have the same shape")
-    if grating1["ppd"] != grating2["ppd"]:
-        raise ValueError("Gratings must have same ppd")
-    if grating1["origin"] != grating2["origin"]:
-        raise ValueError("Grating origins must be the same")
-
-    # Create Gaussian window
-    window = gaussian(
-        visual_size=grating1["visual_size"],
-        ppd=grating1["ppd"],
-        sigma=sigma,
-        origin=grating1["origin"],
-    )
-
-    img = (weight1 * grating1["img"] + weight2 * grating2["img"]) * window["img"]
-    img = img / (weight1 + weight2)
-
-    # Update parameters
-    grating1["img"] = img
-    grating1["sigma"] = sigma
-    grating1["grating_mask2"] = grating2["grating_mask"]
-    grating1["frequency2"] = grating2["frequency"]
-    grating1["bar_width2"] = grating2["bar_width"]
-    grating1["n_bars2"] = grating2["n_bars"]
-    grating1["gaussian_mask"] = window["gaussian_mask"]
-    return grating1
-
-
-if __name__ == "__main__":
-    from stimupy.utils.plotting import plot_stimuli
-
-    rotation = 90
-    origin = "center"
-    phase_shift = 30
-
-    p1 = {
-        "visual_size": (10, 5),
-        "ppd": 10,
-        "n_bars": 11,
-        "phase_shift": phase_shift,
-    }
-
-    p2 = {
-        "visual_size": 5,
-        "ppd": 20,
-        "frequency": 2,
-        "phase_shift": phase_shift,
-    }
-
-    p3 = {
-        "visual_size": 15,
-        "ppd": 10,
-        "bar_width": 3.5,
-        "period": "odd",
-        "phase_shift": phase_shift,
-        "origin": origin,
-        "rotation": rotation,
-    }
-
-    p4 = {
-        "visual_size": 15,
-        "ppd": 10,
-        "bar_width": 3.5,
-        "period": "ignore",
-        "origin": origin,
-        "rotation": rotation + 90,
-    }
-
-    p5 = {
-        "ppd": 20,
-        "n_bars": 5,
-        "bar_width": 4,
-        "period": "ignore",
-    }
-
-    p6 = {
-        "visual_size": 4.0,
-        "ppd": 20,
-        "bar_width": 0.08,
-    }
-
-    stims = {
-        "n_bars": square_wave(**p1, rotation=rotation, origin=origin),
-        "even": square_wave(**p2, rotation=rotation, origin=origin),
-        "odd": square_wave(**p3),
-        "ignore": square_wave(**p4),
-        "no_size": square_wave(**p5, rotation=rotation, origin=origin),
-        "tough_params": square_wave(**p6, rotation=rotation, origin=origin),
-        "sine_n_bars": sine_wave(**p1, rotation=rotation, origin=origin),
-        "sine_even": sine_wave(**p2, rotation=rotation, origin=origin),
-        "sine_odd": sine_wave(**p3),
-        "sine_ignore": sine_wave(**p4),
-        "sine_no_size": sine_wave(**p5, rotation=rotation, origin=origin),
-        "gabor_even": gabor(**p2, sigma=1, rotation=rotation, origin=origin),
-        "gabor_odd": gabor(**p3, sigma=5),
-        "gabor_ignore": gabor(**p4, sigma=3),
-        "staircase": staircase(**p5, rotation=rotation),
-        "plaid": plaid(p3, p4, sigma=4.0),
-    }
-    plot_stimuli(stims, mask=False)
-    plot_stimuli(stims, mask=True)
diff --git a/stimupy/components/mondrians.py b/stimupy/components/mondrians.py
deleted file mode 100644
index f2241740..00000000
--- a/stimupy/components/mondrians.py
+++ /dev/null
@@ -1,188 +0,0 @@
-import numpy as np
-
-from stimupy.components.shapes import parallelogram
-from stimupy.utils import resolution
-
-__all__ = [
-    "mondrians",
-]
-
-
-def mondrians(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    mondrian_positions=None,
-    mondrian_sizes=None,
-    mondrian_intensities=None,
-    intensity_background=0.5,
-):
-    """Draw Mondrians of given size and intensity at given position
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    mondrian_positions : Sequence[tuple, ... ] or None (default)
-        position (y, x) of each Mondrian in degrees visual angle
-    mondrian_sizes : Sequence[tuple, ... ] or None (default)
-        size (height, width, depth) of Mondrian parallelograms in degrees visual angle;
-        if only one number is given, squares will be drawn
-    mondrian_intensities : Sequence[Number, ... ] or None (default)
-        intensity values of each Mondrian, if only one number is given
-        all will have the same intensity
-    intensity_background : float
-        intensity value of background
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each Mondrian (key: "mondrian_mask"),
-        and additional keys containing stimulus parameters
-    """
-    if mondrian_positions is None:
-        raise ValueError("mondrians() missing argument 'mondrian_positions' which is not 'None'")
-    if mondrian_sizes is None:
-        raise ValueError("mondrians() missing argument 'mondrian_sizes' which is not 'None'")
-    if mondrian_intensities is None:
-        raise ValueError("mondrians() missing argument 'mondrian_intensities' which is not 'None'")
-
-    # Resolve resolution
-    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
-    if len(np.unique(ppd)) > 1:
-        raise ValueError("ppd should be equal in x and y direction")
-
-    img = np.ones(shape) * intensity_background
-    mask = np.zeros(shape)
-
-    n_mondrians = len(mondrian_positions)
-
-    if isinstance(mondrian_intensities, (float, int)):
-        mondrian_intensities = (mondrian_intensities,) * n_mondrians
-
-    if isinstance(mondrian_sizes, (float, int)):
-        mondrian_sizes = ((mondrian_sizes, mondrian_sizes),) * n_mondrians
-
-    if any(
-        len(lst) != n_mondrians
-        for lst in [mondrian_positions, mondrian_sizes, mondrian_intensities]
-    ):
-        raise Exception(
-            "There need to be as many mondrian_positions as there are "
-            "mondrian_sizes and mondrian_intensities."
-        )
-
-    mondrian_positions_px = []
-    mondrian_shapes = []
-
-    for m in range(n_mondrians):
-        try:
-            if len(mondrian_positions[m]) != 2:
-                raise ValueError("Mondrian position tuples should be (ypos, xpos)")
-        except Exception:
-            raise ValueError("Mondrian position tuples should be (ypos, xpos)")
-
-        ypos, xpos = resolution.lengths_from_visual_angles_ppd(mondrian_positions[m], ppd[0])
-        individual_shapes = resolution.lengths_from_visual_angles_ppd(mondrian_sizes[m], ppd[0])
-
-        try:
-            if len(individual_shapes) == 2:
-                depth = 0
-                individual_shapes = individual_shapes + [
-                    depth,
-                ]
-            elif len(individual_shapes) == 3:
-                depth = mondrian_sizes[m][2]
-            else:
-                raise ValueError(
-                    "Mondrian size tuples should be (height, width) for "
-                    "rectangles or (height, width, depth) for parallelograms"
-                )
-        except Exception:
-            raise ValueError(
-                "Mondrian size tuples should be (height, width) for"
-                "rectangles or (height, width, depth) for parallelograms"
-            )
-
-        if depth < 0:
-            xpos += int(depth * ppd[0])
-        mondrian_positions_px.append(tuple([ypos, xpos]))
-        mondrian_shapes.append(tuple(individual_shapes))
-
-        # Create parallelogram
-        patch = parallelogram(
-            visual_size=(mondrian_sizes[m][0], mondrian_sizes[m][1] + np.abs(depth)),
-            ppd=ppd,
-            parallelogram_size=(mondrian_sizes[m][0], mondrian_sizes[m][1], depth),
-            intensity_background=intensity_background,
-            intensity_parallelogram=mondrian_intensities[m],
-        )
-
-        # Place it into Mondrian mosaic
-        yshape, xshape = patch["img"].shape
-        if ypos < 0 or xpos < 0:
-            raise ValueError("There are no negative position coordinates")
-        if (ypos + yshape > shape[0]) or (xpos + xshape > shape[1]):
-            raise ValueError("Not all Mondrians fit into the stimulus")
-        mask_large = np.zeros(shape)
-        mask_large[ypos : ypos + yshape, xpos : xpos + xshape] = patch["shape_mask"]
-
-        img[mask_large == 1] = mondrian_intensities[m]
-        mask[mask_large == 1] = m + 1
-
-    stim = {
-        "img": img,
-        "mondrian_mask": mask.astype(int),
-        "ppd": ppd,
-        "visual_size": visual_size,
-        "shape": shape,
-        "mondrian_positions": tuple(mondrian_positions),
-        "mondrian_positions_px": tuple(mondrian_positions_px),
-        "mondrian_sizes": tuple(mondrian_sizes),
-        "mondrian_shapes": tuple(mondrian_shapes),
-        "mondrian_intensities": tuple(mondrian_intensities),
-        "intensity_background": intensity_background,
-    }
-    return stim
-
-
-if __name__ == "__main__":
-    from stimupy.utils.plotting import plot_stimuli
-
-    p1 = {
-        "mondrian_positions": ((0, 0), (0, 4), (1, 3), (4, 4), (5, 1)),
-        "mondrian_sizes": 3,
-        "mondrian_intensities": np.random.rand(5),
-    }
-
-    p2 = {
-        "mondrian_positions": ((0, 0), (8, 4), (1, 6), (4, 4), (5, 1)),
-        "mondrian_sizes": ((3, 4, 1), (2, 2, 0), (5, 4, -1), (3, 4, 1), (5, 2, 0)),
-        "mondrian_intensities": np.random.rand(5),
-    }
-
-    p3 = {
-        "mondrian_positions": ((0, 0), (0, 2)),
-        "mondrian_sizes": ((2, 2, 0), (2, 2, 0)),
-        "mondrian_intensities": (0.2, 0.8),
-    }
-
-    p4 = {
-        "mondrian_positions": ((0, 0), (0, 2)),
-        "mondrian_sizes": ((2, 2, 1), (2, 2, 1)),
-        "mondrian_intensities": (0.2, 0.8),
-    }
-
-    stims = {
-        "mondrians1": mondrians(visual_size=8, ppd=10, **p1),
-        "mondrians2": mondrians(visual_size=10, ppd=10, **p2),
-        "mondrians3": mondrians(visual_size=(2, 6), ppd=10, **p3),
-        "mondrians4": mondrians(visual_size=(2, 6), ppd=10, **p4),
-    }
-
-    plot_stimuli(stims, mask=False)
diff --git a/stimupy/components/radials.py b/stimupy/components/radials.py
new file mode 100644
index 00000000..302c8a8f
--- /dev/null
+++ b/stimupy/components/radials.py
@@ -0,0 +1,316 @@
+import itertools
+
+import numpy as np
+
+from stimupy.components import image_base, mask_elements
+from stimupy.utils import resolution
+
+__all__ = [
+    "disc",
+    "ring",
+    "annulus",
+    "rings",
+]
+
+
+def mask_rings(
+    radii,
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    origin="mean",
+):
+    """Generate mask with integer indices for rings
+
+    Parameters
+    ----------
+    radii : Sequence[Number]
+        outer radii of rings (& disc) in degree visual angle
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each ring (key: "ring_mask"),
+        and additional keys containing stimulus parameters
+
+    Raises
+    ------
+    ValueError
+        if largest radius does not fit in visual size
+    """
+
+    # no axes are None; check if fits
+    if visual_size.height < np.max(radii) * 2 or visual_size.width < np.max(radii) * 2:
+        raise ValueError(
+            f"Largest radius {np.max(radii)} does not fit in visual size {visual_size}"
+        )
+
+    # Mark elements with integer idx-value
+    stim = mask_elements(
+        orientation="radial",
+        edges=radii,
+        rotation=0.0,
+        shape=shape,
+        visual_size=visual_size,
+        ppd=ppd,
+        origin=origin,
+    )
+    stim["ring_mask"] = stim["mask"]
+    del stim["mask"]
+    return stim
+
+
+def rings(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    radii=None,
+    intensity_rings=(0, 1),
+    intensity_background=0.5,
+    origin="mean",
+):
+    """Draw a central solid disc with zero or more solid rings (annuli)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    radii : Sequence[Number]
+        outer radii of rings (& disc) in degree visual angle
+    intensity_rings : Sequence[Number, ...]
+        intensity value for each ring, from inside to out.
+        If fewer intensities are passed than number of radii, cycles through intensities
+    intensity_background : float (optional)
+        value of background, by default 0.5
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each ring (key: "ring_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if radii is None:
+        raise ValueError("disc_and_rings() missing argument 'radii' which is not 'None'")
+
+    # Try to resolve resolution;
+    try:
+        shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
+    except resolution.TooManyUnknownsError:
+        # Check visual_size
+        visual_size = resolution.validate_visual_size(visual_size)
+
+    if visual_size is None or visual_size == (None, None):
+        # Two axes are None; make image large enought to fit
+        visual_size = resolution.validate_visual_size(np.max(radii) * 2)
+    elif None in visual_size:
+        # one axis is None; make square
+        visual_size = [x for x in visual_size if x is not None]
+        visual_size = resolution.validate_visual_size(visual_size)
+
+    if np.diff(radii).min() < 0:
+        raise ValueError("radii need to monotonically increase")
+
+    # Get masks for rings
+    params = mask_rings(radii=radii, shape=shape, visual_size=visual_size, ppd=ppd, origin=origin)
+    shape = params["shape"]
+
+    # Draw rings
+    base = image_base(shape=shape, visual_size=visual_size, origin=origin)
+    distances = base["radial"]
+
+    img = np.ones(shape) * intensity_background
+    ints = [*itertools.islice(itertools.cycle(intensity_rings), len(radii))]
+    for radius, intensity in zip(reversed(radii), reversed(ints)):
+        img[distances < radius] = intensity
+
+    # Assemble output
+    params["intensity_rings"] = intensity_rings
+    return {"img": img, **params}
+
+
+def disc(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    radius=None,
+    intensity_disc=1.0,
+    intensity_background=0.5,
+    origin="mean",
+):
+    """Draw a central disc
+
+    Essentially, `dics(radius)` is an alias for `ring(radii=[0, radius])`
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    radius : Number
+        outer radius of disc in degree visual angle
+    intensity_disc : Number
+        intensity value of disc, by default 1.0
+    intensity_background : float (optional)
+        intensity value of background, by default 0.5
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each ring (key: "ring_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if radius is None:
+        raise ValueError("disc() missing argument 'radius' which is not 'None'")
+
+    radius = np.array(radius)
+    intensity = np.array(intensity_disc)
+
+    if radius.size != 1:
+        raise ValueError("Can only pass 1 radius")
+    if intensity.size != 1:
+        raise ValueError("Can only pass 1 intensity")
+
+    stim = ring(
+        radii=np.insert(radius, 0, 0),
+        intensity_ring=intensity,
+        visual_size=visual_size,
+        ppd=ppd,
+        intensity_background=intensity_background,
+        shape=shape,
+        origin=origin,
+    )
+    return stim
+
+
+def ring(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    radii=None,
+    intensity_ring=1.0,
+    intensity_background=0.5,
+    origin="mean",
+):
+    """Draw a ring (annulus)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    radii : Sequence[Number, Number]
+        inner and outer radius of ring in degree visual angle
+    intensity_ring : Number
+        intensity value of ring, by default 1.0
+    intensity_background : float (optional)
+        intensity value of background, by default 0.5
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each ring (key: "ring_mask"),
+        and additional keys containing stimulus parameters
+
+    Raises
+    ------
+    ValueError
+        if passed in less/more than 2 radii (inner, outer)
+    ValueError
+        if passed in less/more than 1 intensity
+    """
+    if radii is None:
+        raise ValueError("ring() missing argument 'radii' which is not 'None'")
+    if len(radii) != 2:
+        raise ValueError("Can only pass exactly 2 radii")
+    if len([intensity_ring]) != 1:
+        raise ValueError("Can only pass 1 intensity")
+
+    if radii[1] is None:
+        shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
+        radii[1] = np.max(visual_size) / 2
+
+    if radii[1] < radii[0]:
+        raise ValueError("first radius needs to be smaller than second radius")
+
+    stim = rings(
+        radii=radii,
+        intensity_rings=[intensity_background, intensity_ring],
+        shape=shape,
+        visual_size=visual_size,
+        ppd=ppd,
+        intensity_background=intensity_background,
+        origin=origin,
+    )
+    stim["ring_mask"] = np.where(stim["ring_mask"] == 2, 1, 0)
+    return stim
+
+
+annulus = ring
+
+
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "visual_size": (10, 10),
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "disc": disc(**default_params, radius=3),
+        "disc_and_rings": rings(**default_params, radii=(1, 2, 3)),
+        "ring": ring(**default_params, radii=(1, 2)),
+    }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/components/shapes.py b/stimupy/components/shapes.py
index 307a1556..f3e67100 100644
--- a/stimupy/components/shapes.py
+++ b/stimupy/components/shapes.py
@@ -2,7 +2,7 @@
 
 from stimupy.components import image_base
 from stimupy.components.angulars import wedge
-from stimupy.components.circulars import annulus, ring, disc
+from stimupy.components.radials import annulus, disc, ring
 from stimupy.utils import resolution
 
 __all__ = [
diff --git a/stimupy/components/waves.py b/stimupy/components/waves.py
new file mode 100644
index 00000000..7d0ef408
--- /dev/null
+++ b/stimupy/components/waves.py
@@ -0,0 +1,752 @@
+import itertools
+import warnings
+from copy import deepcopy
+
+import numpy as np
+
+from stimupy.components import draw_regions, image_base
+from stimupy.utils import int_factorize, resolution
+from stimupy.utils.contrast_conversions import adapt_intensity_range
+from stimupy.utils.utils import apply_bessel, round_to_vals
+
+__all__ = [
+    "sine",
+    "square",
+    "staircase",
+    "bessel",
+]
+
+
+def resolve_grating_params(
+    length=None,
+    visual_angle=None,
+    ppd=None,
+    frequency=None,
+    n_phases=None,
+    phase_width=None,
+    period="ignore",
+    round_phase_width=True,
+):
+    """Resolve (if possible) spatial parameters for a grating
+
+    Gratings take the regular resolution parameters (shape, ppd, visual_size).
+    In addition, there has to be an additional specification
+    of the frequency, number and with of phases (e.g., bars).
+    This can be done in two ways:
+    a phase_width (in degrees) and n_phases,
+    and/or by specifying the spatial frequency of the grating (in cycles per degree)
+
+    The total shape (in pixels) and visual size (in degrees) has to match the
+    specification of the phases and their widths.
+    Thus, not all 6 parameters have to be specified, as long as the both the resolution
+    and the distribution of phases can be resolved.
+
+    Note: all phases in a grating have the same width
+
+    Parameters
+    ----------
+    length : Number, or None (default)
+        lenght of grating, in pixels
+    visual_angle :Number, or None (default)
+        visual angle of grating, in degrees
+    ppd : Number, or None (default)
+        pixels per degree along the axis of grating
+    frequency : Number, or None (default)
+        frequency of grating, in cycles per degree
+    n_phases : int, or None (default)
+        number of phases (e.g., bars), i.e., half the number of full periods
+    phase_width : Number, or None (default)
+        extend of a single phase (e.g., bar), in degrees
+    period : "even", "odd", "either", "ignore" (default)
+        whether to ensure the grating has "even" number of phases,
+        "odd" number of phases, or "either" even/odd,
+        or no guarantees ("ignore")
+    round_phase_width : bool,
+        whether to round phase_width to an integer number of pixels, by default True
+
+
+    Returns
+    -------
+    dict[str, Any]
+        dictionary with all six resolution & size parameters resolved.
+    """
+    old_frequency = deepcopy(frequency)
+    old_n_phases = deepcopy(n_phases)
+    old_phase_width = deepcopy(phase_width)
+
+    if period not in ["ignore", "even", "odd", "either"]:
+        raise TypeError(f"period not understood: {period}")
+
+    # Try to resolve resolution
+    try:
+        length, visual_angle, ppd = resolution.resolve_1D(
+            length=length, visual_angle=visual_angle, ppd=ppd
+        )
+    except resolution.TooManyUnknownsError:
+        ppd = ppd
+        length = length
+        visual_angle = visual_angle
+
+    # Try to resolve number and width(s) of phases:
+    # Logic here is that phase_width expresses "degrees per phase",
+    # which we can convert to "phases_per_degree"
+    # phase_width = degrees_per_phase = 1 / phases_per_degree = 1 / (2*frequency)
+    if phase_width is not None:
+        phases_pd = 1 / phase_width
+        if frequency is not None and phases_pd != 2 * frequency:
+            raise ValueError(f"phase_width {phase_width} and frequency {frequency} don't match")
+    elif frequency is not None:
+        phases_pd = 2 * frequency
+    else:  # both are None:
+        phases_pd = None
+
+    # Logic here is that phase_width expresses "degrees per phase",
+    # which we can invert to phases_per_degree, analogous to ppd:
+    # n_phases = phases_per_degree * n_degrees
+    # is analogous to
+    # pix = ppd * n_degrees
+    # Thus we can resolve the number and spacing of phases also as a resolution
+    try:
+        n_phases, min_angle, phases_pd = resolution.resolve_1D(
+            length=n_phases,
+            visual_angle=visual_angle,
+            ppd=phases_pd,
+            round=False,
+        )
+    except Exception as e:
+        raise Exception("Could not resolve grating frequency, phase_width, n_phases") from e
+
+    # Now resolve resolution
+    visual_angle = min_angle if visual_angle is None else visual_angle
+    length, visual_angle, ppd = resolution.resolve_1D(
+        length=length, visual_angle=visual_angle, ppd=ppd
+    )
+
+    # Ensure n phases fit in length?
+    if period != "ignore":
+        n_phases = round_n_phases(n_phases=n_phases, length=length, period=period)
+
+        # Calculate other params again:
+        n_phases, min_angle, phases_pd = resolution.resolve_1D(
+            length=n_phases,
+            visual_angle=visual_angle,
+            ppd=None,
+            round=False,
+        )
+
+    # Ensure each phase consists of integer number of pixels?
+    phase_width = 1 / phases_pd
+    if round_phase_width:
+        phase_width = np.round(phase_width * ppd) / ppd
+        n_phases = length / np.round(phase_width * ppd)
+
+    # Calculate frequency
+    frequency = 1 / (2 * phase_width)
+
+    # Check & warn if we changed some params
+    if period == "ignore":
+        if old_n_phases is not None and n_phases != old_n_phases:
+            warnings.warn(
+                f"Adjusted n_phases={old_n_phases} -> {n_phases} because of poor resolution"
+            )
+
+        if old_phase_width is not None and phase_width != old_phase_width:
+            warnings.warn(
+                f"Adjusted phase_width={old_phase_width} -> {phase_width} because of poor"
+                " resolution"
+            )
+
+        if old_frequency is not None and frequency != old_frequency:
+            warnings.warn(
+                f"Adjusted frequency={old_frequency} -> {frequency} because of poor resolution"
+            )
+    else:
+        if old_n_phases is not None and n_phases != old_n_phases:
+            warnings.warn(
+                f"Adjusted n_phases={old_n_phases} -> {n_phases} because of period={period}"
+            )
+
+        if old_phase_width is not None and phase_width != old_phase_width:
+            warnings.warn(
+                f"Adjusted phase_width={old_phase_width} -> {phase_width} because of"
+                f" period={period}"
+            )
+
+        if old_frequency is not None and frequency != old_frequency:
+            warnings.warn(
+                f"Adjusted frequency={old_frequency} -> {frequency} because of period={period}"
+            )
+
+    # Check that frequency does not exceed Nyquist limit:
+    if frequency > (ppd / 2):
+        raise ValueError(
+            f"Grating frequency ({frequency}) should not exceed Nyquist limit {ppd/2} (ppd/2)"
+        )
+
+    # Accumulate edges of phases (rounding to avoid accumulation of
+    # floating point imprecisions)
+    edges = [*itertools.accumulate(itertools.repeat(phase_width, int(np.ceil(n_phases))))]
+    edges = np.round(np.array(edges), 8)
+    edges = list(edges)
+
+    return {
+        "length": length,
+        "visual_angle": visual_angle,
+        "ppd": ppd,
+        "frequency": frequency,
+        "phase_width": phase_width,
+        "n_phases": n_phases,
+        "edges": edges,
+        "period": period,
+    }
+
+
+def round_n_phases(n_phases, length, period="either"):
+    """Round n_phases of grating to integer, even, or odd -- taking into account pixels
+
+    Finds the nearest integer (optionally limited to even or odd) n_phases
+    that length (in pixels) can be divided into.
+    Note that this maybe be quite far away from the input.
+
+
+    Parameters
+    ----------
+    n_phases : int
+        number of phases (e.g., bars), i.e., half the number of full periods
+    length : Number
+        lenght of grating, in pixels
+    period : "even", "odd", "either" (default)
+        whether to ensure the grating has "even" number of phases,
+        "odd" number of phases, or "either" even/odd
+
+    Returns
+    -------
+    n_phases : int
+        rounded n_phases
+    """
+
+    # n_phases has to integer-divide length
+    possible_phase_pix = np.array(list(int_factorize(length)))
+    possible_n_phases = length / possible_phase_pix
+
+    if period == "even":
+        # only look at possible_n_phases that are even
+        possible_n_phases = possible_n_phases[possible_n_phases % 2 == 0]
+    elif period == "odd":
+        # only look at possible_n_phases that are odd
+        possible_n_phases = possible_n_phases[possible_n_phases % 2 != 0]
+
+    if len(possible_n_phases) == 0:
+        raise ValueError(f"Cannot fit {period} number of phases into {length} px")
+
+    closest = possible_n_phases[np.argmin(np.abs(possible_n_phases - n_phases))]
+
+    return int(closest)
+
+
+def sine(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_phases=None,
+    phase_width=None,
+    period="ignore",
+    rotation=0.0,
+    phase_shift=None,
+    intensities=(0.0, 1.0),
+    origin=None,
+    base_type=None,
+    round_phase_width=None,
+):
+    """Draw a sine-wave grating given a certain base_type
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_phases : int, or None (default)
+        number of phases in the grating
+    phase_width : Number, or None (default)
+        width of a single phase, in degrees visual angle
+    period : "even", "odd", "either", "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float or None (default)
+        rotation of grating in degrees
+    phase_shift : float or None (default)
+        phase shift of grating in degrees
+    intensities : Sequence[float, float]
+        min and max intensity of sine-wave, by default (0.0, 1.0).
+    origin : "corner", "mean", "center" or None (default)
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center
+        if "center": set origin to real center (closest existing value to mean)
+    base_type : str or None
+        if "horizontal", use distance from origin in x-direction,
+        if "vertical", use distance from origin in x-direction;
+        if "rotated", use combined and rotated distance from origin in x-y;
+        if "radial", use radial distance from origin,
+        if "angular", use angular distance from origin,
+        if "cityblock", use cityblock distance from origin
+    round_phase_width : Bool or None (default)
+        if True, round width of bars given resolution
+
+    Returns
+    ----------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each bar (key: "grating_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if phase_shift is None:
+        raise ValueError("waves.sine() missing argument 'phase_shift' which is not 'None'")
+    if intensities is None:
+        raise ValueError("waves.sine() missing argument 'intensities' which is not 'None'")
+    if origin is None:
+        raise ValueError("waves.sine() missing argument 'origin' which is not 'None'")
+    if round_phase_width is None:
+        raise ValueError("waves.sine() missing argument 'round_phase_width' which is not 'None'")
+    if period is None:
+        period = "ignore"
+
+    base_types = ["horizontal", "vertical", "rotated", "radial", "angular", "cityblock"]
+    if base_type not in base_types:
+        raise ValueError(f"base_type needs to be one of {base_types}")
+
+    lst = [visual_size, ppd, shape, frequency, n_phases, phase_width]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'grating()' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'n_phases', 'phase_width'"
+        )
+
+    # Try to resolve resolution
+    try:
+        shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
+    except ValueError:
+        ppd = resolution.validate_ppd(ppd)
+        shape = resolution.validate_shape(shape)
+        visual_size = resolution.validate_visual_size(visual_size)
+
+    alpha = [np.abs(np.cos(np.deg2rad(rotation))), np.abs(np.sin(np.deg2rad(rotation)))]
+
+    if shape.width is not None:
+        length = np.round(alpha[0] * shape.width + alpha[1] * shape.height)
+    else:
+        length = None
+
+    if visual_size.width is not None:
+        visual_angle = alpha[0] * visual_size.width + alpha[1] * visual_size.height
+    else:
+        visual_angle = None
+
+    if ppd.horizontal is not None:
+        ppd_1D = ppd.horizontal
+    else:
+        ppd_1D = None
+
+    if rotation % 90 != 0 and round_phase_width:
+        round_phase_width = False
+        warnings.warn("Rounding phase width is turned off for oblique gratings")
+
+    if rotation % 90 != 0 and period != "ignore":
+        period = "ignore"
+        warnings.warn("Period ignored for oblique gratings")
+
+    # Resolve params
+    params = resolve_grating_params(
+        length=length,
+        visual_angle=visual_angle,
+        n_phases=n_phases,
+        phase_width=phase_width,
+        ppd=ppd_1D,
+        frequency=frequency,
+        period=period,
+        round_phase_width=round_phase_width,
+    )
+    length = params["length"]
+    ppd_1D = params["ppd"]
+    visual_angle = params["visual_angle"]
+    frequency = params["frequency"]
+    phase_width = params["phase_width"]
+    n_phases = params["n_phases"]
+
+    # Determine size/shape of whole image
+    if None in shape:
+        shape = [length * alpha[1], length * alpha[0]]
+        if np.round(alpha[1], 5) == 0:
+            shape[0] = shape[1]
+        if np.round(alpha[0], 5) == 0:
+            shape[1] = shape[0]
+
+    if None in ppd:
+        ppd = (ppd_1D, ppd_1D)
+
+    if None in visual_size:
+        visual_size = resolution.visual_size_from_shape_ppd(shape=shape, ppd=ppd)
+
+    shape = resolution.validate_shape(shape)
+    visual_size = resolution.validate_visual_size(visual_size)
+    ppd = resolution.validate_ppd(ppd)
+
+    # Set up coordinates
+    base = image_base(
+        shape=shape, visual_size=visual_size, ppd=ppd, rotation=rotation, origin=origin
+    )
+    distances = base[base_type]
+    distances = np.round(distances, 6)
+
+    # Shift distances minimally to ensure proper behavior
+    if origin == "corner":
+        distances = adapt_intensity_range(distances, 1e-03, distances.max() - 1e-03)
+    else:
+        distances = adapt_intensity_range(
+            distances, distances.min() - 1e-05, distances.max() - 1e-05
+        )
+
+    # Draw image
+    img = np.sin(frequency * 2 * np.pi * distances + np.deg2rad(phase_shift))
+    img = adapt_intensity_range(img, intensities[0], intensities[1])
+
+    # Create mask
+    dmax = max(distances.max(), -distances.min()) + (phase_width / 2)
+    if origin == "corner" or base_type == "radial" or base_type == "cityblock":
+        edges = np.arange(distances.min() + (phase_width / 2), dmax, phase_width)
+
+        if origin == "mean":
+            edges -= distances.min()
+    else:
+        edges_pos = np.arange(0 + (phase_width / 2), dmax, phase_width) + 1e-4
+        edges_neg = -np.arange(0 + (phase_width / 2), dmax, phase_width) - 1e-4
+        edges = np.unique(np.append(edges_neg[::-1], edges_pos))
+
+    edges -= (((phase_shift) % 360) / 180) * phase_width
+    # edges = np.round(edges, 8)
+    regions = round_to_vals(distances, edges)
+
+    mask = np.zeros(shape=regions.shape)
+    for idx, val in enumerate(np.unique(regions)):
+        mask = np.where(regions == val, idx + 1, mask)
+
+    # Package and output
+    stim = {
+        "img": img,
+        "grating_mask": mask.astype(int),
+        "visual_size": visual_size,
+        "ppd": ppd,
+        "shape": shape,
+        "frequency": frequency,
+        "n_phases": n_phases,
+        "phase_width": phase_width,
+        "period": period,
+        "rotation": rotation,
+        "phase_shift": phase_shift,
+        "round_phase_width": round_phase_width,
+        "origin": origin,
+        "base_type": base_type,
+        "intensities": intensities,
+    }
+    return stim
+
+
+def square(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_phases=None,
+    phase_width=None,
+    period="ignore",
+    rotation=0.0,
+    phase_shift=None,
+    intensities=(0.0, 1.0),
+    origin=None,
+    base_type=None,
+    round_phase_width=None,
+):
+    """Draw a square-wave grating given a certain base_type
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_phases : int, or None (default)
+        number of phases in the grating
+    phase_width : Number, or None (default)
+        width of a single phase, in degrees visual angle
+    period : "even", "odd", "either", "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float or None (default)
+        rotation of grating in degrees
+    phase_shift : float or None (default)
+        phase shift of grating in degrees
+    intensities : Sequence[float, float]
+        min and max intensity of square-wave, by default (0.0, 1.0).
+    origin : "corner", "mean", "center" or None (default)
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center
+        if "center": set origin to real center (closest existing value to mean)
+    base_type : str or None
+        if "horizontal", use distance from origin in x-direction,
+        if "vertical", use distance from origin in x-direction;
+        if "rotated", use combined and rotated distance from origin in x-y;
+        if "radial", use radial distance from origin,
+        if "angular", use angular distance from origin,
+        if "cityblock", use cityblock distance from origin
+    round_phase_width : Bool or None (default)
+        if True, round width of bars given resolution
+
+    Returns
+    ----------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each bar (key: "grating_mask"),
+        and additional keys containing stimulus parameters
+    """
+
+    stim = sine(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_phases,
+        phase_width=phase_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        intensities=intensities,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type=base_type,
+    )
+
+    # Round sine-wave to create square wave
+    stim["img"] = round_to_vals(stim["img"], intensities)
+    return stim
+
+
+def staircase(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_phases=None,
+    phase_width=None,
+    period="ignore",
+    rotation=0.0,
+    phase_shift=None,
+    origin=None,
+    base_type=None,
+    round_phase_width=None,
+    intensities=(0.0, 1.0),
+):
+    """Draw a luminance staircase
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_phases : int, or None (default)
+        number of phases in the grating
+    phase_width : Number, or None (default)
+        width of a single phase, in degrees visual angle
+    period : "even", "odd", "either", "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float or None (default)
+        rotation of grating in degrees
+    phase_shift : float or None (default)
+        phase shift of grating in degrees
+    origin : "corner", "mean", "center" or None (default)
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center
+        if "center": set origin to real center (closest existing value to mean)
+    base_type : str or None
+        if "horizontal", use distance from origin in x-direction,
+        if "vertical", use distance from origin in x-direction;
+        if "rotated", use combined and rotated distance from origin in x-y;
+        if "radial", use radial distance from origin,
+        if "angular", use angular distance from origin,
+        if "cityblock", use cityblock distance from origin
+    round_phase_width : Bool or None (default)
+        if True, round width of bars given resolution
+    intensities : Sequence[float, ...]
+        if len(intensities)==2, intensity range of staircase (default 0.0, 1.0);
+        if len(intensities)>2, intensity value for each phase.
+        Can specify as many intensities as n_phases.
+        If fewer intensities are passed than n_phases, cycles through intensities.
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each phase (key: "grating_mask"),
+        and additional keys containing stimulus parameters
+    """
+
+    stim = square(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_phases,
+        phase_width=phase_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type=base_type,
+    )
+
+    if len(intensities) == 2:
+        intensities = np.linspace(intensities[0], intensities[1], int(np.ceil(stim["n_phases"])))
+
+    # Use grating_mask to draw staircase
+    stim["img"] = draw_regions(mask=stim["grating_mask"], intensities=intensities)
+    stim["intensity_phases"] = intensities
+    return stim
+
+
+def bessel(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    order=0,
+    intensities=(1.0, 0.0),
+    origin="mean",
+):
+    """Draw a Bessel stimulus, i.e. draw circular rings following an nth order
+    Bessel function of a given frequency.
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of circular grating, in cycles per degree
+    order : int
+        n-th order Bessel function
+    intensities : (float, float)
+        intensity values of rings, first value indicating center intensity
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+
+    Returns
+    ----------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        empty mask (key: "ring_mask"),
+        and additional keys containing stimulus parameters
+    """
+
+    base = image_base(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        rotation=0,
+        origin=origin,
+    )
+
+    arr = base["radial"] * frequency * 2 * np.pi
+    img = apply_bessel(arr, order=order)
+    img = (img - img.min()) / (img.max() - img.min())
+    img = img * (intensities[0] - intensities[1]) + intensities[1]
+
+    stim = {
+        "img": img,
+        "ring_mask": np.zeros(base["shape"]).astype(int),
+        "visual_size": base["visual_size"],
+        "ppd": base["ppd"],
+        "shape": base["shape"],
+        "order": order,
+        "frequency": frequency,
+        "intensity_rings": intensities,
+    }
+    return stim
+
+
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {"visual_size": 15, "ppd": 30, "intensities": (0.0, 1.0)}
+    default_params.update(kwargs)
+
+    grating_params = {
+        "phase_width": 3.5,
+        "period": "either",
+        "phase_shift": 90,
+        "origin": "center",
+        "round_phase_width": False,
+    }
+
+    # fmt: off
+    stimuli = {
+        "sine wave - horizontal": sine(**default_params, **grating_params, base_type="horizontal"),
+        "sine wave - vertical": sine(**default_params, **grating_params, base_type="vertical"),
+        "sine wave - oblique": sine(**default_params, **grating_params, base_type="rotated", rotation=30),
+        "sine wave - radial": sine(**default_params, **grating_params, base_type="radial"),
+        "sine wave - angular": sine(**default_params, **grating_params, base_type="angular"),
+        "sine wave - cityblock": sine(**default_params, **grating_params, base_type="cityblock"),
+
+        "square wave - horizontal": square(**default_params, **grating_params, base_type="horizontal"),
+        "square wave - vertical": square(**default_params, **grating_params, base_type="vertical"),
+        "square wave - oblique": square(**default_params, **grating_params, base_type="rotated", rotation=30),
+        "square wave - radial": square(**default_params, **grating_params, base_type="radial"),
+        "square wave - angular": square(**default_params, **grating_params, base_type="angular"),
+        "square wave - cityblock": square(**default_params, **grating_params, base_type="cityblock"),
+
+        "staircase - horizontal": staircase(**default_params, **grating_params, base_type="horizontal"),
+        "staircase - vertical": staircase(**default_params, **grating_params, base_type="vertical"),
+        "staircase - oblique": staircase(**default_params, **grating_params, base_type="rotated", rotation=30),
+        "staircase - radial": staircase(**default_params, **grating_params, base_type="radial"),
+        "staircase - angular": staircase(**default_params, **grating_params, base_type="angular"),
+        "staircase - cityblock": staircase(**default_params, **grating_params, base_type="cityblock"),
+
+        "bessel": bessel(**default_params, frequency=0.5),
+    }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=False, save=None)
diff --git a/stimupy/illusions/__init__.py b/stimupy/illusions/__init__.py
deleted file mode 100644
index a8c5bebd..00000000
--- a/stimupy/illusions/__init__.py
+++ /dev/null
@@ -1,169 +0,0 @@
-from . import (
-    angulars,
-    benarys,
-    checkerboards,
-    circulars,
-    cornsweets,
-    cubes,
-    delboeufs,
-    dungeons,
-    frames,
-    gratings,
-    hermanns,
-    mondrians,
-    mueller_lyers,
-    ponzos,
-    sbcs,
-    todorovics,
-    wedding_cakes,
-    whites,
-)
-
-
-def create_overview():
-    """
-    Create dictionary with examples from all stimulus-illusions
-
-    Returns
-    -------
-    stims : dict
-        dict with all stimuli containing individual stimulus dicts.
-    """
-
-    p = {
-        "visual_size": (10, 10),
-        "ppd": 20,
-    }
-
-    p_mondrians = {
-        "mondrian_depths": (0.0, 1.0, 0.0, -1.0),
-        "target_indices": ((1, 1), (3, 1)),
-        "mondrian_intensities": (
-            (0.4, 0.75, 0.4, 0.75),
-            (0.75, 0.4, 0.75, 1.0),
-            (0.4, 0.75, 0.4, 0.75),
-            (0.0, 0.4, 0.0, 0.4),
-        ),
-    }
-
-    p_small_grating = {
-        "ppd": 20,
-        "frequency": 1,
-        "intensity_bars": (1, 0),
-    }
-
-    # fmt: off
-    stims = {
-        # Angular
-        "pinwheel": angulars.pinwheel(**p, n_segments=8, target_width=1, target_indices=3),
-        # Benary
-        "benary_general": benarys.cross_generalized(**p, target_size=1, cross_thickness=2, target_x=(3, 6, 3, 6), target_y=(4, 6, 6, 4)),
-        "benary_rectangles": benarys.cross_rectangles(**p, target_size=1, cross_thickness=2),
-        "benary_triangles": benarys.cross_triangles(**p, target_size=1, cross_thickness=2),
-        "todorovic_benary_general": benarys.todorovic_generalized(**p, L_width=2, target_size=1, target_x=(3, 6, 3, 6), target_y=(4, 6, 6, 4)),
-        "todorovic_benary_rectangles": benarys.todorovic_rectangles(**p, target_size=1, L_width=2),
-        "todorovic_benary_triangles": benarys.todorovic_triangles(**p, target_size=1, L_width=2),
-        # Checkerboards
-        "checkerboard": checkerboards.checkerboard(**p, board_shape=8, target_indices=[(3, 2), (3, 5)]),
-        "contrast-contrast": checkerboards.contrast_contrast(**p, board_shape=8, target_shape=(4, 4)),
-        # Circular
-        "circular_rings": circulars.rings(**p, frequency=1.0),
-        "circular_rings_two_sided": circulars.two_sided_rings(**p, frequency=1.0),
-        "circular_bullseye": circulars.bullseye(**p, frequency=1.0),
-        "circular_bullseye_two_sided": circulars.two_sided_bullseye(**p, frequency=1.0),
-        # Cornsweet
-        "cornsweet": cornsweets.cornsweet(**p, ramp_width=3),
-        # Cube
-        "cube_variable": cubes.varying_cells(ppd=20, cell_lengths=(1, 1.5, 1), cell_thickness=0.5, cell_spacing=0.5, target_indices=1),
-        "cube": cubes.cube(**p, n_cells=5, target_indices=(1, 2), cell_thickness=1, cell_spacing=0.5),
-        # Delbouef
-        "delboeuf": delboeufs.delboeuf(**p, outer_radius=4, target_radius=1),
-        "2sided_delboeuf": delboeufs.two_sided(**p, outer_radii=(2, 1.1), target_radius=1),
-        # Dungeon
-        "dungeon": dungeons.dungeon(**p, n_cells=5),
-        # Frames
-        "frames": frames.rings(**p, frequency=0.5, target_indices=3),
-        "frames_general": frames.rings_generalized(**p, radii=(1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5), target_indices=3),
-        "2sided_frames": frames.two_sided_rings(**p, frequency=1, target_indices=3),
-        "frames_bullseye": frames.bullseye(**p, frequency=0.5),
-        "frames_bullseye_general": frames.bullseye_generalized(**p, radii=(1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5)),
-        "2sided_bullseye": frames.two_sided_bullseye(**p, frequency=1),
-        # Grating
-        "square_wave": gratings.square_wave(**p, frequency=0.5, target_indices=(3,)),
-        "grating_uniform": gratings.uniform(**p, frequency=1, grating_size=3, target_indices=(3,)),
-        "grating_grating1": gratings.grating(large_grating_params={**p, "frequency": 2, "rotation": 45},
-                                             small_grating_params={**p_small_grating, "visual_size": 4, "target_indices": (1, 3, 5, 7),}),
-        "grating_grating2": gratings.grating(large_grating_params={**p, "frequency": 1},
-                                             small_grating_params={**p_small_grating, "visual_size": 4,"target_indices": (1, 3, 5, 7),}),
-        "grating_grating3": gratings.grating(large_grating_params={**p, "frequency": 1, "intensity_bars": (0, 1),},
-                                             small_grating_params={**p_small_grating, "visual_size": (4, 10), "target_indices": (9, 11, 13),}),
-        "grating_grating_masked": gratings.grating_masked(large_grating_params={**p, "frequency": 1, "rotation": 90,},
-                                                          small_grating_params={**p_small_grating, "visual_size": 4, "target_indices": (1, 3, 5, 7),},
-                                                          mask_size=(2, 2, 1)),
-        "counterphase_induction": gratings.counterphase_induction(**p, frequency=1, target_size=4, target_phase_shift=90,),
-        "grating_induction": gratings.induction(**p, frequency=0.5, target_width=0.5),
-        "grating_induction_blur": gratings.induction_blur(**p, frequency=0.5, target_width=0.5, sigma=0.1),
-        # Hermann´
-        "hermann": hermanns.grid(**p, element_size=(1.5, 1.5, 0.2)),
-        # Mondrians
-        "mondrians": mondrians.corrugated_mondrians(**p, **p_mondrians),
-        # Mueller-Lyer
-        "mueller-lyer": mueller_lyers.mueller_lyer(**p, outer_lines_length=1.5, outer_lines_angle=45, target_length=6, line_width=0.1),
-        "2sided_mueller-lyer": mueller_lyers.two_sided(**p, outer_lines_length=1.5, outer_lines_angle=45, target_length=2.5, line_width=0.1),
-        # Ponzo
-        "ponzo": ponzos.ponzo(**p, outer_lines_length=8, outer_lines_width=0.1, target_lines_length=3, target_lines_width=0.1, target_distance=3),
-        # SBC
-        "sbc_generalized": sbcs.generalized(**p, target_size=3, target_position=(0, 2)),
-        "sbc_basic": sbcs.basic(**p, target_size=3),
-        "sbc_two_sided": sbcs.two_sided(**p, target_size=3),
-        "sbc_with_dots": sbcs.with_dots(ppd=20, n_dots=5, dot_radius=3, distance=0.5, target_shape=3),
-        "sbc_dotted": sbcs.dotted(ppd=20, n_dots=5, dot_radius=3, distance=0.5, target_shape=3),
-        "2sided_sbc_with_dots": sbcs.two_sided_with_dots(ppd=20, n_dots=5, dot_radius=3, distance=0.5, target_shape=3),
-        "2sided_dotted_sbc": sbcs.two_sided_dotted(ppd=20, n_dots=5, dot_radius=3, distance=0.5, target_shape=3),
-        # Todorovic
-        "todorovic_rectangle_general": todorovics.rectangle_generalized(**p, target_size=4, target_position=3, covers_size=2, covers_x=(2, 6), covers_y=(2, 6)),
-        "todorovic_rectangle": todorovics.rectangle(**p, target_size=4, covers_size=2, covers_offset=2),
-        "todorovic_cross_general": todorovics.cross_generalized(**p, cross_size=4, cross_arm_ratios=1., cross_thickness=2, covers_size=2, covers_x=(2, 6), covers_y=(2, 6)),
-        "todorovic_cross": todorovics.cross(**p, cross_size=4, cross_thickness=2, covers_size=2),
-        "todorovic_equal": todorovics.equal(**p, cross_size=4, cross_thickness=2),
-        "2sided_todorovic_rectangle": todorovics.two_sided_rectangle(**p, target_size=3, covers_size=1.5, covers_offset=1.5),
-        "2sided_todorovic_cross": todorovics.two_sided_cross(**p, cross_size=3, cross_thickness=1.5, covers_size=1.5),
-        "2sided_todorovic_equal": todorovics.two_sided_equal(**p, cross_size=3, cross_thickness=1.5),
-        # Wedding cake
-        "wedding_cake": wedding_cakes.wedding_cake(**p, L_size=(3, 3, 1), target_height=1, target_indices1=((1, 1), (2, 1)),),
-        # White
-        "white_general": whites.generalized(**p, frequency=0.5, target_indices=(1, 3, 5), target_center_offsets=(-1, -3, -1), target_heights=(2, 3, 2)),
-        "white_basic": whites.white(**p, frequency=0.5, target_indices=(2, -3), target_height=2),
-        "white_two-rows": whites.white_two_rows(**p, frequency=0.5, target_indices_top=(2,4), target_indices_bottom=(-2, -4), target_height=1, target_center_offset=2),
-        "Anderson White": whites.anderson(**p, frequency=0.5, target_indices_top=3, target_indices_bottom=-2, target_center_offset=2, target_height=2, stripe_center_offset=1.5, stripe_height=2),
-        "Yazdanbakhsh White": whites.yazdanbakhsh(**p, frequency=0.5, target_indices_top=3, target_indices_bottom=-2, target_center_offset=2, target_height=2, gap_size=0.5),
-        "Howe White": whites.howe(**p, frequency=0.5, target_indices_top=3, target_indices_bottom=-2, target_center_offset=2, target_height=2),
-    }
-    # fmt: on
-
-    return stims
-
-
-def overview(mask=False, save=None, extent_key="shape"):
-    """
-    Plot overview with examples from all stimulus-illusions
-
-    Parameters
-    ----------
-    mask : bool or str, optional
-        If True, plot mask on top of stimulus image (default: False).
-        If string is provided, plot this key from stimulus dictionary as mask
-    save : None or str, optional
-        If None (default), do not save the plot.
-        If string is provided, save plot under this name.
-    extent_key : str, optional
-        Key to extent which will be used for plotting.
-        Default is "shape", using the image size in pixels as extent.
-
-    """
-    from stimupy.utils import plot_stimuli
-
-    stims = create_overview()
-
-    # Plotting
-    plot_stimuli(stims, mask=mask, save=save, extent_key=extent_key)
diff --git a/stimupy/illusions/frames.py b/stimupy/illusions/frames.py
deleted file mode 100644
index ae0d3d50..00000000
--- a/stimupy/illusions/frames.py
+++ /dev/null
@@ -1,545 +0,0 @@
-import itertools
-
-import numpy as np
-
-from stimupy.components import frames as frames_component
-from stimupy.utils import resolution, stack_dicts
-
-__all__ = [
-    "rings",
-    "rings_generalized",
-    "two_sided_rings",
-    "bullseye",
-    "bullseye_generalized",
-    "two_sided_bullseye",
-]
-
-
-def rings(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_frames=None,
-    frame_width=None,
-    phase_shift=0,
-    intensity_frames=(1.0, 0.0),
-    intensity_background=0.5,
-    target_indices=(),
-    intensity_target=0.5,
-    origin="center",
-    clip=True,
-):
-    """Draw set of square frames, with some frame(s) as target
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_frames : int, or None (default)
-        number of frames in the grating
-    frame_width : Number, or None (default)
-        width of a single frame, in degrees visual angle
-    phase_shift : float
-        phase shift of grating in degrees
-    intensity_frames : Sequence[float, float]
-        min and max intensity of square-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    target_indices : int, or Sequence[int, ...]
-        indices frames where targets will be placed
-    intensity_target : float, or Sequence[float, ...], optional
-        intensity value for each target, by default 0.5.
-        Can specify as many intensities as number of target_indices;
-        If fewer intensities are passed than target_indices, cycles through intensities
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-    clip : Bool
-        if True, clip stimulus to image size (default: True)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each target (key: "target_mask"),
-        and additional keys containing stimulus parameters
-
-    References
-    ----------
-    Domijan, D. (2015).
-        A neurocomputational account
-        of the role of contour facilitation in brightness perception.
-        Frontiers in Human Neuroscience, 9, 93.
-        https://doi.org/10.3389/fnhum.2015.00093
-    """
-
-    # Frames component
-    stim = frames_component.square_wave(
-        shape=shape,
-        visual_size=visual_size,
-        ppd=ppd,
-        frequency=frequency,
-        n_frames=n_frames,
-        frame_width=frame_width,
-        phase_shift=phase_shift,
-        period="ignore",
-        intensity_frames=intensity_frames,
-        intensity_background=intensity_background,
-        origin=origin,
-        clip=clip,
-    )
-
-    # Resolve target parameters
-    if isinstance(target_indices, (int)):
-        target_indices = [
-            target_indices,
-        ]
-    if isinstance(intensity_target, (int, float)):
-        intensity_target = [
-            intensity_target,
-        ]
-    intensity_target = itertools.cycle(intensity_target)
-
-    # Place target(s)
-    targets_mask = np.zeros_like(stim["frame_mask"])
-    for target_idx, (bar_idx, intensity) in enumerate(zip(target_indices, intensity_target)):
-        targets_mask = np.where(stim["frame_mask"] == bar_idx, target_idx + 1, targets_mask)
-        stim["img"] = np.where(targets_mask == target_idx + 1, intensity, stim["img"])
-        if bar_idx > stim["frame_mask"].max():
-            raise ValueError("target_idx is outside stimulus")
-
-    # Update and return stimulus
-    stim["target_mask"] = targets_mask.astype(int)
-    return stim
-
-
-def two_sided_rings(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_frames=None,
-    frame_width=None,
-    phase_shift=0,
-    intensity_frames=(1.0, 0.0),
-    intensity_background=0.5,
-    target_indices=(),
-    intensity_target=0.5,
-):
-    """Draw set of square frames, with some frame(s) as target
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_frames : int, or None (default)
-        number of frames in the grating
-    frame_width : Number, or None (default)
-        width of a single frame, in degrees visual angle
-    phase_shift : float
-        phase shift of grating in degrees
-    intensity_frames : Sequence[float, float]
-        min and max intensity of square-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    target_indices : int, or Sequence[int, ...]
-        indices frames where targets will be placed
-    intensity_target : float, or Sequence[float, ...], optional
-        intensity value for each target, by default 0.5.
-        Can specify as many intensities as number of target_indices;
-        If fewer intensities are passed than target_indices, cycles through intensities
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each target (key: "target_mask"),
-        and additional keys containing stimulus parameters
-
-    References
-    ----------
-    Domijan, D. (2015).
-        A neurocomputational account
-        of the role of contour facilitation in brightness perception.
-        Frontiers in Human Neuroscience, 9, 93.
-        https://doi.org/10.3389/fnhum.2015.00093
-    """
-
-    # Resolve resolution
-    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
-
-    stim1 = rings(
-        visual_size=(visual_size[0], visual_size[1] / 2),
-        ppd=ppd,
-        frequency=frequency,
-        n_frames=n_frames,
-        frame_width=frame_width,
-        phase_shift=phase_shift,
-        intensity_target=intensity_target,
-        intensity_frames=intensity_frames,
-        target_indices=target_indices,
-    )
-
-    stim2 = rings(
-        visual_size=(visual_size[0], visual_size[1] / 2),
-        ppd=ppd,
-        frequency=frequency,
-        n_frames=n_frames,
-        frame_width=frame_width,
-        phase_shift=phase_shift,
-        intensity_target=intensity_target,
-        intensity_frames=intensity_frames[::-1],
-        target_indices=target_indices,
-    )
-
-    stim = stack_dicts(stim1, stim2)
-    stim["shape"] = shape
-    stim["visual_size"] = visual_size
-    return stim
-
-
-def rings_generalized(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    radii=None,
-    intensity_frames=(1.0, 0.0),
-    intensity_background=0.5,
-    target_indices=(),
-    intensity_target=0.5,
-    origin="center",
-):
-    """Draw sequential set of square frames with specified radii and targets
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    radii : Sequence[Number] or None (default)
-        radii of each frame, in degrees visual angle
-    intensity_frames : Sequence[float, float]
-        min and max intensity of square-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    target_indices : int, or Sequence[int, ...]
-        indices frames where targets will be placed
-    intensity_target : float, or Sequence[float, ...], optional
-        intensity value for each target, by default 0.5.
-        Can specify as many intensities as number of target_indices;
-        If fewer intensities are passed than target_indices, cycles through intensities
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each frame (key: "target_mask"),
-        and additional keys containing stimulus parameters
-    """
-
-    # Frames component
-    stim = frames_component.frames(
-        radii=radii,
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        intensity_frames=intensity_frames,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-
-    # Resolve target parameters
-    if isinstance(target_indices, (int)):
-        target_indices = [
-            target_indices,
-        ]
-    if isinstance(intensity_target, (int, float)):
-        intensity_target = [
-            intensity_target,
-        ]
-    intensity_target = itertools.cycle(intensity_target)
-
-    # Place target(s)
-    targets_mask = np.zeros_like(stim["frame_mask"])
-    for target_idx, (bar_idx, intensity) in enumerate(zip(target_indices, intensity_target)):
-        targets_mask = np.where(stim["frame_mask"] == bar_idx, target_idx + 1, targets_mask)
-        stim["img"] = np.where(targets_mask == target_idx + 1, intensity, stim["img"])
-        if bar_idx > stim["frame_mask"].max():
-            raise ValueError("target_idx is outside stimulus")
-
-    # Update and return stimulus
-    stim["target_mask"] = targets_mask
-    return stim
-
-
-def bullseye(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_frames=None,
-    frame_width=None,
-    phase_shift=0,
-    intensity_frames=(1.0, 0.0),
-    intensity_background=0.5,
-    target_indices=(),
-    intensity_target=0.5,
-    origin="center",
-    clip=True,
-):
-    """Square "bullseye", i.e., set of rings with target in center
-
-    Essentially frames(target_indices=1)
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_frames : int, or None (default)
-        number of frames in the grating
-    frame_width : Number, or None (default)
-        width of a single frame, in degrees visual angle
-    phase_shift : float
-        phase shift of grating in degrees
-    intensity_frames : Sequence[float, float]
-        min and max intensity of square-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    intensity_target : float, or Sequence[float, ...], optional
-        intensity value for each target, by default 0.5.
-        Can specify as many intensities as number of target_indices;
-        If fewer intensities are passed than target_indices, cycles through intensities
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-    clip : Bool
-        if True, clip stimulus to image size (default: True)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each target (key: "target_mask"),
-        and additional keys containing stimulus parameters
-
-    References
-    ----------
-    Bindman, D., & Chubb, C. (2004).
-        Brightness assimilation in bullseye displays.
-        Vision Research, 44, 309-319.
-        https://doi.org/10.1016/S0042-6989(03)00430-9
-    """
-
-    stim = rings(
-        shape=shape,
-        visual_size=visual_size,
-        ppd=ppd,
-        frequency=frequency,
-        n_frames=n_frames,
-        frame_width=frame_width,
-        phase_shift=phase_shift,
-        intensity_frames=intensity_frames,
-        target_indices=1,
-        intensity_target=intensity_target,
-        origin=origin,
-        clip=clip,
-    )
-    return stim
-
-
-def bullseye_generalized(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    radii=None,
-    intensity_frames=(1.0, 0.0),
-    intensity_background=0.5,
-    intensity_target=0.5,
-    origin="center",
-):
-    """Draw sequential set of square frames with specified radii with central target
-
-    Parameters
-    ----------
-    frame_radii : Sequence[Number]
-        radii of each frame, in degrees visual angle
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    radii : Sequence[Number] or None (default)
-        radii of each frame, in degrees visual angle
-    intensity_frames : Sequence[float, float]
-        min and max intensity of square-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-    intensity_target : float, or Sequence[float, ...], optional
-        intensity value for each target, by default 0.5.
-        Can specify as many intensities as number of target_indices;
-        If fewer intensities are passed than target_indices, cycles through intensities
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner
-        if "mean": set origin to hypothetical image center (default)
-        if "center": set origin to real center (closest existing value to mean)
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each frame (key: "target_mask"),
-        and additional keys containing stimulus parameters
-    """
-    stim = rings_generalized(
-        radii=radii,
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        intensity_frames=intensity_frames,
-        intensity_background=intensity_background,
-        target_indices=1,
-        intensity_target=intensity_target,
-        origin=origin,
-    )
-    return stim
-
-
-def two_sided_bullseye(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_frames=None,
-    frame_width=None,
-    phase_shift=0,
-    intensity_target=0.5,
-    intensity_frames=(1.0, 0.0),
-    intensity_background=0.5,
-):
-    """Two-sided square "bullseye", i.e., set of rings with target in center
-
-    Essentially frames(target_indices=1)
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_frames : int, or None (default)
-        number of frames in the grating
-    frame_width : Number, or None (default)
-        width of a single frame, in degrees visual angle
-    phase_shift : float
-        phase shift of grating in degrees
-    period : "full", "half", "ignore" (default)
-        whether to ensure the grating only has "full" periods,
-        half "periods", or no guarantees ("ignore")
-    intensity_target : float, or Sequence[float, ...], optional
-        intensity value for each target, by default 0.5.
-        Can specify as many intensities as number of target_indices;
-        If fewer intensities are passed than target_indices, cycles through intensities
-    intensity_frames : Sequence[float, float]
-        min and max intensity of square-wave, by default (0.0, 1.0)
-    intensity_background : float (optional)
-        intensity value of background, by default 0.5
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each target (key: "target_mask"),
-        and additional keys containing stimulus parameters
-
-    References
-    ----------
-        Bindman, D., & Chubb, C. (2004).
-        Brightness assimilation in bullseye displays.
-        Vision Research, 44, 309-319.
-        https://doi.org/10.1016/S0042-6989(03)00430-9
-    """
-
-    # Resolve resolution
-    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
-
-    stim1 = bullseye(
-        visual_size=(visual_size[0], visual_size[1] / 2),
-        ppd=ppd,
-        frequency=frequency,
-        n_frames=n_frames,
-        frame_width=frame_width,
-        phase_shift=phase_shift,
-        intensity_target=intensity_target,
-        intensity_frames=intensity_frames,
-    )
-
-    stim2 = bullseye(
-        visual_size=(visual_size[0], visual_size[1] / 2),
-        ppd=ppd,
-        frequency=frequency,
-        n_frames=n_frames,
-        frame_width=frame_width,
-        phase_shift=phase_shift,
-        intensity_target=intensity_target,
-        intensity_frames=intensity_frames[::-1],
-    )
-
-    stim = stack_dicts(stim1, stim2)
-    stim["shape"] = shape
-    stim["visual_size"] = visual_size
-    return stim
-
-
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
-
-    p1 = {
-        "radii": (1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5),
-        "visual_size": 10,
-        "ppd": 10,
-    }
-
-    stims = {
-        "rings": rings(visual_size=10, ppd=10, frequency=0.5, target_indices=(1, 3)),
-        "rings_generalized": rings_generalized(**p1, target_indices=(1, 3)),
-        "two_sided_rings": two_sided_rings(
-            visual_size=10, ppd=10, frequency=1, target_indices=(1, 3)
-        ),
-        "bullseye": bullseye(visual_size=10, ppd=10, frequency=0.5),
-        "bullseye_generalized": bullseye_generalized(**p1),
-        "two_sided_bullseye": two_sided_bullseye(visual_size=10, ppd=10, frequency=1),
-    }
-    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/mondrians.py b/stimupy/illusions/mondrians.py
deleted file mode 100644
index 9a8caa90..00000000
--- a/stimupy/illusions/mondrians.py
+++ /dev/null
@@ -1,171 +0,0 @@
-import numpy as np
-
-from stimupy.components.mondrians import mondrians
-from stimupy.utils import resolution
-
-__all__ = [
-    "corrugated_mondrians",
-]
-
-
-def corrugated_mondrians(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    mondrian_depths=None,
-    mondrian_intensities=None,
-    target_indices=None,
-    intensity_background=0.5,
-    intensity_target=None,
-):
-    """Corrugated Mondrians
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    mondrian_depths : Sequence[Number, ... ], Number, or None (default)
-        depth of Mondrian parallelograms per row
-    mondrian_intensities : nested tuples
-        intensities of mondrians; as many tuples as there are rows and as many
-        numbers in each tuple as there are columns
-    target_indices : nested tuples
-        indices of targets; as many tuples as there are targets with (y, x) indices
-    intensity_background : float
-        intensity value for background
-    intensity_target : float or None
-            target intensity value. If None, use values defined in mondrian_intensities
-
-    Returns
-    ------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each target (key: "target_mask"),
-        and additional keys containing stimulus parameters
-
-    References
-    ----------
-    Adelson, E. H. (1993).
-        Perceptual organization and the judgment of brightness.
-        Science, 262, 2042-2044.
-        https://doi.org/10.1126/science.8266102
-    """
-    if mondrian_depths is None:
-        raise ValueError(
-            "corrugated_mondrians() missing argument 'mondrian_depths' which is not 'None'"
-        )
-    if mondrian_intensities is None:
-        raise ValueError(
-            "corrugated_mondrians() missing argument 'mondrian_intensities' which is not 'None'"
-        )
-
-    # Resolve resolution
-    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
-    if len(np.unique(ppd)) > 1:
-        raise ValueError("ppd should be equal in x and y direction")
-    nrows = len(mondrian_intensities)
-    ncols = len(mondrian_intensities[0])
-
-    if isinstance(mondrian_depths, (float, int)):
-        mondrian_depths = (mondrian_depths,) * nrows
-
-    if len(mondrian_depths) != nrows:
-        raise ValueError(
-            "Unclear number of Mondrians in y-direction, check elements "
-            "in mondrian_intensities and mondrian_depths"
-        )
-
-    height, width = visual_size
-    mdepths_px = resolution.lengths_from_visual_angles_ppd(mondrian_depths, ppd[0])
-    max_depth = np.abs(np.array(mdepths_px)).max()
-    sum_depth = np.abs(np.array(mdepths_px).sum())
-    red_depth = np.maximum(max_depth, sum_depth + max_depth)
-    mheight_px, mwidth_px = int(shape[0] / nrows), int((shape[1] - red_depth) / ncols)
-
-    # Initial y coordinates
-    yst = 0
-
-    # Calculate initial x coordinates
-    xstarts = np.cumsum(np.hstack([0, mdepths_px]))
-    temp = np.hstack([mdepths_px, 0])
-    temp[temp > 0] = 0.0
-    xstarts += temp
-    xstarts += np.abs(xstarts.min())
-
-    sizes = []
-    poses = []
-    ints = []
-    tlist = []
-    target_counter = 1
-
-    for r in range(nrows):
-        xst = xstarts[r]
-        if mondrian_depths[r] < 0:
-            xst -= int(mondrian_depths[r] * ppd[0])
-
-        for c in range(ncols):
-            if c != ncols - 1:
-                msize = (mheight_px / ppd[0], (mwidth_px + 1) / ppd[1], mondrian_depths[r])
-            else:
-                msize = (mheight_px / ppd[0], mwidth_px / ppd[1], mondrian_depths[r])
-            mpos = (yst / ppd[0], xst / ppd[1])
-            mint = mondrian_intensities[r][c]
-
-            sizes.append(msize)
-            poses.append(mpos)
-            ints.append(mint)
-
-            if (target_indices is not None) and (r, c) in target_indices:
-                tlist.append(target_counter)
-
-            xst += mwidth_px
-            target_counter += 1
-        yst += mheight_px
-
-    stim = mondrians(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        mondrian_positions=poses,
-        mondrian_sizes=sizes,
-        mondrian_intensities=ints,
-        intensity_background=intensity_background,
-    )
-    target_mask = np.zeros(shape)
-    for t in range(len(tlist)):
-        target_mask[stim["mondrian_mask"] == tlist[t]] = t + 1
-
-    stim["target_mask"] = target_mask.astype(int)
-    stim["target_indices"] = target_indices
-
-    if intensity_target is not None:
-        for t in range(len(tlist)):
-            stim["img"] = np.where(target_mask == t + 1, intensity_target, stim["img"])
-
-    if len(np.unique(stim["img"][target_mask != 0])) > 1:
-        raise Exception("targets are not equiluminant.")
-    return stim
-
-
-if __name__ == "__main__":
-    from stimupy.utils import plot_stim
-
-    p = {
-        "visual_size": 10,
-        "ppd": 30,
-        "mondrian_depths": (1, 0, -1, 0),
-        "mondrian_intensities": (
-            (0.4, 0.75, 0.4, 0.75),
-            (0.75, 0.4, 0.75, 1.0),
-            (0.4, 0.75, 0.4, 0.75),
-            (0.0, 0.4, 0.0, 0.4),
-        ),
-        "target_indices": ((1, 1), (3, 1)),
-    }
-
-    stim = corrugated_mondrians(**p)
-    plot_stim(stim, stim_name="corrugated_mondrians", mask=True, save=None)
diff --git a/stimupy/papers/RHS2007.py b/stimupy/papers/RHS2007.py
index d89598eb..82b83c63 100644
--- a/stimupy/papers/RHS2007.py
+++ b/stimupy/papers/RHS2007.py
@@ -32,7 +32,7 @@
 
 import numpy as np
 
-from stimupy import illusions
+import stimupy
 from stimupy.utils import (
     flip_dict,
     pad_dict_by_visual_size,
@@ -137,14 +137,14 @@ def WE_thick(ppd=PPD, pad=True):
     params = {
         "ppd": ppd,
         "frequency": 4.0 / width,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
         "intensity_target": v2,
         "target_indices": (2, 5),
         "target_height": 4.0,
         "period": "even",
     }
 
-    stim = illusions.whites.white(
+    stim = stimupy.whites.white(
         visual_size=(height, width),
         **params,
     )
@@ -198,14 +198,14 @@ def WE_thin_wide(ppd=PPD, pad=True):
     params = {
         "ppd": ppd,
         "frequency": 8.0 / width,
-        "intensity_bars": (v1, v3),
+        "intensity_bars": (v3, v1),
         "intensity_target": v2,
         "target_indices": (3, 12),
         "target_height": 2.0,
         "period": "even",
     }
 
-    stim = illusions.whites.white(
+    stim = stimupy.whites.white(
         visual_size=(height, width),
         **params,
     )
@@ -250,19 +250,19 @@ def WE_dual(ppd=PPD, pad=True):
     params = {
         "ppd": ppd,
         "frequency": 4.0 / width,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
         "intensity_target": v2,
         "target_indices": (2, 5),
         "target_height": 2.0,
         "period": "even",
     }
 
-    stim1 = illusions.whites.white(
+    stim1 = stimupy.whites.white(
         visual_size=(height, width),
         **params,
     )
 
-    stim2 = illusions.whites.white(
+    stim2 = stimupy.whites.white(
         visual_size=(height, width),
         **params,
     )
@@ -320,7 +320,7 @@ def WE_anderson(ppd=PPD, pad=True):
     params = {
         "ppd": ppd,
         "frequency": 8.0 / width,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
         "intensity_target": v2,
         "target_indices_top": (5,),
         "target_indices_bottom": (10,),
@@ -332,7 +332,7 @@ def WE_anderson(ppd=PPD, pad=True):
         "period": "even",
     }
 
-    stim = illusions.whites.anderson(
+    stim = stimupy.whites.anderson(
         visual_size=(height, width),
         **params,
     )
@@ -389,7 +389,7 @@ def WE_howe(ppd=PPD, pad=True):
     params = {
         "ppd": ppd,
         "frequency": 8.0 / width,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
         "intensity_target": v2,
         "target_indices_top": (5,),
         "target_indices_bottom": (10,),
@@ -399,7 +399,7 @@ def WE_howe(ppd=PPD, pad=True):
         "period": "even",
     }
 
-    stim = illusions.whites.howe(
+    stim = stimupy.whites.howe(
         visual_size=(height, width),
         **params,
     )
@@ -445,7 +445,7 @@ def WE_zigzag(ppd=PPD, pad=True):
     """
 
     i1, i2 = 1, 2
-    stim = illusions.wedding_cakes.wedding_cake(
+    stim = stimupy.wedding_cakes.wedding_cake(
         visual_size=(16.0, 17.0),
         ppd=PPD,
         L_size=(4.0, 4, 1.0),
@@ -500,7 +500,7 @@ def WE_radial_thick_small(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim = illusions.whites.radial(
+    stim = stimupy.whites.radial(
         visual_size=(16, 16),
         **params,
     )
@@ -555,7 +555,7 @@ def WE_radial_thick(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim = illusions.whites.radial(
+    stim = stimupy.whites.radial(
         visual_size=(24, 24),
         **params,
     )
@@ -610,7 +610,7 @@ def WE_radial_thin_small(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim = illusions.whites.radial(
+    stim = stimupy.whites.radial(
         visual_size=(16, 16),
         **params,
     )
@@ -665,7 +665,7 @@ def WE_radial_thin(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim = illusions.whites.radial(
+    stim = stimupy.whites.radial(
         visual_size=(24, 24),
         **params,
     )
@@ -715,17 +715,18 @@ def WE_circular1(ppd=PPD, pad=True):
         "target_indices": 4,
         "intensity_background": v2,
         "intensity_target": v2,
+        "clip": True,
     }
 
-    stim1 = illusions.whites.circular(
+    stim1 = stimupy.whites.circular(
         visual_size=(height, width),
-        intensity_rings=(v1, v3),
+        intensity_rings=(v3, v1),
         **params,
     )
 
-    stim2 = illusions.whites.circular(
+    stim2 = stimupy.whites.circular(
         visual_size=(height, width),
-        intensity_rings=(v3, v1),
+        intensity_rings=(v1, v3),
         **params,
     )
     stim = stack_dicts(stim1, stim2)
@@ -775,17 +776,18 @@ def WE_circular05(ppd=PPD, pad=True):
         "target_indices": 10,
         "intensity_background": v2,
         "intensity_target": v2,
+        "clip": True,
     }
 
-    stim1 = illusions.whites.circular(
+    stim1 = stimupy.whites.circular(
         visual_size=(height, width),
-        intensity_rings=(v1, v3),
+        intensity_rings=(v3, v1),
         **params,
     )
 
-    stim2 = illusions.whites.circular(
+    stim2 = stimupy.whites.circular(
         visual_size=(height, width),
-        intensity_rings=(v3, v1),
+        intensity_rings=(v1, v3),
         **params,
     )
     stim = stack_dicts(stim1, stim2)
@@ -835,17 +837,18 @@ def WE_circular025(ppd=PPD, pad=True):
         "target_indices": 22,
         "intensity_background": v2,
         "intensity_target": v2,
+        "clip": True,
     }
 
-    stim1 = illusions.whites.circular(
+    stim1 = stimupy.whites.circular(
         visual_size=(height, width),
-        intensity_rings=(v1, v3),
+        intensity_rings=(v3, v1),
         **params,
     )
 
-    stim2 = illusions.whites.circular(
+    stim2 = stimupy.whites.circular(
         visual_size=(height, width),
-        intensity_rings=(v3, v1),
+        intensity_rings=(v1, v3),
         **params,
     )
     stim = stack_dicts(stim1, stim2)
@@ -897,12 +900,12 @@ def grating_induction(ppd=PPD, pad=True):
         "ppd": ppd,
         "frequency": 4.0 / width,
         "target_width": 1.0,
-        "intensity_bars": (0.0, 1.0),
+        "intensities": (0.0, 1.0),
         "intensity_target": (0.5),
         "period": "ignore",
     }
 
-    stim = illusions.gratings.induction(
+    stim = stimupy.gratings.grating_induction(
         visual_size=(height, width),
         **params,
     )
@@ -958,12 +961,12 @@ def sbc_large(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim1 = illusions.sbcs.basic(
+    stim1 = stimupy.sbcs.basic(
         visual_size=(13.0, 15.5),
         intensity_background=0.0,
         **params,
     )
-    stim2 = illusions.sbcs.basic(
+    stim2 = stimupy.sbcs.basic(
         visual_size=(13.0, 15.5),
         intensity_background=1.0,
         **params,
@@ -1018,12 +1021,12 @@ def sbc_small(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim1 = illusions.sbcs.basic(
+    stim1 = stimupy.sbcs.basic(
         visual_size=(13.0, 15.5),
         intensity_background=0.0,
         **params,
     )
-    stim2 = illusions.sbcs.basic(
+    stim2 = stimupy.sbcs.basic(
         visual_size=(13.0, 15.5),
         intensity_background=1.0,
         **params,
@@ -1083,13 +1086,13 @@ def todorovic_equal(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim1 = illusions.todorovics.equal(
+    stim1 = stimupy.todorovics.equal(
         intensity_background=1.0,
         intensity_covers=0.0,
         **params,
     )
 
-    stim2 = illusions.todorovics.equal(
+    stim2 = stimupy.todorovics.equal(
         intensity_background=0.0,
         intensity_covers=1.0,
         **params,
@@ -1150,12 +1153,12 @@ def todorovic_in_large(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim1 = illusions.todorovics.rectangle(
+    stim1 = stimupy.todorovics.rectangle(
         intensity_background=1.0,
         intensity_covers=0.0,
         **params,
     )
-    stim2 = illusions.todorovics.rectangle(
+    stim2 = stimupy.todorovics.rectangle(
         intensity_background=0.0,
         intensity_covers=1.0,
         **params,
@@ -1216,12 +1219,12 @@ def todorovic_in_small(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim1 = illusions.todorovics.cross(
+    stim1 = stimupy.todorovics.cross(
         intensity_background=1.0,
         intensity_covers=0.0,
         **params,
     )
-    stim2 = illusions.todorovics.cross(
+    stim2 = stimupy.todorovics.cross(
         intensity_background=0.0,
         intensity_covers=1.0,
         **params,
@@ -1284,12 +1287,12 @@ def todorovic_out(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim1 = illusions.todorovics.cross(
+    stim1 = stimupy.todorovics.cross(
         intensity_background=1.0,
         intensity_covers=0.0,
         **params,
     )
-    stim2 = illusions.todorovics.cross(
+    stim2 = stimupy.todorovics.cross(
         intensity_background=0.0,
         intensity_covers=1.0,
         **params,
@@ -1352,7 +1355,7 @@ def checkerboard_016(ppd=PPD, pad=True):
         "intensity_checks": (v3, v1),
         "intensity_target": v2,
     }
-    stim = illusions.checkerboards.checkerboard(**params)
+    stim = stimupy.checkerboards.checkerboard(**params)
 
     if pad:
         stim = pad_dict_to_visual_size(stim, VISEXTENT, ppd, pad_value=v2)
@@ -1410,7 +1413,7 @@ def checkerboard_094(ppd=PPD, pad=True):
         "intensity_checks": (v1, v3),
         "intensity_target": v2,
     }
-    stim = illusions.checkerboards.checkerboard(**params)
+    stim = stimupy.checkerboards.checkerboard(**params)
 
     if pad:
         stim = pad_dict_to_visual_size(stim, VISEXTENT, ppd, pad_value=v2)
@@ -1468,7 +1471,7 @@ def checkerboard_21(ppd=PPD, pad=True):
         "intensity_checks": (v1, v3),
         "intensity_target": v2,
     }
-    stim = illusions.checkerboards.checkerboard(**params)
+    stim = stimupy.checkerboards.checkerboard(**params)
 
     if pad:
         stim = pad_dict_to_visual_size(stim, VISEXTENT, ppd, pad_value=v2)
@@ -1531,7 +1534,7 @@ def corrugated_mondrian(ppd=PPD, pad=True):
         "intensity_background": 0.5,
     }
 
-    stim = illusions.mondrians.corrugated_mondrians(**params)
+    stim = stimupy.mondrians.corrugated_mondrian(**params)
 
     if pad:
         stim = pad_dict_to_visual_size(stim, VISEXTENT, ppd, pad_value=0.5)
@@ -1586,7 +1589,7 @@ def benary_cross(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim = illusions.benarys.cross_triangles(
+    stim = stimupy.benarys.cross_triangles(
         visual_size=(13, 23),
         **params,
     )
@@ -1655,7 +1658,7 @@ def todorovic_benary1_2(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim = illusions.benarys.todorovic_generalized(
+    stim = stimupy.benarys.todorovic_generalized(
         visual_size=(13.0, 31.0),
         **params,
     )
@@ -1717,7 +1720,7 @@ def todorovic_benary3_4(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim = illusions.benarys.todorovic_generalized(
+    stim = stimupy.benarys.todorovic_generalized(
         visual_size=(13.0, 31.0),
         **params,
     )
@@ -1779,7 +1782,7 @@ def todorovic_benary1_2_3_4(ppd=PPD, pad=True):
         "intensity_target": v2,
     }
 
-    stim = illusions.benarys.todorovic_generalized(
+    stim = stimupy.benarys.todorovic_generalized(
         visual_size=(13.0, 31.0),
         **params,
     )
@@ -1831,11 +1834,11 @@ def bullseye_thin(ppd=PPD, pad=True):
         "origin": "mean",
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = stimupy.bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(v1, v3),
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = stimupy.bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(v3, v1),
     )
@@ -1892,11 +1895,11 @@ def bullseye_thick(ppd=PPD, pad=True):
         "origin": "mean",
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = stimupy.bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(v1, v3),
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = stimupy.bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(v3, v1),
     )
diff --git a/stimupy/papers/bindmann2004.py b/stimupy/papers/bindmann2004.py
index 5f4d9357..8c875682 100644
--- a/stimupy/papers/bindmann2004.py
+++ b/stimupy/papers/bindmann2004.py
@@ -28,9 +28,10 @@
 
 import copy
 import warnings
+
 import numpy as np
 
-from stimupy import illusions
+from stimupy import bullseyes
 from stimupy.utils.contrast_conversions import adapt_intensity_range_dict
 from stimupy.utils.pad import pad_dict_by_visual_size
 
@@ -167,12 +168,12 @@ def bullseye_thin_gw45_gb31(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=31.0,
@@ -229,12 +230,12 @@ def bullseye_thin_gw45_gb38(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=38.0,
@@ -291,12 +292,12 @@ def bullseye_thin_gw45_gb45(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=45.0,
@@ -353,12 +354,12 @@ def bullseye_thin_gw45_gb52(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=52.0,
@@ -415,12 +416,12 @@ def bullseye_thin_gw45_gb59(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=59.0,
@@ -477,12 +478,12 @@ def bullseye_thin_gw60_gb46(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=46.0,
@@ -539,12 +540,12 @@ def bullseye_thin_gw60_gb53(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=53.0,
@@ -601,12 +602,12 @@ def bullseye_thin_gw60_gb60(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=60.0,
@@ -663,12 +664,12 @@ def bullseye_thin_gw60_gb67(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=67.0,
@@ -725,12 +726,12 @@ def bullseye_thin_gw60_gb74(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=74.0,
@@ -787,12 +788,12 @@ def bullseye_thin_gw75_gb61(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=61.0,
@@ -849,12 +850,12 @@ def bullseye_thin_gw75_gb68(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=68.0,
@@ -911,12 +912,12 @@ def bullseye_thin_gw75_gb75(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=75.0,
@@ -973,12 +974,12 @@ def bullseye_thin_gw75_gb82(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=82.0,
@@ -1035,12 +1036,12 @@ def bullseye_thin_gw75_gb89(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=89.0,
@@ -1097,12 +1098,12 @@ def bullseye_thick_gw45_gb31(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=31.0,
@@ -1159,12 +1160,12 @@ def bullseye_thick_gw45_gb38(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=38.0,
@@ -1221,12 +1222,12 @@ def bullseye_thick_gw45_gb45(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=45.0,
@@ -1283,12 +1284,12 @@ def bullseye_thick_gw45_gb52(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=52.0,
@@ -1345,12 +1346,12 @@ def bullseye_thick_gw45_gb59(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=45.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=59.0,
@@ -1407,12 +1408,12 @@ def bullseye_thick_gw60_gb46(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=46.0,
@@ -1469,12 +1470,12 @@ def bullseye_thick_gw60_gb53(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=53.0,
@@ -1531,12 +1532,12 @@ def bullseye_thick_gw60_gb60(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=60.0,
@@ -1593,12 +1594,12 @@ def bullseye_thick_gw60_gb67(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=67.0,
@@ -1655,12 +1656,12 @@ def bullseye_thick_gw60_gb74(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=60.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=74.0,
@@ -1717,12 +1718,12 @@ def bullseye_thick_gw75_gb61(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=61.0,
@@ -1779,12 +1780,12 @@ def bullseye_thick_gw75_gb68(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=68.0,
@@ -1841,12 +1842,12 @@ def bullseye_thick_gw75_gb75(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=75.0,
@@ -1903,12 +1904,12 @@ def bullseye_thick_gw75_gb82(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=82.0,
@@ -1965,12 +1966,12 @@ def bullseye_thick_gw75_gb89(ppd=PPD):
         "origin": ORIGIN,
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_BLACK, INTENSITY_WHITE),
         intensity_target=75.0,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(INTENSITY_WHITE, INTENSITY_BLACK),
         intensity_target=89.0,
diff --git a/stimupy/papers/domijan2015.py b/stimupy/papers/domijan2015.py
index 52eb59cf..456adc9c 100644
--- a/stimupy/papers/domijan2015.py
+++ b/stimupy/papers/domijan2015.py
@@ -38,13 +38,8 @@
 
 import numpy as np
 
-from stimupy import illusions
-from stimupy.utils import (
-    pad_dict_by_visual_size,
-    pad_dict_to_shape,
-    resolution,
-    stack_dicts,
-)
+import stimupy
+from stimupy.utils import pad_dict_by_visual_size, pad_dict_to_shape, resolution, stack_dicts
 
 __all__ = [
     "dungeon",
@@ -227,13 +222,13 @@ def dungeon(visual_size=VSIZES["dungeon"], ppd=PPD, shape=SHAPES["dungeon"]):
     }
 
     # Generate each side
-    stim1 = illusions.dungeons.dungeon(
+    stim1 = stimupy.dungeons.dungeon(
         **params,
         intensity_background=v1,
         intensity_grid=v3,
         intensity_target=v2,
     )
-    stim2 = illusions.dungeons.dungeon(
+    stim2 = stimupy.dungeons.dungeon(
         **params,
         intensity_background=v3,
         intensity_grid=v1,
@@ -297,13 +292,13 @@ def cube(visual_size=VSIZES["cube"], ppd=PPD, shape=SHAPES["cube"]):
         "cell_spacing": 0.5 * visual_resize,
     }
 
-    stim1 = illusions.cubes.varying_cells(
+    stim1 = stimupy.cubes.varying_cells(
         **params,
         intensity_background=v1,
         intensity_cells=v3,
         intensity_target=v2,
     )
-    stim2 = illusions.cubes.varying_cells(
+    stim2 = stimupy.cubes.varying_cells(
         **params,
         intensity_background=v3,
         intensity_cells=v1,
@@ -367,18 +362,18 @@ def grating(visual_size=VSIZES["grating"], ppd=PPD, shape=SHAPES["grating"]):
         "visual_size": single_vissize,
         "ppd": ppd,
         "n_bars": 9,
-        "target_indices": (5,),
+        "target_indices": (4,),
         "bar_width": 1.0 * visual_resize,
     }
 
-    stim1 = illusions.gratings.square_wave(
+    stim1 = stimupy.waves.square_linear(
         **params,
-        intensity_bars=(v1, v3),
+        intensity_bars=(v3, v1),
         intensity_target=v2,
     )
-    stim2 = illusions.gratings.square_wave(
+    stim2 = stimupy.waves.square_linear(
         **params,
-        intensity_bars=(v3, v1),
+        intensity_bars=(v1, v3),
         intensity_target=v2,
     )
 
@@ -438,13 +433,13 @@ def rings(visual_size=VSIZES["rings"], ppd=PPD, shape=SHAPES["rings"]):
         "origin": "mean",
     }
 
-    stim1 = illusions.frames.rings_generalized(
+    stim1 = stimupy.rings.rectangular_generalized(
         **params,
         target_indices=5,
         intensity_frames=(v1, v3),
         intensity_target=v2,
     )
-    stim2 = illusions.frames.rings_generalized(
+    stim2 = stimupy.rings.rectangular_generalized(
         **params,
         target_indices=4,
         intensity_frames=(v1, v3),
@@ -506,12 +501,12 @@ def bullseye(visual_size=VSIZES["bullseye"], ppd=PPD, shape=SHAPES["bullseye"]):
         "origin": "mean",
     }
 
-    stim1 = illusions.frames.bullseye_generalized(
+    stim1 = stimupy.bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(v1, v3),
         intensity_target=v2,
     )
-    stim2 = illusions.frames.bullseye_generalized(
+    stim2 = stimupy.bullseyes.rectangular_generalized(
         **params,
         intensity_frames=(v3, v1),
         intensity_target=v2,
@@ -578,12 +573,12 @@ def simultaneous_brightness_contrast(
         "target_position": (3.8 * visual_resize, 3.8 * visual_resize),
     }
 
-    stim1 = illusions.sbcs.generalized(
+    stim1 = stimupy.sbcs.generalized(
         **params,
         intensity_background=v3,
         intensity_target=v2,
     )
-    stim2 = illusions.sbcs.generalized(
+    stim2 = stimupy.sbcs.generalized(
         **params,
         intensity_background=v1,
         intensity_target=v2,
@@ -646,9 +641,9 @@ def white(visual_size=VSIZES["white"], ppd=PPD, pad=PAD, shape=SHAPES["white"]):
         "period": "even",
     }
 
-    stim = illusions.whites.white(
+    stim = stimupy.whites.white(
         **params,
-        intensity_bars=(v3, v1),
+        intensity_bars=(v1, v3),
         intensity_target=v2,
     )
 
@@ -712,7 +707,7 @@ def benary(visual_size=VSIZES["benary"], ppd=PPD, shape=SHAPES["benary"]):
         "target_size": 1.1 * visual_resize,
     }
 
-    stim = illusions.benarys.cross_rectangles(
+    stim = stimupy.benarys.cross_rectangles(
         **params,
         intensity_background=v3,
         intensity_cross=v1,
@@ -776,13 +771,13 @@ def todorovic(visual_size=VSIZES["todorovic"], ppd=PPD, shape=SHAPES["todorovic"
         "covers_offset": 2.0 * visual_resize,
     }
 
-    stim1 = illusions.todorovics.rectangle(
+    stim1 = stimupy.todorovics.rectangle(
         **params,
         intensity_background=0.0,
         intensity_target=0.5,
         intensity_covers=1.0,
     )
-    stim2 = illusions.todorovics.rectangle(
+    stim2 = stimupy.todorovics.rectangle(
         **params,
         intensity_background=1.0,
         intensity_target=0.5,
@@ -853,13 +848,13 @@ def checkerboard_contrast_contrast(
     }
 
     # Large checkerboard, embedded target region
-    stim1 = illusions.checkerboards.contrast_contrast(
+    stim1 = stimupy.checkerboards.contrast_contrast(
         **params,
         board_shape=(8, 8),
     )
 
     # Isolated target region (smaller checkerboard)
-    stim2 = illusions.checkerboards.contrast_contrast(
+    stim2 = stimupy.checkerboards.contrast_contrast(
         **params,
         board_shape=(4, 4),
     )
@@ -942,7 +937,7 @@ def checkerboard(
         "intensity_checks": (v1, v3),
         "intensity_target": v2,
     }
-    stim = illusions.checkerboards.checkerboard(**params)
+    stim = stimupy.checkerboards.checkerboard(**params)
 
     if pad:
         padding = np.array((0.9, 1.1)) * visual_resize
@@ -1015,7 +1010,7 @@ def checkerboard_extended(
         "intensity_checks": (v1, v3),
         "intensity_target": v2,
     }
-    stim = illusions.checkerboards.checkerboard(**params)
+    stim = stimupy.checkerboards.checkerboard(**params)
 
     if pad:
         padding = np.array((0.9, 1.1)) * visual_resize
@@ -1093,9 +1088,9 @@ def white_yazdanbakhsh(
         "period": "even",
     }
 
-    stim = illusions.whites.yazdanbakhsh(
+    stim = stimupy.whites.yazdanbakhsh(
         **params,
-        intensity_bars=(v3, v1),
+        intensity_bars=(v1, v3),
         intensity_target=v2,
         intensity_stripes=(v3, v1),
     )
@@ -1180,9 +1175,9 @@ def white_anderson(
         "period": "even",
     }
 
-    stim = illusions.whites.anderson(
+    stim = stimupy.whites.anderson(
         **params,
-        intensity_bars=(v1, v3),
+        intensity_bars=(v3, v1),
         intensity_target=v2,
         intensity_stripes=(v1, v3),
     )
@@ -1262,9 +1257,9 @@ def white_howe(visual_size=VSIZES["white_howe"], ppd=PPD, shape=SHAPES["white_ho
         "period": "even",
     }
 
-    stim = illusions.whites.howe(
+    stim = stimupy.whites.howe(
         **params,
-        intensity_bars=(v1, v3),
+        intensity_bars=(v3, v1),
         intensity_target=v2,
         intensity_stripes=(v1, v3),
     )
diff --git a/stimupy/papers/modelfest.py b/stimupy/papers/modelfest.py
index 479e7b5a..5f882391 100644
--- a/stimupy/papers/modelfest.py
+++ b/stimupy/papers/modelfest.py
@@ -37,10 +37,12 @@
 import numpy as np
 import pandas as pd
 
-from stimupy.components import checkerboards, gaussians, gratings, lines, shapes
-from stimupy.components.circulars import bessel
+from stimupy import checkerboards
+from stimupy.components import gaussians, lines, shapes
 from stimupy.components.edges import gaussian_edge
+from stimupy.components.waves import bessel
 from stimupy.noises.binaries import binary as binary_noise
+from stimupy.stimuli.gabors import gabor
 from stimupy.utils import pad_dict_to_shape, resize_dict, roll_dict, stack_dicts
 
 __all__ = [
@@ -157,7 +159,7 @@ def GaborPatch1(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 1
     experimental_data = {
@@ -206,7 +208,7 @@ def GaborPatch2(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 5
     experimental_data = {
@@ -255,7 +257,7 @@ def GaborPatch3(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 9
     experimental_data = {
@@ -304,7 +306,7 @@ def GaborPatch4(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 13
     experimental_data = {
@@ -353,7 +355,7 @@ def GaborPatch5(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 17
     experimental_data = {
@@ -402,7 +404,7 @@ def GaborPatch6(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 21
     experimental_data = {
@@ -451,7 +453,7 @@ def GaborPatch7(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 25
     experimental_data = {
@@ -500,7 +502,7 @@ def GaborPatch8(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 29
     experimental_data = {
@@ -549,7 +551,7 @@ def GaborPatch9(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 33
     experimental_data = {
@@ -598,7 +600,7 @@ def GaborPatch10(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 37
     experimental_data = {
@@ -647,7 +649,7 @@ def GaborPatch11(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 41
     experimental_data = {
@@ -696,7 +698,7 @@ def GaborPatch12(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 45
     experimental_data = {
@@ -745,7 +747,7 @@ def GaborPatch13(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 49
     experimental_data = {
@@ -794,7 +796,7 @@ def GaborPatch14(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 53
     experimental_data = {
@@ -843,7 +845,7 @@ def ElongatedGabor15(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 57
     experimental_data = {
@@ -892,7 +894,7 @@ def ElongatedGabor16(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 61
     experimental_data = {
@@ -941,7 +943,7 @@ def ElongatedGabor17(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 65
     experimental_data = {
@@ -990,7 +992,7 @@ def Baguette18(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 69
     experimental_data = {
@@ -1039,7 +1041,7 @@ def Baguette19(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 73
     experimental_data = {
@@ -1088,7 +1090,7 @@ def Baguette20(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 77
     experimental_data = {
@@ -1137,7 +1139,7 @@ def Baguette21(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 81
     experimental_data = {
@@ -1185,14 +1187,14 @@ def Subthreshold22(ppd=PPD):
         "origin": "center",
     }
 
-    stim1 = gratings.gabor(**params, frequency=2)
-    stim2 = gratings.gabor(**params, frequency=2 * np.sqrt(2))
+    stim1 = gabor(**params, frequency=2)
+    stim2 = gabor(**params, frequency=2 * np.sqrt(2))
 
     stim1["img"] = stim1["img"] / 2 + stim2["img"] / 2
     stim1["grating_mask2"] = stim2["grating_mask"]
     stim1["frequency2"] = stim2["frequency"]
-    stim1["bar_width2"] = stim2["bar_width"]
-    stim1["n_bars2"] = stim2["n_bars"]
+    stim1["phase_width2"] = stim2["phase_width"]
+    stim1["n_phases2"] = stim2["n_phases"]
 
     v = 85
     experimental_data = {
@@ -1240,14 +1242,14 @@ def Subthreshold23(ppd=PPD):
         "origin": "center",
     }
 
-    stim1 = gratings.gabor(**params, frequency=2)
-    stim2 = gratings.gabor(**params, frequency=4)
+    stim1 = gabor(**params, frequency=2)
+    stim2 = gabor(**params, frequency=4)
 
     stim1["img"] = stim1["img"] / 2 + stim2["img"] / 2
     stim1["grating_mask2"] = stim2["grating_mask"]
     stim1["frequency2"] = stim2["frequency"]
-    stim1["bar_width2"] = stim2["bar_width"]
-    stim1["n_bars2"] = stim2["n_bars"]
+    stim1["phase_width2"] = stim2["phase_width"]
+    stim1["n_phases2"] = stim2["n_phases"]
 
     v = 89
     experimental_data = {
@@ -1295,14 +1297,14 @@ def Subthreshold24(ppd=PPD):
         "origin": "center",
     }
 
-    stim1 = gratings.gabor(**params, frequency=4)
-    stim2 = gratings.gabor(**params, frequency=4 * np.sqrt(2))
+    stim1 = gabor(**params, frequency=4)
+    stim2 = gabor(**params, frequency=4 * np.sqrt(2))
 
     stim1["img"] = stim1["img"] / 2 + stim2["img"] / 2
     stim1["grating_mask2"] = stim2["grating_mask"]
     stim1["frequency2"] = stim2["frequency"]
-    stim1["bar_width2"] = stim2["bar_width"]
-    stim1["n_bars2"] = stim2["n_bars"]
+    stim1["phase_width2"] = stim2["phase_width"]
+    stim1["n_phases2"] = stim2["n_phases"]
 
     v = 93
     experimental_data = {
@@ -1350,14 +1352,14 @@ def Subthreshold25(ppd=PPD):
         "origin": "center",
     }
 
-    stim1 = gratings.gabor(**params, frequency=4)
-    stim2 = gratings.gabor(**params, frequency=8)
+    stim1 = gabor(**params, frequency=4)
+    stim2 = gabor(**params, frequency=8)
 
     stim1["img"] = stim1["img"] / 2 + stim2["img"] / 2
     stim1["grating_mask2"] = stim2["grating_mask"]
     stim1["frequency2"] = stim2["frequency"]
-    stim1["bar_width2"] = stim2["bar_width"]
-    stim1["n_bars2"] = stim2["n_bars"]
+    stim1["phase_width2"] = stim2["phase_width"]
+    stim1["n_phases2"] = stim2["n_phases"]
 
     v = 97
     experimental_data = {
@@ -1737,7 +1739,7 @@ def GaborString33(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     # Stack collinear Gabors horizontally and pad
     stimc = stack_dicts(stim, stim)
@@ -1798,8 +1800,8 @@ def GaborString34(ppd=PPD):
         "origin": "center",
     }
 
-    stim1 = gratings.gabor(**params, phase_shift=-90)
-    stim2 = gratings.gabor(**params, phase_shift=90)
+    stim1 = gabor(**params, phase_shift=-90)
+    stim2 = gabor(**params, phase_shift=90)
 
     # Stack collinear Gabors horizontally and pad
     stimc = stack_dicts(stim2, stim1)
@@ -1946,7 +1948,7 @@ def Orientation36(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 141
     experimental_data = {
@@ -1996,7 +1998,7 @@ def Orientation37(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params)
+    stim = gabor(**params)
 
     v = 145
     experimental_data = {
@@ -2045,8 +2047,8 @@ def Plaids38(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params, rotation=0)
-    stim2 = gratings.gabor(**params, rotation=90)
+    stim = gabor(**params, rotation=0)
+    stim2 = gabor(**params, rotation=90)
 
     stim["img"] = stim["img"] / 2 + stim2["img"] / 2
     stim["rotation"] = stim2["rotation"]
@@ -2099,8 +2101,8 @@ def Plaids39(ppd=PPD):
         "origin": "center",
     }
 
-    stim = gratings.gabor(**params, rotation=45)
-    stim2 = gratings.gabor(**params, rotation=90)
+    stim = gabor(**params, rotation=45)
+    stim2 = gabor(**params, rotation=90)
 
     stim["img"] = stim["img"] / 2 + stim2["img"] / 2
     stim["rotation"] = stim2["rotation"]
@@ -2234,6 +2236,7 @@ def Checkerboard42(ppd=PPD):
         "frequency": 2.81,
         "rotation": 45,
         "round_phase_width": False,
+        "intensity_checks": (1.0, 0.0),
     }
 
     stim = checkerboards.checkerboard(**params)
diff --git a/stimupy/papers/murray2020.py b/stimupy/papers/murray2020.py
index 0f4de038..b67a2d1e 100644
--- a/stimupy/papers/murray2020.py
+++ b/stimupy/papers/murray2020.py
@@ -36,7 +36,7 @@
 import numpy as np
 import scipy.io
 
-from stimupy import illusions
+import stimupy
 from stimupy.utils import pad_dict_by_visual_size, rotate_dict
 
 __all__ = [
@@ -568,7 +568,7 @@ def checkassim(ppd=PPD, pad=PAD):
         "intensity_checks": (17.5, 70.0),
         "intensity_target": 35.0,
     }
-    stim = illusions.checkerboards.checkerboard(
+    stim = stimupy.checkerboards.checkerboard(
         **params,
     )
 
@@ -735,12 +735,12 @@ def white(ppd=PPD):
         "target_indices_bottom": (2, 4, 6),
         "target_center_offset": 2,
         "target_height": 2,
-        "intensity_bars": (17.5, 70),
+        "intensity_bars": (70, 17.5),
         "intensity_target": 35.0,
         "period": "even",
     }
 
-    stim = illusions.whites.white_two_rows(**params)
+    stim = stimupy.whites.white_two_rows(**params)
     stim = rotate_dict(stim)
     reduced_mask = np.where(stim["target_mask"] == 2, 2, 0)
     reduced_mask = np.where(stim["target_mask"] == 5, 1, reduced_mask).astype(int)
diff --git a/stimupy/papers/white1981.py b/stimupy/papers/white1981.py
index 832d446a..571517d2 100644
--- a/stimupy/papers/white1981.py
+++ b/stimupy/papers/white1981.py
@@ -34,7 +34,7 @@
 
 import numpy as np
 
-from stimupy import illusions
+import stimupy
 
 __all__ = [
     "square_white",
@@ -127,7 +127,7 @@ def square_white(ppd=PPD):
         "intensity_target": v2,
     }
 
-    stim = illusions.sbcs.basic(**params)
+    stim = stimupy.sbcs.basic(**params)
     return stim
 
 
@@ -164,7 +164,7 @@ def square_black(ppd=PPD):
         "intensity_target": v2,
     }
 
-    stim = illusions.sbcs.basic(**params)
+    stim = stimupy.sbcs.basic(**params)
     return stim
 
 
@@ -202,11 +202,11 @@ def grating_white_white(ppd=PPD):
         "bar_width": bar_width,
         "rotation": ORIENTATION,
         "intensity_background": v3,
-        "intensity_bars": (v2, v3),
+        "intensity_bars": (v3, v2),
         "intensity_target": v2,
-        "target_indices": (2, 4, 6, 8),
+        "target_indices": (1, 3, 5, 7),
     }
-    stim = illusions.gratings.uniform(**params)
+    stim = stimupy.gratings.on_uniform(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -245,11 +245,11 @@ def grating_white_black(ppd=PPD):
         "bar_width": bar_width,
         "rotation": ORIENTATION,
         "intensity_background": v1,
-        "intensity_bars": (v2, v3),
+        "intensity_bars": (v3, v2),
         "intensity_target": v2,
-        "target_indices": (2, 4, 6, 8),
+        "target_indices": (1, 3, 5, 7),
     }
-    stim = illusions.gratings.uniform(**params)
+    stim = stimupy.gratings.on_uniform(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -288,11 +288,11 @@ def grating_black_white(ppd=PPD):
         "bar_width": bar_width,
         "rotation": ORIENTATION,
         "intensity_background": v3,
-        "intensity_bars": (v2, v1),
+        "intensity_bars": (v1, v2),
         "intensity_target": v2,
-        "target_indices": (2, 4, 6, 8),
+        "target_indices": (1, 3, 5, 7),
     }
-    stim = illusions.gratings.uniform(**params)
+    stim = stimupy.gratings.on_uniform(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -331,11 +331,11 @@ def grating_black_black(ppd=PPD):
         "bar_width": bar_width,
         "rotation": ORIENTATION,
         "intensity_background": v1,
-        "intensity_bars": (v2, v1),
+        "intensity_bars": (v1, v2),
         "intensity_target": v2,
-        "target_indices": (2, 4, 6, 8),
+        "target_indices": (1, 3, 5, 7),
     }
-    stim = illusions.gratings.uniform(**params)
+    stim = stimupy.gratings.on_uniform(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -372,18 +372,18 @@ def grating_white_in(ppd=PPD):
         "n_bars": 9,
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v1, v3),
+        "intensity_bars": (v3, v1),
         "intensity_target": v2,
-        "target_indices": (2, 4, 6, 8),
+        "target_indices": (1, 3, 5, 7),
     }
     large_params = {
         "bar_width": bar_width,
         "visual_size": 51 * bar_width,
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
     }
-    stim = illusions.gratings.grating(
+    stim = stimupy.gratings.on_grating(
         small_grating_params=small_params,
         large_grating_params=large_params,
     )
@@ -424,18 +424,18 @@ def grating_black_in(ppd=PPD):
         "n_bars": 9,
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
         "intensity_target": v2,
-        "target_indices": (2, 4, 6, 8),
+        "target_indices": (1, 3, 5, 7),
     }
     large_params = {
         "bar_width": bar_width,
         "visual_size": 51 * bar_width,
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v1, v3),
+        "intensity_bars": (v3, v1),
     }
-    stim = illusions.gratings.grating(
+    stim = stimupy.gratings.on_grating(
         small_grating_params=small_params,
         large_grating_params=large_params,
     )
@@ -476,18 +476,18 @@ def grating_white_out(ppd=PPD):
         "visual_size": (50 * bar_width, 9 * bar_width),
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v1, v3),
+        "intensity_bars": (v3, v1),
         "intensity_target": v2,
-        "target_indices": (24, 26, 28, 30),
+        "target_indices": (23, 25, 27, 29),
     }
     large_params = {
         "bar_width": bar_width,
         "visual_size": 50 * bar_width,
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
     }
-    stim = illusions.gratings.grating(
+    stim = stimupy.gratings.on_grating(
         small_grating_params=small_params,
         large_grating_params=large_params,
     )
@@ -528,18 +528,18 @@ def grating_black_out(ppd=PPD):
         "visual_size": (50 * bar_width, 9 * bar_width),
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
         "intensity_target": v2,
-        "target_indices": (24, 26, 28, 30),
+        "target_indices": (23, 25, 27, 29),
     }
     large_params = {
         "bar_width": bar_width,
         "visual_size": 50 * bar_width,
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v1, v3),
+        "intensity_bars": (v3, v1),
     }
-    stim = illusions.gratings.grating(
+    stim = stimupy.gratings.on_grating(
         small_grating_params=small_params,
         large_grating_params=large_params,
     )
@@ -580,18 +580,18 @@ def grating_white_orthogonal(ppd=PPD):
         "bar_width": bar_width,
         "ppd": ppd,
         "rotation": ORIENTATION + 90,
-        "intensity_bars": (v1, v3),
+        "intensity_bars": (v3, v2),
         "intensity_target": v2,
-        "target_indices": (2, 4, 6, 8),
+        "target_indices": (1, 3, 5, 7),
     }
     large_params = {
         "bar_width": bar_width,
         "visual_size": 51 * bar_width,
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
     }
-    stim = illusions.gratings.grating_masked(
+    stim = stimupy.gratings.on_grating_masked(
         small_grating_params=small_params,
         large_grating_params=large_params,
         mask_size=(9 * bar_width, 9 * bar_width, 8 * bar_width),
@@ -634,18 +634,18 @@ def grating_black_orthogonal(ppd=PPD):
         "bar_width": bar_width,
         "ppd": ppd,
         "rotation": ORIENTATION + 90,
-        "intensity_bars": (v3, v1),
+        "intensity_bars": (v1, v3),
         "intensity_target": v2,
-        "target_indices": (2, 4, 6, 8),
+        "target_indices": (1, 3, 5, 7),
     }
     large_params = {
         "bar_width": bar_width,
         "visual_size": 51 * bar_width,
         "ppd": ppd,
         "rotation": ORIENTATION,
-        "intensity_bars": (v1, v3),
+        "intensity_bars": (v3, v1),
     }
-    stim = illusions.gratings.grating_masked(
+    stim = stimupy.gratings.on_grating_masked(
         small_grating_params=small_params,
         large_grating_params=large_params,
         mask_size=(9 * bar_width, 9 * bar_width, 8 * bar_width),
diff --git a/stimupy/papers/white1985.py b/stimupy/papers/white1985.py
index 385cd91f..8481313d 100644
--- a/stimupy/papers/white1985.py
+++ b/stimupy/papers/white1985.py
@@ -33,7 +33,7 @@
 
 import numpy as np
 
-from stimupy.illusions import gratings as grating
+from stimupy import gratings
 
 __all__ = [
     "wide_0phase",
@@ -56,6 +56,12 @@
 START_PHASE1 = -180
 START_PHASE2 = 0
 
+COMMON_PARAMS = {
+    "intensity_bars": (v1, v3),
+    "intensity_target": v2,
+    "orientation": "vertical",
+}
+
 
 def gen_all(ppd=PPD, skip=False):
     stims = {}  # save the stimulus-dicts in a larger dict, with name as key
@@ -125,12 +131,10 @@ def wide_0phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, 0.75),
         "target_phase_shift": START_PHASE1 + 0,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -168,12 +172,10 @@ def wide_36phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, 0.75),
         "target_phase_shift": START_PHASE1 + 36,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -212,12 +214,10 @@ def wide_72phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, 0.75),
         "target_phase_shift": START_PHASE1 + 72,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -256,12 +256,10 @@ def wide_108phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, 0.75),
         "target_phase_shift": START_PHASE1 + 108,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -300,12 +298,10 @@ def wide_144phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, 0.75),
         "target_phase_shift": START_PHASE1 + 144,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -344,12 +340,10 @@ def wide_180phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, 0.75),
         "target_phase_shift": START_PHASE1 + 180,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -388,12 +382,10 @@ def square_0phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, bar_width),
         "target_phase_shift": START_PHASE2 + 0,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -432,12 +424,10 @@ def square_36phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, bar_width),
         "target_phase_shift": START_PHASE2 + 36,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -476,12 +466,10 @@ def square_72phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, bar_width),
         "target_phase_shift": START_PHASE2 + 72,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -520,12 +508,10 @@ def square_108phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, bar_width),
         "target_phase_shift": START_PHASE2 + 108,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -564,12 +550,10 @@ def square_144phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, bar_width),
         "target_phase_shift": START_PHASE2 + 144,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
@@ -608,12 +592,10 @@ def square_180phase(ppd=PPD):
         "bar_width": bar_width,
         "target_size": (bar_width * 9, bar_width),
         "target_phase_shift": START_PHASE2 + 180,
-        "intensity_bars": (v3, v1),
-        "intensity_target": v2,
-        "orientation": "vertical",
+        **COMMON_PARAMS,
     }
 
-    stim = grating.counterphase_induction(**params)
+    stim = gratings.phase_shifted(**params)
     stim["target_mask"] = np.where(stim["target_mask"] != 0, 1, 0)
     return stim
 
diff --git a/stimupy/stimuli/__init__.py b/stimupy/stimuli/__init__.py
new file mode 100644
index 00000000..28a82134
--- /dev/null
+++ b/stimupy/stimuli/__init__.py
@@ -0,0 +1,61 @@
+from stimupy.stimuli import *
+
+__all__ = [
+    "overview",
+    "benarys",
+    "bullseyes",
+    "checkerboards",
+    "cornsweets",
+    "cubes",
+    "delboeufs",
+    "dungeons",
+    "gabors",
+    "gratings",
+    "hermanns",
+    "mondrians",
+    "mueller_lyers",
+    "plaids",
+    "ponzos",
+    "rings",
+    "sbcs",
+    "todorovics",
+    "waves",
+    "wedding_cakes",
+    "whites",
+]
+
+
+def overview(skip=False):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    dict[str, dict]
+        Dict mapping names to individual stimulus dicts
+    """
+
+    stimuli = {}
+    for stimmodule_name in __all__:
+        print(f"Generating stimuli from {stimmodule_name}")
+        # Get a reference to the actual module
+        stimmodule = globals()[stimmodule_name]
+        try:
+            stims = stimmodule.overview()
+
+            # Accumulate
+            stimuli.update(stims)
+        except NotImplementedError as e:
+            if not skip:
+                raise e
+            # Skip stimuli that aren't implemented
+            print("-- not implemented")
+            pass
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=False, save=None)
diff --git a/stimupy/illusions/benarys.py b/stimupy/stimuli/benarys.py
similarity index 96%
rename from stimupy/illusions/benarys.py
rename to stimupy/stimuli/benarys.py
index a9952b82..f9df0580 100644
--- a/stimupy/illusions/benarys.py
+++ b/stimupy/stimuli/benarys.py
@@ -2,7 +2,7 @@
 
 import numpy as np
 
-from stimupy.components.shapes import cross, triangle, rectangle
+from stimupy.components.shapes import cross, rectangle, triangle
 from stimupy.utils import resolution
 
 __all__ = [
@@ -718,19 +718,26 @@ def add_targets(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    params_benary = {
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
         "visual_size": 10,
-        "ppd": 20,
-        "cross_thickness": 2,
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
+
+    params_benary = {
         "target_size": 1,
+        "cross_thickness": 2,
     }
 
     params_todo = {
-        "visual_size": 10,
-        "ppd": 20,
         "target_size": 1,
         "L_width": 2,
     }
@@ -741,12 +748,21 @@ def add_targets(
     }
 
     # fmt: off
-    stims = {
-        "cross": cross_generalized(**params_benary, **target_pos),
-        "rectangles": cross_rectangles(**params_benary),
-        "triangles": cross_triangles(**params_benary),
-        "todorovic_general": todorovic_generalized(**params_todo, **target_pos),
-        "todorovic_rectangles": todorovic_rectangles(**params_todo),
-        "todorovic_triangles": todorovic_triangles(**params_todo),
+    stimuli = {
+        "cross": cross_generalized(**default_params, **params_benary, **target_pos),
+        "rectangles": cross_rectangles(**default_params, **params_benary),
+        "triangles": cross_triangles(**default_params, **params_benary),
+        "todorovic_general": todorovic_generalized(**default_params, **params_todo, **target_pos),
+        "todorovic_rectangles": todorovic_rectangles(**default_params, **params_todo),
+        "todorovic_triangles": todorovic_triangles(**default_params, **params_todo),
     }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
     plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/circulars.py b/stimupy/stimuli/bullseyes.py
similarity index 61%
rename from stimupy/illusions/circulars.py
rename to stimupy/stimuli/bullseyes.py
index 7b2be8fa..7c0da912 100644
--- a/stimupy/illusions/circulars.py
+++ b/stimupy/stimuli/bullseyes.py
@@ -1,12 +1,15 @@
-import numpy as np
+from stimupy.stimuli import rings
 
-from stimupy.components.circulars import square_wave as grating
-from stimupy.utils import resolution, stack_dicts
+__all__ = [
+    "circular",
+    "circular_two_sided",
+    "rectangular",
+    "rectangular_generalized",
+    "rectangular_two_sided",
+]
 
-__all__ = ["rings", "two_sided_rings", "bullseye", "two_sided_bullseye"]
 
-
-def rings(
+def circular(
     visual_size=None,
     ppd=None,
     shape=None,
@@ -14,14 +17,16 @@ def rings(
     n_rings=None,
     ring_width=None,
     phase_shift=0,
-    target_indices=None,
     intensity_target=0.5,
     intensity_rings=(1.0, 0.0),
     intensity_background=0.5,
     origin="mean",
     clip=True,
 ):
-    """Circular grating, with one or more target rings
+    """Circular Bullseye stimulus
+
+    Circular grating, where the target is the central disc.
+    Alias for circular_white(target_indices=0,...)
 
     Specification of the number of rings, and their width can be done in two ways:
     a ring_width (in degrees) and n_rings, and/or by specifying the spatial frequency
@@ -52,8 +57,6 @@ def rings(
         width of a single ring, in degrees
     phase_shift : float
         phase shift of grating in degrees
-    target_indices : int or Sequence[int, ] (optional)
-        indices of target discs. If not specified, use middle ring (round down)
     intensity_target : float (optional)
         intensity value of target ring(s), by default 0.5
     intensity_rings : Sequence[Number, ...]
@@ -77,6 +80,10 @@ def rings(
 
     References
     ----------
+    Bindman, D., & Chubb, C. (2004).
+        Brightness assimilation in bullseye displays.
+        Vision Research, 44, 309-319.
+        https://doi.org/10.1016/S0042-6989(03)00430-9
     Hong, S. W., and Shevell, S. K. (2004).
         Brightness contrast and assimilation from patterned inducing backgrounds.
         Vision Research, 44, 35-43.
@@ -87,43 +94,25 @@ def rings(
         Perception, 34, 557-564.
         https://doi.org/10.1068/p5414
     """
-
-    # Get stim
-    stim = grating(
+    stim = rings.circular(
         visual_size=visual_size,
         ppd=ppd,
+        shape=shape,
         frequency=frequency,
         n_rings=n_rings,
         ring_width=ring_width,
         phase_shift=phase_shift,
-        intensity_background=intensity_background,
         intensity_rings=intensity_rings,
-        shape=shape,
+        intensity_background=intensity_background,
+        intensity_target=intensity_target,
+        target_indices=0,
         origin=origin,
         clip=clip,
     )
-
-    if isinstance(target_indices, (float, int)):
-        target_indices = [target_indices]
-
-    if target_indices is None:
-        target_indices = [int(stim["ring_mask"].max() / 2)]
-
-    # Add targets
-    stim["target_mask"] = np.zeros(stim["shape"])
-    for i, ring_idx in enumerate(target_indices):
-        stim["img"] = np.where(stim["ring_mask"] == ring_idx + 1, intensity_target, stim["img"])
-        stim["target_mask"] = np.where(stim["ring_mask"] == ring_idx + 1, i + 1, 0).astype(int)
-        if ring_idx > stim["ring_mask"].max() - 1:
-            raise ValueError(f"target idx {ring_idx} is above maximum ring idx")
-
-    # Update stim dict
-    stim["target_indices"] = target_indices
-    stim["intensity_target"] = intensity_target
     return stim
 
 
-def two_sided_rings(
+def circular_two_sided(
     visual_size=None,
     ppd=None,
     shape=None,
@@ -131,13 +120,15 @@ def two_sided_rings(
     n_rings=None,
     ring_width=None,
     phase_shift=0,
-    target_indices=None,
     intensity_target=0.5,
     intensity_rings=(1.0, 0.0),
     intensity_background=0.5,
     origin="mean",
 ):
-    """Two-sided circular grating, with one or more target rings
+    """Two-sided circular Bullseye stimulus
+
+    Circular grating, where the target is the central disc.
+    Alias for circular_white(target_indices=0,...)
 
     Specification of the number of rings, and their width can be done in two ways:
     a ring_width (in degrees) and n_rings, and/or by specifying the spatial frequency
@@ -168,8 +159,6 @@ def two_sided_rings(
         width of a single ring, in degrees
     phase_shift : float
         phase shift of grating in degrees
-    target_indices : int or Sequence[int, ] (optional)
-        indices of target discs. If not specified, use middle ring (round down)
     intensity_target : float (optional)
         intensity value of target ring(s), by default 0.5
     intensity_rings : Sequence[Number, ...]
@@ -191,6 +180,10 @@ def two_sided_rings(
 
     References
     ----------
+    Bindman, D., & Chubb, C. (2004).
+        Brightness assimilation in bullseye displays.
+        Vision Research, 44, 309-319.
+        https://doi.org/10.1016/S0042-6989(03)00430-9
     Hong, S. W., and Shevell, S. K. (2004).
         Brightness contrast and assimilation from patterned inducing backgrounds.
         Vision Research, 44, 35-43.
@@ -202,76 +195,40 @@ def two_sided_rings(
         https://doi.org/10.1068/p5414
     """
 
-    # Resolve resolution
-    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
-
-    stim1 = rings(
-        visual_size=(visual_size[0], visual_size[1] / 2),
+    stim = rings.circular_two_sided(
+        visual_size=visual_size,
         ppd=ppd,
+        shape=shape,
         frequency=frequency,
         n_rings=n_rings,
         ring_width=ring_width,
         phase_shift=phase_shift,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
         intensity_rings=intensity_rings,
         intensity_background=intensity_background,
-        origin=origin,
-        clip=True,
-    )
-
-    stim2 = rings(
-        visual_size=(visual_size[0], visual_size[1] / 2),
-        ppd=ppd,
-        frequency=frequency,
-        n_rings=n_rings,
-        ring_width=ring_width,
-        phase_shift=phase_shift,
-        target_indices=target_indices,
         intensity_target=intensity_target,
-        intensity_rings=intensity_rings[::-1],
-        intensity_background=intensity_background,
+        target_indices=0,
         origin=origin,
-        clip=True,
     )
-
-    stim = stack_dicts(stim1, stim2)
-    stim["shape"] = shape
-    stim["visual_size"] = visual_size
     return stim
 
 
-def bullseye(
+def rectangular(
     visual_size=None,
     ppd=None,
     shape=None,
     frequency=None,
-    n_rings=None,
-    ring_width=None,
+    n_frames=None,
+    frame_width=None,
     phase_shift=0,
-    intensity_target=0.5,
-    intensity_rings=(1.0, 0.0),
+    intensity_frames=(1.0, 0.0),
     intensity_background=0.5,
+    intensity_target=0.5,
     origin="mean",
     clip=True,
 ):
-    """Circular Bullseye stimulus
+    """Square "bullseye", i.e., set of rings with target in center
 
-    Circular grating, where the target is the central disc.
-    Alias for circular_white(target_indices=0,...)
-
-    Specification of the number of rings, and their width can be done in two ways:
-    a ring_width (in degrees) and n_rings, and/or by specifying the spatial frequency
-    of a circular grating (in cycles per degree)
-
-    The total shape (in pixels) and visual size (in degrees) has to match the
-    specification of the rings and their widths.
-    Thus, not all 6 parameters (visual_size, ppd, shape, frequency, ring_width, n_rings)
-    have to be specified, as long as both the resolution, and the distribution of rings,
-    can be resolved.
-
-    Note: all rings in a grating have the same width -- if more control is required
-    see disc_and_rings
+    Essentially frames(target_indices=1)
 
     Parameters
     ----------
@@ -282,20 +239,21 @@ def bullseye(
     shape : Sequence[Number, Number], Number, or None (default)
         shape [height, width] of image, in pixels
     frequency : Number, or None (default)
-        spatial frequency of circular grating, in cycles per degree
-    n_rings : int, or None (default)
-        number of rings
-    ring_width : Number, or None (default)
-        width of a single ring, in degrees
+        spatial frequency of grating, in cycles per degree visual angle
+    n_frames : int, or None (default)
+        number of frames in the grating
+    frame_width : Number, or None (default)
+        width of a single frame, in degrees visual angle
     phase_shift : float
         phase shift of grating in degrees
-    intensity_target : float (optional)
-        intensity value of target ring(s), by default 0.5
-    intensity_rings : Sequence[Number, ...]
-        intensity value for each ring, from inside to out, by default [1,0]
-        If fewer intensities are passed than number of radii, cycles through intensities
+    intensity_frames : Sequence[float, float]
+        min and max intensity of square-wave, by default (0.0, 1.0)
     intensity_background : float (optional)
         intensity value of background, by default 0.5
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
     origin : "corner", "mean" or "center"
         if "corner": set origin to upper left corner
         if "mean": set origin to hypothetical image center (default)
@@ -316,64 +274,100 @@ def bullseye(
         Brightness assimilation in bullseye displays.
         Vision Research, 44, 309-319.
         https://doi.org/10.1016/S0042-6989(03)00430-9
-    Hong, S. W., and Shevell, S. K. (2004).
-        Brightness contrast and assimilation from patterned inducing backgrounds.
-        Vision Research, 44, 35-43.
-        https://doi.org/10.1016/j.visres.2003.07.010
-    Howe, P. D. L. (2005).
-        White's effect:
-        removing the junctions but preserving the strength of the illusion.
-        Perception, 34, 557-564.
-        https://doi.org/10.1068/p5414
     """
-    stim = rings(
+
+    stim = rings.rectangular(
+        shape=shape,
         visual_size=visual_size,
         ppd=ppd,
-        shape=shape,
         frequency=frequency,
-        n_rings=n_rings,
-        ring_width=ring_width,
+        n_frames=n_frames,
+        frame_width=frame_width,
         phase_shift=phase_shift,
-        intensity_rings=intensity_rings,
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
+        intensity_frames=intensity_frames,
         target_indices=0,
+        intensity_target=intensity_target,
         origin=origin,
         clip=clip,
+        intensity_background=intensity_background,
     )
     return stim
 
 
-def two_sided_bullseye(
+def rectangular_generalized(
     visual_size=None,
     ppd=None,
     shape=None,
-    frequency=None,
-    n_rings=None,
-    ring_width=None,
-    phase_shift=0,
-    intensity_target=0.5,
-    intensity_rings=(1.0, 0.0),
+    radii=None,
+    intensity_frames=(1.0, 0.0),
     intensity_background=0.5,
+    intensity_target=0.5,
     origin="mean",
 ):
-    """Two-sided circular Bullseye stimulus
+    """Draw sequential set of square frames with specified radii with central target
 
-    Circular grating, where the target is the central disc.
-    Alias for circular_white(target_indices=0,...)
+    Parameters
+    ----------
+    frame_radii : Sequence[Number]
+        radii of each frame, in degrees visual angle
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    radii : Sequence[Number] or None (default)
+        radii of each frame, in degrees visual angle
+    intensity_frames : Sequence[float, float]
+        min and max intensity of square-wave, by default (0.0, 1.0)
+    intensity_background : float (optional)
+        intensity value of background, by default 0.5
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
 
-    Specification of the number of rings, and their width can be done in two ways:
-    a ring_width (in degrees) and n_rings, and/or by specifying the spatial frequency
-    of a circular grating (in cycles per degree)
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each frame (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+    stim = rings.rectangular_generalized(
+        radii=radii,
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        intensity_frames=intensity_frames,
+        intensity_background=intensity_background,
+        target_indices=1,
+        intensity_target=intensity_target,
+        origin=origin,
+    )
+    return stim
 
-    The total shape (in pixels) and visual size (in degrees) has to match the
-    specification of the rings and their widths.
-    Thus, not all 6 parameters (visual_size, ppd, shape, frequency, ring_width, n_rings)
-    have to be specified, as long as both the resolution, and the distribution of rings,
-    can be resolved.
 
-    Note: all rings in a grating have the same width -- if more control is required
-    see disc_and_rings
+def rectangular_two_sided(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_frames=None,
+    frame_width=None,
+    phase_shift=0,
+    intensity_target=0.5,
+    intensity_frames=(1.0, 0.0),
+    intensity_background=0.5,
+    origin="mean",
+):
+    """Two-sided square "bullseye", i.e., set of rings with target in center
+
+    Essentially frames(target_indices=1)
 
     Parameters
     ----------
@@ -384,18 +378,22 @@ def two_sided_bullseye(
     shape : Sequence[Number, Number], Number, or None (default)
         shape [height, width] of image, in pixels
     frequency : Number, or None (default)
-        spatial frequency of circular grating, in cycles per degree
-    n_rings : int, or None (default)
-        number of rings
-    ring_width : Number, or None (default)
-        width of a single ring, in degrees
+        spatial frequency of grating, in cycles per degree visual angle
+    n_frames : int, or None (default)
+        number of frames in the grating
+    frame_width : Number, or None (default)
+        width of a single frame, in degrees visual angle
     phase_shift : float
         phase shift of grating in degrees
-    intensity_target : float (optional)
-        intensity value of target ring(s), by default 0.5
-    intensity_rings : Sequence[Number, ...]
-        intensity value for each ring, from inside to out, by default [1,0]
-        If fewer intensities are passed than number of radii, cycles through intensities
+    period : "full", "half", "ignore" (default)
+        whether to ensure the grating only has "full" periods,
+        half "periods", or no guarantees ("ignore")
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    intensity_frames : Sequence[float, float]
+        min and max intensity of square-wave, by default (0.0, 1.0)
     intensity_background : float (optional)
         intensity value of background, by default 0.5
     origin : "corner", "mean" or "center"
@@ -403,6 +401,7 @@ def two_sided_bullseye(
         if "mean": set origin to hypothetical image center (default)
         if "center": set origin to real center (closest existing value to mean)
 
+
     Returns
     -------
     dict[str, Any]
@@ -412,66 +411,57 @@ def two_sided_bullseye(
 
     References
     ----------
-    Bindman, D., & Chubb, C. (2004).
+        Bindman, D., & Chubb, C. (2004).
         Brightness assimilation in bullseye displays.
         Vision Research, 44, 309-319.
         https://doi.org/10.1016/S0042-6989(03)00430-9
-    Hong, S. W., and Shevell, S. K. (2004).
-        Brightness contrast and assimilation from patterned inducing backgrounds.
-        Vision Research, 44, 35-43.
-        https://doi.org/10.1016/j.visres.2003.07.010
-    Howe, P. D. L. (2005).
-        White's effect:
-        removing the junctions but preserving the strength of the illusion.
-        Perception, 34, 557-564.
-        https://doi.org/10.1068/p5414
     """
 
-    # Resolve resolution
-    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
-
-    stim1 = bullseye(
-        visual_size=(visual_size[0], visual_size[1] / 2),
+    stim = rings.rectangular_two_sided(
+        visual_size=visual_size,
         ppd=ppd,
+        shape=shape,
         frequency=frequency,
-        n_rings=n_rings,
-        ring_width=ring_width,
+        n_frames=n_frames,
+        frame_width=frame_width,
         phase_shift=phase_shift,
-        intensity_target=intensity_target,
-        intensity_rings=intensity_rings,
+        intensity_frames=intensity_frames,
         intensity_background=intensity_background,
-        origin=origin,
-        clip=True,
-    )
-
-    stim2 = bullseye(
-        visual_size=(visual_size[0], visual_size[1] / 2),
-        ppd=ppd,
-        frequency=frequency,
-        n_rings=n_rings,
-        ring_width=ring_width,
-        phase_shift=phase_shift,
         intensity_target=intensity_target,
-        intensity_rings=intensity_rings[::-1],
-        intensity_background=intensity_background,
+        target_indices=0,
         origin=origin,
-        clip=True,
     )
-
-    stim = stack_dicts(stim1, stim2)
-    stim["shape"] = shape
-    stim["visual_size"] = visual_size
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    stims = {
-        "rings": rings(visual_size=(8, 8), ppd=32, frequency=1.0),
-        "two_sided_rings": two_sided_rings(visual_size=(8, 8), ppd=32, frequency=1.0),
-        "bullseye": bullseye(visual_size=(8, 8), ppd=32, frequency=1.0),
-        "two_sided_bullseye": two_sided_bullseye(visual_size=(8, 16), ppd=32, frequency=1.0),
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "visual_size": (10, 10),
+        "ppd": 30,
     }
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "circular": circular(**default_params, frequency=1.0, clip=True),
+        "circular, two sided": circular_two_sided(**default_params, frequency=1.0),
+        "rectangular": rectangular(**default_params, frequency=1.0, clip=True),
+        "rectangular, two sided": rectangular_two_sided(**default_params, frequency=1.0),
+    }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
 
+    stims = overview()
     plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/checkerboards.py b/stimupy/stimuli/checkerboards.py
similarity index 67%
rename from stimupy/illusions/checkerboards.py
rename to stimupy/stimuli/checkerboards.py
index c9661fe4..2ff72b5c 100644
--- a/stimupy/illusions/checkerboards.py
+++ b/stimupy/stimuli/checkerboards.py
@@ -1,6 +1,8 @@
+import warnings
+
 import numpy as np
 
-from stimupy.components.checkerboards import checkerboard as board
+from stimupy.components import waves
 from stimupy.utils import resolution
 from stimupy.utils.contrast_conversions import transparency
 
@@ -30,10 +32,6 @@ def mask_from_idx(checkerboard_stim, check_idc):
     ------
     ValueError
         Check index is invalid given the board shape
-
-    See also
-    --------
-    stimupy.components.checkerboard
     """
     board_shape = checkerboard_stim["board_shape"]
     mask = np.zeros(checkerboard_stim["shape"])
@@ -95,10 +93,6 @@ def add_targets(checkerboard_stim, target_indices, extend_targets=False, intensi
         dict with the updated stimulus (key: "img"),
         mask with integer index for each target (key: "target_mask"),
         and additional keys containing stimulus parameters
-
-    See also
-    --------
-    stimupy.components.checkerboard
     """
     mask = np.zeros(checkerboard_stim["shape"])
     for i, target in enumerate(target_indices):
@@ -174,10 +168,6 @@ def checkerboard(
         mask with integer index for each target (key: "target_mask"),
         and additional keys containing stimulus parameters
 
-    See also
-    --------
-    stimupy.components.checkerboard
-
     References
     ----------
     Blakeslee, B., & McCourt, M. E. (2004).
@@ -189,20 +179,121 @@ def checkerboard(
         Spatial Vision. Oxford University Press.
     """
 
-    # Set up basic checkerboard
-    stim = board(
+    lst = [visual_size, ppd, shape, frequency, board_shape, check_visual_size]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'checkerboard()' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'board_shape', 'check_visual_size'"
+        )
+
+    if isinstance(frequency, (float, int)) or frequency is None:
+        frequency = (frequency, frequency)
+    if isinstance(board_shape, (float, int)) or board_shape is None:
+        board_shape = (board_shape, board_shape)
+    if isinstance(check_visual_size, (float, int)) or check_visual_size is None:
+        check_visual_size = (check_visual_size, check_visual_size)
+
+    create_twice = visual_size is None and shape is None
+
+    # Create checkerboard by treating it as a plaid
+    sw1 = waves.square(
         visual_size=visual_size,
         ppd=ppd,
         shape=shape,
-        frequency=frequency,
-        board_shape=board_shape,
-        check_visual_size=check_visual_size,
+        frequency=frequency[0],
+        n_phases=board_shape[0],
+        phase_width=check_visual_size[0],
         period=period,
         rotation=rotation,
-        intensity_checks=intensity_checks,
+        phase_shift=0,
+        intensities=intensity_checks,
+        origin="corner",
+        round_phase_width=round_phase_width,
+        base_type="rotated",
+    )
+
+    sw2 = waves.square(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency[1],
+        n_phases=board_shape[1],
+        phase_width=check_visual_size[1],
+        period=period,
+        rotation=rotation + 90,
+        phase_shift=0,
+        intensities=intensity_checks,
+        origin="corner",
         round_phase_width=round_phase_width,
+        base_type="rotated",
     )
 
+    # If neither a visual_size nor a shape was given, each square wave
+    # grating is always a square. An easy solution is to just recreate
+    # both gratings with the resolved parameters
+    if create_twice:
+        warnings.filterwarnings("ignore")
+        sw1 = waves.square(
+            visual_size=(sw1["visual_size"][0], sw2["visual_size"][1]),
+            ppd=sw1["ppd"],
+            shape=None,
+            frequency=frequency[0],
+            n_phases=board_shape[0],
+            phase_width=check_visual_size[0],
+            period=period,
+            rotation=rotation,
+            phase_shift=0,
+            intensities=intensity_checks,
+            origin="corner",
+            round_phase_width=round_phase_width,
+            base_type="rotated",
+        )
+
+        sw2 = waves.square(
+            visual_size=(sw1["visual_size"][0], sw2["visual_size"][1]),
+            ppd=sw1["ppd"],
+            shape=None,
+            frequency=frequency[1],
+            n_phases=board_shape[1],
+            phase_width=check_visual_size[1],
+            period=period,
+            rotation=rotation + 90,
+            phase_shift=0,
+            intensities=intensity_checks,
+            origin="corner",
+            round_phase_width=round_phase_width,
+            base_type="rotated",
+        )
+        warnings.filterwarnings("default")
+
+    # Add the two square-wave gratings into a checkerboard
+    img = sw1["img"] + sw2["img"]
+    img = np.where(
+        img == intensity_checks[0] + intensity_checks[1], intensity_checks[1], intensity_checks[0]
+    )
+
+    # Create a mask with target indices for each check
+    mask = sw1["grating_mask"] + sw2["grating_mask"] * sw1["grating_mask"].max() * 10
+    unique_vals = np.unique(mask)
+    for v in range(len(unique_vals)):
+        mask[mask == unique_vals[v]] = v + 1
+
+    stim = {
+        "img": img,
+        "checker_mask": mask.astype(int),
+        "grating_mask": sw1["grating_mask"],
+        "grating_mask2": sw2["grating_mask"],
+        "visual_size": sw1["visual_size"],
+        "ppd": sw1["ppd"],
+        "shape": sw1["shape"],
+        "frequency": (sw2["frequency"], sw1["frequency"]),
+        "board_shape": (sw2["n_phases"], sw1["n_phases"]),
+        "check_visual_size": (sw2["phase_width"], sw1["phase_width"]),
+        "period": period,
+        "rotation": rotation,
+        "intensity_checks": intensity_checks,
+    }
+
     # Add targets
     if target_indices is not None:
         stim = add_targets(
@@ -282,7 +373,7 @@ def contrast_contrast(
     """
 
     # Set up basic checkerboard
-    stim = board(
+    stim = checkerboard(
         visual_size=visual_size,
         ppd=ppd,
         shape=shape,
@@ -320,20 +411,37 @@ def contrast_contrast(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    p = {
-        "ppd": 32,
-        "board_shape": (8, 8),
-        "check_visual_size": (2, 2),
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {"visual_size": (10, 10), "ppd": 30}
+    default_params.update(kwargs)
+
+    # p = {
+    #     "board_shape": (10, 10),
+    # }
+
+    # fmt: off
+    stimuli = {
+        "checkerboard": checkerboard(**default_params, check_visual_size=(1, 1)),
+        "checkerboard rotated": checkerboard(**default_params, check_visual_size=(1, 1), rotation=45),
+        "checkerboard from frequency": checkerboard(**default_params, frequency=1),
+        "checkerboard from frequency, rotated": checkerboard(**default_params, frequency=1, rotation=45),
+        "checkerboard with targets": checkerboard(**default_params, check_visual_size=(1, 1), target_indices=[(3, 2), (5, 5)]),
+        "Checkerboard Contrast-Contrast illusion": contrast_contrast(**default_params, check_visual_size=(1, 1), target_shape=(4, 4)),
     }
+    # fmt: on
 
-    stims = {
-        "checkerboard": checkerboard(
-            **p,
-            target_indices=[(3, 2), (5, 5)],
-        ),
-        "contrast_contrast": contrast_contrast(**p, target_shape=(4, 4)),
-    }
-    plot_stimuli(stims, mask=True, save=None)
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=False, save=None)
diff --git a/stimupy/illusions/cornsweets.py b/stimupy/stimuli/cornsweets.py
similarity index 83%
rename from stimupy/illusions/cornsweets.py
rename to stimupy/stimuli/cornsweets.py
index 4f028f55..ce06f9a7 100644
--- a/stimupy/illusions/cornsweets.py
+++ b/stimupy/stimuli/cornsweets.py
@@ -84,8 +84,28 @@ def cornsweet(
     return stim
 
 
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {"visual_size": 15, "ppd": 30}
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "(Craik-O'Brien-)Cornsweet edge/illusion": cornsweet(**default_params, ramp_width=3)
+    }
+    # fmt: on
+
+    return stimuli
+
+
 if __name__ == "__main__":
-    from stimupy.utils import plot_stim
+    from stimupy.utils import plot_stimuli
 
-    stim = cornsweet(visual_size=10, ppd=10, ramp_width=3)
-    plot_stim(stim, stim_name="cornsweet", mask=True, save=None)
+    stims = overview()
+    plot_stimuli(stims, mask=False, save=None)
diff --git a/stimupy/illusions/cubes.py b/stimupy/stimuli/cubes.py
similarity index 95%
rename from stimupy/illusions/cubes.py
rename to stimupy/stimuli/cubes.py
index d05c8432..60be4b88 100644
--- a/stimupy/illusions/cubes.py
+++ b/stimupy/stimuli/cubes.py
@@ -301,28 +301,33 @@ def cube(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    p1 = {
-        "ppd": 10,
-        "cell_lengths": (2, 4, 2),
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "ppd": 30,
         "cell_thickness": 1,
         "cell_spacing": 0.5,
-        "target_indices": 1,
     }
+    default_params.update(kwargs)
 
-    p2 = {
-        "visual_size": 10,
-        "ppd": 10,
-        "n_cells": 5,
-        "target_indices": (1, 2),
-        "cell_thickness": 1,
-        "cell_spacing": 0.5,
+    # fmt: off
+    stimuli = {
+        "Cube stimulus": cube(**default_params, visual_size=10, n_cells=5, target_indices=(1,2)),
+        "Cube - varying cells": varying_cells(**default_params, cell_lengths=(2,4,2), target_indices=1),
     }
+    # fmt: on
 
-    stims = {
-        "varying cells": varying_cells(**p1),
-        "cube": cube(**p2),
-    }
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
     plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/delboeufs.py b/stimupy/stimuli/delboeufs.py
similarity index 90%
rename from stimupy/illusions/delboeufs.py
rename to stimupy/stimuli/delboeufs.py
index a1b2da7d..b315618b 100644
--- a/stimupy/illusions/delboeufs.py
+++ b/stimupy/stimuli/delboeufs.py
@@ -178,17 +178,32 @@ def two_sided(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    p1 = {
-        "visual_size": (10, 20),
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "visual_size": (10, 10),
         "ppd": 30,
-        "target_radius": 2,
     }
+    default_params.update(kwargs)
 
-    stims = {
-        "single": delboeuf(**p1, outer_radius=4),
-        "two_sided": two_sided(**p1, outer_radii=(4, 2.5)),
+    # fmt: off
+    stimuli = {
+        "Mueller-Lyer illusion": delboeuf(**default_params, target_radius=1, outer_radius=4),
+        "Mueller-Lyer, two-sided": two_sided(**default_params, target_radius=1, outer_radii=(2, 1.1)),
     }
-    plot_stimuli(stims, mask=False, save=None)
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/dungeons.py b/stimupy/stimuli/dungeons.py
similarity index 91%
rename from stimupy/illusions/dungeons.py
rename to stimupy/stimuli/dungeons.py
index 581500a9..f61042e1 100644
--- a/stimupy/illusions/dungeons.py
+++ b/stimupy/stimuli/dungeons.py
@@ -177,8 +177,31 @@ def resolve_cells_1d(
     return n_cells, visual_angle, cell_size
 
 
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "visual_size": 10,
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "Dungeon illusion": dungeon(**default_params, n_cells=5)
+    }
+    # fmt: on
+
+    return stimuli
+
+
 if __name__ == "__main__":
-    from stimupy.utils import plot_stim
+    from stimupy.utils import plot_stimuli
 
-    stim = dungeon(visual_size=10, ppd=10, n_cells=5)
-    plot_stim(stim, stim_name="dungeon", mask=True, save=None)
+    stims = overview()
+    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/stimuli/gabors.py b/stimupy/stimuli/gabors.py
new file mode 100644
index 00000000..fc3cf198
--- /dev/null
+++ b/stimupy/stimuli/gabors.py
@@ -0,0 +1,115 @@
+from stimupy.components import waves
+from stimupy.components.gaussians import gaussian
+
+__all__ = ["gabor"]
+
+
+def gabor(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_phases=None,
+    phase_width=None,
+    period="ignore",
+    rotation=0.0,
+    phase_shift=None,
+    intensities=(0.0, 1.0),
+    origin=None,
+    round_phase_width=False,
+    sigma=None,
+):
+    """Draw a Gabor: a sinewave grating in a Gaussian envelope
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    bar_width : Number, or None (default)
+        width of a single bar, in degrees visual angle
+    sigma : float or (float, float)
+        sigma of Gaussian in degree visual angle (y, x)
+    period : "even", "odd", "either" or "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float
+        rotation of grating in degrees (default: 0 = horizontal)
+    phase_shift : float
+        phase shift of grating in degrees
+    intensity_bars : Sequence[float, ...]
+        maximal intensity value for each bar, by default (0.0, 1.0).
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each bar (key: "grating_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if sigma is None:
+        raise ValueError("gabor() missing argument 'sigma' which is not 'None'")
+
+    stim = waves.sine(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_phases,
+        phase_width=phase_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        intensities=intensities,
+        origin=origin,
+        base_type="rotated",
+        round_phase_width=round_phase_width,
+    )
+
+    gaussian_window = gaussian(
+        visual_size=visual_size,
+        ppd=ppd,
+        sigma=sigma,
+        origin=origin,
+    )
+    mean_int = (intensities[0] + intensities[1]) / 2
+    stim["img"] = (stim["img"] - mean_int) * gaussian_window["img"] + mean_int
+
+    return {
+        **stim,
+        "sigma": sigma,
+        "gaussian_mask": gaussian_window["gaussian_mask"],
+    }
+
+
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "visual_size": 10,
+        "ppd": 20,
+    }
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "gabor": gabor(**default_params, frequency=1, sigma=2, phase_shift=0, round_phase_width=False, origin="center"),
+    }
+    # fmt: on
+
+    return stimuli
diff --git a/stimupy/illusions/gratings.py b/stimupy/stimuli/gratings.py
similarity index 76%
rename from stimupy/illusions/gratings.py
rename to stimupy/stimuli/gratings.py
index f93b9274..6f3d922c 100644
--- a/stimupy/illusions/gratings.py
+++ b/stimupy/stimuli/gratings.py
@@ -1,130 +1,27 @@
-import itertools
 import warnings
 
 import numpy as np
 
-from stimupy.components.gratings import sine_wave
-from stimupy.components.gratings import square_wave as square_wave_component
 from stimupy.components.gaussians import gaussian
 from stimupy.components.shapes import parallelogram, rectangle
+from stimupy.stimuli.waves import sine_linear as sinewave
+from stimupy.stimuli.waves import square_linear as squarewave
 from stimupy.utils import pad_dict_to_shape, pad_dict_to_visual_size, resolution
 from stimupy.utils.filters import convolve
 
 __all__ = [
-    "square_wave",
-    "uniform",
-    "grating_masked",
-    "grating",
-    "counterphase_induction",
-    "induction",
-    "induction_blur",
+    "sinewave",
+    "squarewave",
+    "on_uniform",
+    "on_grating",
+    "on_grating_masked",
+    "phase_shifted",
+    "grating_induction",
+    "grating_induction_blur",
 ]
 
 
-def square_wave(
-    visual_size=None,
-    ppd=None,
-    shape=None,
-    frequency=None,
-    n_bars=None,
-    bar_width=None,
-    period="ignore",
-    rotation=0,
-    phase_shift=0,
-    intensity_bars=(1.0, 0.0),
-    target_indices=(),
-    intensity_target=0.5,
-    origin="corner",
-    round_phase_width=True,
-):
-    """Spatial square-wave grating (set of bars), with some bar(s) as target(s)
-
-    Parameters
-    ----------
-    visual_size : Sequence[Number, Number], Number, or None (default)
-        visual size [height, width] of image, in degrees
-    ppd : Sequence[Number, Number], Number, or None (default)
-        pixels per degree [vertical, horizontal]
-    shape : Sequence[Number, Number], Number, or None (default)
-        shape [height, width] of image, in pixels
-    frequency : Number, or None (default)
-        spatial frequency of grating, in cycles per degree visual angle
-    n_bars : int, or None (default)
-        number of bars in the grating
-    bar_width : Number, or None (default)
-        width of a single bar, in degrees visual angle
-    period : "even", "odd", "either" or "ignore" (default)
-        ensure whether the grating has "even" number of phases, "odd"
-        number of phases, either or whether not to round the number of
-        phases ("ignore")
-    rotation : float
-        rotation of grating in degrees (default: 0 = horizontal)
-    phase_shift : float
-        phase shift of grating in degrees
-    intensity_bars : Sequence[float, ...]
-        intensity value for each bar, by default (1.0, 0.0).
-        Can specify as many intensities as n_bars;
-        If fewer intensities are passed than n_bars, cycles through intensities
-    target_indices : int, or Sequence[int, ...]
-        indices segments where targets will be placed
-    intensity_target : float, or Sequence[float, ...], optional
-        intensity value for each target, by default 0.5.
-        Can specify as many intensities as number of target_indices;
-        If fewer intensities are passed than target_indices, cycles through intensities
-    origin : "corner", "mean" or "center"
-        if "corner": set origin to upper left corner (default)
-        if "mean": set origin to hypothetical image center
-        if "center": set origin to real center (closest existing value to mean)
-    round_phase_width : Bool
-        if True, round width of bars given resolution
-
-    Returns
-    -------
-    dict[str, Any]
-        dict with the stimulus (key: "img"),
-        mask with integer index for each target (key: "target_mask"),
-        and additional keys containing stimulus parameters
-    """
-
-    # Spatial square-wave grating
-    stim = square_wave_component(
-        visual_size=visual_size,
-        ppd=ppd,
-        shape=shape,
-        frequency=frequency,
-        n_bars=n_bars,
-        bar_width=bar_width,
-        rotation=rotation,
-        phase_shift=phase_shift,
-        period=period,
-        intensity_bars=intensity_bars,
-        origin=origin,
-        round_phase_width=round_phase_width,
-    )
-
-    # Resolve target parameters
-    if isinstance(target_indices, (int)):
-        target_indices = [
-            target_indices,
-        ]
-    if isinstance(intensity_target, (int, float)):
-        intensity_target = [
-            intensity_target,
-        ]
-    intensity_target = itertools.cycle(intensity_target)
-
-    # Place target(s)
-    targets_mask = np.zeros_like(stim["grating_mask"])
-    for target_idx, (bar_idx, intensity) in enumerate(zip(target_indices, intensity_target)):
-        targets_mask = np.where(stim["grating_mask"] == bar_idx, target_idx + 1, targets_mask)
-        stim["img"] = np.where(targets_mask == target_idx + 1, intensity, stim["img"])
-
-    # Update and return stimulus
-    stim["target_mask"] = targets_mask.astype(int)
-    return stim
-
-
-def uniform(
+def on_uniform(
     visual_size=None,
     ppd=None,
     shape=None,
@@ -142,7 +39,7 @@ def uniform(
     origin="corner",
     round_phase_width=True,
 ):
-    """Spatial square-wave grating (set of bars), on a background
+    """Spatial square-wave grating (set of bars), on a uniform background
 
     Parameters
     ----------
@@ -207,7 +104,7 @@ def uniform(
     shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
 
     # Spatial square-wave grating
-    stim = square_wave(
+    stim = squarewave(
         visual_size=grating_size,
         ppd=ppd,
         frequency=frequency,
@@ -240,13 +137,13 @@ def uniform(
     return stim
 
 
-def grating_masked(
+def on_grating_masked(
     small_grating_params,
     large_grating_params,
     mask_size=None,
     mask_rotation=None,
 ):
-    """Grating with a parallelogram-like shape on a grating
+    """Small grating, with a parallelogram-like shape, on a larger grating
 
     Parameters
     ----------
@@ -276,8 +173,8 @@ def grating_masked(
     """
 
     # Create gratings
-    small_grating = square_wave(**small_grating_params)
-    large_grating = square_wave(**large_grating_params)
+    small_grating = squarewave(**small_grating_params)
+    large_grating = squarewave(**large_grating_params)
 
     if small_grating["ppd"] != large_grating["ppd"]:
         raise ValueError("Gratings must have same ppd")
@@ -308,11 +205,11 @@ def grating_masked(
     return stim
 
 
-def grating(
+def on_grating(
     small_grating_params,
     large_grating_params,
 ):
-    """Grating on a grating
+    """Small grating on a larger grating
 
     Parameters
     ----------
@@ -337,14 +234,14 @@ def grating(
         https://doi.org/10.1068/p100215
     """
 
-    stim = grating_masked(
+    stim = on_grating_masked(
         small_grating_params,
         large_grating_params,
     )
     return stim
 
 
-def counterphase_induction(
+def phase_shifted(
     visual_size=None,
     ppd=None,
     shape=None,
@@ -372,7 +269,7 @@ def counterphase_induction(
         raise ValueError("orientation must be horizontal or vertical")
 
     # Spatial square-wave grating
-    stim = square_wave_component(
+    stim = squarewave(
         visual_size=visual_size,
         ppd=ppd,
         shape=shape,
@@ -387,14 +284,14 @@ def counterphase_induction(
         round_phase_width=True,
     )
 
-    stim_target = square_wave_component(
+    stim_target = squarewave(
         visual_size=target_size,
         ppd=stim["ppd"],
         bar_width=stim["bar_width"],
         rotation=rotation,
         phase_shift=0,
         period=period,
-        intensity_bars=(intensity_target, 0),
+        intensity_bars=(0, intensity_target),
         origin=origin,
         round_phase_width=True,
     )
@@ -456,7 +353,7 @@ def counterphase_induction(
     return stim
 
 
-def induction(
+def grating_induction(
     visual_size=None,
     ppd=None,
     shape=None,
@@ -466,12 +363,12 @@ def induction(
     period="ignore",
     rotation=0,
     phase_shift=0,
-    intensity_bars=(1.0, 0.0),
+    intensities=(0.0, 1.0),
     target_width=None,
     intensity_target=0.5,
     origin="corner",
 ):
-    """Grating induction illusion using a sine-wave grating
+    """Grating induction illusion using a sinewave grating
 
     Parameters
     ----------
@@ -501,10 +398,8 @@ def induction(
         If fewer intensities are passed than n_bars, cycles through intensities
     target_width : float
         width of target stripe in degrees visual angle
-    intensity_target : float, or Sequence[float, ...], optional
-        intensity value for each target, by default 0.5.
-        Can specify as many intensities as number of target_indices;
-        If fewer intensities are passed than target_indices, cycles through intensities
+    intensities : Sequence[float, float] or None (default)
+        min and max intensity of sinewave
     origin : "corner", "mean" or "center"
         if "corner": set origin to upper left corner (default)
         if "mean": set origin to hypothetical image center
@@ -528,7 +423,7 @@ def induction(
         raise ValueError("induction() missing argument 'target_width' which is not 'None'")
 
     # Draw grating
-    stim = sine_wave(
+    stim = sinewave(
         shape=shape,
         visual_size=visual_size,
         ppd=ppd,
@@ -538,7 +433,7 @@ def induction(
         period=period,
         rotation=rotation,
         phase_shift=phase_shift,
-        intensity_bars=intensity_bars,
+        intensities=intensities,
         origin=origin,
     )
 
@@ -559,7 +454,7 @@ def induction(
     return stim
 
 
-def induction_blur(
+def grating_induction_blur(
     visual_size=None,
     ppd=None,
     shape=None,
@@ -634,7 +529,7 @@ def induction_blur(
         raise ValueError("induction_blur() missing argument 'target_width' which is not 'None'")
 
     # Draw grating
-    stim = square_wave_component(
+    stim = squarewave(
         shape=shape,
         visual_size=visual_size,
         ppd=ppd,
@@ -675,9 +570,14 @@ def induction_blur(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
     params = {
         "ppd": 40,
         "n_bars": 8,
@@ -689,7 +589,8 @@ def induction_blur(
         "bar_width": 1.0,
         "n_bars": 7,
         "intensity_bars": (0.2, 0.8),
-        "target_indices": (0, 1, 3, 5, 7),
+        "target_indices": (0, 2, 4, 6),
+        "rotation": 90,
     }
 
     large_grating = {
@@ -698,20 +599,28 @@ def induction_blur(
         "n_bars": 21,
     }
 
-    stims = {
-        "square_wave": square_wave(**params, target_indices=(4, 6)),
-        "uniform": uniform(**params, visual_size=20, grating_size=5, target_indices=3),
-        "grating": grating(large_grating_params=large_grating, small_grating_params=small_grating),
-        "grating_masked": grating_masked(
+    # fmt: off
+    stimuli = {
+        "On uniform background": on_uniform(**params, visual_size=20, grating_size=5, target_indices=2),
+        "On grating": on_grating(large_grating_params=large_grating, small_grating_params=small_grating),
+        "On grating, masked": on_grating_masked(
             large_grating_params=large_grating,
-            small_grating_params={**small_grating, "rotation": 90},
+            small_grating_params=small_grating,
             mask_size=(5, 5, 2),
         ),
-        "counterphase_induction": counterphase_induction(
+        "Phase shifted": phase_shifted(
             **params, target_size=4, target_phase_shift=90
         ),
-        "induction": induction(**params, target_width=0.5),
-        "induction_blur": induction_blur(**params, target_width=0.5, sigma=0.1),
+        "Grating Induction (sinewave)": grating_induction(**params, target_width=0.5),
+        "Grating Induction (blurred squarewave)": grating_induction_blur(**params, target_width=0.5, sigma=0.1),
     }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
 
+    stims = overview()
     plot_stimuli(stims, mask=False, save=None)
diff --git a/stimupy/illusions/hermanns.py b/stimupy/stimuli/hermanns.py
similarity index 81%
rename from stimupy/illusions/hermanns.py
rename to stimupy/stimuli/hermanns.py
index f1966c60..6c7fdf34 100644
--- a/stimupy/illusions/hermanns.py
+++ b/stimupy/stimuli/hermanns.py
@@ -83,8 +83,31 @@ def grid(
     return stim
 
 
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "visual_size": 10,
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "Hermann grid": grid(**default_params, element_size=(1.5, 1.5, 0.2))
+    }
+    # fmt: on
+
+    return stimuli
+
+
 if __name__ == "__main__":
-    from stimupy.utils import plot_stim
+    from stimupy.utils import plot_stimuli
 
-    stim = grid(visual_size=10, ppd=10, element_size=(1.5, 1.5, 0.2))
-    plot_stim(stim, stim_name="hermann_grid", mask=True, save=None)
+    stims = overview()
+    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/stimuli/mondrians.py b/stimupy/stimuli/mondrians.py
new file mode 100644
index 00000000..c9432cc4
--- /dev/null
+++ b/stimupy/stimuli/mondrians.py
@@ -0,0 +1,352 @@
+import numpy as np
+
+from stimupy.components.shapes import parallelogram
+from stimupy.utils import resolution
+
+__all__ = [
+    "mondrian",
+    "corrugated_mondrian",
+]
+
+
+def mondrian(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    mondrian_positions=None,
+    mondrian_sizes=None,
+    mondrian_intensities=None,
+    intensity_background=0.5,
+):
+    """Draw Mondrian of given size and intensity at given position
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    mondrian_positions : Sequence[tuple, ... ] or None (default)
+        position (y, x) of each Mondrian in degrees visual angle
+    mondrian_sizes : Sequence[tuple, ... ] or None (default)
+        size (height, width, depth) of Mondrian parallelograms in degrees visual angle;
+        if only one number is given, squares will be drawn
+    mondrian_intensities : Sequence[Number, ... ] or None (default)
+        intensity values of each Mondrian, if only one number is given
+        all will have the same intensity
+    intensity_background : float
+        intensity value of background
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each Mondrian (key: "mondrian_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if mondrian_positions is None:
+        raise ValueError("mondrians() missing argument 'mondrian_positions' which is not 'None'")
+    if mondrian_sizes is None:
+        raise ValueError("mondrians() missing argument 'mondrian_sizes' which is not 'None'")
+    if mondrian_intensities is None:
+        raise ValueError("mondrians() missing argument 'mondrian_intensities' which is not 'None'")
+
+    # Resolve resolution
+    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
+    if len(np.unique(ppd)) > 1:
+        raise ValueError("ppd should be equal in x and y direction")
+
+    img = np.ones(shape) * intensity_background
+    mask = np.zeros(shape)
+
+    n_mondrians = len(mondrian_positions)
+
+    if isinstance(mondrian_intensities, (float, int)):
+        mondrian_intensities = (mondrian_intensities,) * n_mondrians
+
+    if isinstance(mondrian_sizes, (float, int)):
+        mondrian_sizes = ((mondrian_sizes, mondrian_sizes),) * n_mondrians
+
+    if any(
+        len(lst) != n_mondrians
+        for lst in [mondrian_positions, mondrian_sizes, mondrian_intensities]
+    ):
+        raise Exception(
+            "There need to be as many mondrian_positions as there are "
+            "mondrian_sizes and mondrian_intensities."
+        )
+
+    mondrian_positions_px = []
+    mondrian_shapes = []
+
+    for m in range(n_mondrians):
+        try:
+            if len(mondrian_positions[m]) != 2:
+                raise ValueError("Mondrian position tuples should be (ypos, xpos)")
+        except Exception:
+            raise ValueError("Mondrian position tuples should be (ypos, xpos)")
+
+        ypos, xpos = resolution.lengths_from_visual_angles_ppd(mondrian_positions[m], ppd[0])
+        individual_shapes = resolution.lengths_from_visual_angles_ppd(mondrian_sizes[m], ppd[0])
+
+        try:
+            if len(individual_shapes) == 2:
+                depth = 0
+                individual_shapes = individual_shapes + [
+                    depth,
+                ]
+            elif len(individual_shapes) == 3:
+                depth = mondrian_sizes[m][2]
+            else:
+                raise ValueError(
+                    "Mondrian size tuples should be (height, width) for "
+                    "rectangles or (height, width, depth) for parallelograms"
+                )
+        except Exception:
+            raise ValueError(
+                "Mondrian size tuples should be (height, width) for"
+                "rectangles or (height, width, depth) for parallelograms"
+            )
+
+        if depth < 0:
+            xpos += int(depth * ppd[0])
+        mondrian_positions_px.append(tuple([ypos, xpos]))
+        mondrian_shapes.append(tuple(individual_shapes))
+
+        # Create parallelogram
+        patch = parallelogram(
+            visual_size=(mondrian_sizes[m][0], mondrian_sizes[m][1] + np.abs(depth)),
+            ppd=ppd,
+            parallelogram_size=(mondrian_sizes[m][0], mondrian_sizes[m][1], depth),
+            intensity_background=intensity_background,
+            intensity_parallelogram=mondrian_intensities[m],
+        )
+
+        # Place it into Mondrian mosaic
+        yshape, xshape = patch["img"].shape
+        if ypos < 0 or xpos < 0:
+            raise ValueError("There are no negative position coordinates")
+        if (ypos + yshape > shape[0]) or (xpos + xshape > shape[1]):
+            raise ValueError("Not all Mondrians fit into the stimulus")
+        mask_large = np.zeros(shape)
+        mask_large[ypos : ypos + yshape, xpos : xpos + xshape] = patch["shape_mask"]
+
+        img[mask_large == 1] = mondrian_intensities[m]
+        mask[mask_large == 1] = m + 1
+
+    stim = {
+        "img": img,
+        "mondrian_mask": mask.astype(int),
+        "ppd": ppd,
+        "visual_size": visual_size,
+        "shape": shape,
+        "mondrian_positions": tuple(mondrian_positions),
+        "mondrian_positions_px": tuple(mondrian_positions_px),
+        "mondrian_sizes": tuple(mondrian_sizes),
+        "mondrian_shapes": tuple(mondrian_shapes),
+        "mondrian_intensities": tuple(mondrian_intensities),
+        "intensity_background": intensity_background,
+    }
+    return stim
+
+
+def corrugated_mondrian(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    mondrian_depths=None,
+    mondrian_intensities=None,
+    target_indices=None,
+    intensity_background=0.5,
+    intensity_target=None,
+):
+    """Corrugated Mondrian
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    mondrian_depths : Sequence[Number, ... ], Number, or None (default)
+        depth of Mondrian parallelograms per row
+    mondrian_intensities : nested tuples
+        intensities of mondrians; as many tuples as there are rows and as many
+        numbers in each tuple as there are columns
+    target_indices : nested tuples
+        indices of targets; as many tuples as there are targets with (y, x) indices
+    intensity_background : float
+        intensity value for background
+    intensity_target : float or None
+            target intensity value. If None, use values defined in mondrian_intensities
+
+    Returns
+    ------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+
+    References
+    ----------
+    Adelson, E. H. (1993).
+        Perceptual organization and the judgment of brightness.
+        Science, 262, 2042-2044.
+        https://doi.org/10.1126/science.8266102
+    """
+    if mondrian_depths is None:
+        raise ValueError(
+            "corrugated_mondrians() missing argument 'mondrian_depths' which is not 'None'"
+        )
+    if mondrian_intensities is None:
+        raise ValueError(
+            "corrugated_mondrians() missing argument 'mondrian_intensities' which is not 'None'"
+        )
+
+    # Resolve resolution
+    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
+    if len(np.unique(ppd)) > 1:
+        raise ValueError("ppd should be equal in x and y direction")
+    nrows = len(mondrian_intensities)
+    ncols = len(mondrian_intensities[0])
+
+    if isinstance(mondrian_depths, (float, int)):
+        mondrian_depths = (mondrian_depths,) * nrows
+
+    if len(mondrian_depths) != nrows:
+        raise ValueError(
+            "Unclear number of Mondrians in y-direction, check elements "
+            "in mondrian_intensities and mondrian_depths"
+        )
+
+    height, width = visual_size
+    mdepths_px = resolution.lengths_from_visual_angles_ppd(mondrian_depths, ppd[0])
+    max_depth = np.abs(np.array(mdepths_px)).max()
+    sum_depth = np.abs(np.array(mdepths_px).sum())
+    red_depth = np.maximum(max_depth, sum_depth + max_depth)
+    mheight_px, mwidth_px = int(shape[0] / nrows), int((shape[1] - red_depth) / ncols)
+
+    # Initial y coordinates
+    yst = 0
+
+    # Calculate initial x coordinates
+    xstarts = np.cumsum(np.hstack([0, mdepths_px]))
+    temp = np.hstack([mdepths_px, 0])
+    temp[temp > 0] = 0.0
+    xstarts += temp
+    xstarts += np.abs(xstarts.min())
+
+    sizes = []
+    poses = []
+    ints = []
+    tlist = []
+    target_counter = 1
+
+    for r in range(nrows):
+        xst = xstarts[r]
+        if mondrian_depths[r] < 0:
+            xst -= int(mondrian_depths[r] * ppd[0])
+
+        for c in range(ncols):
+            if c != ncols - 1:
+                msize = (mheight_px / ppd[0], (mwidth_px + 1) / ppd[1], mondrian_depths[r])
+            else:
+                msize = (mheight_px / ppd[0], mwidth_px / ppd[1], mondrian_depths[r])
+            mpos = (yst / ppd[0], xst / ppd[1])
+            mint = mondrian_intensities[r][c]
+
+            sizes.append(msize)
+            poses.append(mpos)
+            ints.append(mint)
+
+            if (target_indices is not None) and (r, c) in target_indices:
+                tlist.append(target_counter)
+
+            xst += mwidth_px
+            target_counter += 1
+        yst += mheight_px
+
+    stim = mondrian(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        mondrian_positions=poses,
+        mondrian_sizes=sizes,
+        mondrian_intensities=ints,
+        intensity_background=intensity_background,
+    )
+    target_mask = np.zeros(shape)
+    for t in range(len(tlist)):
+        target_mask[stim["mondrian_mask"] == tlist[t]] = t + 1
+
+    stim["target_mask"] = target_mask.astype(int)
+    stim["target_indices"] = target_indices
+
+    if intensity_target is not None:
+        for t in range(len(tlist)):
+            stim["img"] = np.where(target_mask == t + 1, intensity_target, stim["img"])
+
+    if len(np.unique(stim["img"][target_mask != 0])) > 1:
+        raise Exception("targets are not equiluminant.")
+    return stim
+
+
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "mondrian1": mondrian(**default_params,
+                              visual_size=8,
+                              mondrian_positions=((0, 0), (0, 4), (1, 3), (4, 4), (5, 1)),
+                              mondrian_sizes=3,
+                              mondrian_intensities=np.random.rand(5)),
+        "mondrian2": mondrian(**default_params,
+                              visual_size=10,
+                              mondrian_positions=((0, 0), (8, 4), (1, 6), (4, 4), (5, 1)),
+                              mondrian_sizes=((3, 4, 1), (2, 2, 0), (5, 4, -1), (3, 4, 1), (5, 2, 0)),
+                              mondrian_intensities=np.random.rand(5)),
+        "mondrian3": mondrian(**default_params,
+                              visual_size=(2,6),
+                              mondrian_positions=((0, 0), (0, 2)),
+                              mondrian_sizes=((2, 2, 0), (2, 2, 0)),
+                              mondrian_intensities=(0.2, 0.8)),
+        "mondrian4": mondrian(**default_params,
+                              visual_size=(2,6),
+                              mondrian_positions=((0, 0), (0, 2)),
+                              mondrian_sizes=((2, 2, 1), (2, 2, 1)),
+                              mondrian_intensities=(0.2, 0.8)),
+        "corrugated_mondrian": corrugated_mondrian(**default_params,
+                                                   visual_size=10,
+                                                   mondrian_depths=(1, 0, -1, 0),
+                                                   mondrian_intensities=(
+                                                       (0.4, 0.75, 0.4, 0.75),
+                                                       (0.75, 0.4, 0.75, 1.0),
+                                                       (0.4, 0.75, 0.4, 0.75),
+                                                       (0.0, 0.4, 0.0, 0.4)),
+                                                   target_indices=((1, 1), (3, 1)))
+    }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/mueller_lyers.py b/stimupy/stimuli/mueller_lyers.py
similarity index 93%
rename from stimupy/illusions/mueller_lyers.py
rename to stimupy/stimuli/mueller_lyers.py
index 3a823b81..0a7f2f06 100644
--- a/stimupy/illusions/mueller_lyers.py
+++ b/stimupy/stimuli/mueller_lyers.py
@@ -251,20 +251,39 @@ def two_sided(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    p1 = {
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
         "visual_size": 10,
-        "ppd": 20,
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
+
+    stim_params = {
         "outer_lines_length": 1,
         "outer_lines_angle": 45,
         "target_length": 3,
         "line_width": 0.1,
     }
 
-    stims = {
-        "single": mueller_lyer(**p1),
-        "two_sided": two_sided(**p1),
+    # fmt: off
+    stimuli = {
+        "Mueller-Lyer illusion": mueller_lyer(**default_params, **stim_params),
+        "Mueller-Lyer, two-sided": two_sided(**default_params, **stim_params),
     }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
     plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/angulars.py b/stimupy/stimuli/pinwheels.py
similarity index 92%
rename from stimupy/illusions/angulars.py
rename to stimupy/stimuli/pinwheels.py
index f912e2dc..26f252c2 100644
--- a/stimupy/illusions/angulars.py
+++ b/stimupy/stimuli/pinwheels.py
@@ -160,13 +160,31 @@ def pinwheel(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "visual_size": (10, 10),
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
 
-    stims = {
-        "pinwheel": pinwheel(
-            visual_size=8, ppd=32, n_segments=10, target_width=1, target_indices=3
-        ),
+    # fmt: off
+    stimuli = {
+        "pinwheel": pinwheel(**default_params, n_segments=10, target_width=1, target_indices=3),
     }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
 
+    stims = overview()
     plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/stimuli/plaids.py b/stimupy/stimuli/plaids.py
new file mode 100644
index 00000000..a9c6ecdb
--- /dev/null
+++ b/stimupy/stimuli/plaids.py
@@ -0,0 +1,120 @@
+from stimupy.components import waves
+from stimupy.components.gaussians import gaussian
+
+__all__ = [
+    "plaid",
+]
+
+
+def plaid(
+    grating_parameters1,
+    grating_parameters2,
+    weight1=1,
+    weight2=1,
+    sigma=None,
+):
+    """Draw plaid consisting of two sine-wave gratings
+
+    Parameters
+    ----------
+    grating_parameters1 : dict
+        kwargs to generate first sine-wave grating
+    grating_parameters2 : dict
+        kwargs to generate second sine-wave grating
+    weight1 : float
+        weight of first grating (default: 1)
+    weight2 : float
+        weight of second grating (default: 1)
+    sigma : float or (float, float)
+        sigma of Gaussian window in degree visual angle (y, x)
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each phase (key: "grating_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if sigma is None:
+        raise ValueError("plaid() missing argument 'sigma' which is not 'None'")
+
+    # Create sine-wave gratings
+    grating1 = waves.sine(**grating_parameters1)
+    grating2 = waves.sine(**grating_parameters2)
+
+    if grating1["shape"] != grating2["shape"]:
+        raise ValueError("Gratings must have the same shape")
+    if grating1["ppd"] != grating2["ppd"]:
+        raise ValueError("Gratings must have same ppd")
+    if grating1["origin"] != grating2["origin"]:
+        raise ValueError("Grating origins must be the same")
+
+    # Create Gaussian window
+    window = gaussian(
+        visual_size=grating1["visual_size"],
+        ppd=grating1["ppd"],
+        sigma=sigma,
+        origin=grating1["origin"],
+    )
+
+    img = (weight1 * grating1["img"] + weight2 * grating2["img"]) * window["img"]
+    img = img / (weight1 + weight2)
+
+    # Update parameters
+    grating1["img"] = img
+    grating1["sigma"] = sigma
+    grating1["grating_mask2"] = grating2["grating_mask"]
+    grating1["frequency2"] = grating2["frequency"]
+    grating1["phase_width2"] = grating2["phase_width"]
+    grating1["n_phases2"] = grating2["n_phases"]
+    grating1["gaussian_mask"] = window["gaussian_mask"]
+    return grating1
+
+
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "visual_size": 15,
+        "ppd": 10,
+        "origin": "center",
+        "phase_shift": 30,
+        "base_type": "rotated",
+    }
+    default_params.update(kwargs)
+
+    grating1 = {
+        **default_params,
+        "phase_width": 3.5,
+        "period": "odd",
+        "rotation": 0,
+        "round_phase_width": False,
+    }
+
+    grating2 = {
+        **default_params,
+        "phase_width": 3.5,
+        "period": "ignore",
+        "rotation": 90,
+        "round_phase_width": False,
+    }
+
+    # fmt: off
+    stimuli = {
+        "plaid": plaid(grating1, grating2, sigma=4.0),
+    }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=False, save=None)
diff --git a/stimupy/illusions/ponzos.py b/stimupy/stimuli/ponzos.py
similarity index 87%
rename from stimupy/illusions/ponzos.py
rename to stimupy/stimuli/ponzos.py
index 85f3e211..99276a08 100644
--- a/stimupy/illusions/ponzos.py
+++ b/stimupy/stimuli/ponzos.py
@@ -140,17 +140,35 @@ def ponzo(
     return line1
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stim
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    p1 = {
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
         "visual_size": 10,
-        "ppd": 20,
-        "outer_lines_length": 8,
-        "outer_lines_angle": 10,
-        "target_lines_length": 3,
-        "target_distance": 5,
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "Ponzo illusion": ponzo(**default_params,
+                                outer_lines_angle=10,
+                                outer_lines_length=8,
+                                target_lines_length=3,
+                                target_distance=5)
     }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
 
-    stim = ponzo(**p1)
-    plot_stim(stim, stim_name="ponzo", mask=True, save=None)
+    stims = overview()
+    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/stimuli/rings.py b/stimupy/stimuli/rings.py
new file mode 100644
index 00000000..15afd39d
--- /dev/null
+++ b/stimupy/stimuli/rings.py
@@ -0,0 +1,350 @@
+import itertools
+
+import numpy as np
+
+from stimupy.components.frames import frames
+from stimupy.stimuli.waves import square_cityblock as rectangular
+from stimupy.stimuli.waves import square_radial as circular
+from stimupy.utils import resolution, stack_dicts
+
+__all__ = [
+    "circular",
+    "circular_two_sided",
+    "rectangular",
+    "rectangular_generalized",
+    "rectangular_two_sided",
+]
+
+
+def circular_two_sided(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_rings=None,
+    ring_width=None,
+    phase_shift=0,
+    target_indices=None,
+    intensity_target=0.5,
+    intensity_rings=(1.0, 0.0),
+    intensity_background=0.5,
+    origin="mean",
+):
+    """Two-sided circular grating, with one or more target rings
+
+    Specification of the number of rings, and their width can be done in two ways:
+    a ring_width (in degrees) and n_rings, and/or by specifying the spatial frequency
+    of a circular grating (in cycles per degree)
+
+    The total shape (in pixels) and visual size (in degrees) has to match the
+    specification of the rings and their widths.
+    Thus, not all 6 parameters (visual_size, ppd, shape, frequency, ring_width, n_rings)
+    have to be specified, as long as both the resolution, and the distribution of rings,
+    can be resolved.
+
+    Note: all rings in a grating have the same width -- if more control is required
+    see disc_and_rings
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of circular grating, in cycles per degree
+    n_rings : int, or None (default)
+        number of rings
+    ring_width : Number, or None (default)
+        width of a single ring, in degrees
+    phase_shift : float
+        phase shift of grating in degrees
+    target_indices : int or Sequence[int, ] (optional)
+        indices of target discs. If not specified, use middle ring (round down)
+    intensity_target : float (optional)
+        intensity value of target ring(s), by default 0.5
+    intensity_rings : Sequence[Number, ...]
+        intensity value for each ring, from inside to out, by default [1,0]
+        If fewer intensities are passed than number of radii, cycles through intensities
+    intensity_background : float (optional)
+        intensity value of background, by default 0.5
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+
+    References
+    ----------
+    Hong, S. W., and Shevell, S. K. (2004).
+        Brightness contrast and assimilation from patterned inducing backgrounds.
+        Vision Research, 44, 35-43.
+        https://doi.org/10.1016/j.visres.2003.07.010
+    Howe, P. D. L. (2005).
+        White's effect:
+        removing the junctions but preserving the strength of the illusion.
+        Perception, 34, 557-564.
+        https://doi.org/10.1068/p5414
+    """
+
+    # Resolve resolution
+    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
+
+    stim1 = circular(
+        visual_size=(visual_size[0], visual_size[1] / 2),
+        ppd=ppd,
+        frequency=frequency,
+        n_rings=n_rings,
+        ring_width=ring_width,
+        phase_shift=phase_shift,
+        target_indices=target_indices,
+        intensity_target=intensity_target,
+        intensity_rings=intensity_rings,
+        intensity_background=intensity_background,
+        origin=origin,
+        clip=True,
+    )
+
+    stim2 = circular(
+        visual_size=(visual_size[0], visual_size[1] / 2),
+        ppd=ppd,
+        frequency=frequency,
+        n_rings=n_rings,
+        ring_width=ring_width,
+        phase_shift=phase_shift,
+        target_indices=target_indices,
+        intensity_target=intensity_target,
+        intensity_rings=intensity_rings[::-1],
+        intensity_background=intensity_background,
+        origin=origin,
+        clip=True,
+    )
+
+    stim = stack_dicts(stim1, stim2)
+    stim["shape"] = shape
+    stim["visual_size"] = visual_size
+    return stim
+
+
+def rectangular_generalized(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    radii=None,
+    intensity_frames=(1.0, 0.0),
+    intensity_background=0.5,
+    target_indices=(),
+    intensity_target=0.5,
+    origin="mean",
+):
+    """Draw sequential set of square frames with specified radii and targets
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    radii : Sequence[Number] or None (default)
+        radii of each frame, in degrees visual angle
+    intensity_frames : Sequence[float, float]
+        min and max intensity of square-wave, by default (0.0, 1.0)
+    intensity_background : float (optional)
+        intensity value of background, by default 0.5
+    target_indices : int, or Sequence[int, ...]
+        indices frames where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each frame (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+
+    # Frames component
+    stim = frames(
+        radii=radii,
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        intensity_frames=intensity_frames,
+        intensity_background=intensity_background,
+        origin=origin,
+    )
+
+    # Resolve target parameters
+    if isinstance(target_indices, (int)):
+        target_indices = [
+            target_indices,
+        ]
+    if isinstance(intensity_target, (int, float)):
+        intensity_target = [
+            intensity_target,
+        ]
+    intensity_target = itertools.cycle(intensity_target)
+
+    # Place target(s)
+    targets_mask = np.zeros_like(stim["frame_mask"])
+    for target_idx, (bar_idx, intensity) in enumerate(zip(target_indices, intensity_target)):
+        targets_mask = np.where(stim["frame_mask"] == bar_idx, target_idx + 1, targets_mask)
+        stim["img"] = np.where(targets_mask == target_idx + 1, intensity, stim["img"])
+        if bar_idx > stim["frame_mask"].max():
+            raise ValueError("target_idx is outside stimulus")
+
+    # Update and return stimulus
+    stim["target_mask"] = targets_mask
+    return stim
+
+
+def rectangular_two_sided(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_frames=None,
+    frame_width=None,
+    phase_shift=0,
+    intensity_frames=(1.0, 0.0),
+    intensity_background=0.5,
+    target_indices=(),
+    intensity_target=0.5,
+    origin="mean",
+):
+    """Draw set of square frames, with some frame(s) as target
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_frames : int, or None (default)
+        number of frames in the grating
+    frame_width : Number, or None (default)
+        width of a single frame, in degrees visual angle
+    phase_shift : float
+        phase shift of grating in degrees
+    intensity_frames : Sequence[float, float]
+        min and max intensity of square-wave, by default (0.0, 1.0)
+    intensity_background : float (optional)
+        intensity value of background, by default 0.5
+    target_indices : int, or Sequence[int, ...]
+        indices frames where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+
+    References
+    ----------
+    Domijan, D. (2015).
+        A neurocomputational account
+        of the role of contour facilitation in brightness perception.
+        Frontiers in Human Neuroscience, 9, 93.
+        https://doi.org/10.3389/fnhum.2015.00093
+    """
+
+    # Resolve resolution
+    shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
+
+    stim1 = rectangular(
+        visual_size=(visual_size[0], visual_size[1] / 2),
+        ppd=ppd,
+        frequency=frequency,
+        n_frames=n_frames,
+        frame_width=frame_width,
+        phase_shift=phase_shift,
+        intensity_target=intensity_target,
+        intensity_frames=intensity_frames,
+        target_indices=target_indices,
+        origin=origin,
+        clip=True,
+        intensity_background=intensity_background,
+    )
+
+    stim2 = rectangular(
+        visual_size=(visual_size[0], visual_size[1] / 2),
+        ppd=ppd,
+        frequency=frequency,
+        n_frames=n_frames,
+        frame_width=frame_width,
+        phase_shift=phase_shift,
+        intensity_target=intensity_target,
+        intensity_frames=intensity_frames[::-1],
+        target_indices=target_indices,
+        origin=origin,
+        clip=True,
+        intensity_background=intensity_background,
+    )
+
+    stim = stack_dicts(stim1, stim2)
+    stim["shape"] = shape
+    stim["visual_size"] = visual_size
+    return stim
+
+
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "visual_size": (10, 10),
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "circular": circular(**default_params, frequency=1.0, clip=True),
+        "circular, two sided": circular_two_sided(**default_params, frequency=1.0),
+        "rectangular": rectangular(**default_params, frequency=1.0, clip=True),
+        "rectangular, two sided": rectangular_two_sided(**default_params, frequency=1.0),
+    }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/sbcs.py b/stimupy/stimuli/sbcs.py
similarity index 95%
rename from stimupy/illusions/sbcs.py
rename to stimupy/stimuli/sbcs.py
index 2cfe5ce9..da19ecc3 100644
--- a/stimupy/illusions/sbcs.py
+++ b/stimupy/stimuli/sbcs.py
@@ -654,24 +654,36 @@ def two_sided_dotted(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    p = {
-        "ppd": 20,
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
+        "ppd": 30,
     }
-
-    stims = {
-        "generalized": generalized(**p, visual_size=10, target_size=3, target_position=(0, 2)),
-        "basic": basic(**p, visual_size=10, target_size=3),
-        "two_sided": two_sided(**p, visual_size=10, target_size=2),
-        "with_dots": with_dots(**p, n_dots=5, dot_radius=2, distance=0.5, target_shape=3),
-        "dotted": dotted(**p, n_dots=5, dot_radius=2, distance=0.5, target_shape=3),
-        "2sided_with_dots": two_sided_with_dots(
-            **p, n_dots=5, dot_radius=2, distance=0.5, target_shape=3
-        ),
-        "2sided_dotted": two_sided_dotted(
-            **p, n_dots=5, dot_radius=2, distance=0.5, target_shape=3
-        ),
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "generalized": generalized(**default_params, visual_size=10, target_size=3, target_position=(0, 2)),
+        "basic": basic(**default_params, visual_size=10, target_size=3),
+        "two_sided": two_sided(**default_params, visual_size=10, target_size=2),
+        "with_dots": with_dots(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3),
+        "dotted": dotted(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3),
+        "2sided_with_dots": two_sided_with_dots(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3),
+        "2sided_dotted": two_sided_dotted(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3),
     }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
     plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/todorovics.py b/stimupy/stimuli/todorovics.py
similarity index 96%
rename from stimupy/illusions/todorovics.py
rename to stimupy/stimuli/todorovics.py
index e8b2b7b3..df4bde70 100644
--- a/stimupy/illusions/todorovics.py
+++ b/stimupy/stimuli/todorovics.py
@@ -813,33 +813,47 @@ def two_sided_equal(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    p1 = {
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
         "visual_size": 10,
-        "ppd": 10,
+        "ppd": 30,
+    }
+    default_params.update(kwargs)
+
+    rectangle_params = {
         "target_size": 3,
         "covers_size": 1.5,
     }
 
-    p2 = {
-        "visual_size": 10,
-        "ppd": 10,
+    cross_params = {
         "cross_size": 4,
         "cross_thickness": 1.5,
     }
-
-    stims = {
-        "rectangle": rectangle(**p1, covers_offset=1.5),
-        "rectangle_general": rectangle_generalized(
-            **p1, target_position=3.5, covers_x=(2, 6), covers_y=(2, 6)
-        ),
-        "cross": cross(**p2, covers_size=2),
-        "cross_general": cross_generalized(**p2, covers_size=2, covers_x=(2, 6), covers_y=(2, 6)),
-        "equal": equal(**p2),
-        "two_sided_rectangle": two_sided_rectangle(**p1, covers_offset=1),
-        "two_sided_cross": two_sided_cross(**p2, covers_size=1),
-        "two_sided_equal": two_sided_equal(**p2),
+    # fmt: off
+    stimuli = {
+        "rectangle": rectangle(**default_params, **rectangle_params, covers_offset=1.5),
+        "rectangle_general": rectangle_generalized(**default_params, **rectangle_params, target_position=3.5, covers_x=(2, 6), covers_y=(2, 6)),
+        "cross": cross(**default_params, **cross_params, covers_size=2),
+        "cross_general": cross_generalized(**default_params, **cross_params, covers_size=2, covers_x=(2, 6), covers_y=(2, 6)),
+        "equal": equal(**default_params, **cross_params,),
+        "two_sided_rectangle": two_sided_rectangle(**default_params, **rectangle_params, covers_offset=1),
+        "two_sided_cross": two_sided_cross(**default_params, **cross_params, covers_size=1),
+        "two_sided_equal": two_sided_equal(**default_params, **cross_params,),
     }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
     plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/stimuli/waves.py b/stimupy/stimuli/waves.py
new file mode 100644
index 00000000..1affd7ab
--- /dev/null
+++ b/stimupy/stimuli/waves.py
@@ -0,0 +1,1040 @@
+import itertools
+
+import numpy as np
+
+from stimupy.components import draw_regions, waves
+from stimupy.components.shapes import disc, rectangle
+
+__all__ = [
+    "sine_linear",
+    "square_linear",
+    # "staircase_linear",
+    "sine_radial",
+    "square_radial",
+    "sine_cityblock",
+    "square_cityblock",
+    "sine_angular",
+    "square_angular",
+]
+
+
+def add_targets(wave_stim, target_indices, intensity_target):
+    # Create target-mask
+    if isinstance(target_indices, (int)):
+        target_indices = [target_indices]
+
+    targets_mask = np.zeros_like(wave_stim["grating_mask"])
+    for target_idx, bar_idx in enumerate(target_indices):
+        targets_mask = np.where(
+            wave_stim["grating_mask"] == (bar_idx + 1), target_idx + 1, targets_mask
+        )
+    targets_mask = targets_mask.astype(int)
+    wave_stim["target_mask"] = targets_mask
+
+    # Place target(s)
+    if isinstance(intensity_target, (int, float)):
+        intensities = [intensity_target]
+        intensities = itertools.cycle(intensities)
+    for target_idx, intensity in zip(np.unique(targets_mask[targets_mask > 0]), intensities):
+        wave_stim["img"] = np.where(targets_mask == target_idx, intensity, wave_stim["img"])
+
+    return wave_stim
+
+
+def sine_linear(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_bars=None,
+    bar_width=None,
+    period="ignore",
+    rotation=0,
+    phase_shift=0,
+    intensities=(0.0, 1.0),
+    target_indices=(),
+    intensity_target=0.5,
+    origin="corner",
+    round_phase_width=True,
+):
+    """Linear (horizontal, vertical, oblique) sine-wave grating, with some phase(s) as target(s)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_bars : int, or None (default)
+        number of bars in the grating
+    bar_width : Number, or None (default)
+        width of a single bar, in degrees visual angle
+    period : "even", "odd", "either" or "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float
+        rotation of grating in degrees (default: 0 = horizontal)
+    phase_shift : float
+        phase shift of grating in degrees
+    intensities : Sequence[float, float] or None (default)
+        min and max intensity of sine-wave
+    target_indices : int, or Sequence[int, ...]
+        indices segments where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner (default)
+        if "mean": set origin to hypothetical image center
+        if "center": set origin to real center (closest existing value to mean)
+    round_phase_width : Bool
+        if True, round width of bars given resolution
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if len(intensities) != 2:
+        raise ValueError("intensities should be [float, float]")
+
+    lst = [visual_size, ppd, shape, frequency, n_bars, bar_width]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'grating' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'n_bars', 'bar_width'"
+        )
+
+    if rotation % 180 == 0.0:
+        base_type = "horizontal"
+    elif rotation % 180 == 90.0:
+        base_type = "vertical"
+    else:
+        base_type = "rotated"
+
+    # Spatial square-wave grating
+    stim = waves.sine(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_bars,
+        phase_width=bar_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        intensities=intensities,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type=base_type,
+    )
+
+    # Repackage output
+    stim["n_bars"] = stim.pop("n_phases")
+    stim["bar_width"] = stim.pop("phase_width")
+    stim.pop("base_type")
+
+    # Add targets(?)
+    if target_indices is not None and target_indices != ():
+        stim = add_targets(stim, target_indices=target_indices, intensity_target=intensity_target)
+
+    return stim
+
+
+def square_linear(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_bars=None,
+    bar_width=None,
+    period="ignore",
+    rotation=0,
+    phase_shift=0,
+    intensity_bars=(1.0, 0.0),
+    target_indices=(),
+    intensity_target=0.5,
+    origin="corner",
+    round_phase_width=True,
+):
+    """Linear (horizontal, vertical, oblique) square-wave grating (set of bars), with some bar(s) as target(s)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_bars : int, or None (default)
+        number of bars in the grating
+    bar_width : Number, or None (default)
+        width of a single bar, in degrees visual angle
+    period : "even", "odd", "either" or "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float
+        rotation of grating in degrees (default: 0 = horizontal)
+    phase_shift : float
+        phase shift of grating in degrees
+    intensity_bars : Sequence[float, ...]
+        intensity value for each bar, by default (1.0, 0.0).
+        Can specify as many intensities as n_bars;
+        If fewer intensities are passed than n_bars, cycles through intensities
+    target_indices : int, or Sequence[int, ...]
+        indices segments where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner (default)
+        if "mean": set origin to hypothetical image center
+        if "center": set origin to real center (closest existing value to mean)
+    round_phase_width : Bool
+        if True, round width of bars given resolution
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+    lst = [visual_size, ppd, shape, frequency, n_bars, bar_width]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'grating' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'n_bars', 'bar_width'"
+        )
+
+    # Spatial square-wave grating
+    stim = waves.square(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_bars,
+        phase_width=bar_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type="rotated",
+    )
+
+    # Adjust intensities to passed-in values
+    stim["img"] = draw_regions(mask=stim["grating_mask"], intensities=intensity_bars)
+
+    # Repackage output
+    stim["n_bars"] = stim.pop("n_phases")
+    stim["bar_width"] = stim.pop("phase_width")
+    stim["intensity_bars"] = stim.pop("intensities")
+    stim.pop("base_type")
+
+    # Add targets(?)
+    if target_indices is not None and target_indices != ():
+        stim = add_targets(stim, target_indices=target_indices, intensity_target=intensity_target)
+
+    return stim
+
+
+def sine_radial(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_rings=None,
+    ring_width=None,
+    period="ignore",
+    rotation=0,
+    phase_shift=0,
+    intensities=(0.0, 1.0),
+    target_indices=(),
+    intensity_target=0.5,
+    origin="mean",
+    round_phase_width=True,
+    clip=False,
+    intensity_background=0.5,
+):
+    """Circular sine-wave grating (set of rings) over the whole image, with some ring(s) as target(s)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_rings : int, or None (default)
+        number of rings
+    ring_width : Number, or None (default)
+        width of a single ring, in degrees
+    period : "even", "odd", "either" or "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float
+        rotation of grating in degrees (default: 0 = horizontal)
+    phase_shift : float
+        phase shift of grating in degrees
+    intensities : Sequence[float, float] or None (default)
+        min and max intensity of sine-wave
+    target_indices : int, or Sequence[int, ...]
+        indices segments where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+    round_phase_width : Bool
+        if True, round width of rings given resolution
+    clip : Bool
+        if True, clip stimulus to image size (default: False)
+    intensity_background : float (optional)
+        intensity value of background (if clipped), by default 0.5
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if len(intensities) != 2:
+        raise ValueError("intensity_rings should be [float, float]")
+
+    lst = [visual_size, ppd, shape, frequency, n_rings, ring_width]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'grating' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'n_rings', 'rings_width'"
+        )
+
+    # Spatial square-wave grating
+    stim = waves.sine(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_rings,
+        phase_width=ring_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        intensities=intensities,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type="radial",
+    )
+
+    # Repackage output
+    stim["n_rings"] = stim.pop("n_phases")
+    stim["ring_width"] = stim.pop("phase_width")
+    stim.pop("base_type")
+
+    # Clip?
+    if clip:
+        csize = min(stim["visual_size"]) / 2.0
+        circle = disc(
+            visual_size=stim["visual_size"],
+            ppd=stim["ppd"],
+            radius=csize,
+            origin=origin,
+        )
+        stim["img"] = np.where(circle["ring_mask"], stim["img"], intensity_background)
+        stim["grating_mask"] = np.where(circle["ring_mask"], stim["grating_mask"], 0)
+
+    # Resolve target parameters
+    if target_indices is not None and target_indices != ():
+        stim = add_targets(stim, target_indices=target_indices, intensity_target=intensity_target)
+
+    return stim
+
+
+def square_radial(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_rings=None,
+    ring_width=None,
+    period="ignore",
+    rotation=0,
+    phase_shift=0,
+    intensity_rings=(1.0, 0.0),
+    target_indices=(),
+    intensity_target=0.5,
+    origin="mean",
+    round_phase_width=True,
+    clip=False,
+    intensity_background=0.5,
+):
+    """Circular square-wave grating (set of rings) over the whole image, with some ring(s) as target(s)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_rings : int, or None (default)
+        number of rings in the grating
+    ring_width : Number, or None (default)
+        width of a single ring, in degrees visual angle
+    period : "even", "odd", "either" or "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float
+        rotation of grating in degrees (default: 0 = horizontal)
+    phase_shift : float
+        phase shift of grating in degrees
+    intensity_rings : Sequence[float, ...]
+        intensity value for each ring, by default (1.0, 0.0).
+        Can specify as many intensities as n_rings;
+        If fewer intensities are passed than n_rings, cycles through intensities
+    target_indices : int, or Sequence[int, ...]
+        indices segments where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+    round_phase_width : Bool
+        if True, round width of rings given resolution
+    clip : Bool
+        if True, clip stimulus to image size (default: False)
+    intensity_background : float (optional)
+        intensity value of background (if clipped), by default 0.5
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+    lst = [visual_size, ppd, shape, frequency, n_rings, ring_width]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'grating' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'n_rings', 'ring_width'"
+        )
+
+    # Spatial square-wave grating
+    stim = waves.square(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_rings,
+        phase_width=ring_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type="radial",
+    )
+
+    # Adjust intensities to passed-in values
+    stim["img"] = draw_regions(mask=stim["grating_mask"], intensities=intensity_rings)
+
+    # Repackage output
+    stim["n_rings"] = stim.pop("n_phases")
+    stim["ring_width"] = stim.pop("phase_width")
+    stim["intensity_rings"] = stim.pop("intensities")
+    stim.pop("base_type")
+
+    # Clip?
+    if clip:
+        csize = min(stim["visual_size"]) / 2.0
+        circle = disc(
+            visual_size=stim["visual_size"],
+            ppd=stim["ppd"],
+            radius=csize,
+            origin=origin,
+        )
+        stim["img"] = np.where(circle["ring_mask"], stim["img"], intensity_background)
+        stim["grating_mask"] = np.where(circle["ring_mask"], stim["grating_mask"], 0)
+
+    # Resolve target parameters
+    if target_indices is not None and target_indices != ():
+        stim = add_targets(stim, target_indices=target_indices, intensity_target=intensity_target)
+
+    return stim
+
+
+def sine_cityblock(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_frames=None,
+    frame_width=None,
+    period="ignore",
+    rotation=0,
+    phase_shift=0,
+    intensities=(0.0, 1.0),
+    target_indices=(),
+    intensity_target=0.5,
+    origin="mean",
+    round_phase_width=True,
+    clip=False,
+    intensity_background=0.5,
+):
+    """Rectilinear sine-wave grating (set of frames) over the whole image, with some frame(s) as target(s)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_frames : int, or None (default)
+        number of frames
+    frame_width : Number, or None (default)
+        width of a single frame, in degrees
+    period : "even", "odd", "either" or "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float
+        rotation of grating in degrees (default: 0 = horizontal)
+    phase_shift : float
+        phase shift of grating in degrees
+    intensities : Sequence[float, float] or None (default)
+        min and max intensity of sine-wave
+    target_indices : int, or Sequence[int, ...]
+        indices segments where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+    round_phase_width : Bool
+        if True, round width of frames given resolution
+    clip : Bool
+        if True, clip stimulus to image size (default: False)
+    intensity_background : float (optional)
+        intensity value of background (if clipped), by default 0.5
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if len(intensities) != 2:
+        raise ValueError("intensity_frames should be [float, float]")
+
+    lst = [visual_size, ppd, shape, frequency, n_frames, frame_width]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'grating' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'n_frames', 'frames_width'"
+        )
+
+    # Spatial square-wave grating
+    stim = waves.sine(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_frames,
+        phase_width=frame_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        intensities=intensities,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type="cityblock",
+    )
+
+    # Repackage output
+    stim["n_frames"] = stim.pop("n_phases")
+    stim["frame_width"] = stim.pop("phase_width")
+    stim.pop("base_type")
+
+    # Clip?
+    if clip:
+        if origin == "corner":
+            rsize = min(stim["visual_size"]) / 2
+            rect = rectangle(
+                visual_size=stim["visual_size"],
+                ppd=stim["ppd"],
+                rectangle_size=rsize,
+                rectangle_position=(0, 0),
+            )
+        else:
+            rsize = min(stim["visual_size"])
+            rect = rectangle(
+                visual_size=stim["visual_size"],
+                ppd=stim["ppd"],
+                rectangle_size=rsize,
+            )
+        stim["img"] = np.where(rect["shape_mask"], stim["img"], intensity_background)
+        stim["grating_mask"] = np.where(rect["shape_mask"], stim["grating_mask"], 0)
+
+    # Add targets(?)
+    if target_indices is not None and target_indices != ():
+        stim = add_targets(stim, target_indices=target_indices, intensity_target=intensity_target)
+
+    return stim
+
+
+def square_cityblock(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_frames=None,
+    frame_width=None,
+    period="ignore",
+    rotation=0,
+    phase_shift=0,
+    intensity_frames=(1.0, 0.0),
+    target_indices=(),
+    intensity_target=0.5,
+    origin="mean",
+    round_phase_width=True,
+    clip=False,
+    intensity_background=0.5,
+):
+    """Rectilinear square-wave grating (set of frames) over the whole image, with some frame(s) as target(s)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_frames : int, or None (default)
+        number of frames in the grating
+    frame_width : Number, or None (default)
+        width of a single frame, in degrees visual angle
+    period : "even", "odd", "either" or "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float
+        rotation of grating in degrees (default: 0 = horizontal)
+    phase_shift : float
+        phase shift of grating in degrees
+    intensity_frames : Sequence[float, ...]
+        intensity value for each frame, by default (1.0, 0.0).
+        Can specify as many intensities as n_frames;
+        If fewer intensities are passed than n_frames, cycles through intensities
+    target_indices : int, or Sequence[int, ...]
+        indices segments where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner
+        if "mean": set origin to hypothetical image center (default)
+        if "center": set origin to real center (closest existing value to mean)
+    round_phase_width : Bool
+        if True, round width of frames given resolution
+    clip : Bool
+        if True, clip stimulus to image size (default: False)
+    intensity_background : float (optional)
+        intensity value of background (if clipped), by default 0.5
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+    lst = [visual_size, ppd, shape, frequency, n_frames, frame_width]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'grating' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'n_frames', 'frame_width'"
+        )
+
+    # Spatial square-wave grating
+    stim = waves.square(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_frames,
+        phase_width=frame_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type="cityblock",
+    )
+
+    # Adjust intensities to passed-in values
+    stim["img"] = draw_regions(mask=stim["grating_mask"], intensities=intensity_frames)
+
+    # Repackage output
+    stim["n_frames"] = stim.pop("n_phases")
+    stim["frame_width"] = stim.pop("phase_width")
+    stim["intensity_frames"] = stim.pop("intensities")
+    stim.pop("base_type")
+
+    # Clip?
+    if clip:
+        if origin == "corner":
+            rsize = min(stim["visual_size"]) / 2
+            rect = rectangle(
+                visual_size=stim["visual_size"],
+                ppd=stim["ppd"],
+                rectangle_size=rsize,
+                rectangle_position=(0, 0),
+            )
+        else:
+            rsize = min(stim["visual_size"])
+            rect = rectangle(
+                visual_size=stim["visual_size"],
+                ppd=stim["ppd"],
+                rectangle_size=rsize,
+            )
+        stim["img"] = np.where(rect["shape_mask"], stim["img"], intensity_background)
+        stim["grating_mask"] = np.where(rect["shape_mask"], stim["grating_mask"], 0)
+
+    # Add targets(?)
+    if target_indices is not None and target_indices != ():
+        stim = add_targets(stim, target_indices=target_indices, intensity_target=intensity_target)
+
+    return stim
+
+
+def sine_angular(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_segments=None,
+    segment_width=None,
+    period="ignore",
+    rotation=0,
+    phase_shift=0,
+    intensities=(0.0, 1.0),
+    target_indices=(),
+    intensity_target=0.5,
+    origin="mean",
+    round_phase_width=True,
+):
+    """Angular sine-wave grating (set of segments) over the whole image, with some segment(s) as target(s)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_segments : int, or None (default)
+        number of segments
+    segment_width : Number, or None (default)
+        width of a single segment, in degrees
+    period : "even", "odd", "either" or "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float
+        rotation of grating in degrees (default: 0 = horizontal)
+    phase_shift : float
+        phase shift of grating in degrees
+    intensities : Sequence[float, float] or None (default)
+        min and max intensity of sine-wave
+    target_indices : int, or Sequence[int, ...]
+        indices segments where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner (default)
+        if "mean": set origin to hypothetical image center
+        if "center": set origin to real center (closest existing value to mean)
+    round_phase_width : Bool
+        if True, round width of segments given resolution
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+    if len(intensities) != 2:
+        raise ValueError("intensity_segments should be [float, float]")
+
+    lst = [visual_size, ppd, shape, frequency, n_segments, segment_width]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'grating' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'n_segments', 'segments_width'"
+        )
+
+    # Spatial square-wave grating
+    stim = waves.sine(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_segments,
+        phase_width=segment_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        intensities=intensities,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type="angular",
+    )
+
+    # Repackage output
+    stim["n_segments"] = stim.pop("n_phases")
+    stim["segment_width"] = stim.pop("phase_width")
+    stim.pop("base_type")
+
+    # Resolve target parameters
+    if target_indices is not None and target_indices != ():
+        if isinstance(target_indices, (int)):
+            target_indices = [
+                target_indices,
+            ]
+        if isinstance(intensity_target, (int, float)):
+            intensity_target = [
+                intensity_target,
+            ]
+        intensity_target = itertools.cycle(intensity_target)
+
+        # Place target(s)
+        targets_mask = np.zeros_like(stim["grating_mask"])
+        for target_idx, (segment_idx, intensity) in enumerate(
+            zip(target_indices, intensity_target)
+        ):
+            targets_mask = np.where(
+                stim["grating_mask"] == (segment_idx + 1), target_idx + 1, targets_mask
+            )
+            stim["img"] = np.where(targets_mask == (target_idx + 1), intensity, stim["img"])
+        stim["target_mask"] = targets_mask.astype(int)
+
+    return stim
+
+
+def square_angular(
+    visual_size=None,
+    ppd=None,
+    shape=None,
+    frequency=None,
+    n_segments=None,
+    segment_width=None,
+    period="ignore",
+    rotation=0,
+    phase_shift=0,
+    intensity_segments=(1.0, 0.0),
+    target_indices=(),
+    intensity_target=0.5,
+    origin="mean",
+    round_phase_width=True,
+):
+    """Angular square-wave grating (set of segments) over the whole image, with some segment(s) as target(s)
+
+    Parameters
+    ----------
+    visual_size : Sequence[Number, Number], Number, or None (default)
+        visual size [height, width] of image, in degrees
+    ppd : Sequence[Number, Number], Number, or None (default)
+        pixels per degree [vertical, horizontal]
+    shape : Sequence[Number, Number], Number, or None (default)
+        shape [height, width] of image, in pixels
+    frequency : Number, or None (default)
+        spatial frequency of grating, in cycles per degree visual angle
+    n_segments : int, or None (default)
+        number of segments in the grating
+    segment_width : Number, or None (default)
+        width of a single segment, in degrees visual angle
+    period : "even", "odd", "either" or "ignore" (default)
+        ensure whether the grating has "even" number of phases, "odd"
+        number of phases, either or whether not to round the number of
+        phases ("ignore")
+    rotation : float
+        rotation of grating in degrees (default: 0 = horizontal)
+    phase_shift : float
+        phase shift of grating in degrees
+    intensity_segments : Sequence[float, ...]
+        intensity value for each segment, by default (1.0, 0.0).
+        Can specify as many intensities as n_segments;
+        If fewer intensities are passed than n_segments, cycles through intensities
+    target_indices : int, or Sequence[int, ...]
+        indices segments where targets will be placed
+    intensity_target : float, or Sequence[float, ...], optional
+        intensity value for each target, by default 0.5.
+        Can specify as many intensities as number of target_indices;
+        If fewer intensities are passed than target_indices, cycles through intensities
+    origin : "corner", "mean" or "center"
+        if "corner": set origin to upper left corner (default)
+        if "mean": set origin to hypothetical image center
+        if "center": set origin to real center (closest existing value to mean)
+    round_phase_width : Bool
+        if True, round width of segments given resolution
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        mask with integer index for each target (key: "target_mask"),
+        and additional keys containing stimulus parameters
+    """
+    lst = [visual_size, ppd, shape, frequency, n_segments, segment_width]
+    if len([x for x in lst if x is not None]) < 3:
+        raise ValueError(
+            "'grating' needs 3 non-None arguments for resolving from 'visual_size', "
+            "'ppd', 'shape', 'frequency', 'n_segments', 'segment_width'"
+        )
+
+    # Spatial square-wave grating
+    stim = waves.square(
+        visual_size=visual_size,
+        ppd=ppd,
+        shape=shape,
+        frequency=frequency,
+        n_phases=n_segments,
+        phase_width=segment_width,
+        period=period,
+        rotation=rotation,
+        phase_shift=phase_shift,
+        origin=origin,
+        round_phase_width=round_phase_width,
+        base_type="angular",
+    )
+
+    # Adjust intensities to passed-in values
+    stim["img"] = draw_regions(mask=stim["grating_mask"], intensities=intensity_segments)
+
+    # Repackage output
+    stim["n_segments"] = stim.pop("n_phases")
+    stim["segment_width"] = stim.pop("phase_width")
+    stim["intensity_segments"] = stim.pop("intensities")
+    stim.pop("base_type")
+
+    # Resolve target parameters
+    if target_indices is not None and target_indices != ():
+        if isinstance(target_indices, (int)):
+            target_indices = [
+                target_indices,
+            ]
+        if isinstance(intensity_target, (int, float)):
+            intensity_target = [
+                intensity_target,
+            ]
+        intensity_target = itertools.cycle(intensity_target)
+
+        # Place target(s)
+        targets_mask = np.zeros_like(stim["grating_mask"])
+        for target_idx, (segment_idx, intensity) in enumerate(
+            zip(target_indices, intensity_target)
+        ):
+            targets_mask = np.where(
+                stim["grating_mask"] == (segment_idx + 1), target_idx + 1, targets_mask
+            )
+            stim["img"] = np.where(targets_mask == (target_idx + 1), intensity, stim["img"])
+        stim["target_mask"] = targets_mask.astype(int)
+
+    return stim
+
+
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {"visual_size": 15, "ppd": 30}
+    default_params.update(kwargs)
+
+    grating_params = {
+        "period": "ignore",
+        "phase_shift": 0,
+        "round_phase_width": False,
+        "target_indices": (0, 5),
+    }
+
+    # fmt: off
+    stimuli = {
+        "sine wave - horizontal": sine_linear(**default_params, **grating_params, bar_width=1, rotation=0),
+        "sine wave - vertical": sine_linear(**default_params, **grating_params, bar_width=1, rotation=90),
+        "sine wave - oblique": sine_linear(**default_params, **grating_params, bar_width=1, rotation=45),
+        "sine wave - radial": sine_radial(**default_params, **grating_params, ring_width=1, clip=True),
+        "sine wave - cityblock": sine_cityblock(**default_params, **grating_params, frame_width=1, clip=True),
+        "sine wave - angular": sine_angular(**default_params, **grating_params, segment_width=10),
+
+
+        "square wave - horizontal": square_linear(**default_params, **grating_params, bar_width=1, rotation=0),
+        "square wave - vertical": square_linear(**default_params, **grating_params, bar_width=1, rotation=90),
+        "square wave - oblique": square_linear(**default_params, **grating_params, bar_width=1, rotation=45),
+        "square wave - radial": square_radial(**default_params, **grating_params, ring_width=1, clip=True),
+        "square wave - cityblock": square_cityblock(**default_params, **grating_params, frame_width=1, clip=True),
+        "square wave - angular": square_angular(**default_params, **grating_params, segment_width=10),
+
+
+        "square wave - multiple intensitites": square_linear(**default_params, **grating_params, bar_width=3.5, rotation=0, intensity_bars=(0, 0.25, 0.75)),
+    }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=False, save=None)
diff --git a/stimupy/illusions/wedding_cakes.py b/stimupy/stimuli/wedding_cakes.py
similarity index 90%
rename from stimupy/illusions/wedding_cakes.py
rename to stimupy/stimuli/wedding_cakes.py
index 6b243f70..f674e447 100644
--- a/stimupy/illusions/wedding_cakes.py
+++ b/stimupy/stimuli/wedding_cakes.py
@@ -178,20 +178,35 @@ def wedding_cake(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    params = {
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
         "visual_size": 10,
-        "ppd": 10,
-        "L_size": (3, 3, 1),
-        "target_height": 1,
-        "target_indices1": None,
-        "target_indices2": ((0, 1), (1, 1)),
+        "ppd": 30,
     }
-
-    stims = {
-        "wedding_cake": wedding_cake(**params),
+    default_params.update(kwargs)
+
+    # fmt: off
+    stimuli = {
+        "Wedding cake": wedding_cake(**default_params,
+                                     L_size= (3, 3, 1),
+                                     target_height=1,
+                                     target_indices1=None,
+                                     target_indices2=((0, 1), (1, 1)),)
     }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
 
-    plot_stimuli(stims, mask=False, save=None)
+    stims = overview()
+    plot_stimuli(stims, mask=True, save=None)
diff --git a/stimupy/illusions/whites.py b/stimupy/stimuli/whites.py
similarity index 96%
rename from stimupy/illusions/whites.py
rename to stimupy/stimuli/whites.py
index a4b33a77..3ca7f994 100644
--- a/stimupy/illusions/whites.py
+++ b/stimupy/stimuli/whites.py
@@ -4,10 +4,10 @@
 import numpy as np
 
 from stimupy.components import image_base
-from stimupy.components.gratings import square_wave
-from stimupy.illusions.angulars import pinwheel as radial
-from stimupy.illusions.circulars import rings as circular
-from stimupy.illusions.wedding_cakes import wedding_cake
+from stimupy.stimuli.gratings import squarewave
+from stimupy.stimuli.pinwheels import pinwheel as radial
+from stimupy.stimuli.waves import square_radial as circular
+from stimupy.stimuli.wedding_cakes import wedding_cake
 from stimupy.utils import resolution
 
 __all__ = [
@@ -33,7 +33,7 @@ def generalized(
     period="ignore",
     rotation=0,
     phase_shift=0,
-    intensity_bars=(1.0, 0.0),
+    intensity_bars=(0.0, 1.0),
     target_indices=(),
     intensity_target=0.5,
     target_center_offsets=0,
@@ -101,7 +101,7 @@ def generalized(
         raise ValueError("generalized() missing argument 'target_heights' which is not 'None'")
 
     # Spatial square-wave grating
-    stim = square_wave(
+    stim = squarewave(
         visual_size=visual_size,
         ppd=ppd,
         shape=shape,
@@ -756,29 +756,36 @@ def yazdanbakhsh(
     return stim
 
 
-if __name__ == "__main__":
-    from stimupy.utils import plot_stimuli
+def overview(**kwargs):
+    """Generate example stimuli from this module
 
-    params = {
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+    default_params = {
         "visual_size": 10,
-        "ppd": 10,
+        "ppd": 30,
         "frequency": 0.5,
     }
+    default_params.update(kwargs)
 
-    stims = {
-        "generalized": generalized(
-            **params, target_indices=(1, 3), target_center_offsets=(-1, -3), target_heights=(2, 3)
+    # fmt: off
+    stimuli = {
+        "White's stimulus": white(**default_params, target_indices=(2, -3), target_height=2),
+        "White's, generalized": generalized(
+            **default_params, target_indices=(1, 3, 5), target_center_offsets=(-1, -3, -1), target_heights=(2, 3, 2)
         ),
-        "white": white(**params, target_indices=(2, -3), target_height=2),
-        "white_two_rows": white_two_rows(
-            **params,
+        "White's, two rows": white_two_rows(
+            **default_params,
             target_indices_top=(2, 4),
             target_indices_bottom=(-2, -4),
             target_height=1,
             target_center_offset=2,
         ),
-        "anderson": anderson(
-            **params,
+        "White's - Anderson": anderson(
+            **default_params,
             target_indices_top=3,
             target_indices_bottom=-2,
             target_center_offset=2,
@@ -786,15 +793,15 @@ def yazdanbakhsh(
             stripe_center_offset=1.5,
             stripe_height=2,
         ),
-        "howe": howe(
-            **params,
+        "White's - Howe": howe(
+            **default_params,
             target_indices_top=3,
             target_indices_bottom=-2,
             target_center_offset=2,
             target_height=2,
         ),
-        "yazdanbakhsh": yazdanbakhsh(
-            **params,
+        "White's - Yazdanbakhsh": yazdanbakhsh(
+            **default_params,
             target_indices_top=3,
             target_indices_bottom=-2,
             target_center_offset=2,
@@ -802,5 +809,13 @@ def yazdanbakhsh(
             gap_size=0.5,
         ),
     }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
 
+    stims = overview()
     plot_stimuli(stims, mask=True, save=None)


Test patch:
diff --git a/tests/test_circular.py b/tests/test_circular.py
deleted file mode 100644
index b7851bfd..00000000
--- a/tests/test_circular.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import pytest
-
-from stimupy.components.circulars import resolve_circular_params
-
-
-@pytest.mark.parametrize(
-    "ppd, shape, visual_size, n_rings, ring_width, frequency",
-    (
-        ((32, 32), (1024, 1024), (32, 32), 8, 2, 1 / 4),
-        ((32, 32), (1024, 1024), (32, 32), 4, 1, 1 / 2),
-        ((32, 32), (1024, 1024), (32, 32), 8, None, 1 / 2),
-        ((32, 32), (1024, 1024), (32, 32), None, 2, None),
-        ((32, 32), (1024, 1024), (32, 32), None, None, 1 / 2),
-        ((None, None), (1024, 1024), (32, 32), 8, 2, None),
-        (None, (1024, 1024), (32, 32), 8, 2, None),
-        (None, (1024, 1024), (32, 32), None, None, 1),
-        (None, (1024, 1024), None, 8, None, 1),
-    ),
-)
-def test_valid_params(ppd, shape, visual_size, n_rings, ring_width, frequency):
-    resolve_circular_params(
-        ppd=ppd,
-        shape=shape,
-        visual_size=visual_size,
-        n_rings=n_rings,
-        ring_width=ring_width,
-        frequency=frequency,
-    )
diff --git a/tests/test_overviews.py b/tests/test_overviews.py
index 4c164145..94201837 100644
--- a/tests/test_overviews.py
+++ b/tests/test_overviews.py
@@ -1,4 +1,4 @@
-from stimupy import components, illusions, noises
+from stimupy import components, noises
 
 
 def test_components():
@@ -7,7 +7,3 @@ def test_components():
 
 def test_noises():
     noises.create_overview()
-
-
-def test_illusions():
-    illusions.create_overview()
diff --git a/tests/test_gratings.py b/tests/test_waves.py
similarity index 69%
rename from tests/test_gratings.py
rename to tests/test_waves.py
index 25857da8..4a834549 100644
--- a/tests/test_gratings.py
+++ b/tests/test_waves.py
@@ -1,7 +1,6 @@
 import pytest
 
-from stimupy.components import resolve_grating_params
-from stimupy.components.gratings import square_wave
+from stimupy.components import waves
 
 
 @pytest.mark.parametrize(
@@ -19,7 +18,7 @@
     ),
 )
 def test_valid_params(ppd, length, visual_angle, n_phases, phase_width, frequency):
-    resolve_grating_params(
+    waves.resolve_grating_params(
         ppd=ppd,
         length=length,
         visual_angle=visual_angle,
@@ -33,4 +32,17 @@ def test_rounding():
     ppd = 36
     visual_size = (1.0, 1.0)
     frequency = 2.80
-    stim = square_wave(ppd=ppd, visual_size=visual_size, frequency=frequency, period="either")
+    stim = waves.square(
+        ppd=ppd,
+        visual_size=visual_size,
+        frequency=frequency,
+        period="either",
+        base_type="horizontal",
+        phase_shift=0,
+        origin="corner",
+        round_phase_width=True,
+    )
+
+
+def test_overview():
+    waves.overview()