﻿#!/usr/bin/python
# -*- coding: utf-8 -*-
#
#  The Romulijana Labyrinth
#
#  Unicursal "labyrinth" from 4th century C.E. Roman palace/temple 
#  complex in what is now Serbia; complex was dedicated to Late Roman 
#  goddess Romula (a Dacian pagan priestess prior to her deification by 
#  Emperor Galerius, her son).
#
#  ("Romulijana" is the Serbian spelling using their Latin alphabet;
#  the Serbian "j" is pronounced like an English "y".)
#  ____________________________________________________________________
#
#  lavirint.py
#
#  ("lavirint" is Serbian for "labyrinth")
# 
#  ⓒ Copyright 2014 Damion Lunin <damion@cybermancy.net>
#  
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details:
#
#  http://www.gnu.org/licenses/gpl2.txt

import itertools
import math
import pyglet.graphics

# Installing Pyglet for Sugar/Fedora Pythoneers:
# In Terminal acquire root priviliges via the "su" (no quotes) command.
# Type "yum search pyglet" (no quotes) to find name of pyglet package;
# this will bring up something like "python-pyglet.noarch".
# Then type "yum install python-pyglet.noarch" (no quotes) to install.

# To take full advantage of some graphics cards 
# you may need Pyglet version 1.2 or later

# Code for interpythonic compatibility...
try:
    xrange
except NameError:
    xrange = range

def Cartesian (Radius, Theta, Shift):
    # Sine and cosine convert polar coordinates to Cartesian
    CartesianCoordinates = []
    CartesianCoordinates.append (Radius * math.cos(Theta) + Shift[0])
    CartesianCoordinates.append (Radius * math.sin(Theta) + Shift[1])
    return CartesianCoordinates

def Hedgetrimmer (RingsData, Vertex, Side, Offset):
    # "Trim" the hexagonal "hedges" into a labyrinth
    # Side determines direction from vertices for "trimming".
    # Offset determines distance from the vertices.
    Vertices = []
    for Ring in xrange(0,20):
        Vertices.append (RingsData[Ring][Vertex])
    VertexIncrement = math.copysign (2.0, Side)
    Direction = Vertex + VertexIncrement
    
    # New path segments
    if (Offset > 0.0):  
        Parallelograph(
            Offset, Vertices[1], Vertices[18], Direction, Foreground)
    if (VertexIncrement == -2.0):
        Vertices.reverse()
    Parallelograph(
        Offset+2.0, Vertices[1], Vertices[8], Direction, Foreground)
    Parallelograph(
        Offset+2.0, Vertices[9], Vertices[16], Direction, Foreground)
    Parallelograph(
        Offset+4.0, Vertices[3], Vertices[6], Direction, Foreground)
    Parallelograph(
        Offset+4.0, Vertices[11], Vertices[14], Direction, Foreground)
    
    # Negative spaces
    Parallelograph(
        Offset+1.0, Vertices[0], Vertices[17], Direction, Background)
    Parallelograph(
        Offset+3.0, Vertices[2], Vertices[7], Direction, Background)
    Parallelograph(
        Offset+3.0, Vertices[10], Vertices[15], Direction, Background)

def Parallelograph (Centerline, CoordinatesDistal, 
    CoordinatesCentral, Direction, Colour):
    # Draw a parallelogram
    pyglet.gl.glColor4f (*Colour)
    Parallelogram = []
    Parallelogram.append (Cartesian(
        (Centerline - .5) * RadiusIncrement, 
        Direction * Sextant + Rotation, 
        CoordinatesDistal))
    Parallelogram.append (Cartesian(
        (Centerline - .5) * RadiusIncrement, 
        Direction * Sextant + Rotation, 
        CoordinatesCentral))
    Parallelogram.append (Cartesian(
        (Centerline + .5) * RadiusIncrement, 
        Direction * Sextant + Rotation, 
        CoordinatesCentral))
    Parallelogram.append (Cartesian(
        (Centerline + .5) * RadiusIncrement, 
        Direction * Sextant + Rotation, 
        CoordinatesDistal))
    pyglet.graphics.draw (4, pyglet.gl.GL_POLYGON, ('v2f', 
        (list (itertools.chain.from_iterable (Parallelogram)))))

def SamplesMaximum():
    # Get highest available value of "samples" for multisampling
    Platform = pyglet.window.get_platform()
    Display = Platform.get_default_display()
    Screen = Display.get_default_screen()
    SamplesMax = 0
    for Config in Screen.get_matching_configs(pyglet.gl.Config()):
        if (Config.samples > SamplesMax): 
            SamplesMax = Config.samples
    return SamplesMax

def Spin (x, y, dx, dy, Shift):
    x1 = x - Shift[0]
    y1 = y - Shift[1]
    x2 = x + dx - Shift[0]
    y2 = y + dy - Shift[1]
    return (math.atan2(y2,x2) - math.atan2(y1,x1))
    # Arctangent finds angular coordinate from Cartesian coordinates
    # Difference between angular coordinates is the spin increment
    
def main():
    global Background, Foreground, Sextant, Rotation
    Tau = 2.0 * math.pi       # Tau = 360 degrees
    Sextant = Tau / 6.0       # 6 vertices in a hexagon
    Rotation = Sextant / 4.0  # Initial image rotation
    Background = [ (28.0/255.0),  (15.0/255.0),   (7.0/255.0), 0.0]
    Foreground = [(175.0/255.0), (167.0/255.0), (123.0/255.0), 0.0]
    SamplesMax = SamplesMaximum()

    if (SamplesMax >= 8):
        WindowSize = 1099
    else:
        WindowSize = 667
    # The above if-statement is a shortcut for determining approximate
    # screen resolution availability based on graphics card capability.
    # As it is a shortcut, there are scenarios for which its
    # determination will fail to produce nice results.

    try:
        # Preferred graphics settings
        Config = pyglet.gl.Config(
            double_buffer=True, sample_buffers=1, samples=SamplesMax)
        Window = pyglet.window.Window(
            WindowSize, WindowSize, resizable=True, 
            config=Config, vsync=True)
    except:
        # Failover graphics settings
        Window = pyglet.window.Window(
            WindowSize, WindowSize, resizable=True)
        pyglet.gl.glEnable (pyglet.gl.GL_LINE_SMOOTH)
        pyglet.gl.glHint(
            pyglet.gl.GL_LINE_SMOOTH_HINT, pyglet.gl.GL_NICEST)
        # Edges will be pixelated if GL_LINE_SMOOTH fails.
        
    Window.set_caption( 'The Romulijana Labyrinth' )

    @Window.event
    def on_draw():
        global RadiusIncrement, Shift
        Window.clear()
        Colour = Background
        pyglet.gl.glColor4f (*Colour)
        Width = Window.width
        Height = Window.height
        pyglet.graphics.draw (4, pyglet.gl.GL_POLYGON, ('v2f',
            (0.0,0.0, Width,0.0, Width,Height, 0.0,Height))) 
        Shift = []
        Shift.append (Width / 2.0)
        Shift.append (Height / 2.0)
        Radius = Shift[1]
        RadiusIncrement = Radius / 21.0
        RingsData = []
        for Ring in xrange(0,20):
            # Draw concentric hexagons
            pyglet.gl.glColor4f (*Colour)
            if (Colour == Background):
                Colour = Foreground
            elif (Colour == Foreground):
                Colour = Background
            Hexagon = []
            for Vertex in xrange(0,6):
                Hexagon.append (Cartesian(
                    Radius, Vertex*Sextant+Rotation, Shift))
            
            RingsData.append(Hexagon)  
            # RingsData[ring][vertex][coordinate]
            
            pyglet.graphics.draw (6, pyglet.gl.GL_POLYGON, ('v2f', 
                (list (itertools.chain.from_iterable (Hexagon)))))
            Radius -= RadiusIncrement

        # Leverage the rotational symmetries of the Romulijana labyrinth
        # to call a single modular function six times.
        Hedgetrimmer (RingsData, 0, +0.0, +0.5) 
        Hedgetrimmer (RingsData, 0, -0.0, -0.5) 
        Hedgetrimmer (RingsData, 2, +0.0, +2.0)
        Hedgetrimmer (RingsData, 2, -0.0, +0.0)
        Hedgetrimmer (RingsData, 4, +0.0, +0.5)
        Hedgetrimmer (RingsData, 4, -0.0, -0.5)

        # Path segment to center
        CoordinatesDistal = []
        CoordinatesDistal.append(
            (RingsData[1][2][0] + RingsData[2][2][0]) / 2.0)
        CoordinatesDistal.append(
            (RingsData[1][2][1] + RingsData[2][2][1]) / 2.0)
        Parallelograph(
            0.0, CoordinatesDistal, Shift, 0.0, Foreground)

        # Negative space which breaks perfect rotational symmetry
        # and thereby creates a unicursal "labyrinth"
        Parallelograph(
            1.0, RingsData[0][2], RingsData[19][2], 4.0, Background)
        
        Window.invalid = False  # Disable excessive redraws
        
    @Window.event
    def on_mouse_drag (x, y, dx, dy, buttons, modifiers):
        # Spin the "labyrinth" by dragging the mouse
        global Rotation
        Rotation += Spin (x, y, dx, dy, Shift)
        Window.invalid = True  # Flag for redraw

    @Window.event
    def on_mouse_scroll (x, y, scroll_x, scroll_y):
        # Spin the "labyrinth" by scrolling with the mouse wheel
        global Rotation
        Degree = Tau / 360.0
        Rotation += 3.0 * math.copysign (Degree, scroll_y)
        Window.invalid = True  # Flag for redraw

    @Window.event
    def on_expose():
        # Redraw window when needed
        Window.invalid = True

    @Window.event
    def on_key_press(symbol, modifiers):
        # [Super]+[PrtScr] saves image to file
        if (symbol==65377 and modifiers==48):
            pyglet.image.get_buffer_manager(
                ).get_color_buffer().save('lavirint.png')

    pyglet.app.run()
    return 0
if __name__ == '__main__':
    main()
 
