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