Package: shades
Sh🕶️desAboutShades is a python module for generative 2d image creation.Latest version is available via pip installFull reference documentation is available here!🥳It might be helpful to read the below guide first as an intro.Handy GuideThe 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)CanvasTheCanvasobject 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 theCanvascall 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 withShades.Here's a simple example using just theCanvasobject:importshadescanvas=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:Not too exciting, right? We created a monotone red image that is 100 by 100 pixels. Color is worth taking note, as throughoutShadescolors 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 withCanvas, the idea is just to give us an imageon which we can draw stuff.NoiseFieldNoiseFieldis a pretty handy concept here. A lot of the time with images, we might want an element of randomness, but with a gentler transition.NoiseFieldis 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 topicHere's an example of a simpleNoiseFielduse:importshadesnoise_field=shades.NoiseField(scale=0.002,seed=8)random_number=noise_field.noise((24,100))In this,random_numberis 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 aboutNoiseFieldvs 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 aNoiseField, the two arguments we can put in arescale(which affected how quickly generated numbers will change between points, and 'seed' which is used for generating the semi-random numbers)Shadesuses 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 thenoise_fieldsfunction that works as follows:importshadesnoise_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.ShadeTheShadeobject as you'd guess in a module calledShadesdoes 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 aBlockColorshade, that'll always produce the same color:importshadescanvas=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 aCanvasto draw onCreates aBlockColorshade to draw withDraws a rectangle with theBlockColorobject on coordinates (50, 50) of theCanvasDisplays the canvasThis is what the picture will look like:A lot ofShadeobjects useNoiseField, for example, theNoiseGradientobject which chooses color based on noise responses to (x,y) coordinates. Which we can see here:importshadescanvas=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 threeNoiseFieldobjects taken by thecolor_fieldsparameter, one for red, green and blue, using the sameNoiseFieldseed for each would create a gradient that changes how light/dark the color is without affecting the overall tone)Also, allShadeobjects can be called with 'warp_noise' that will use noise to affect the location of points:importshadescanvas=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 twoNoiseFieldrelating to x and y warping, giving us a wavy green line)Hacking ShadesOne of the goals of theShadeobject, is to make something that's easily extensible. You can create your ownShadeand already have access to the drawing methods and location warping that are present in the abstract base class of theShade.The only requirement of creating aShadeis to include a methoddetermine_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:importshadesfromrandomimportrandintclassRandomGrey(shades.Shade):defdetermine_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()ExamplesHere's a few examples of some short scripts and the images they create.Using SwirlOfShades which will choose another shade based on NoiseField returnsimportshadescanvas=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()Drawing a circle using a DomainWarpGradient objectimportshadescanvas=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()Integrating with Scipy's Delaunay function, to make a triangular gridimportshadesfromrandomimportrandintfromscipy.spatialimportDelaunaycanvas=shades.Canvas(1000,800)ink=shades.NoiseGradient(color_fields=[shades.NoiseField(scale=0.002)foriinrange(3)])points=[(randint(0,canvas.width),randint(0,canvas.height))foriinrange(90)]# plus some edge points to make sure the whole canvas is colouredpoints.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 pointsfortriinDelaunay(points).simplices:ink.color=[randint(180,255)foriinrange(3)]ink.triangle(canvas,points[tri[0]],points[tri[1]],points[tri[2]],)canvas.show()Happy hacking! 🕶️