Parcourir la source

labels now work, added readme

master
Felix Brendel il y a 5 ans
Parent
révision
f430e0a594
5 fichiers modifiés avec 184 ajouts et 69 suppressions
  1. +0
    -2
      .gitignore
  2. BIN
     
  3. BIN
     
  4. +48
    -0
      readme.org
  5. +136
    -67
      traitograph.py

+ 0
- 2
.gitignore Voir le fichier

@@ -1,2 +0,0 @@
*.png
*.org



+ 48
- 0
readme.org Voir le fichier

@@ -0,0 +1,48 @@
#+TITLE: Trait-o-graph

A simple tool to generate circular diagrams that are meant to depict traits of
real or fictional characters.

* Examples
#+BEGIN_SRC sh :exports code :results none
python traitograph.py --labels curious organized energetic friendly confident --values 5 4 3 4 2 --max-value 7 -o examples/example1.png
#+END_SRC


[[./examples/example1.png]]

#+BEGIN_SRC sh :exports code :results none
python traitograph.py \
--labels sanguine phlegmatic choleric melancholic \
--values 5 3 2 4 --max-value 5 -o examples/example2.png \
--dot-size 5 --foreground 255 255 255 --background 46 52 64 \
--trait-color 80 130 150 100 --line-spacing 60
#+END_SRC

[[./examples/example2.png]]

* Requirements
- Python 3.6+
- pygame

* Supported image formats
- bmp (uncompressed)
- tga (uncompressed)
- png
- jpeg

* Command line arguments

| Option | Short form | Type | Default | Description |
|-------------------+------------+-------------+-----------------+-------------------------------------------------|
| =--labels= | =-l= | list of str | | The names of the character traits |
| =--values= | =-v= | list of int | | The values for the character traits |
| =--max-value= | =-m= | int | | The maximum value for any trait |
| =--out-file= | =-o= | str | =chart.png= | The file to which the image will be written |
| =--foreground= | =-fg= | rgb | =0 0 0= | The foreground color of the chart |
| =--background= | =-bg= | rgb | =255 255 255= | The background color of the chart |
| =--trait-color= | =-tc= | rgba | =0 190 190 100= | The color of the colored part of the chart |
| =--outer-padding= | =-op= | pixel size | =20= | The additional padding applied to the chart |
| =--line-spacing= | =-ls= | pixel size | =40= | The distance between the lines |
| =--dot-size= | =-ds= | pixel size | =2= | The size of the dots |
| =--display= | =-d= | bool | =False= | If True, additionally show the plot in a window |

+ 136
- 67
traitograph.py Voir le fichier

@@ -2,19 +2,17 @@ import argparse

sample_usage = """example:

python traitograph.py --names a b c --values 1 2 3 --max-value 4
python traitograph.py -o "character.png" --names n1 n2 n3 n4 --values 1 3 2 3 --max-value 4
python traitograph.py --names curious organized energetic friendly confident --values 5 4 3 4 2 --max-value 7 -d True
python traitograph.py --labels a b c --values 1 2 3 --max-value 4
python traitograph.py -o "character.png" --labels n1 n2 n3 n4 --values 1 3 2 3 --max-value 4
python traitograph.py --labels curious organized energetic friendly confident --values 5 4 3 4 2 --max-value 7 -d True
"""

parser = argparse.ArgumentParser(description="Generate a chart showing character traits.",
epilog=sample_usage,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--out-file", "-o", dest="out_file_name",
metavar="out_file", default="chart.png",
help="The file in which the image will be written. (Defaults to \"chart.png\")")
parser.add_argument("--names", "-n", dest="names",
required=True, nargs="+", metavar="name",

parser.add_argument("--labels", "-l", dest="labels",
required=True, nargs="+", metavar="label",
help="The names of the character traits")
parser.add_argument("--values", "-v", dest="values",
required=True, nargs="+", type=int, metavar="value",
@@ -22,13 +20,16 @@ parser.add_argument("--values", "-v", dest="values",
parser.add_argument("--max-value", "-m", dest="max_value",
required=True, type=int, metavar="max",
help="The maximum value for any trait")
parser.add_argument("--out-file", "-o", dest="out_file_name",
metavar="out_file", default="chart.png",
help="The file to which the image will be written. (Defaults to \"chart.png\")")
parser.add_argument("--foreground", "-fg", dest="foreground",
nargs=3, type=int, default=[0,0,0], metavar="channel",
help="The foreground color of the chart. (Values from 0 to 255 -- defaults to [0,0,0])")
parser.add_argument("--backgournd", "-bg", dest="background",
parser.add_argument("--background", "-bg", dest="background",
nargs=3, type=int, default=[255,255,255], metavar="channel",
help="The background color of the chart. (Values from 0 to 255 -- defaults to [255,255,255])")
parser.add_argument("--trait-color", "-lc", dest="trait_color",
parser.add_argument("--trait-color", "-tc", dest="trait_color",
nargs=4, type=int, default=[0,190,190,100], metavar="channel",
help="The color of the colored part of the chart. (Values from 0 to 255 -- defaults to [0,190,190,100])")
parser.add_argument("--outer-padding", "-op", dest="outer_padding",
@@ -36,7 +37,7 @@ parser.add_argument("--outer-padding", "-op", dest="outer_padding",
help="The additional padding applied around the chart. (Defaults to 20)")
parser.add_argument("--line-spacing", "-ls", dest="line_spacing",
type=int, default=40, metavar="pixel_count",
help="The additional padding applied around the chart. (Defaults to 40)")
help="The distance between the lines. (Defaults to 40)")
parser.add_argument("--dot-size", "-ds", dest="dot_size",
type=int, default=2, metavar="dot_size",
help="The size of the dots. (Defaults to 2)")
@@ -51,36 +52,119 @@ os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
import pygame
from pygame import gfxdraw

# to have the first vertex at the top rather than the bottom, otherwise
# odd-numbered n-gons look like they are upside down
circle_offset = math.pi

def generate_n_gon_points(n, center, radii):
alpha_step = math.tau / n
def generate_angles(n):
return [i * math.tau / n for i in range(n)]


def generate_n_gon_points(n, center, radii):
if type(radii) == int:
radii = [radii for i in range(n)]
radii = [radii] * n
else:
assert(len(radii) == n)

# go clockwise, so flip the sign in the sin and cos, functions (I know
# unnecessary for cos, but looks better when aligned :)
return [(int(math.sin(-i * alpha_step + circle_offset) * radii[i] + center[0]), # x coordintate
int(math.cos(-i * alpha_step + circle_offset) * radii[i] + center[1])) # y coordintate
for i in range(n)]
# to have the first vertex at the top rather than the bottom, otherwise
# odd-numbered n-gons look like they are upside down:
circle_offset = math.pi

# go clockwise, so flip the sign in the sin and cos, functions (I know it's
# unnecessary for cos, but it looks better when aligned :)
return [(int(math.sin(-angle + circle_offset) * radius + center[0]), # x coordintate
int(math.cos(-angle + circle_offset) * radius + center[1])) # y coordintate
for angle, radius in zip(generate_angles(n), radii)]


def draw_trait_polygon(surface, plot_center):
transparent_overlay = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
radii = [value * args.line_spacing for value in args.values]
trait_points = generate_n_gon_points(len(radii), plot_center, radii)

trait_color_full_alpha = args.trait_color[:3]
for point in trait_points:
gfxdraw.filled_circle(surface, point[0], point[1], args.dot_size, trait_color_full_alpha)
gfxdraw.aacircle(surface, point[0], point[1], args.dot_size, trait_color_full_alpha)

pygame.draw.aalines(surface, trait_color_full_alpha, True, trait_points)
# draw the transparent polygon
pygame.draw.polygon(transparent_overlay, args.trait_color, trait_points)
surface.blit(transparent_overlay, (0,0))


def generate_labels(points, surface):
label_surfaces = [font.render(label, True, args.foreground) for label in args.labels]
label_offsets = []

tolerance = 0.05

min_x = 0
min_y = 0
max_x, max_y = surface.get_size()

def generate_plot_surface(dimension_count, values, max_value, line_spacing, foreground, background, trait_color, dot_size):
# calcualte the offsets for all the labels, depeneding on the position
# around the plot
for label_surface, point, alpha in zip(label_surfaces, points, generate_angles(len(points))):
label_width, label_height = label_surface.get_size()

# these offsets are for offsetting the actual label, so that it has soem
# distance form the point it is supposed to stick to
vertical_text_offset = 5
horizontal_text_offset = 10

if abs(alpha) < tolerance: # label is at top
offset = (-label_width//2, -label_height - vertical_text_offset)
elif abs(alpha-math.pi) < tolerance: # label is at bottom
offset = (-label_width//2, + vertical_text_offset)
elif abs(alpha-math.tau/4) < tolerance: # label is at right
offset = (horizontal_text_offset, -label_height//2)
elif abs(alpha-3*math.tau/4) < tolerance: # label is at left
offset = (-label_width-horizontal_text_offset, -label_height//2)
elif 0 < alpha < math.tau/4: # label is at top right
offset = (horizontal_text_offset//2, -label_height-vertical_text_offset//2)
elif math.tau/4 < alpha < math.tau/2: # label is at bottom right
offset = (horizontal_text_offset//2, vertical_text_offset//2)
elif math.tau/2 < alpha < 3*math.tau/4: # label is at bottom left
offset = (-label_width-horizontal_text_offset//2, vertical_text_offset//2)
else: # label is at top left
offset = (-label_width-horizontal_text_offset//2, -label_height-vertical_text_offset//2)

min_x = min(min_x, point[0] + offset[0])
max_x = max(max_x, point[0] + offset[0] + label_width)

min_y = min(min_y, point[1] + offset[1])
max_y = max(max_y, point[1] + offset[1] + label_height)

label_offsets.append(offset)

# calculate new surface size
padding = 10
width = max_x - min_x + 2*padding
height = max_y - min_y + 2*padding

new_surface = pygame.Surface((width, height))
new_surface.fill(args.background)

# blit the original plot
new_surface.blit(surface, (-min_x + padding,-min_y + padding))

# blit all the labels
for label_offset, label_surface, point in zip(label_offsets, label_surfaces, points):
new_surface.blit(label_surface, (point[0] + label_offset[0] - min_x + padding, point[1] + label_offset[1] -min_y + padding))
return new_surface


def generate_plot():
# figure out exactly, how big the surface needs to be, by looking at the
# outer most points
radius = line_spacing * max_value
points = generate_n_gon_points(dimension_count, (0,0), radius)
dimension_count = len(args.labels)
radius = args.line_spacing * args.max_value
points = generate_n_gon_points(dimension_count, (0, 0), radius)

max_x = max((point[0] for point in points))
min_x = -max_x # symmetry: fist point is always straight up
min_y = -radius # fist point is always straight up
max_y = max((point[1] for point in points))

padding = dot_size//2 + 4
padding = args.dot_size//2 + 4
surface_size = (max_x - min_x + 2*(padding),
max_y - min_y + 2*(padding))

@@ -89,63 +173,49 @@ def generate_plot_surface(dimension_count, values, max_value, line_spacing, fore

# generate the surface
surface = pygame.Surface(surface_size)
surface.fill(background)
surface.fill(args.background)

transparent_overlay = pygame.Surface(surface_size, pygame.SRCALPHA)
transparent_overlay.set_alpha(100) #TODO(Felix): make this adjustable

font = pygame.font.Font(pygame.font.get_default_font(), int(line_spacing*0.4))
for i in range(max_value):
radius = args.line_spacing
for i in range(args.max_value):
points = generate_n_gon_points(dimension_count, plot_center, radius)
pygame.draw.aalines(surface, foreground, True, points)

pygame.draw.aalines(surface, args.foreground, True, points)

for point in points:
gfxdraw.filled_circle(surface, point[0], point[1], dot_size, foreground)
gfxdraw.aacircle(surface, point[0], point[1], dot_size, foreground)
gfxdraw.filled_circle(surface, point[0], point[1], args.dot_size, args.foreground)
gfxdraw.aacircle(surface, point[0], point[1], args.dot_size, args.foreground)

textsurface = font.render(f"{max_value-i}", True, foreground)
textsurface = font.render(f"{i+1}", True, args.foreground)

text_location = (points[0][0] - textsurface.get_size()[0] // 2,
points[0][1] + int(line_spacing * 0.2))
surface.blit(textsurface,text_location)
points[0][1] + int(args.line_spacing * 0.2))
surface.blit(textsurface, text_location)

radius -= line_spacing
radius += args.line_spacing

# draw in the trait polygon
radii = [value * line_spacing for value in values]
trait_points = generate_n_gon_points(dimension_count, plot_center, radii)
print(trait_points)

trait_color_full_alpha = trait_color[:3]
for point in trait_points:
gfxdraw.filled_circle(surface, point[0], point[1], 4, trait_color_full_alpha)
pygame.draw.aalines(surface, trait_color_full_alpha, True, trait_points)
draw_trait_polygon(surface, plot_center)

# draw the transparent polygon
pygame.draw.polygon(transparent_overlay, trait_color, trait_points)
surface.blit(transparent_overlay, (0,0))
return surface
# generate the labels
return generate_labels(points, surface)


def plot(names, values, max_value, foreground, background, trait_color, out_file_name, display, outer_padding, line_spacing, dot_size):
number_dimensions = len(names)
assert(number_dimensions == len(values))
assert(max_value > 0)
if __name__ == "__main__":
assert(len(args.labels) == len(args.values))
assert(args.max_value > 0)

pygame.init()
font = pygame.font.Font(pygame.font.get_default_font(), int(args.line_spacing*0.4))

surface = generate_plot()

plot_surface = generate_plot_surface(number_dimensions, values, max_value, line_spacing, foreground, background, trait_color, dot_size)
pygame.image.save(surface, args.out_file_name)

final_surface = plot_surface
pygame.image.save(final_surface, out_file_name)
if args.display:
window = pygame.display.set_mode(surface.get_size())
pygame.display.set_caption("Trait-o-graph")
window.blit(surface, (0, 0))

if display:
window = pygame.display.set_mode(final_surface.get_size())
pygame.display.set_caption("Traitograph")
window.blit(final_surface, (0,0))
pygame.display.update()
running = True
while running:
@@ -154,6 +224,5 @@ def plot(names, values, max_value, foreground, background, trait_color, out_file
running = False
else:
print("done")
pygame.quit()

plot(**vars(args))
pygame.quit()

Chargement…
Annuler
Enregistrer