# 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)