├── .gitignore ├── requirements.in ├── message_for_nazis.jpg ├── start.sh ├── static └── style.css ├── requirements.txt ├── LICENSE ├── Valknut.svg ├── templates └── index.html ├── webapp.py ├── README.md ├── flags.py └── draw_valknut.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | flask 2 | gunicorn 3 | -------------------------------------------------------------------------------- /message_for_nazis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/rainbow-valknuts/live/message_for_nazis.jpg -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | pip3 install --user -r requirements.txt 7 | gunicorn -b "0.0.0.0:5000" -w 4 webapp:app 8 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | svg { 2 | border: 3px solid white; 3 | } 4 | 5 | body { 6 | background: #222; 7 | } 8 | 9 | p { 10 | text-align: center; 11 | color: white; 12 | font: 1.4em monospace; 13 | line-height: 1.35em; 14 | } 15 | 16 | a { 17 | color: #f0e800; 18 | } 19 | 20 | a:hover { 21 | background: rgba(240, 232, 0, 0.4); 22 | } 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | click==7.0 # via flask 8 | flask==1.1.1 9 | gunicorn==20.0.4 10 | itsdangerous==1.1.0 # via flask 11 | jinja2==2.10.3 # via flask 12 | markupsafe==1.1.1 # via jinja2 13 | werkzeug==0.16.0 # via flask 14 | 15 | # The following packages are considered to be unsafe in a requirements file: 16 | # setuptools 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Valknut.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | image/svg+xml 9 | 10 | Valknut 11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | rainbow valknuts 10 | 11 | 12 |

13 | made with 💖 by @alexwlchan, who loves how you look today 14 | • 15 | based on an idea by @KlezmerGryphon 16 | • 17 | source code 18 |

19 | 20 |
21 | {{ svg_xml | safe }} 22 |
23 | 24 |

25 | a mashup of the 26 | {{ flags[0] | name }}, 27 | {{ flags[1] | name }} and 28 | {{ flags[2] | name }} flags 29 |

30 |

31 | surprise me! • 32 | permalink • 33 | download as svg 34 |

35 |

36 | see also: rainbow hearts 37 |

38 | 39 | 40 | -------------------------------------------------------------------------------- /webapp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base64 4 | import random 5 | 6 | from flask import Flask, render_template, request 7 | 8 | from flags import flags 9 | from draw_valknut import get_valknut_svg 10 | 11 | 12 | app = Flask(__name__) 13 | 14 | 15 | @app.template_filter("base64") 16 | def encode_as_base64(xml_string): 17 | return base64.b64encode(xml_string.encode("ascii")).decode("ascii") 18 | 19 | 20 | @app.template_filter("name") 21 | def get_name(flag_pair): 22 | try: 23 | return flag_pair[1]["name"] 24 | except KeyError: 25 | return flag_pair[0] 26 | 27 | 28 | @app.template_filter("url") 29 | def get_url(flag_pair): 30 | return flag_pair[1]["url"] 31 | 32 | 33 | RENAMED_FLAGS = { 34 | # Renamed after I realised the polyamory and polysexual flags are separate. 35 | # See https://github.com/queerjs/website/issues/59 36 | "poly": "polysexual", 37 | } 38 | 39 | 40 | @app.route("/") 41 | def index(): 42 | selected_flags = random.sample(list(flags.items()), 3) 43 | 44 | for index, param_name in enumerate(["flag_0", "flag_1", "flag_2"]): 45 | try: 46 | flag_name = request.args[param_name] 47 | flag_name = RENAMED_FLAGS.get(flag_name, flag_name) 48 | flag_data = flags[flag_name] 49 | except KeyError: 50 | pass 51 | else: 52 | selected_flags[index] = (flag_name, flag_data) 53 | 54 | stripes = tuple( 55 | flag["stripes"] for _, flag in selected_flags 56 | ) 57 | 58 | svg_xml = get_valknut_svg(*stripes) 59 | 60 | return render_template("index.html", svg_xml=svg_xml, flags=selected_flags) 61 | 62 | 63 | if __name__ == "__main__": 64 | app.run(debug=True) 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Note, 8 July 2025:** This repo has a Python app that I ran on Glitch from 2021 until [Glitch shut down in July 2025](https://blog.glitch.com/post/changes-are-coming-to-glitch). 2 | 3 | I've moved it to , and the source code is now in my alexwlchan.net repo: 4 | 5 | # rainbow-valknuts 6 | 7 | This is a tiny web app for generating rainbow [valknuts](https://en.wikipedia.org/wiki/Valknut), based on an idea in [a tweet by @KlezmerGryphon](https://twitter.com/KlezmerGryphon/status/1173897515843735553): 8 | 9 | > Hey, Nazis, I got a message for ya from Odin. 10 | > 11 | > ![Three overlapping triangles form a Valknut, with the stripes of the rainbow pride flag, the asexual pride flag, and the trans flag. They're on a black background with white text reading "Your cowardly bigotry is an affront to the Allfather".](message_for_nazis.jpg) 12 | 13 | It uses some code I wrote last year for [drawing with triangular coordinates in SVG](https://alexwlchan.net/2019/09/triangular-coordinates-in-svg/), then randomly selects three flags to mash up into an image. 14 | 15 | The flag definitions are taken from the [QueerJS website](https://queerjs.com/flags). 16 | 17 | The app is running at , and the source code is [on GitHub](https://github.com/alexwlchan/rainbow-valknuts). 18 | 19 | 20 | 21 | ## Usage 22 | 23 | Clone this repository, then run `start.sh`. 24 | This will install dependencies, then start the app running on : 25 | 26 | ```console 27 | $ git clone https://github.com/alexwlchan/rainbow-valknuts.git 28 | $ cd rainbow-valknuts 29 | $ ./start.sh 30 | ``` 31 | 32 | You need Python 3 installed. 33 | 34 | 35 | 36 | ## License 37 | 38 | MIT. 39 | -------------------------------------------------------------------------------- /flags.py: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/queerjs/website/blob/fc6712d8e48ad185521a54e76f78eb3754bfe715/web/src/helpers/useRainbow.js 2 | 3 | import colorsys 4 | 5 | 6 | def to_pastel(hex_string): 7 | hex_string = hex_string.strip("#") 8 | assert len(hex_string) in {3, 6} 9 | 10 | if len(hex_string) == 6: 11 | r = int(hex_string[0:2], 16) 12 | g = int(hex_string[2:4], 16) 13 | b = int(hex_string[4:6], 16) 14 | else: 15 | r = int(hex_string[0] * 2, 16) 16 | g = int(hex_string[1] * 2, 16) 17 | b = int(hex_string[2] * 2, 16) 18 | 19 | hue, lightness, saturation = colorsys.rgb_to_hls(r, g, b) 20 | saturation *= 0.9 21 | lightness = lightness + (100 - lightness) * 0.2 22 | 23 | r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation) 24 | return f"#%02x%02x%02x" % (int(r), int(g), int(b)) 25 | 26 | 27 | def pastelise(colors): 28 | return [to_pastel(col) for col in colors] 29 | 30 | 31 | flags = { 32 | "rainbow": { 33 | "stripes": ["#FF5D7D", "#FF764E", "#FFC144", "#88DF8E", "#00CCF2", "#B278D3"], 34 | "url": "https://en.wikipedia.org/wiki/Rainbow_flag_(LGBT_movement)", 35 | }, 36 | "asexual": { 37 | "stripes": pastelise(["#000000", "#A3A3A3", "#DDD", "#810082"]), 38 | "url": "https://en.wikipedia.org/wiki/Asexuality", 39 | }, 40 | "agender": { 41 | "stripes": pastelise( 42 | [ 43 | "#000000", # black 44 | "#BBC3C6", 45 | "#BBC3C6", # grey 46 | "#FEFEFE", 47 | "#FEFEFE", # white 48 | "#B7F582", 49 | "#B7F582", # green 50 | "#FEFEFE", 51 | "#FEFEFE", # white 52 | "#BBC3C6", 53 | "#BBC3C6", # grey 54 | "#000000", # black 55 | ] 56 | ), 57 | "url": "https://en.wikipedia.org/wiki/Agender", 58 | }, 59 | "aromantic": { 60 | "stripes": pastelise(["#3BA441", "#A8D378", "#FEFEFE", "#A9A9A9", "#000"]), 61 | "url": "https://en.wikipedia.org/wiki/Romantic_orientation#Aromanticism", 62 | }, 63 | "bear": { 64 | "stripes": pastelise( 65 | ["#4e2801", "#ca4e05", "#fdd951", "#fde2ac", "#EEE", "#424242", "#000000"] 66 | ), 67 | "url": "https://en.wikipedia.org/wiki/Bear_flag_(gay_culture)", 68 | }, 69 | "lazy bisexual boy": { 70 | "stripes": pastelise(["#D9006F", "#D9006F", "#744D98"]), 71 | "url": "https://twitter.com/freezydorito/status/1152168216120221697", 72 | }, 73 | "lazy bisexual girl": { 74 | "stripes": pastelise(["#744D98", "#0033AB", "#0033AB"]), 75 | "url": "https://twitter.com/freezydorito/status/1152168216120221697", 76 | }, 77 | "bi": { 78 | "stripes": pastelise(["#D9006F", "#D9006F", "#744D98", "#0033AB", "#0033AB"]), 79 | "url": "https://en.wikipedia.org/wiki/Bisexuality", 80 | }, 81 | "genderfluid": { 82 | "stripes": pastelise(["#FE75A4", "#FFFFFF", "#A90FC0", "#000000", "#303CBE"]), 83 | "url": "https://en.wikipedia.org/wiki/Genderfluid", 84 | }, 85 | "genderqueer": { 86 | "stripes": pastelise(["#B999DD", "#FEFEFE", "#6A8C3A"]), 87 | "url": "https://en.wikipedia.org/wiki/Genderqueer", 88 | }, 89 | "non-binary": { 90 | "stripes": pastelise(["#FDF333", "#FEFEFE", "#9858CF", "#2D2D2D"]), 91 | "url": "https://en.wikipedia.org/wiki/Non-binary_gender", 92 | }, 93 | "pansexual": { 94 | "stripes": pastelise(["#FF008E", "#FFD800", "#00B3FF"]), 95 | "url": "https://en.wikipedia.org/wiki/Pansexuality", 96 | }, 97 | "philly": { 98 | "stripes": pastelise( 99 | [ 100 | "#000", 101 | "#794F18", 102 | "#E40400", 103 | "#FE8C00", 104 | "#FFED00", 105 | "#008126", 106 | "#064EFF", 107 | "#750687", 108 | ] 109 | ), 110 | "url": "https://en.wikipedia.org/wiki/LGBT_symbols#cite_ref-Philadelphia_93-0", 111 | "label": "Philly’s pride flag", 112 | }, 113 | "polysexual": { 114 | "stripes": pastelise(["#F71BB9", "#08D569", "#1C91F6"]), 115 | "url": "https://rationalwiki.org/wiki/Polysexuality", 116 | }, 117 | "trans": { 118 | "stripes": ["#55CDFC", "#F7A8B8", "#DDD", "#F7A8B8", "#55CDFC"], 119 | "url": "https://en.wikipedia.org/wiki/Transgender_flags", 120 | }, 121 | "black trans": { 122 | "stripes": ["#55CDFC", "#F7A8B8", to_pastel("#2D2D2D"), "#F7A8B8", "#55CDFC"], 123 | "url": "https://en.wikipedia.org/wiki/File:Black_trans_flag.svg", 124 | }, 125 | "lesbian": { 126 | "stripes": [ 127 | "#B60063", 128 | "#C84896", 129 | "#E253AB", 130 | "#DDD", 131 | "#F0A7D2", 132 | "#D73F4F", 133 | "#990200", 134 | ], 135 | "url": "https://en.wikipedia.org/wiki/LGBT_symbols#Lesbianism", 136 | }, 137 | } 138 | 139 | black_stripe = ["#000000"] * 4 140 | blue_stripe = ["#0000c0"] * 4 141 | red_stripe = ["#fb0006"] 142 | white_stripe = ["#EEE"] * 4 143 | 144 | flags["leather"] = { 145 | "stripes": pastelise( 146 | black_stripe 147 | + blue_stripe 148 | + red_stripe 149 | + black_stripe 150 | + blue_stripe 151 | + white_stripe 152 | + blue_stripe 153 | + black_stripe 154 | + blue_stripe 155 | + black_stripe 156 | ), 157 | "url": "https://en.wikipedia.org/wiki/Leather_Pride_flag", 158 | } 159 | -------------------------------------------------------------------------------- /draw_valknut.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import math 4 | 5 | 6 | def x_component(triangular_x, triangular_y): 7 | result = triangular_x + triangular_y * math.cos(math.radians(60)) 8 | return "%.6f" % result 9 | 10 | 11 | def y_component(triangular_x, triangular_y): 12 | result = -triangular_y * math.sin(math.radians(60)) 13 | return "%.6f" % result 14 | 15 | 16 | def xy_position(triangular_x, triangular_y): 17 | return "%s,%s" % ( 18 | x_component(triangular_x, triangular_y), 19 | y_component(triangular_x, triangular_y) 20 | ) 21 | 22 | 23 | def get_valknut_tri_coordinates(bar_width, gap_width, stripe_count, stripe_start, stripe_end): 24 | """ 25 | Stripes are 1-indexed. 26 | """ 27 | # Coordinate axes 28 | # 29 | # (0, t2) 30 | # / 31 | # / 32 | # +------> (t1, 0) 33 | # 34 | # The shape to be drawn is defined as follows: 35 | # 36 | # (0, 6B+5W) 37 | # /\ 38 | # 39 | # (B, 4B+5W) 40 | # /\ 41 | # 42 | # .. .. .. .. 43 | # / / \ \ 44 | # / / \ \ 45 | # (0, 3B+3W) +-----+ (B, 3B+3W) (2B+2W, 3B+3W) +----+ (3B+2W, 3B+3W) 46 | # 47 | # (0, 2B+W) +-----+ (B, 2B+W) (3B+4W, 2B+W) +-----+ (4B+4W, 2B+W) 48 | # / / \ \ 49 | # / / \ \ 50 | # / +--------------------------------------------+ \ 51 | # / (B, B) (4B+5W, B) \ 52 | # / \ 53 | # (0, 0) +--------------------------------------------------------------+ (6B+5W, 0) 54 | # 55 | # But then we need to adjust for the part of the stripe we're drawing. 56 | # 57 | lower_stripe = (stripe_start - 1) / stripe_count 58 | upper_stripe = stripe_end / stripe_count 59 | 60 | coordinates_lower = [ 61 | ( 62 | lower_stripe * bar_width, 63 | lower_stripe * bar_width 64 | ), 65 | ( 66 | 6 * bar_width + 5 * gap_width - lower_stripe * (2 * bar_width), 67 | lower_stripe * bar_width 68 | ), 69 | ( 70 | 4 * bar_width + 4 * gap_width - lower_stripe * bar_width, 71 | 2 * bar_width + gap_width 72 | ), 73 | ( 74 | 4 * bar_width + 4 * gap_width - upper_stripe * bar_width, 75 | 2 * bar_width + gap_width, 76 | ), 77 | ( 78 | 6 * bar_width + 5 * gap_width - upper_stripe * (2 * bar_width), 79 | upper_stripe * bar_width, 80 | ), 81 | ( 82 | upper_stripe * bar_width, 83 | upper_stripe * bar_width 84 | ), 85 | ( 86 | upper_stripe * bar_width, 87 | 2 * bar_width + gap_width 88 | ), 89 | ( 90 | lower_stripe * bar_width, 91 | 2 * bar_width + gap_width 92 | ), 93 | ] 94 | 95 | coordinates_upper = [ 96 | (lower_stripe * bar_width, 3 * bar_width + 3 * gap_width), 97 | (upper_stripe * bar_width, 3 * bar_width + 3 * gap_width), 98 | ( 99 | upper_stripe * bar_width, 100 | 6 * bar_width + 5 * gap_width - upper_stripe * (2 * bar_width) 101 | ), 102 | ( 103 | 3 * bar_width + 2 * gap_width - upper_stripe * bar_width, 104 | 3 * bar_width + 3 * gap_width 105 | ), 106 | ( 107 | 3 * bar_width + 2 * gap_width - lower_stripe * bar_width, 108 | 3 * bar_width + 3 * gap_width 109 | ), 110 | ( 111 | lower_stripe * bar_width, 112 | 6 * bar_width + 5 * gap_width - lower_stripe * (2 * bar_width) 113 | ) 114 | ] 115 | 116 | return [ 117 | coordinates_lower, 118 | coordinates_upper, 119 | ] 120 | 121 | 122 | def draw_valknut(bar_width, gap_width, stripes): 123 | for index, fill_color in enumerate(stripes, start=1): 124 | stripe_start = index 125 | try: 126 | if stripes[index] == fill_color: 127 | stripe_end = index + 1 128 | else: 129 | stripe_end = index 130 | except IndexError: 131 | stripe_end = index 132 | 133 | for tri_coords in get_valknut_tri_coordinates( 134 | bar_width=bar_width, 135 | gap_width=gap_width, 136 | stripe_count=len(stripes), 137 | stripe_start=stripe_start, 138 | stripe_end=stripe_end 139 | ): 140 | xy_coords = [xy_position(*tc) for tc in tri_coords] 141 | xy_points = " ".join(xy_coords) 142 | 143 | yield f'' 144 | 145 | 146 | def get_valknut_svg(stripe1, stripe2, stripe3): 147 | lines = [ 148 | '' 149 | ] 150 | 151 | has_black = any("#000000" in s for s in (stripe1, stripe2, stripe3)) 152 | 153 | if has_black: 154 | background = "#222222" 155 | else: 156 | background = "black" 157 | 158 | lines.extend([ 159 | f'', 160 | 161 | # Centre the valknut on the page by trial and error. 162 | '', 163 | ]) 164 | 165 | bar_width = 50 166 | gap_width = 10 167 | 168 | for svg_line in draw_valknut( 169 | bar_width=bar_width, gap_width=gap_width, stripes=stripe1 170 | ): 171 | lines.append(svg_line) 172 | 173 | center = xy_position( 174 | 2 * bar_width + 5/3 * gap_width, 175 | 3 * bar_width + (8/3) * gap_width, 176 | ) 177 | 178 | lines.append(f'') 179 | 180 | for svg_line in draw_valknut( 181 | bar_width=bar_width, gap_width=gap_width, stripes=stripe2 182 | ): 183 | lines.append(svg_line) 184 | 185 | lines.append('') 186 | 187 | lines.append(f'') 188 | 189 | for svg_line in draw_valknut( 190 | bar_width=bar_width, gap_width=gap_width, stripes=stripe3 191 | ): 192 | lines.append(svg_line) 193 | 194 | lines.append('') 195 | lines.append('') 196 | 197 | lines.append(f''' 198 | 200 | YOUR COWARDLY BIGOTRY IS AN AFFRONT TO THE ALLFATHER''') 201 | 202 | lines.append('') 203 | 204 | return '\n'.join(lines) 205 | 206 | 207 | if __name__ == "__main__": 208 | print( 209 | get_valknut_svg( 210 | stripe1=["red", "orange", "yellow", "green", "blue", "purple"], 211 | stripe2=["#5BCEFA", "#F5A9B8", "white", "#F5A9B8", "#5BCEFA"], 212 | stripe3=["#000000", "#A3A3A3", "white", "#780378"], 213 | ) 214 | ) 215 | --------------------------------------------------------------------------------