Skip to main content

Sh🕶️des

About

Shades is a python module for generative 2d image creation.

Latest version is available via pip install

Full reference documentation is available here! 🥳

It might be helpful to read the below guide first as an intro.

Handy Guide

The shades module is based around a few key objects:

  • Canvas (basically just a PIL image, with a couple of additions)
  • Shade (determine color based on rules, have methods to draw on canvas)
  • NoiseField (calculates Perlin noise based on xy coords)

Canvas

The Canvas object is just a wrapper for a PIL image object (with some minor functionalities added in like Canvas.random_point() and Canvas.center). The idea is that you don't have to import two libraries all the time, but if you'd prefer, you can switch out the Canvas call for a PIL image one. This is useful to know as well if you'd like to work between the two modules, or import images/photos to draw on or use with Shades.

Here's a simple example using just the Canvas object:

import shades

canvas = shades.Canvas(width=100, height=100, color=(200, 0, 0))

canvas.save('exciting_red_square.png')
canvas.show()

This script will save, and bring up the following image:

Red square

Not too exciting, right? We created a monotone red image that is 100 by 100 pixels. Color is worth taking note, as throughout Shades colors are treated and expected to be tuples of the Red, Green and Blue color values (on a scale of 0 to 255). So (200, 0, 0) gives us a red tone.

There's not much else you can do with Canvas, the idea is just to give us an image on which we can draw stuff.

NoiseField

NoiseField is a pretty handy concept here. A lot of the time with images, we might want an element of randomness, but with a gentler transition. NoiseField is the answer to this, as it gives us random gradients between numbers. There's more resources online about Perlin noise itself if you're interested- t's a really cool topic

Here's an example of a simple NoiseField use:

import shades

noise_field = shades.NoiseField(scale=0.002, seed=8)
random_number = noise_field.noise((24, 100))

In this, random_number is the return of the 'noise' call, which takes in coordinates (in the form of an (x, y) tuple) and spits out a float between 0 and 1. The important thing about NoiseField vs purely random number generation, is that if we get the return of a nearby point, say (25, 101), then the float between 0 and 1 will be close to our first call. The further away from the point we get, the further away the noise value is likely to be.

When creating a NoiseField, the two arguments we can put in are scale (which affected how quickly generated numbers will change between points, and 'seed' which is used for generating the semi-random numbers)

Shades uses numpy to calculate perlin noise, and tries to strike a balance between frontloading noise-fields (so they can be batch calculated with vectorisation) and calculating as it goes (so that you're not waiting ages for something that'll never be used).

One quality of life feature that's worth mentioning is the noise_fields function that works as follows:

import shades
noise_fields = shades.noise_fields(seed=[4, 4, 9], scale=[0.04, 0.2, 0.002], channels=3)
more_fields = shades.noise_fields(scale=0.002, channels=2)

This doesn't introduce new functionality, but in a lot of cases (like with working with color, or x/y coordinates) you'll want multiple noise fields (and might want to specify seeds or scales, or leave them blank for defaults), so this is a handy way of doing that without lots of repetitive calls.

Shade

The Shade object as you'd guess in a module called Shades does pretty much all the work. You can think of them as like code-pencils - they have properties that affect what color stuff will be, and a bunch of methods to let us draw stuff.

Here's a simple example using a BlockColor shade, that'll always produce the same color:

import shades

canvas = shades.Canvas(200, 200)
cyan = shades.BlockColor((0, 255, 255))
cyan.rectangle(canvas, (50, 50), width=100, height=150)

canvas.show()

This script does a few things:

  • Creates a Canvas to draw on
  • Creates a BlockColor shade to draw with
  • Draws a rectangle with the BlockColor object on coordinates (50, 50) of the Canvas
  • Displays the canvas

This is what the picture will look like:

A cyan rectangle

A lot of Shade objects use NoiseField, for example, the NoiseGradient object which chooses color based on noise responses to (x,y) coordinates. Which we can see here:

import shades

canvas = shades.Canvas(200, 200)
gradient = shades.NoiseGradient(
  color=(200, 200, 200),
  color_fields=shades.noise_fields(scale=0.02, channels=3),
)
gradient.circle(
  canvas,
  canvas.center,
  radius=50,
)

canvas.show()

(There are three NoiseField objects taken by the color_fields parameter, one for red, green and blue, using the same NoiseField seed for each would create a gradient that changes how light/dark the color is without affecting the overall tone)

A gradient circle

Also, all Shade objects can be called with 'warp_noise' that will use noise to affect the location of points:

import shades

canvas = shades.Canvas(200, 200)

warped_shade = shades.BlockColor(
  color=(100, 200, 100),
  warp_size=50,
  warp_noise=shades.noise_fields(scale=0.01, channels=3)
)

warped_shade.line(canvas,(0, 0), (canvas.width, canvas.height))

canvas.show()

(the line's location is affected by the two NoiseField relating to x and y warping, giving us a wavy green line)

A wavy green line

Hacking Shades

One of the goals of the Shade object, is to make something that's easily extensible. You can create your own Shade and already have access to the drawing methods and location warping that are present in the abstract base class of the Shade.

The only requirement of creating a Shade is to include a method determine_shade, taking only (x,y) coordinates as an argument, and returning a color.

Here's an example of creating a shade that returns a completely random shade of grey each time:

import shades
from random import randint

class RandomGrey(shades.Shade):
  def determine_shade(self, xy):
    mono = randint(0, 255)
    return (mono, mono, mono)

canvas = shades.Canvas(300,300)
my_shade = RandomGrey()

my_shade.triangle(canvas, (0, 0), (0, 300), (300, 150))

canvas.show()

A triangle with varying shades of grey

Examples

Here's a few examples of some short scripts and the images they create.

Using SwirlOfShades which will choose another shade based on NoiseField returns

import shades

canvas = shades.Canvas()
shade = shades.SwirlOfShades(
  swirl_field=shades.NoiseField(scale=0.005),
  shades=([
    (0.4, 0.6, shades.BlockColor((63, 151, 197)))
  ]),
)

shade.fill(canvas)
canvas.show()

Ripples

Drawing a circle using a DomainWarpGradient object

import shades

canvas = shades.Canvas()

shade = shades.DomainWarpGradient(
    color=(200,200,200),
    color_variance=70,
    color_fields=shades.noise_fields(scale=0.01, channels=3),
    depth=2,
)

shade.circle(canvas, canvas.center, canvas.width/3)

canvas.show()

Domain warped circle

Integrating with Scipy's Delaunay function, to make a triangular grid

import shades
from random import randint
from scipy.spatial import Delaunay

canvas = shades.Canvas(1000, 800)
ink = shades.NoiseGradient(
    color_fields=[shades.NoiseField(scale=0.002) for i in range(3)]
)

points = [(randint(0, canvas.width), randint(0, canvas.height)) for i in range(90)]
# plus some edge points to make sure the whole canvas is coloured
points.append((0, 0))
points.append((0, canvas.height))
points.append((canvas.width, 0))
points.append((canvas.width, canvas.height))
points.append((0, canvas.height/2))
points.append((canvas.width, canvas.height/2))
points.append((canvas.width/2, 0))
points.append((canvas.width/2, canvas.height))

# drawing triangles between points
for tri in Delaunay(points).simplices:
    ink.color = [randint(180, 255) for i in range(3)]
    ink.triangle(
        canvas,
        points[tri[0]],
        points[tri[1]],
        points[tri[2]],
    )

canvas.show()

Delaunay grid

Happy hacking! 🕶️

Release files for shades 1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for shades 1.2
File Size Uploaded
shades-1.2.tar.gz 16.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for shades 1.2
File Interpreter ABI Platform
shades-1.2-py3-none-any.whl Python 3 none any Details

Total release size: 31.4 kB

Release files / shades-1.2.tar.gz

Download URL shades-1.2.tar.gz
Size 16.6 kB
Tags Source
SHA-256 checksum
How to use checksums
43589716a5c34f2e1aa0d262cbc1ada2736136774e1cd81123b44cb8f9e4188b
BLAKE2b-256 checksum
How to use checksums
ca8c9e7b827dc2fe7619cd882f552297bc8a0358b19ed50b1c5e69ed12b0480f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.7.1 importlib_metadata/4.8.2 pkginfo/1.8.2 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.7

Release files / shades-1.2-py3-none-any.whl

Download URL shades-1.2-py3-none-any.whl
Size 14.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
57d015fe112535be6d0435299663a7725141419b32f8b09cc77f5cb12fdf12d8
BLAKE2b-256 checksum
How to use checksums
75ce12fb56d5d912a6e9d0dc193edb844bcef14b0a0ea768d3de9b1c60a5c991
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.7.1 importlib_metadata/4.8.2 pkginfo/1.8.2 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.7

Release history Release notifications | RSS feed

This release

1.2 This release

2 release files

1.1

2 release files

1.0

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page