Tweak the positioning algorithm to draw on a fixed grid
- ID
e4dcb98- date
2021-02-21 07:23:51+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
208dd26- message
Tweak the positioning algorithm to draw on a fixed grid- changed files
1 file, 131 additions, 60 deletions
Changed files
happybackgrounds.py (7849 → 9707)
diff --git a/happybackgrounds.py b/happybackgrounds.py
index 5d7543d..b14f894 100755
--- a/happybackgrounds.py
+++ b/happybackgrounds.py
@@ -22,7 +22,7 @@ FA_LOCAL_PATH = os.path.basename(FA_URL)
def get_font_awesome_icon_path(*, icon_name):
"""
- Returns the <path> element of a Font Awesome icon.
+ Returns the (width, height, <path> element) of a Font Awesome icon.
"""
if not os.path.exists(FA_LOCAL_PATH):
urlretrieve(url=FA_URL, filename=FA_LOCAL_PATH)
@@ -49,15 +49,95 @@ def get_font_awesome_icon_path(*, icon_name):
# We care about getting the <path> element which defines the icon.
root = ET.fromstring(icon_contents)
+ _, _, width, height = root.attrib["viewBox"].split()
+
for element in root:
if element.tag == "{http://www.w3.org/2000/svg}path":
- return ET.tostring(element).decode("ascii")
+ return (int(width), int(height), ET.tostring(element).decode("ascii"))
else: # no return
raise RuntimeError(f"Could not find <path> in {inner_name}!")
+def create_xy_positions(
+ *, width, height, columns, rows, min_nudge, max_nudge, count, avoid_center
+):
+ """
+ Based on an algorithm from Kate's friend.
+
+ This will give you the (x, y) coordinates for the top left-hand corner
+ of the icon.
+ """
+ # Put the icons randomly on an N×M grid
+ valid_positions = [(x, y) for x in range(columns) for y in range(rows)]
+ random.shuffle(valid_positions)
+
+ if avoid_center:
+ valid_positions = [
+ (x, y)
+ for (x, y) in valid_positions
+ if (x <= columns / 4 or x >= 3 * columns / 4)
+ and (y <= rows / 4 or y >= 3 * rows / 4)
+ ]
+
+ # Choose a direction and amount to nudge them by
+ nudge = random.uniform(min_nudge, max_nudge)
+ for x, y in valid_positions[:count]:
+ # x = x + ((1 - random.random() * 2) * nudge)
+ # y = y + ((1 - random.random() * 2) * nudge)
+
+ # How does this map to our canvas?
+ row_height = height / rows
+ column_width = width / columns
+
+ yield (x * column_width, y * row_height)
+
+
+def create_fill_colors(*, background_color):
+ """
+ Given the hex string of the background colour (e.g. #ff0000), generate
+ the hex strings of other, similar colours.
+ """
+ # Parse the CSS colour. We're only going to vary the lightness of the
+ # generated icons, so work out what we're varying it between -- i.e. are
+ # we going darker than the background, or lighter?
+ red = int(background_color[1:3], 16) / 255
+ green = int(background_color[3:5], 16) / 255
+ blue = int(background_color[5:7], 16) / 255
+
+ hue, lightness, saturation = colorsys.rgb_to_hls(red, green, blue)
+
+ # Light background => darken
+ # Dark background => lighten
+ # Middling => choose at random
+ #
+ # 0.2 is chosen at random
+ is_light_bg = lightness > 0.8
+ is_dark_bg = lightness < 0.2
+
+ darken_icons = is_light_bg or (not is_dark_bg and random.random() > 0.5)
+
+ if darken_icons:
+ min_lightness, max_lightness = (max(0, lightness - 0.2), lightness)
+ else:
+ min_lightness, max_lightness = (lightness, min(lightness + 0.2, 1))
+
+ while True:
+ icon_lightness = random.uniform(min_lightness, max_lightness)
+ r, g, b = colorsys.hls_to_rgb(hue, icon_lightness, saturation)
+ r, g, b = (int(r * 255), int(g * 255), int(b * 255))
+ yield f"#{r:02x}{g:02x}{b:02x}"
+
+
def create_svg(
- *, background, icon_name, min_icon_count, max_icon_count, min_scale, max_scale, avoid_center=False, out_path=None
+ *,
+ background,
+ icon_name,
+ min_icon_count,
+ max_icon_count,
+ min_scale,
+ max_scale,
+ avoid_center=False,
+ out_path=None,
):
"""
Creates an SVG file. Returns the path to the generated SVG.
@@ -94,60 +174,43 @@ def create_svg(
""",
]
- # Parse the CSS colour. We're only going to vary the lightness of the
- # generated icons, so work out what we're varying it between -- i.e. are
- # we going darker than the background, or lighter?
- red = int(background[1:3], 16) / 255
- green = int(background[3:5], 16) / 255
- blue = int(background[5:7], 16) / 255
-
- hue, lightness, saturation = colorsys.rgb_to_hls(red, green, blue)
-
- # Light background => darken
- # Dark background => lighten
- # Middling => choose at random
- #
- # 0.2 is chosen at random
- is_light_bg = lightness > 0.8
- is_dark_bg = lightness < 0.2
-
- darken_icons = is_light_bg or (not is_dark_bg and random.random() > 0.5)
-
- if darken_icons:
- min_lightness, max_lightness = (max(0, lightness - 0.2), lightness)
- else:
- min_lightness, max_lightness = (lightness, min(lightness + 0.2, 1))
-
# Get the icon path, and render it at various points on the background.
- icon_count = random.randint(min_icon_count, max_icon_count)
- icon_path = get_font_awesome_icon_path(icon_name=icon_name)
-
- for _ in range(icon_count):
- if avoid_center:
- x_start = random.choice([
- random.randint(-50, width // 4),
- random.randint(3 * width // 4, width + 50)
- ])
- y_start = random.choice([
- random.randint(-50, height // 4),
- random.randint(3 * height // 4, height + 50)
- ])
- else:
- x_start = random.randint(-50, width + 50)
- y_start = random.randint(-50, height + 50)
+ count = random.randint(min_icon_count, max_icon_count)
+ icon_width, icon_height, icon_path = get_font_awesome_icon_path(icon_name=icon_name)
+
+ for (x, y), fill_color in zip(
+ create_xy_positions(
+ width=width,
+ height=height,
+ columns=width // 100,
+ rows=height // 100,
+ min_nudge=-0.2,
+ max_nudge=0.2,
+ count=count,
+ avoid_center=avoid_center,
+ ),
+ create_fill_colors(background_color=background)
+ ):
+ scale = random.uniform(min_scale, max_scale)
rotation_angle = random.randint(0, 360)
- scale = random.uniform(min_scale, max_scale)
+ rotation_center_x = scale * icon_width // 2
+ rotation_center_y = scale * icon_height // 2
- icon_lightness = random.uniform(min_lightness, max_lightness)
- r, g, b = colorsys.hls_to_rgb(hue, icon_lightness, saturation)
- r, g, b = (int(r * 255), int(g * 255), int(b * 255))
- fill_color = f"#{r:02x}{g:02x}{b:02x}"
+ # icon_blur = random.randint(0, 20)
+ icon_blur = 0
lines.append(
f"""
- <g transform="rotate({rotation_angle} 250 250) translate({x_start} {y_start}) scale({scale} {scale})"
- style="fill: {fill_color}">
+ <defs>
+ <filter id="blur{icon_blur}" x="0" y="0">
+ <feGaussianBlur in="SourceGraphic" stdDeviation="{icon_blur} {icon_blur}"/>
+ </filter>
+ </defs>
+ <g transform="translate({x} {y})
+ rotate({rotation_angle} {rotation_center_x} {rotation_center_y})
+ scale({scale} {scale})"
+ style="fill: {fill_color}; filter: url(#blur{icon_blur})">
{icon_path}
</g>"""
)
@@ -168,12 +231,18 @@ def parse_args():
parser = argparse.ArgumentParser(
description="Create low-contrast backgrounds from Font Awesome icons. Prints the path to a generated SVG file."
)
- parser.add_argument("--background", help="a six-digit CSS colour (e.g. #ff0000)", required=True)
- parser.add_argument("--icon_name", help="the Font Awesome icon to use (e.g. snowflake)", required=True)
+ parser.add_argument(
+ "--background", help="a six-digit CSS colour (e.g. #ff0000)", required=True
+ )
+ parser.add_argument(
+ "--icon_name",
+ help="the Font Awesome icon to use (e.g. snowflake)",
+ required=True,
+ )
parser.add_argument("--min_icon_count", type=int, default=5, help="(default: 5)")
- parser.add_argument("--max_icon_count", type=int, default=10, help="(default: 15)")
- parser.add_argument("--min_scale", type=float, default=0.2, help="(default: 0.2)")
- parser.add_argument("--max_scale", type=float, default=1, help="(default: 1)")
+ parser.add_argument("--max_icon_count", type=int, default=30, help="(default: 30)")
+ parser.add_argument("--min_scale", type=float, default=0.15, help="(default: 0.15)")
+ parser.add_argument("--max_scale", type=float, default=0.3, help="(default: 0.3)")
parser.add_argument("--avoid_center", action="store_true")
# Make a consideration for British people ;-)
@@ -184,15 +253,17 @@ def parse_args():
result = parser.parse_args()
if result.min_icon_count > result.max_icon_count:
- sys.exit(f"error: --min_icon_count={result.min_icon_count} should be less than or equal to --max_icon_count={result.max_icon_count}")
+ sys.exit(
+ f"error: --min_icon_count={result.min_icon_count} should be less than or equal to --max_icon_count={result.max_icon_count}"
+ )
if result.min_scale > result.max_scale:
- sys.exit(f"error: --min_scale={result.min_scale} should be less than or equal to --max_scale={result.max_scale}")
+ sys.exit(
+ f"error: --min_scale={result.min_scale} should be less than or equal to --max_scale={result.max_scale}"
+ )
result = {
- name: getattr(result, name)
- for name in dir(result)
- if not name.startswith("_")
+ name: getattr(result, name) for name in dir(result) if not name.startswith("_")
}
if result["avoid_centre"]: