# Flexible code for colorful raster images with randomized colors # Language: Simple Inkscape Scripting (SIS, apparently) # Details: https://inkscape.org/~pakin/%E2%98%85simple-inkscape-scripting # Github: https://github.com/spakin/SimpInkScr # Creates a multicolor background grid consisting of squares or circles (cells) # Multicolor circles (eyes) are placed on top of it # Colors used for cell and eyes are to a degree random: # - default behavior is to use entirely random colors # - the set of available colors can be limited by supplying a palette file # The whole thing can be topped by a 'raster': a unicolor layer with round holes in it # The result is supposed to look cool! Or at least it can be useful for further processing. # Only circles and squares are created (no ovals or rectangles) # Next step: import user-defines shapes # Since this is already getting somewhat serious code, let's plant a license on it: # # Copyright 2022 Joseph Smith # # Permission to use, copy, modify, and/or distribute this software for any purpose with or without # fee is hereby granted, provided that the above copyright notice and this permission notice # appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS # SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # OF THIS SOFTWARE. def make_smiley(fill_color='yellow'): 'Return a smiley face of a given color.' head = circle((0, 0), 25, stroke='black', fill=fill_color) r_eye = circle((-9.2, -9.8), 5.4) l_eye = circle((9.2, -9.8), 5.4) mouth = path([Move(16.2, 4.2), Curve(17.6, 10.3, 9.0, 19.4, 0.2, 19.4), Curve(-9.0, 19.4, -17.8, 10.2, -16.2, 4.2), Curve(-8.7, 9.4, 6.3, 9.4, 16.2, 4.2), ZoneClose()]) smiley = group([head, r_eye, l_eye, mouth]) return smiley # Draw a grid of slightly different smiley faces. style(stroke='none', fill='black') for y in range(30, int(height) - 30, 60): for x in range(30, int(width) - 30, 60): smiley = make_smiley('#%02x%02x00' % (randint(225, 255), randint(225, 255))) xform = inkex.Transform() xform.add_translate(x, y) xform.add_scale(uniform(0.8, 1.1)) xform.add_rotate(uniform(-10, 10)) smiley.transform = xform # ************************************************************************************************************ # Doc section # TODO: UPDATE THIS SECTION # If a matrix file is used with n elements per column, make sure that n == pagewidth / cellsize # (as a floating point that is, not an integer). Same for pageheight and rows. # Without a matrix, if page dimensions aren't an exact multiple of the cell size, rows and columns # will extend one partial cell beyond the page. This ensures that the page is filled. # Palette file:one RGB value per line, nothing else; no error checks are performed # (It turns out that a palettefile will work with the value followed by an arbitrary description: # # --------------------------------------------------------- # ff073a Neon Red # e60000 Electric Red # ... # --------------------------------------------------------- # # This is unexpected behavior and has to do with what the fill parameter accepts as valid input: # XML: style="stroke:black;fill:#ff073a Neon Red;stroke-width:0" # Not sure if this is acceptable behavior at any level..) # Matrix: has precedence over palette: disable matrix if you wish to use a palette file # If you decide to use a matrix file. make sure the page width:cellsize ration matches the number of rows and columns # Example matrix file # # --------------------------------------------------------- # # Basicolor matrix # # # Color values # b = 0000ff # p = 6600ff # comment # r = ff0000 # o = ff6600 # y = ffff00 # g = 00ff00 # # # c1 c2 c3 c4 c5 # # o:g b:r r:y g:p o:y # r1 # # g:p y:b o:g b:g r:p # r2 # # r:g p:y g:r y:r g:b # r3 # # o:g b:r r:y g:p o:y # r4 # # g:p y:b o:g b:g r:p # r5 # # --------------------------------------------------------- # # Code names on lines containing '=' are user-defined, e.g. Picasso = ff0000 is also ok # - names should not contain spaces or the '=' character # - short codes will save you typing # The 'x:y' pair applies to cellcolor:eyecolor; all elements are necessary # - examples: b:r, blue:blue, LightBlue:BrightOchre # (Possibly indented) lines starting with '#', lines with whitespace only and empty lines are ignored. # Leading whitespace is tolerated. # If matrix is set, process contents of matrix file into a pair of lists # colormatrix() will work the matrix file shown above into the following lists: # colorlist: ['b', '0000ff', 'p', '6600ff', 'r', 'ff0000', 'o', 'ff6600', 'y', 'ffff00', 'g', '00ff00'] # - i.e. [code, rgb, code, rgb, ...] # matrixlist: ['o', 'g', 'b', 'r', 'r', 'y', 'g', 'p', 'y', 'b', 'o', 'g', 'r', 'g', 'p', 'y', 'g', 'r'] # - i.e. [cellcolor, eyecolor, cellcolor, eyecolor, ...] # ************************************************************************************************************ # User section # Entries below with a DEFAULT value can be commented out # If either of thse is not set, page dimensions of the document are used. Values may differ. pagewidth = 1000 pageheight = 1000 # Background #cellshape = 'circle' # DEFAULTS TO 'square'. Almost always suitable. cellsize = 200 # MUST BE SET. This setting is used rather globally. #cellstrokecolor = 'ffffff' # DEFAULTS TO '000000'. #cellstrokewidth = 4 # DEFAULTS TO '0'. For 'line art' effect. # Eyes eyeshape = 'square' # DEFAULTS TO 'circle'. eyesize = 80 # MUST BE SET. eye size: diameter, not radius (!) #eyestrokecolor = 'ffffff' # DEFAULTS TO '000000'. #eyestrokewidth = 4 # DEFAULTS TO '0'. For 'line art' effect. #wobbly = 'yes' # DEFAULTS TO 'no'. Randomize 'eye' positions? # Raster raster = 'yes' # DEFAULTS TO 'no'. A 'raster' is an opaque layer with holes in it. holeshape = 'square' # DEFAULTS TO 'circle'. holesize = 140 rastercolor = '007f80' # DEFAULTS TO 'ffffff' #rastercolor = 'transparent' # Makes the raster seem transparent # Palette palette = 'yes' # DEFAULTS TO 'no'. load palette? requires a palette file. palettefile = '/home/nobody/palette.txt' # MUST BE SET with palette. matrix = 'yes' # DEFAULTS TO 'no'. If yes, a palette is ignored. matrixfile = '/home/nobody/matrix.txt' # MUST BE SET with matrix. # ************************************************************************************************************ # Code section # ALL-CAPS vars are fundemental CELLSIZE = cellsize CELLROWS = 0 CELLCOLS = 0 # This ugly construct allows user to comment out definitions for convenience if not 'cellshape' in globals(): cellshape = 'square' if not 'cellstrokewidth' in globals(): cellstrokewidth = '0' if not 'cellstrokecolor' in globals(): cellstrokecolor = '000000' if not 'eyeshape' in globals(): eyeshape = 'circle' if not 'eyestrokewidth' in globals(): eyestrokewidth = '0' if not 'eyestrokecolor' in globals(): eyestrokecolor = '000000' if not 'wobbly' in globals(): wobbly = 'no' if not 'raster' in globals(): raster = 'no' if not 'holeshape' in globals(): holeshape = 'circle' if not 'rastercolor' in globals(): rastercolor = 'ffffff' if not 'palette' in globals(): palette = 'no' if not 'matrix' in globals(): matrix = 'no' # Set page dimensions if not set already. Doc: "SVG works in nominal "pixel" units." if ('pageheight' in globals()) and ('pagewidth' in globals()): svg_root.set('width', '%spx' % pagewidth) svg_root.set('height', '%spx' % pageheight) width, height = svg_root.width, svg_root.height svg_root.set('viewBox', '0 0 %.0f %.0f' % (width, height)) else: pagewidth = float(svg_root.get('width')) pageheight = float(svg_root.get('height')) # ************************************************************************************************************ # Function definitions # function for creating a shape; default is square def makeshape(kind, xpos, ypos, size, sfill, swidth, objfill): if kind == 'square': offs = (CELLSIZE - size) / 2 obj = rect((xpos + offs, ypos + offs), (xpos + size + offs, ypos + size + offs), stroke=sfill, stroke_width=swidth, fill=objfill) else: offs = CELLSIZE / 2 radius = size / 2 obj = circle((xpos + offs, ypos + offs), radius, stroke=sfill, stroke_width=swidth, fill=objfill) return obj # Different ways for fetching a color, depending on config # With a matrix, cell colors are accessed by index (flat list) def getcolor(mindx): if matrix == 'yes': code = matrixlist[mindx] index = colorlist.index(code) clr = '#%s' % colorlist[index+1] elif palette == 'yes': rgbindex = randrange(rgbmax) clr = '#%s' % rgblist[rgbindex] else: clr = '#%02x%02x%02x' % (randrange(256), randrange(256), randrange(256)) return clr # function for getting a 'random' object position; used for eyes def getwobble(): # For 'random' positioning of eye xwob = 0 ywob = 0 if wobbly == 'yes': xwob = uniform(-2, 2) ywob = uniform(-2.5, 2.5) return (xwob, ywob) # Create both a matrix and a definition list def colormatrix(): with open(matrixfile) as fp: rowcount = 0 colcount = 0 line = fp.readline().lstrip() while line: if '#' in line: parts = line.split('#') line = parts[0].rstrip() else: line = line.rstrip() if line == '': pass elif '=' in line: entry = line.split('=') for i in range(len(entry)): colorlist.append(entry[i].strip()) else: entry = line.split() colcount = len(entry) for i in range(colcount): code = entry[i].split(':') for j in range(len(code)): matrixlist.append(code[j]) rowcount += 1 line = fp.readline() return(rowcount, colcount) # ************************************************************************************************************ # main code if matrix == 'yes': import os colorlist = [] matrixlist = [] (CELLROWS, CELLCOLS) = colormatrix() # Unused: user should take care of setting sensible page dimensions #colwidth = CELLCOLS * CELLSIZE #rowheight = CELLROWS * CELLSIZE else: # Keep the output within the page boundaries CELLROWS = int(pagewidth // cellsize) CELLCOLS = int(pageheight // cellsize) # Hanlde matrix or palette; matrix has priority if (matrix != 'yes') and (palette == 'yes'): with open(palettefile, 'r') as pf: rgblist = pf.read().splitlines() rgbmax = len(rgblist) - 1 pf.close() # Three layers: 1. cells (background), 2. eyes, 3. raster with holes l1 = layer('Background') l2 = layer('Eyes') if (raster == 'yes') and (rastercolor != 'transparent'): l3 = layer('Raster') # Every single element is created and positioned individually, then added to its layer for i in range(CELLROWS): for j in range(CELLCOLS): # For getcolor() with matrix mindex = (i + j) * 2 # Cell position (y, x) = (i * CELLSIZE, j * CELLSIZE) # background layer color = getcolor(mindex) scolor = '#%s' % cellstrokecolor if (raster == 'yes') and (rastercolor == 'transparent'): # Why clip afterwards if you can make cellshape fit holeshape to begin with bgcell = makeshape(holeshape, x, y, holesize, scolor, cellstrokewidth, color) else: bgcell = makeshape(cellshape, x, y, CELLSIZE, scolor, cellstrokewidth, color) l1.add(bgcell) # eyes layer color = getcolor(mindex+1) scolor = '#%s' % eyestrokecolor (xwobble, ywobble) = getwobble() eye = makeshape(eyeshape, x + xwobble, y + ywobble, eyesize, scolor, eyestrokewidth, color) l2.add(eye) # raster layer: a new cell covering the background cell, with a hole punched into it if (raster == 'yes') and (rastercolor != 'transparent'): # raster cell color = '#%s' % rastercolor rstcell = makeshape('square', x, y, CELLSIZE, 'unused', 0, color).to_path() # Hole shape color = '#ffffff' rsthole = makeshape(holeshape, x, y, holesize, 'unused', 0, color).to_path() # Punch hole. Follows even-odd fill rule and draw direction (?) if holeshape == 'circle': rstcell.append(rsthole) else: rstcell.append(rsthole.reverse()) l3.add(rstcell)