├── .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 | > 
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 |
--------------------------------------------------------------------------------