PyNuts
PyNuts

Reputation: 73

Python Turtle color change

im currently playing around with pythons turtle module and im trying to make a grid of opaque square shapes, say 30x30 that can change color based on some property (doesn't matter what property) my question is, is there anyway to change the color of a shape once its already been drawn down on the canvas?

Ive tried adding all the square shapes to an array, both stamps and polygons, but it seems impossible to change the color of any of them once they have been drawn.

i know stamp doesn't work because its like a footprint of where the turtle was but is there any method at all that allows for this with polygons or some other method im not aware of?

I didn't add any snippets of code because its a pretty basic question and can be used for many things.

Upvotes: 1

Views: 19866

Answers (2)

cdlane
cdlane

Reputation: 41872

Yes, you can do it. The key to this, and many complicated turtle problems, is using stamps. They are individually, or collectively, removable. And since they take on the shape of the turtle itself, they can be images or arbitrary polygons of any size or color you wish:

from turtle import Turtle, Screen
from random import randrange, choice
from collections import namedtuple
from math import ceil

GRID = 15  # GRID by GRID of squares
SIZE = 30  # each square is SIZE by SIZE

INCREASE = 1.5  # how much to lighten the square's color
WHITE = [255, 255, 255]  # color at which we stop changing square
DELAY = 100  # time between calls to change() in milliseconds
DARK = 32 # range (ceil(INCREASE) .. DARK - 1) of dark colors

def change():
    block = choice(blocks)
    blocks.remove(block)

    color = [min(int(primary * INCREASE), WHITE[i]) for i, primary in enumerate(block.color)]  # lighten color

    turtle.color(color)
    turtle.setposition(block.position)
    turtle.clearstamp(block.stamp)

    stamp = turtle.stamp()

    if color != WHITE:
        blocks.append(Block(block.position, color, stamp))  # not white yet so keep changing this block

    if blocks:  # stop all changes if/when all blocks turn white
        screen.ontimer(change, DELAY)

HALF_SIZE = SIZE // 2

screen = Screen()
screen.colormode(WHITE[0])
screen.register_shape("block", ((HALF_SIZE, -HALF_SIZE), (HALF_SIZE, HALF_SIZE), (-HALF_SIZE, HALF_SIZE), (-HALF_SIZE, -HALF_SIZE)))
screen.tracer(GRID ** 2)  # ala @PyNuts

turtle = Turtle(shape="block", visible=False)
turtle.speed("fastest")
turtle.up()

Block = namedtuple('Block', ['position', 'color', 'stamp'])

blocks = list()

HALF_GRID = GRID // 2

for x in range(-HALF_GRID, HALF_GRID):
    for y in range(-HALF_GRID, HALF_GRID):
        turtle.goto(x * SIZE, y * SIZE)
        color = [randrange(ceil(INCREASE), DARK) for primary in WHITE]
        turtle.color(color)
        blocks.append(Block(turtle.position(), color, turtle.stamp()))

screen.ontimer(change, DELAY)

screen.exitonclick()

Upvotes: 3

jsbueno
jsbueno

Reputation: 110218

The idea of the Turtle is that images, once drawn, become pixels, commited to the canvas - even though the Turtle commands themselves are "vector" commands, when one is talking about "vectorial" vs "raster" graphics.

That means the Turtle drawing context can't know about what is already drawn "by itself" - there is no way for you to reference an already drawn shape, or polygon and change its properties. (update: but for the stamps - the Turtle object does record information about those - thanks @cdlane)

What you'd have to do,in this case, is to annotate on your code data about any shapes you'd like to further modify - and them redraw them in someway, when you need it. In other words: you develop code for a vector graphics model -which allows you to do that.

However, you should note, that the tkinter Canvas widget, on top of which the Turtle runs (at least, I believe), is itself a Vector model - so it might be more appropriate for what you have in mind. Its documentation and way of working may be hard to grasp, though.

So, going back to "having your own Vector graphics implementation", you could take advantage of Python Object Orientation and data model to build "history-turtle" - which could record a log and replay blocks of commands within the same position context (but allowing for different color, width, etc, ... settings).

It is a bitty tricker done than said, but then, here is a sample one:

from turtle import Turtle

"""
Python Turtle with logging and replay capabilities

Author: João S. O. Bueno <[email protected]>
License: LGPL 3.0+

This implements HistoryTurtle - a subclass of
Python's turtle.Turtle wich features a simple
command history and   new `replay` and `log`
methods -
`turtle.replay(<start>, <end>)` will reissue
the commands logged in those positions of the history,
but with the current Pen settings.

The optional kwarg `position_independent` can be passed
as `True` to `replay` so that the turtle's position
and heading are not restored to the ones recorded
along with the command.

https://gist.github.com/jsbueno/cb413e985747392c460f39cc138436bc

"""


class Command:
    __slots__ = ( "method_name", "args", "kwargs", "pos", "heading")

    def __init__(self, method_name, args=(),
                 kwargs=None, pos=None, heading=None):
        self.method_name = method_name
        self.args = args
        self.kwargs = kwargs or {}
        self.pos = pos
        self.heading = heading

    def __repr__(self):
        return "<{0}> - {1}".format(self.pos, self.method_name)


class Instrumented:
    def __init__(self, turtle, method):
        self.turtle = turtle
        self.method = method

    def __call__(self, *args, **kwargs):
        command = Command(self.method.__name__, args, kwargs,
                          self.pos(), self.heading())
        result = self.method(*args, **kwargs)
        if (self.method.__name__ not in self.turtle.methods_to_skip or
                not kwargs.pop('skip_self', True)):
            self.turtle.history.append(command)
        return result

    def pos(self):
        return self.turtle._execute(Command('pos'), True)

    def heading(self):
        return self.turtle._execute(Command('heading'), True)


class HistoryTurtle(Turtle):

    methods_to_skip = ('replay', '_execute', 'log')

    def __init__(self, *args, **kw):
        self._inited = False
        super().__init__(*args, **kw)
        self.history = []
        self._replaying = [False]
        self._inited = True

    def __getattribute__(self, attr):
        result = super().__getattribute__(attr)
        if (not callable(result) or
                attr.startswith('_') or
                not self._inited or
                self._replaying[-1]):
            return result
        return Instrumented(self, result)

    def replay(self, start, end=None, *,
               position_independent=False,
               skip_self=True,
               restore=True
               ):
        results = []

        self._replaying.append(True)
        if restore:
            pos, heading, state = self.pos(), self.heading(), self.pen()['pendown']
        for command in self.history[start:end]:
            results.append(
                self._execute(command, position_independent))
        if restore:
            self.setpos(pos)
            self.setheading(heading)
            (self.pendown() if state else self.penup())
        self._replaying.pop()
        return results

    def log(self, start=0, end=None):
        for i, command in enumerate(self.history[start:end], start):
            print(i, command)

    def _execute(self, command, position_independent):
        """ Execute a method without triggering the log
        """
        # Warning: not thread-safe:
        self._replaying.append(True)
        method = getattr(self, command.method_name)
        if not position_independent:
            state = self.pen()['pendown']
            self.setpos(command.pos)
            self.setheading(command.heading)
            (self.pendown() if state else self.penup())
        result = method(*command.args, **command.kwargs)
        self._replaying.pop()
        return result

Upvotes: 0

Related Questions