Compare commits
	
		
			No commits in common. "main" and "feature-human-alt-text" have entirely different histories.
		
	
	
		
			main
			...
			feature-hu
		
	
		
							
								
								
									
										12
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -1,17 +1,5 @@ | |||||||
| # CHANGELOG.md | # CHANGELOG.md | ||||||
| 
 | 
 | ||||||
| ## v1.2.3 |  | ||||||
| 
 |  | ||||||
| Gave the bot the ability to reply with a customised pattern |  | ||||||
| 
 |  | ||||||
| ## v1.2.1 |  | ||||||
| 
 |  | ||||||
| Minor refactoring |  | ||||||
| 
 |  | ||||||
| ## v1.2.0 |  | ||||||
| 
 |  | ||||||
| I think this was when I added the gotosocial bot |  | ||||||
| 
 |  | ||||||
| ## v1.1.1 | ## v1.1.1 | ||||||
| 
 | 
 | ||||||
| Made the flashing transitions a bit better, but it still needs more work | Made the flashing transitions a bit better, but it still needs more work | ||||||
|  | |||||||
							
								
								
									
										136
									
								
								colour.txt
									
									
									
									
									
								
							
							
						
						
									
										136
									
								
								colour.txt
									
									
									
									
									
								
							| @ -1,136 +0,0 @@ | |||||||
| 255 250 250 snow |  | ||||||
| 248 248 255 ghost white |  | ||||||
| 245 245 245 white smoke |  | ||||||
| 220 220 220 gainsboro |  | ||||||
| 255 250 240 floral white |  | ||||||
| 253 245 230 old lace |  | ||||||
| 250 240 230 linen |  | ||||||
| 250 235 215 antique white |  | ||||||
| 255 239 213 papaya whip |  | ||||||
| 255 235 205 blanched almond |  | ||||||
| 255 228 196 bisque |  | ||||||
| 255 218 185 peach puff |  | ||||||
| 255 222 173 navajo white |  | ||||||
| 255 228 181 moccasin |  | ||||||
| 255 248 220 cornsilk |  | ||||||
| 255 255 240 ivory |  | ||||||
| 255 250 205 lemon chiffon |  | ||||||
| 255 245 238 seashell |  | ||||||
| 240 255 240 honeydew |  | ||||||
| 245 255 250 mint cream |  | ||||||
| 240 255 255 azure |  | ||||||
| 240 248 255 alice blue |  | ||||||
| 230 230 250 lavender |  | ||||||
| 255 240 245 lavender blush |  | ||||||
| 255 228 225 misty rose |  | ||||||
| 255 255 255 white |  | ||||||
| 0 0 0 black |  | ||||||
| 47 79 79 dark slate grey |  | ||||||
| 105 105 105 dim grey |  | ||||||
| 112 128 144 slate grey |  | ||||||
| 119 136 153 light slate grey |  | ||||||
| 190 190 190 grey |  | ||||||
| 211 211 211 light grey |  | ||||||
| 25 25 112 midnight blue |  | ||||||
| 0 0 128 navy |  | ||||||
| 0 0 128 navy blue |  | ||||||
| 100 149 237 cornflower blue |  | ||||||
| 72 61 139 dark slate blue |  | ||||||
| 106 90 205 slate blue |  | ||||||
| 123 104 238 medium slate blue |  | ||||||
| 132 112 255 light slate blue |  | ||||||
| 0 0 205 medium blue |  | ||||||
| 65 105 225 royal blue |  | ||||||
| 0 0 255 blue |  | ||||||
| 30 144 255 dodger blue |  | ||||||
| 0 191 255 deep sky blue |  | ||||||
| 135 206 235 sky blue |  | ||||||
| 135 206 250 light sky blue |  | ||||||
| 70 130 180 steel blue |  | ||||||
| 176 196 222 light steel blue |  | ||||||
| 173 216 230 light blue |  | ||||||
| 176 224 230 powder blue |  | ||||||
| 175 238 238 pale turquoise |  | ||||||
| 0 206 209 dark turquoise |  | ||||||
| 72 209 204 medium turquoise |  | ||||||
| 64 224 208 turquoise |  | ||||||
| 0 255 255 cyan |  | ||||||
| 224 255 255 light cyan |  | ||||||
| 95 158 160 cadet blue |  | ||||||
| 102 205 170 medium aquamarine |  | ||||||
| 127 255 212 aquamarine |  | ||||||
| 0 100 0 dark green |  | ||||||
| 85 107 47 dark olive green |  | ||||||
| 143 188 143 dark sea green |  | ||||||
| 46 139 87 sea green |  | ||||||
| 60 179 113 medium sea green |  | ||||||
| 32 178 170 light sea green |  | ||||||
| 152 251 152 pale green |  | ||||||
| 0 255 127 spring green |  | ||||||
| 124 252 0 lawn green |  | ||||||
| 0 255 0 green |  | ||||||
| 127 255 0 chartreuse |  | ||||||
| 0 250 154 medium spring green |  | ||||||
| 173 255 47 green yellow |  | ||||||
| 50 205 50 lime green |  | ||||||
| 154 205 50 yellow green |  | ||||||
| 34 139 34 forest green |  | ||||||
| 107 142 35 olive drab |  | ||||||
| 189 183 107 dark khaki |  | ||||||
| 240 230 140 khaki |  | ||||||
| 238 232 170 pale goldenrod |  | ||||||
| 250 250 210 light goldenrod yellow |  | ||||||
| 255 255 224 light yellow |  | ||||||
| 255 255 0 yellow |  | ||||||
| 255 215 0 gold |  | ||||||
| 238 221 130 light goldenrod |  | ||||||
| 218 165 32 goldenrod |  | ||||||
| 184 134 11 dark goldenrod |  | ||||||
| 188 143 143 rosy brown |  | ||||||
| 205 92 92 indian red |  | ||||||
| 139 69 19 saddle brown |  | ||||||
| 160 82 45 sienna |  | ||||||
| 205 133 63 peru |  | ||||||
| 222 184 135 burlywood |  | ||||||
| 245 245 220 beige |  | ||||||
| 245 222 179 wheat |  | ||||||
| 244 164 96 sandy brown |  | ||||||
| 210 180 140 tan |  | ||||||
| 210 105 30 chocolate |  | ||||||
| 178 34 34 firebrick |  | ||||||
| 165 42 42 brown |  | ||||||
| 233 150 122 dark salmon |  | ||||||
| 250 128 114 salmon |  | ||||||
| 255 160 122 light salmon |  | ||||||
| 255 165 0 orange |  | ||||||
| 255 140 0 dark orange |  | ||||||
| 255 127 80 coral |  | ||||||
| 240 128 128 light coral |  | ||||||
| 255 99 71 tomato |  | ||||||
| 255 69 0 orange red |  | ||||||
| 255 0 0 red |  | ||||||
| 255 105 180 hot pink |  | ||||||
| 255 20 147 deep pink |  | ||||||
| 255 192 203 pink |  | ||||||
| 255 182 193 light pink |  | ||||||
| 219 112 147 pale violet red |  | ||||||
| 176 48 96 maroon |  | ||||||
| 199 21 133 medium violet red |  | ||||||
| 208 32 144 violet red |  | ||||||
| 255 0 255 magenta |  | ||||||
| 238 130 238 violet |  | ||||||
| 221 160 221 plum |  | ||||||
| 218 112 214 orchid |  | ||||||
| 186 85 211 medium orchid |  | ||||||
| 153 50 204 dark orchid |  | ||||||
| 148 0 211 dark violet |  | ||||||
| 138 43 226 blue violet |  | ||||||
| 160 32 240 purple |  | ||||||
| 147 112 219 medium purple |  | ||||||
| 216 191 216 thistle |  | ||||||
| 169 169 169 dark grey |  | ||||||
| 0 0 139 dark blue |  | ||||||
| 0 139 139 dark cyan |  | ||||||
| 139 0 139 dark magenta |  | ||||||
| 139 0 0 dark red |  | ||||||
| 144 238 144 light green |  | ||||||
							
								
								
									
										101
									
								
								grayscale.txt
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								grayscale.txt
									
									
									
									
									
								
							| @ -1,101 +0,0 @@ | |||||||
| 255 255 255 white |  | ||||||
| 0 0 0 black |  | ||||||
| 3 3 3 99% grey |  | ||||||
| 5 5 5 98% grey |  | ||||||
| 8 8 8 97% grey |  | ||||||
| 10 10 10 96% grey |  | ||||||
| 13 13 13 95% grey |  | ||||||
| 15 15 15 94% grey |  | ||||||
| 18 18 18 93% grey |  | ||||||
| 20 20 20 92% grey |  | ||||||
| 23 23 23 91% grey |  | ||||||
| 26 26 26 90% grey |  | ||||||
| 28 28 28 89% grey |  | ||||||
| 31 31 31 88% grey |  | ||||||
| 33 33 33 87% grey |  | ||||||
| 36 36 36 86% grey |  | ||||||
| 38 38 38 85% grey |  | ||||||
| 41 41 41 84% grey |  | ||||||
| 43 43 43 83% grey |  | ||||||
| 46 46 46 82% grey |  | ||||||
| 48 48 48 81% grey |  | ||||||
| 51 51 51 80% grey |  | ||||||
| 54 54 54 79% grey |  | ||||||
| 56 56 56 78% grey |  | ||||||
| 59 59 59 77% grey |  | ||||||
| 61 61 61 76% grey |  | ||||||
| 64 64 64 75% grey |  | ||||||
| 66 66 66 74% grey |  | ||||||
| 69 69 69 73% grey |  | ||||||
| 71 71 71 72% grey |  | ||||||
| 74 74 74 71% grey |  | ||||||
| 77 77 77 70% grey |  | ||||||
| 79 79 79 69% grey |  | ||||||
| 82 82 82 68% grey |  | ||||||
| 84 84 84 67% grey |  | ||||||
| 87 87 87 66% grey |  | ||||||
| 89 89 89 65% grey |  | ||||||
| 92 92 92 64% grey |  | ||||||
| 94 94 94 63% grey |  | ||||||
| 97 97 97 62% grey |  | ||||||
| 99 99 99 61% grey |  | ||||||
| 102 102 102 60% grey |  | ||||||
| 105 105 105 59% grey |  | ||||||
| 107 107 107 58% grey |  | ||||||
| 110 110 110 57% grey |  | ||||||
| 112 112 112 56% grey |  | ||||||
| 115 115 115 55% grey |  | ||||||
| 117 117 117 54% grey |  | ||||||
| 120 120 120 53% grey |  | ||||||
| 122 122 122 52% grey |  | ||||||
| 125 125 125 51% grey |  | ||||||
| 127 127 127 50% grey |  | ||||||
| 130 130 130 49% grey |  | ||||||
| 133 133 133 48% grey |  | ||||||
| 135 135 135 47% grey |  | ||||||
| 138 138 138 46% grey |  | ||||||
| 140 140 140 45% grey |  | ||||||
| 143 143 143 44% grey |  | ||||||
| 145 145 145 43% grey |  | ||||||
| 148 148 148 42% grey |  | ||||||
| 150 150 150 41% grey |  | ||||||
| 153 153 153 40% grey |  | ||||||
| 156 156 156 39% grey |  | ||||||
| 158 158 158 38% grey |  | ||||||
| 161 161 161 37% grey |  | ||||||
| 163 163 163 36% grey |  | ||||||
| 166 166 166 35% grey |  | ||||||
| 168 168 168 34% grey |  | ||||||
| 171 171 171 33% grey |  | ||||||
| 173 173 173 32% grey |  | ||||||
| 176 176 176 31% grey |  | ||||||
| 179 179 179 30% grey |  | ||||||
| 181 181 181 29% grey |  | ||||||
| 184 184 184 28% grey |  | ||||||
| 186 186 186 27% grey |  | ||||||
| 189 189 189 26% grey |  | ||||||
| 191 191 191 25% grey |  | ||||||
| 194 194 194 24% grey |  | ||||||
| 196 196 196 23% grey |  | ||||||
| 199 199 199 22% grey |  | ||||||
| 201 201 201 21% grey |  | ||||||
| 204 204 204 20% grey |  | ||||||
| 207 207 207 19% grey |  | ||||||
| 209 209 209 18% grey |  | ||||||
| 212 212 212 17% grey |  | ||||||
| 214 214 214 16% grey |  | ||||||
| 217 217 217 15% grey |  | ||||||
| 219 219 219 14% grey |  | ||||||
| 222 222 222 13% grey |  | ||||||
| 224 224 224 12% grey |  | ||||||
| 227 227 227 11% grey |  | ||||||
| 229 229 229 10% grey |  | ||||||
| 232 232 232 9% grey |  | ||||||
| 235 235 235 8% grey |  | ||||||
| 237 237 237 7% grey |  | ||||||
| 240 240 240 6% grey |  | ||||||
| 242 242 242 5% grey |  | ||||||
| 245 245 245 4% grey |  | ||||||
| 247 247 247 3% grey |  | ||||||
| 250 250 250 2% grey |  | ||||||
| 252 252 252 1% grey |  | ||||||
							
								
								
									
										116
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										116
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -7,12 +7,11 @@ | |||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@observablehq/framework": "^1.13.0", |         "@observablehq/framework": "^1.13.0", | ||||||
|         "@resvg/resvg-js": "^2.6.2", |         "@resvg/resvg-js": "^2.6.2", | ||||||
|         "await-spawn": "^4.0.2", |  | ||||||
|         "d3": "^7.9.0", |         "d3": "^7.9.0", | ||||||
|         "d3-color": "^3.1.0", |         "d3-color": "^3.1.0", | ||||||
|         "d3-color-difference": "^0.1.3", |  | ||||||
|         "d3-dsv": "^3.0.1", |         "d3-dsv": "^3.0.1", | ||||||
|         "d3-time-format": "^4.1.0", |         "d3-time-format": "^4.1.0", | ||||||
|  |         "hex-color-to-color-name": "^1.0.2", | ||||||
|         "jsdom": "^26.0.0", |         "jsdom": "^26.0.0", | ||||||
|         "lodash.shuffle": "^4.2.0", |         "lodash.shuffle": "^4.2.0", | ||||||
|         "random": "^5.1.1", |         "random": "^5.1.1", | ||||||
| @ -1444,17 +1443,6 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", |       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", | ||||||
|       "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" |       "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" | ||||||
|     }, |     }, | ||||||
|     "node_modules/await-spawn": { |  | ||||||
|       "version": "4.0.2", |  | ||||||
|       "resolved": "https://registry.npmjs.org/await-spawn/-/await-spawn-4.0.2.tgz", |  | ||||||
|       "integrity": "sha512-GdADmeLJiMvGKJD3xWBcX40DMn07JNH1sqJYgYJZH7NTGJ3B1qDjKBKzxhhyR1hjIcnUGFUmE/+4D1HcHAJBAA==", |  | ||||||
|       "dependencies": { |  | ||||||
|         "bl": "^4.0.3" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">=10" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/b4a": { |     "node_modules/b4a": { | ||||||
|       "version": "1.6.7", |       "version": "1.6.7", | ||||||
|       "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", |       "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", | ||||||
| @ -1471,25 +1459,6 @@ | |||||||
|       "integrity": "sha512-Bw2PgKSrZ3uCuSV9WQ998c/GTJTd+9bWj97n7aDQMP8dP/exAZQlJeswPty0ISy+HZD+9Ex+C7CCnc9Q5QJFmQ==", |       "integrity": "sha512-Bw2PgKSrZ3uCuSV9WQ998c/GTJTd+9bWj97n7aDQMP8dP/exAZQlJeswPty0ISy+HZD+9Ex+C7CCnc9Q5QJFmQ==", | ||||||
|       "optional": true |       "optional": true | ||||||
|     }, |     }, | ||||||
|     "node_modules/base64-js": { |  | ||||||
|       "version": "1.5.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", |  | ||||||
|       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", |  | ||||||
|       "funding": [ |  | ||||||
|         { |  | ||||||
|           "type": "github", |  | ||||||
|           "url": "https://github.com/sponsors/feross" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "patreon", |  | ||||||
|           "url": "https://www.patreon.com/feross" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "consulting", |  | ||||||
|           "url": "https://feross.org/support" |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     }, |  | ||||||
|     "node_modules/bidi-js": { |     "node_modules/bidi-js": { | ||||||
|       "version": "1.0.3", |       "version": "1.0.3", | ||||||
|       "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", |       "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", | ||||||
| @ -1498,29 +1467,6 @@ | |||||||
|         "require-from-string": "^2.0.2" |         "require-from-string": "^2.0.2" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/bl": { |  | ||||||
|       "version": "4.1.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", |  | ||||||
|       "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", |  | ||||||
|       "dependencies": { |  | ||||||
|         "buffer": "^5.5.0", |  | ||||||
|         "inherits": "^2.0.4", |  | ||||||
|         "readable-stream": "^3.4.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/bl/node_modules/readable-stream": { |  | ||||||
|       "version": "3.6.2", |  | ||||||
|       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", |  | ||||||
|       "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", |  | ||||||
|       "dependencies": { |  | ||||||
|         "inherits": "^2.0.3", |  | ||||||
|         "string_decoder": "^1.1.1", |  | ||||||
|         "util-deprecate": "^1.0.1" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">= 6" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/brace-expansion": { |     "node_modules/brace-expansion": { | ||||||
|       "version": "2.0.1", |       "version": "2.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", |       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", | ||||||
| @ -1529,29 +1475,6 @@ | |||||||
|         "balanced-match": "^1.0.0" |         "balanced-match": "^1.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/buffer": { |  | ||||||
|       "version": "5.7.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", |  | ||||||
|       "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", |  | ||||||
|       "funding": [ |  | ||||||
|         { |  | ||||||
|           "type": "github", |  | ||||||
|           "url": "https://github.com/sponsors/feross" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "patreon", |  | ||||||
|           "url": "https://www.patreon.com/feross" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "consulting", |  | ||||||
|           "url": "https://feross.org/support" |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       "dependencies": { |  | ||||||
|         "base64-js": "^1.3.1", |  | ||||||
|         "ieee754": "^1.1.13" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/bundle-name": { |     "node_modules/bundle-name": { | ||||||
|       "version": "4.1.0", |       "version": "4.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", |       "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", | ||||||
| @ -1843,19 +1766,6 @@ | |||||||
|         "node": ">=12" |         "node": ">=12" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/d3-color-difference": { |  | ||||||
|       "version": "0.1.3", |  | ||||||
|       "resolved": "https://registry.npmjs.org/d3-color-difference/-/d3-color-difference-0.1.3.tgz", |  | ||||||
|       "integrity": "sha512-yAGiVdTZR/wpI66n85xvkTvjxFth0IuGrEeX/anl1Q5rzNc2/V7oOjoJdqQnahOuS+SgbAR0zu8T0SYY7hGTtw==", |  | ||||||
|       "dependencies": { |  | ||||||
|         "d3-color": "^1.1.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/d3-color-difference/node_modules/d3-color": { |  | ||||||
|       "version": "1.4.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", |  | ||||||
|       "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" |  | ||||||
|     }, |  | ||||||
|     "node_modules/d3-contour": { |     "node_modules/d3-contour": { | ||||||
|       "version": "4.0.2", |       "version": "4.0.2", | ||||||
|       "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", |       "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", | ||||||
| @ -2572,6 +2482,11 @@ | |||||||
|         "he": "bin/he" |         "he": "bin/he" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/hex-color-to-color-name": { | ||||||
|  |       "version": "1.0.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/hex-color-to-color-name/-/hex-color-to-color-name-1.0.2.tgz", | ||||||
|  |       "integrity": "sha512-YKPBFTSbYIHH8YKcJB4Q5PV+Tr+kvDXpV60BcPMUu5CSZUcc/qOOx7lkr7luk6MSXKd5A82yfPGZTgedIdQ+aA==" | ||||||
|  |     }, | ||||||
|     "node_modules/highlight.js": { |     "node_modules/highlight.js": { | ||||||
|       "version": "11.11.1", |       "version": "11.11.1", | ||||||
|       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", |       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", | ||||||
| @ -2649,25 +2564,6 @@ | |||||||
|         "node": ">=0.10.0" |         "node": ">=0.10.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/ieee754": { |  | ||||||
|       "version": "1.2.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", |  | ||||||
|       "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", |  | ||||||
|       "funding": [ |  | ||||||
|         { |  | ||||||
|           "type": "github", |  | ||||||
|           "url": "https://github.com/sponsors/feross" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "patreon", |  | ||||||
|           "url": "https://www.patreon.com/feross" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "type": "consulting", |  | ||||||
|           "url": "https://feross.org/support" |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     }, |  | ||||||
|     "node_modules/immediate": { |     "node_modules/immediate": { | ||||||
|       "version": "3.0.6", |       "version": "3.0.6", | ||||||
|       "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", |       "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", | ||||||
|  | |||||||
| @ -11,12 +11,11 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@observablehq/framework": "^1.13.0", |     "@observablehq/framework": "^1.13.0", | ||||||
|     "@resvg/resvg-js": "^2.6.2", |     "@resvg/resvg-js": "^2.6.2", | ||||||
|     "await-spawn": "^4.0.2", |  | ||||||
|     "d3": "^7.9.0", |     "d3": "^7.9.0", | ||||||
|     "d3-color": "^3.1.0", |     "d3-color": "^3.1.0", | ||||||
|     "d3-color-difference": "^0.1.3", |  | ||||||
|     "d3-dsv": "^3.0.1", |     "d3-dsv": "^3.0.1", | ||||||
|     "d3-time-format": "^4.1.0", |     "d3-time-format": "^4.1.0", | ||||||
|  |     "hex-color-to-color-name": "^1.0.2", | ||||||
|     "jsdom": "^26.0.0", |     "jsdom": "^26.0.0", | ||||||
|     "lodash.shuffle": "^4.2.0", |     "lodash.shuffle": "^4.2.0", | ||||||
|     "random": "^5.1.1", |     "random": "^5.1.1", | ||||||
|  | |||||||
							
								
								
									
										261
									
								
								poptimal.js
									
									
									
									
									
								
							
							
						
						
									
										261
									
								
								poptimal.js
									
									
									
									
									
								
							| @ -2,27 +2,24 @@ import {  Resvg } from "@resvg/resvg-js"; | |||||||
| import { promises } from "fs"; | import { promises } from "fs"; | ||||||
| import { JSDOM } from "jsdom"; | import { JSDOM } from "jsdom"; | ||||||
| import * as d3 from "d3"; | import * as d3 from "d3"; | ||||||
| import * as d3c from "d3-color"; |  | ||||||
| import yargs from "yargs/yargs"; | import yargs from "yargs/yargs"; | ||||||
| import { hideBin } from "yargs/helpers"; | import { hideBin } from "yargs/helpers"; | ||||||
| import spawn from "await-spawn"; | 
 | ||||||
| import random from "random"; | import random from "random"; | ||||||
| 
 | 
 | ||||||
| const xmlns = "http://www.w3.org/2000/xmlns/"; | const xmlns = "http://www.w3.org/2000/xmlns/"; | ||||||
| const xlinkns = "http://www.w3.org/1999/xlink"; | const xlinkns = "http://www.w3.org/1999/xlink"; | ||||||
| const svgns = "http://www.w3.org/2000/svg"; | const svgns = "http://www.w3.org/2000/svg"; | ||||||
| 
 | 
 | ||||||
| import {RADIUS_OPTS, RADIUS_DESC, radius_func, DotMaker} from './src/components/dots.js'; | import {RADIUS_OPTS, DotMaker} from './src/components/dots.js'; | ||||||
| import {PALETTES} from './src/components/palettes.js'; | import {PALETTES} from './src/components/palettes.js'; | ||||||
| import {ColourNamer} from './src/components/colour_namer.js'; | 
 | ||||||
|  | import { GetColorName } from "hex-color-to-color-name"; | ||||||
| 
 | 
 | ||||||
| const CELL = 10; | const CELL = 10; | ||||||
| const MAG = 2; | const MAG = 2; | ||||||
| const WIDTH = 200; | const WIDTH = 20; | ||||||
| const HEIGHT = WIDTH; | const HEIGHT = WIDTH; | ||||||
| // number of pixels which have to be visible for a colour to be
 |  | ||||||
| // mentioned in the alt text
 |  | ||||||
| const VISIBLE_DOG = 400; |  | ||||||
| 
 | 
 | ||||||
| function randomise_params() { | function randomise_params() { | ||||||
| 	const palette_name = random.choice(Array.from(PALETTES.keys())); | 	const palette_name = random.choice(Array.from(PALETTES.keys())); | ||||||
| @ -36,98 +33,26 @@ function randomise_params() { | |||||||
| 		f: random.choice(RADIUS_OPTS), | 		f: random.choice(RADIUS_OPTS), | ||||||
| 		r: random.float(0, 0.4), | 		r: random.float(0, 0.4), | ||||||
| 	}}); | 	}}); | ||||||
| 	const cell = 5 + random.float() * random.float() * 55; |  | ||||||
| 	return { | 	return { | ||||||
| 		background: palette[0], | 		background: palette[0], | ||||||
| 		palette: palette_name, | 		palette: palette_name, | ||||||
| 		patterns: patterns, | 		patterns: patterns | ||||||
| 		cell: cell, |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function load_or_random_params(paramf) { | function colour_to_text(d3color) { | ||||||
| 	if( paramf ) { | 	const rawname = GetColorName(d3color.formatHex()).toLowerCase(); | ||||||
| 		const pjson = await promises.readFile(paramf); | 	// some return values are things like "cyan / aqua": take the first
 | ||||||
|   		const params = JSON.parse(pjson); | 	const parts = rawname.split(/\//); | ||||||
| 		params.background = d3c.color(params.background); | 	return parts[0].trim(); | ||||||
| 		params.patterns.forEach((p) => { |  | ||||||
| 			p.colour = d3c.color(p.colour); |  | ||||||
| 		});	 |  | ||||||
| 		return params; |  | ||||||
| 	} |  | ||||||
| 	return randomise_params(); |  | ||||||
| }  | }  | ||||||
| 
 | 
 | ||||||
| async function save_params(paramf, params) { | function image_description(params) { | ||||||
| 	params.background = params.background.formatHex(); | 	const bgc = colour_to_text(params.background); | ||||||
| 	params.patterns.forEach((p) => { | 	const dotc = params.patterns.map((p) => colour_to_text(p.colour)); | ||||||
| 		p.colour = p.colour.formatHex(); |  | ||||||
| 	}); |  | ||||||
| 	await promises.writeFile(paramf, JSON.stringify(params)); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| // lol the best way I found to to this was imagemagick!
 |  | ||||||
| 
 |  | ||||||
| async function get_histogram(imgfile) { |  | ||||||
| 	try { |  | ||||||
| 		const bl = await spawn('convert', [ imgfile, '-format', '%c', 'histogram:info:' ]); |  | ||||||
| 		return parse_histogram(bl.toString()); |  | ||||||
| 	} catch (e) { |  | ||||||
| 		console.log(e); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| function parse_histogram(convert_out) { |  | ||||||
| 	const colour_re = /(\d+): \(\d+,\d+,\d+,\d+\) (#[A-F0-9]+) /; |  | ||||||
| 	const colours = new Map(); |  | ||||||
| 	convert_out.split("\n").forEach((l) => { |  | ||||||
| 		const m = l.match(colour_re); |  | ||||||
| 		if( m ) { |  | ||||||
| 			const colour = m[2].substring(0, 7); |  | ||||||
| 			colours.set(colour, Number(m[1])); |  | ||||||
| 		} |  | ||||||
| 	}); |  | ||||||
| 	return new Map([...colours].sort((a, b) => b[1] - a[1])); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| function colour_visible(hist, colour) { |  | ||||||
| 	const hexcolour = colour.formatHex().toUpperCase(); |  | ||||||
| 	return hist.get(hexcolour) > VISIBLE_DOG; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| function image_description(namer, params, histogram) { |  | ||||||
| 	const colours = [ |  | ||||||
| 		params.background, |  | ||||||
| 		params.patterns[0].colour, |  | ||||||
| 		params.patterns[1].colour, |  | ||||||
| 	]; |  | ||||||
| 	const i_vis = [0, 1, 2].filter((i) => colour_visible(histogram, colours[i])); |  | ||||||
| 	const named_colours = i_vis.map((i) => namer.colour_to_text(colours[i])); |  | ||||||
| 	const gradients = i_vis.map((i) => { |  | ||||||
| 		if( i > 0 ) { |  | ||||||
| 			return RADIUS_DESC[params.patterns[i - 1].f]; |  | ||||||
| 		} else { |  | ||||||
| 			return ''; |  | ||||||
| 		} |  | ||||||
| 	}); |  | ||||||
| 	if( named_colours.length == 1 ) { |  | ||||||
| 		return `A solid field of ${named_colours[0]}`; |  | ||||||
| 	} |  | ||||||
| 	if( named_colours.length > 1 ) { |  | ||||||
| 		const bgc = named_colours[0]; |  | ||||||
| 	const a = bgc.match(/^[aeiou]/) ? 'an' : 'a'; | 	const a = bgc.match(/^[aeiou]/) ? 'an' : 'a'; | ||||||
| 		const dot_desc = named_colours.slice(1); | 	return `A pattern of ${dotc[0]} and ${dotc[1]} dots on ${a} ${bgc} background`; | ||||||
| 		const gs = gradients.slice(1); | 
 | ||||||
| 		const pattern_desc = dot_desc.map((d, i) => `${d} dots ${gs[i]}`);  |  | ||||||
| 		const patterns = pattern_desc.join(' and '); |  | ||||||
| 		return `A pattern of ${patterns} on ${a} ${bgc} background`; |  | ||||||
| 	} |  | ||||||
| 	return ""; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -136,34 +61,30 @@ function poptimal_svg(params) { | |||||||
| 	const document = window.document; | 	const document = window.document; | ||||||
| 	const container = d3.select(document.body).append("div"); | 	const container = d3.select(document.body).append("div"); | ||||||
| 
 | 
 | ||||||
| 	const width = WIDTH / params.cell; | 	const dm = new DotMaker(WIDTH); | ||||||
| 	const height = WIDTH / params.cell; |  | ||||||
| 
 |  | ||||||
| 	const dm = new DotMaker(width, height); |  | ||||||
| 
 | 
 | ||||||
| 	const svg = container.append("svg") | 	const svg = container.append("svg") | ||||||
| 	  .attr("width", WIDTH * MAG) | 	  .attr("width", WIDTH * CELL * MAG) | ||||||
| 	  .attr("height", HEIGHT * MAG) | 	  .attr("height", HEIGHT * CELL * MAG) | ||||||
| 	  .attr("viewBox", [ 0, 0, width, height ]); | 	  .attr("viewBox", [ 0, 0, WIDTH, HEIGHT ]); | ||||||
| 
 | 
 | ||||||
| 	const background = svg.append("rect") | 	const background = svg.append("rect") | ||||||
| 	  .attr("x", 0) | 	  .attr("x", 0) | ||||||
| 	  .attr("y", 0) | 	  .attr("y", 0) | ||||||
| 	  .attr("width", width) | 	  .attr("width", WIDTH) | ||||||
| 	  .attr("height", height) | 	  .attr("height", WIDTH) | ||||||
| 	  .attr("fill", params.background); | 	  .attr("fill", params.background); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	params.patterns.map((p) =>  { | 	params.patterns.map((p) =>  { | ||||||
| 		const dots = dm.dots(1 / p.m, p.n, false); | 		const dots = dm.dots(1 / p.m, p.n); | ||||||
| 		const rfunc = radius_func(p.f); |  | ||||||
| 		const dots_g = svg.append("g") | 		const dots_g = svg.append("g") | ||||||
| 	  			.attr("id", `dots${p.i}`); | 	  			.attr("id", `dots${p.i}`); | ||||||
| 	 | 	 | ||||||
| 		dots_g.selectAll("circle") | 		dots_g.selectAll("circle") | ||||||
| 		  .data(dots) | 		  .data(dots) | ||||||
| 		  .join("circle") | 		  .join("circle") | ||||||
| 		  .attr("r", (d) => rfunc(d, p.r, width, height)) | 		  .attr("r", (d) => dm.radius(d, p.f, p.r)) | ||||||
| 		  .attr("fill", p.colour) | 		  .attr("fill", p.colour) | ||||||
| 		  .attr("cx", (d) => d.x) | 		  .attr("cx", (d) => d.x) | ||||||
| 		  .attr("cy", (d) => d.y); | 		  .attr("cy", (d) => d.y); | ||||||
| @ -179,61 +100,7 @@ function poptimal_svg(params) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async function send_replies(argv, cf) { | async function post_image(image, alt_text, cf) { | ||||||
| 
 |  | ||||||
| 	const mentionsjson = await promises.readFile(cf.mentions); |  | ||||||
|   	const oldids = JSON.parse(mentionsjson).map((m) => m.id); |  | ||||||
| 
 |  | ||||||
| 	const params = new URLSearchParams(); |  | ||||||
| 
 |  | ||||||
| 	params.append("types", ["mention"]) // this isn't working
 |  | ||||||
| 	                                    // so filter it explicitly
 |  | ||||||
| 	const notifications_url = `${cf.base_url}/api/v1/notifications?${params}`; |  | ||||||
| 	const headers = { |  | ||||||
| 		'Authorization': `Bearer ${cf.access_token}`, |  | ||||||
| 	}; |  | ||||||
| 	const resp = await fetch(notifications_url, { |  | ||||||
| 		method: 'GET', |  | ||||||
| 		headers: headers, |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	const bodyjson = await resp.text(); |  | ||||||
| 	const response = JSON.parse(bodyjson); |  | ||||||
| 	const mentions = response.filter((s) => s.type === "mention").map((s) => { |  | ||||||
|     		const account = s.account; |  | ||||||
|     		const status = s.status; |  | ||||||
|     		return { |  | ||||||
|         		id: s.id, |  | ||||||
|         		created: s.created_at, |  | ||||||
|         		status_id: status.id, |  | ||||||
|         		account: account.acct, |  | ||||||
|         		url: status.url, |  | ||||||
|         		in_reply_to_id: status.in_reply_to_id, |  | ||||||
|         		visibility: status.visibility |  | ||||||
|     		}; |  | ||||||
|     	}); |  | ||||||
| 	const newmentions = mentions.filter((m) => !oldids.includes(m.id)); |  | ||||||
| 	for (const mention of newmentions) { |  | ||||||
|     	    console.log(JSON.stringify(mention, null, 2)); |  | ||||||
|     	    if( mention.in_reply_to_id ) { |  | ||||||
|         	    console.log(`won't reply to ${mention.id} as it's a reply to ${mention.in_reply_to_id}`); |  | ||||||
|     	    } else { |  | ||||||
|     	    	console.log(`new mention ${mention.id}`); |  | ||||||
|         	    const reply = { |  | ||||||
|             	    	id: mention.status_id, |  | ||||||
|             	    	account: `@${mention.account}`, |  | ||||||
|             	    	visibility: mention.visibility, |  | ||||||
|         	    }; |  | ||||||
|         	     |  | ||||||
|     	    	post(argv, cf, reply); |  | ||||||
|     	    } |  | ||||||
| 	} |  | ||||||
|         if( !argv.n ) { |  | ||||||
| 		await promises.writeFile(cf.mentions, JSON.stringify(mentions, null, 4)); |  | ||||||
|         } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| async function post_image(image, alt_text, cf, reply) { |  | ||||||
| 	const status_url = `${cf.base_url}/api/v1/statuses`; | 	const status_url = `${cf.base_url}/api/v1/statuses`; | ||||||
| 	const media_url = `${cf.base_url}/api/v1/media`; | 	const media_url = `${cf.base_url}/api/v1/media`; | ||||||
| 	const headers = { | 	const headers = { | ||||||
| @ -253,52 +120,34 @@ async function post_image(image, alt_text, cf, reply) { | |||||||
| 	const bodyjson = await resp.text(); | 	const bodyjson = await resp.text(); | ||||||
| 	const response = JSON.parse(bodyjson); | 	const response = JSON.parse(bodyjson); | ||||||
| 	const media_id = response["id"]; | 	const media_id = response["id"]; | ||||||
| 	const status_body = { media_ids: [ media_id ] }; | 
 | ||||||
| 	if( reply ) { |  | ||||||
|     	    status_body["in_reply_to_id"] = reply.id; |  | ||||||
|     	    status_body["status"] = reply.account; |  | ||||||
|     	    status_body["visibility"] = reply.visibility; |  | ||||||
| 	} |  | ||||||
| 	headers['Accept'] = 'application/json'; | 	headers['Accept'] = 'application/json'; | ||||||
| 	headers['Content-Type'] = 'application/json'; | 	headers['Content-Type'] = 'application/json'; | ||||||
| 	const resp2 = await fetch(status_url, { | 	const resp2 = await fetch(status_url, { | ||||||
| 		method: 'POST', | 		method: 'POST', | ||||||
| 		headers: headers, | 		headers: headers, | ||||||
| 		body: JSON.stringify(status_body) | 		body: JSON.stringify({ media_ids: [ media_id ] }) | ||||||
| 	}); | 	}); | ||||||
| 	const bodyjson2 = await resp2.text(); | 	const bodyjson2 = await resp2.text(); | ||||||
|  | 	console.log(bodyjson2); | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | async function main() { | ||||||
|  | 	const argv = yargs(hideBin(process.argv)) | ||||||
|  | 		.usage("Usage: -s SIZE -o output.png -c config.json") | ||||||
|  | 		.default('s', 1200) | ||||||
|  | 		.default('g', 'config.json').argv; | ||||||
| 
 | 
 | ||||||
|  | 	const cfjson = await promises.readFile(argv.g); | ||||||
|  |   	const cf = JSON.parse(cfjson); | ||||||
| 
 | 
 | ||||||
| 
 |   	const fn = argv.o || String(Date.now()) + '.png'; | ||||||
| function make_unique_filename(reply_to) { |  | ||||||
| 
 |  | ||||||
|         const ts = String(Date.now()); |  | ||||||
|         if( reply_to ) { |  | ||||||
|             return `${ts}-${reply_to.id}`; |  | ||||||
|         } else { |  | ||||||
|             return ts; |  | ||||||
|         } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| async function post(argv, cf, reply_to) { |  | ||||||
| 	const filebase = make_unique_filename(reply_to); |  | ||||||
|  	const fn = (argv.o || filebase) + '.png'; |  | ||||||
| 	const jsfn = (argv.o || filebase) + '.json'; |  | ||||||
| 
 | 
 | ||||||
|   	const imgfile = cf['working_dir'] + '/' + fn; |   	const imgfile = cf['working_dir'] + '/' + fn; | ||||||
| 	const paramsfile = cf['working_dir'] + '/' + jsfn; |  | ||||||
| 
 |  | ||||||
| 	console.log(`Generating ${imgfile}`); |  | ||||||
| 	const params = await load_or_random_params(argv.p); |  | ||||||
| 	const colourf = params.palette === 'grayscale' ? cf['grayscale'] : cf['colour']; |  | ||||||
| 
 |  | ||||||
| 	const namer = new ColourNamer(); |  | ||||||
| 	await namer.load_colours(colourf); |  | ||||||
| 
 | 
 | ||||||
|  | 	const params = randomise_params(); | ||||||
|  | 	const alt_text = image_description(params); | ||||||
| 
 | 
 | ||||||
| 	const svg = poptimal_svg(params); | 	const svg = poptimal_svg(params); | ||||||
| 	const opts = { | 	const opts = { | ||||||
| @ -314,41 +163,13 @@ async function post(argv, cf, reply_to) { | |||||||
|   	const pngBuffer = pngData.asPng(); |   	const pngBuffer = pngData.asPng(); | ||||||
| 
 | 
 | ||||||
|   	await promises.writeFile(imgfile, pngBuffer); |   	await promises.writeFile(imgfile, pngBuffer); | ||||||
|   	// generate the alt_text last to check the image file histogram
 | 	console.log(imgfile); | ||||||
|   	// so we don't include obscured colours
 |  | ||||||
|   	const hist = await get_histogram(imgfile); |  | ||||||
| 	const alt_text = image_description(namer, params, hist); |  | ||||||
| 	params.alt_text = alt_text; |  | ||||||
| 	await save_params(paramsfile, params); |  | ||||||
| 	console.log(alt_text); | 	console.log(alt_text); | ||||||
|   	if( cf['base_url'] && !argv.n ) { |   	if( cf['base_url'] ) { | ||||||
|   		await post_image(imgfile, alt_text, cf, reply_to); |   		await post_image(imgfile, alt_text, cf); | ||||||
|   	} |   	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async function main() { |  | ||||||
| 	const argv = yargs(hideBin(process.argv)) |  | ||||||
| 		.usage("Usage: -s SIZE [-o output] [-r] [-n] -c config.json [-p params.json]") |  | ||||||
| 		.default('s', 1200) |  | ||||||
| 		.default('c', 'config.json').argv; |  | ||||||
| 
 |  | ||||||
| 	const cfjson = await promises.readFile(argv.c); |  | ||||||
|   	const cf = JSON.parse(cfjson); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         if( argv.r ) { |  | ||||||
|                 if( !cf.base_url ) { |  | ||||||
|                     	console.log("Can't check mentions without base_url"); |  | ||||||
|                 } else { |  | ||||||
|        	     		send_replies(argv, cf); |  | ||||||
|                 } |  | ||||||
|         } else { |  | ||||||
|         	post(argv, cf); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| main(); | main(); | ||||||
| @ -1,39 +0,0 @@ | |||||||
| 
 |  | ||||||
| import { promises } from "fs"; |  | ||||||
| import * as d3 from "d3-color"; |  | ||||||
| import * as d3cd from "d3-color-difference"; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class ColourNamer { |  | ||||||
| 	constructor() { |  | ||||||
| 		this.colmap = new Map(); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	async load_colours(colourfile) { |  | ||||||
| 		const colourBuff = await promises.readFile(colourfile); |  | ||||||
| 		const colours = colourBuff.toString(); |  | ||||||
| 		const seen = new Set();  |  | ||||||
| 		colours.split(/\n/).map((l) => { |  | ||||||
| 			const m = l.match(/(\d+)\s+(\d+)\s+(\d+)\s+(.*)/); |  | ||||||
| 			if( m ) { |  | ||||||
| 				const sig = `${m[1]},${m[2]},${m[3]}`; |  | ||||||
| 				if( !seen.has(sig) ) { |  | ||||||
| 					this.colmap.set(m[4], d3.rgb(m[1], m[2], m[3])); |  | ||||||
| 					seen.add(sig); |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	colour_to_text(d3color) { |  | ||||||
| 		const diffs = [] |  | ||||||
| 		for( const [ name, colour] of this.colmap) { |  | ||||||
| 			diffs.push({name: name, dist: d3cd.differenceCie76(colour, d3color)}); |  | ||||||
| 		} |  | ||||||
| 		diffs.sort((a, b) => a.dist - b.dist); |  | ||||||
| 		return diffs[0].name; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| export { ColourNamer} |  | ||||||
| @ -11,118 +11,87 @@ const RADIUS_OPTS = [ | |||||||
| 	"right-down", | 	"right-down", | ||||||
| 	"left-up", | 	"left-up", | ||||||
| 	"left-down", | 	"left-down", | ||||||
| 	"circle-in", | 	"in", | ||||||
| 	"circle-out", | 	"out", | ||||||
| 	"hyper-in", |  | ||||||
| 	"hyper-out", |  | ||||||
| 	"grid", |  | ||||||
| 	"noise", | 	"noise", | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const RADIUS_DESC = { |  | ||||||
| 	"const": "of constant size", |  | ||||||
| 	"right": "getting larger towards the right", |  | ||||||
| 	"left": "getting larger towards the left", |  | ||||||
| 	"up": "getting larger towards the top", |  | ||||||
| 	"down": "getting larger towards the bottom", |  | ||||||
| 	"right-up": "getting larger towards the upper right", |  | ||||||
| 	"right-down": "getting larger towards the lower right", |  | ||||||
| 	"left-up": "getting larger towards the upper left", |  | ||||||
| 	"left-down": "getting larger towards the lower left", |  | ||||||
| 	"circle-in": "forming a circle at the centre", |  | ||||||
| 	"circle-out": "leaving a circular gap at the centre", |  | ||||||
| 	"hyper-in": "forming a starlike pattern at the centre", |  | ||||||
| 	"hyper-out": "leaving a starlike gap in the centre", |  | ||||||
| 	"noise": "of random sizes", |  | ||||||
| 	"grid": "forming a grid pattern", |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function distance(dx, dy) { | function distance(dx, dy) { | ||||||
| 	return Math.sqrt(dx ** 2 + dy ** 2); | 	return Math.sqrt(dx ** 2 + dy ** 2); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| function int_range(v1, v2) { | function int_range(v1, v2) { | ||||||
| 	const vs = [v1, v2]; | 	const vs = [v1, v2]; | ||||||
| 	vs.sort((a, b) => a - b); | 	vs.sort((a, b) => a - b); | ||||||
| 	const low = Math.floor(vs[0] - 1); | 	const low = Math.floor(vs[0]); | ||||||
| 	const high = Math.ceil(vs[1] + 1); | 	const high = Math.ceil(vs[1]); | ||||||
| 	return [...Array(high - low + 1).keys()].map((i) => i + low); | 	return [...Array(high - low + 1).keys()].map((i) => i + low); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function randint(min, max) { |  | ||||||
| 	return min + Math.floor(Math.random() * (max + 1)); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class DotMaker { | class DotMaker { | ||||||
| 	constructor(width, height) { | 	constructor(width) { | ||||||
| 		this.width = width; | 		this.width = width; | ||||||
| 		this.height = height; |  | ||||||
| 		this.wh = 0.5 * (width + height); |  | ||||||
| 		this.cx = 0.5 * width; | 		this.cx = 0.5 * width; | ||||||
| 		this.cy = 0.5 * height; | 		this.cy = 0.5 * width; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	dots(m, n, clip) { | 	dots(m, n) { | ||||||
| 		if( m - n === 0 ) { | 		if( m - n === 0 ) { | ||||||
| 			return []; | 			return []; | ||||||
| 		} | 		} | ||||||
| 		const ps = []; | 		const ps = []; | ||||||
| 		const is = int_range(-this.width, this.height / m) | 		const is = int_range(-this.width, this.width / m) | ||||||
| 		is.map((i) => { | 		is.map((i) => { | ||||||
| 			const js = int_range(m * i + (m - n) * this.width, m * i) | 			const js = int_range(m * i + (m - n) * this.width, m * i) | ||||||
| 			js.map((j) => { | 			js.map((j) => { | ||||||
| 				const x = (j - m * i) / (m - n); | 				const x = (j - m * i) / (m - n); | ||||||
| 				const y = m * (x + i); | 				const y = m * (x + i); | ||||||
| 				if( !clip || (x > 0 && y > 0 && x < this.width && y < this.height) ) { | 				if( x > 0 && y > 0 && x < this.width && y < this.width ) { | ||||||
| 					ps.push({i:i, j:j, x:x, y:y}); | 					ps.push({i:i, j:j, x:x, y:y}); | ||||||
| 				} | 				} | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
| 		return ps; | 		return ps; | ||||||
| 	} | 	} | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| 
 | 	radius(d, func, maxr) { | ||||||
| function radius_func(func) { |  | ||||||
| 	  	switch (func) { | 	  	switch (func) { | ||||||
| 	    	case "const": | 	    	case "const": | ||||||
|       		return (d, r, w, h) => r; | 	      		return maxr; | ||||||
| 	    	case "right": | 	    	case "right": | ||||||
|       		return (d, r, w, h) => r * d.x / w; | 	      		return maxr * d.x / this.width; | ||||||
| 	    	case "left": | 	    	case "left": | ||||||
|       		return (d, r, w, h) => r * (w - d.x) / w; | 	      		return maxr * (this.width - d.x) / this.width; | ||||||
| 	    	case "down": | 	    	case "down": | ||||||
|       		return (d, r, w, h) => r * d.y / h; | 	      		return maxr * d.y / this.width; | ||||||
| 	    	case "up": | 	    	case "up": | ||||||
|       		return (d, r, w, h) => r * (h - d.y) / h; | 	      		return maxr * (this.width - d.y) / this.width; | ||||||
| 	      	case "right-up": | 	      	case "right-up": | ||||||
|       		return (d, r, w, h) => r * (d.x + h - d.y) / (w + h); | 	      		return 0.5 * maxr * (d.x + this.width - d.y) / this.width; | ||||||
| 	      	case "left-up": | 	      	case "left-up": | ||||||
|       		return (d, r, w, h) => r * (w - d.x + h - d.y) / (w + h); | 	      		return 0.5 * maxr * (this.width - d.x + this.width - d.y) / this.width; | ||||||
| 	      	case "right-down": | 	      	case "right-down": | ||||||
|       		return (d, r, w, h) => r * (d.x + d.y) / (w + h); | 	      		return 0.5 * maxr * (d.x + d.y) / this.width; | ||||||
| 	      	case "left-down": | 	      	case "left-down": | ||||||
|       		return (d, r, w, h) => r * (w - d.x + d.y) / (w + h); | 	      		return 0.5 * maxr * (this.width - d.x + d.y) / this.width; | ||||||
|       	case "circle-out": | 	      	case "out": | ||||||
|       		return (d, r, w, h) => 4 * r * distance((d.x - w / 2), (d.y - h / 2)) / (w + h); | 	      		return 2 * maxr * distance((d.x - this.cx), (d.y - this.cy)) / this.width; | ||||||
|       	case "circle-in": | 	      	case "in": | ||||||
|       		return (d, r, w, h) => 4 * r * (0.5 * (w + h) - distance((d.x - w / 2), (d.y - h / 2))) / (w + h); | 	      		return 2 * maxr * (0.5 * this.width - distance((d.x - this.cx), (d.y - this.cy))) / this.width; | ||||||
|       	case "hyper-in": | 
 | ||||||
|       		return (d, r, w, h) => 2 * r * (1 - Math.abs((d.x - w / 2) * (d.y - h / 2)) / (w + h)); | 	      	// case "hyper-out":
 | ||||||
|       	case "hyper-out": | 	      	// 	return 2 * maxr * Math.abs(d.x - this.cx) (d.y - this.cy)) / this.width;
 | ||||||
|       		return (d, r, w, h) => 2 * r * Math.abs((d.x - w / 2) * (d.y - h / 2)) / (w + h); | 	      	// case "hyoer-in":
 | ||||||
|  | 	      	// 	return 2 * maxr * (0.5 * this.width - distance((d.x - this.cx), (d.y - this.cy))) / this.width;
 | ||||||
|  | 
 | ||||||
| 	      	case "noise": | 	      	case "noise": | ||||||
|       		return (d, r, w, h) => r * Math.random(); | 	      		return maxr * Math.random(); | ||||||
|       	case "grid": |  | ||||||
|       		const xk = Math.PI * (2 * randint(1, 10) - 1); |  | ||||||
|       		const yk = Math.PI * (2 * randint(1, 10) - 1); |  | ||||||
|       		return (d, r, w, h) => r * (0.5 + 0.5 * (Math.sin(xk * d.x / w) + Math.sin(yk * d.y / h))); |  | ||||||
| 	    	default: | 	    	default: | ||||||
|       		return (d, r, w, h) => r; | 	      		return maxr; | ||||||
|  | 	  } | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export { RADIUS_OPTS, RADIUS_DESC, DotMaker, radius_func }; | export { RADIUS_OPTS, DotMaker }; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -73,7 +73,7 @@ export async function download_as_png (svg) { | |||||||
|   const opts = { |   const opts = { | ||||||
|       fitTo: { |       fitTo: { | ||||||
|         mode: 'width', // If you need to change the size
 |         mode: 'width', // If you need to change the size
 | ||||||
|         value: 1200, |         value: 400, | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|   const resvgJS = new resvg.Resvg(svgstr, opts) |   const resvgJS = new resvg.Resvg(svgstr, opts) | ||||||
|  | |||||||
| @ -3,7 +3,6 @@ import * as d3 from "d3-color"; | |||||||
| import shuffle from "lodash.shuffle"; | import shuffle from "lodash.shuffle"; | ||||||
| import random from "random"; | import random from "random"; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| const PALETTES = new Map([ | const PALETTES = new Map([ | ||||||
| 	[ "random RGB", palette_random ],  | 	[ "random RGB", palette_random ],  | ||||||
| 	[ "grayscale", palette_grayscale ], | 	[ "grayscale", palette_grayscale ], | ||||||
| @ -76,6 +75,4 @@ function palette_cmy() { | |||||||
| 	return shuffle(cols); | 	return shuffle(cols); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| export { PALETTES } | export { PALETTES } | ||||||
|  | |||||||
							
								
								
									
										80
									
								
								src/index.md
									
									
									
									
									
								
							
							
						
						
									
										80
									
								
								src/index.md
									
									
									
									
									
								
							| @ -8,15 +8,15 @@ toc: false | |||||||
| 
 | 
 | ||||||
| colourful generative patterns using [d3](https://d3js.org/) and [Observable Framework](https://observablehq.com/framework/) | colourful generative patterns using [d3](https://d3js.org/) and [Observable Framework](https://observablehq.com/framework/) | ||||||
| 
 | 
 | ||||||
| <p>v1.2.3 | by <a href="https://mikelynch.org">mike lynch</a> | <a href="https://aus.social/@mikelynch">@mikelynch@aus.social</a> | <a href="https://git.tilde.town/bombinans/poptimal">source</a> | <a href="https://mikelynch.org/2025/Mar/09/poptimal/">about</a></p> | <p>v1.1.1 | by <a href="https://mikelynch.org">mike lynch</a> | <a href="https://aus.social/@mikelynch">@mikelynch@aus.social</a> | <a href="https://git.tilde.town/bombinans/poptimal">source</a></p> | ||||||
| 
 | 
 | ||||||
| <div class="grid grid-cols-3"> | <div class="grid grid-cols-2"> | ||||||
| 
 | 
 | ||||||
|   <div class="card grid-colspan-1"> |   <div class="card"> | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| 
 | 
 | ||||||
| import {RADIUS_OPTS, DotMaker, radius_func} from './components/dots.js'; | import {RADIUS_OPTS, DotMaker} from './components/dots.js'; | ||||||
| import {DotControls} from './components/controls.js'; | import {DotControls} from './components/controls.js'; | ||||||
| import {PALETTES} from './components/palettes.js'; | import {PALETTES} from './components/palettes.js'; | ||||||
| import {download, download_as_svg, download_as_png} from './components/download.js'; | import {download, download_as_svg, download_as_png} from './components/download.js'; | ||||||
| @ -24,26 +24,18 @@ import random from "npm:random"; | |||||||
| 
 | 
 | ||||||
| import * as resvg from 'npm:@resvg/resvg-wasm'; | import * as resvg from 'npm:@resvg/resvg-wasm'; | ||||||
| 
 | 
 | ||||||
|  | const CELL = 10; | ||||||
| const MAG = 2; | const MAG = 2; | ||||||
| const WIDTH = 200; | const WIDTH = 20; | ||||||
| const HEIGHT = 200; | const HEIGHT = WIDTH; | ||||||
| 
 | 
 | ||||||
| const cell_input = Inputs.range([5,60], {value: 10,  label: "cell size"}); | const dm = new DotMaker(WIDTH); | ||||||
| const cell = view(cell_input); |  | ||||||
| 
 | 
 | ||||||
| 
 | const bg_input = Inputs.color({label: "background", value: d3.color("yellow").formatHex()}); | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ```js |  | ||||||
| 
 |  | ||||||
| const bg_input = Inputs.color({ |  | ||||||
|   label: "background", value: d3.color("white").formatHex() |  | ||||||
| }); |  | ||||||
| const bg = view(bg_input); | const bg = view(bg_input); | ||||||
| 
 | 
 | ||||||
| const ctrl1 = new DotControls(d3.color("white").formatHex(), RADIUS_OPTS); | const ctrl1 = new DotControls(d3.color("red").formatHex(), RADIUS_OPTS); | ||||||
| const ctrl2 = new DotControls(d3.color("white").formatHex(), RADIUS_OPTS); | const ctrl2 = new DotControls(d3.color("blue").formatHex(), RADIUS_OPTS); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const fg1 = view(ctrl1.fg); | const fg1 = view(ctrl1.fg); | ||||||
| @ -95,8 +87,6 @@ ctrl1.null_grid(); | |||||||
| ctrl2.null_grid(); | ctrl2.null_grid(); | ||||||
| palette_input.value = PALETTES.get(rpalette); | palette_input.value = PALETTES.get(rpalette); | ||||||
| palette_input.dispatchEvent(new Event("input")); | palette_input.dispatchEvent(new Event("input")); | ||||||
| cell_input.value = 5 + random.float() * random.float() * 55; |  | ||||||
| cell_input.dispatchEvent(new Event("input")); |  | ||||||
| ctrl1.random_grid(); | ctrl1.random_grid(); | ||||||
| ctrl2.random_grid(); | ctrl2.random_grid(); | ||||||
| 
 | 
 | ||||||
| @ -123,26 +113,14 @@ if( palette_fn ) { | |||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| </div> | </div> | ||||||
| <div class="card grid-colspan-2"> | <div> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| 
 | 
 | ||||||
| const width = WIDTH / cell; | const dots1 = dm.dots(1 / m1, n1); | ||||||
| const height = HEIGHT / cell; | const dots2 = dm.dots(1 / m2, n2); | ||||||
| 
 | 
 | ||||||
| const dm = new DotMaker(width, height); |  | ||||||
| 
 |  | ||||||
| const dots1 = dm.dots(1 / m1, n1, false); |  | ||||||
| const dots2 = dm.dots(1 / m2, n2, false); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ```js |  | ||||||
| 
 |  | ||||||
| const rfunc1 = radius_func(f1); |  | ||||||
| const rfunc2 = radius_func(f2); |  | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
|    |    | ||||||
| @ -151,18 +129,9 @@ const rfunc2 = radius_func(f2); | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const svg = d3.create("svg") | const svg = d3.create("svg") | ||||||
|   .attr("width", WIDTH * MAG) |   .attr("width", WIDTH * CELL * MAG) | ||||||
|   .attr("height", HEIGHT * MAG) |   .attr("height", HEIGHT * CELL * MAG) | ||||||
|   .attr("viewBox", [ 0, 0, width, height ]); |   .attr("viewBox", [ 0, 0, WIDTH, HEIGHT ]); | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| svg.append("clipPath") |  | ||||||
|     .attr("id", "clipRect") |  | ||||||
|     .append("rect") |  | ||||||
|     .attr("x", 0) |  | ||||||
|     .attr("y", 0) |  | ||||||
|     .attr("width", width) |  | ||||||
|     .attr("height", height); |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| // re transitions: they should only run when updating the palette and | // re transitions: they should only run when updating the palette and | ||||||
| @ -182,15 +151,14 @@ bg_g.selectAll("rect") | |||||||
|   .join("rect") |   .join("rect") | ||||||
|   .attr("x", 0) |   .attr("x", 0) | ||||||
|   .attr("y", 0) |   .attr("y", 0) | ||||||
|   .attr("width", width) |   .attr("width", WIDTH) | ||||||
|   .attr("height", height) |   .attr("height", WIDTH) | ||||||
|   .attr("fill", (d) => d.bg) |   .attr("fill", (d) => d.bg) | ||||||
| ; | ; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const dots_g1 = svg.append("g") | const dots_g1 = svg.append("g") | ||||||
|   .attr("id", "dots1") |   .attr("id", "dots1"); | ||||||
|   .attr("clip-path", "url(#clipRect)"); |  | ||||||
| 
 | 
 | ||||||
| dots_g1.selectAll("circle") | dots_g1.selectAll("circle") | ||||||
|   .data(dots1) |   .data(dots1) | ||||||
| @ -200,8 +168,7 @@ dots_g1.selectAll("circle") | |||||||
|   .attr("fill", fg1); |   .attr("fill", fg1); | ||||||
| 
 | 
 | ||||||
| const dots_g2 = svg.append("g") | const dots_g2 = svg.append("g") | ||||||
|   .attr("id", "dots2") |   .attr("id", "dots2"); | ||||||
|   .attr("clip-path", "url(#clipRect)"); |  | ||||||
| 
 | 
 | ||||||
| dots_g2.selectAll("circle") | dots_g2.selectAll("circle") | ||||||
|   .data(dots2) |   .data(dots2) | ||||||
| @ -217,8 +184,8 @@ display(svg.node()); | |||||||
| ```js | ```js | ||||||
|   // separate code block for when I understand transitions better |   // separate code block for when I understand transitions better | ||||||
|    |    | ||||||
|   dots_g1.selectAll("circle").attr("r", (d) => rfunc1(d, r1, width, height)); |   dots_g1.selectAll("circle").attr("r", (d) => dm.radius(d, f1, r1)); | ||||||
|   dots_g2.selectAll("circle").attr("r", (d) => rfunc2(d, r2, width, height)); |   dots_g2.selectAll("circle").attr("r", (d) => dm.radius(d, f2, r2)); | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -244,7 +211,6 @@ display(download(() => { | |||||||
| ``` | ``` | ||||||
| (PNGs made with <a href="https://github.com/thx/resvg-js">resvg-wasm</a> in-browser) | (PNGs made with <a href="https://github.com/thx/resvg-js">resvg-wasm</a> in-browser) | ||||||
| 
 | 
 | ||||||
| </div> |  | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user