Compare commits
No commits in common. "main" and "feature-more-radius-fns" have entirely different histories.
main
...
feature-mo
@ -1,13 +1,5 @@
|
||||
# CHANGELOG.md
|
||||
|
||||
## v1.1.1
|
||||
|
||||
Made the flashing transitions a bit better, but it still needs more work
|
||||
|
||||
## v1.1.0
|
||||
|
||||
Added SVG and PNG downloads
|
||||
|
||||
## v1.0.2
|
||||
|
||||
* Fixed bug which was stopping slanted grids
|
||||
|
49
README.md
49
README.md
@ -3,11 +3,7 @@
|
||||
An experiment in using Observable to generate colourful SVG patterns
|
||||
reminiscent of op art or halftone dots.
|
||||
|
||||
You can play with it [here.](https://etc.mikelynch.org/poptimal/)
|
||||
|
||||

|
||||
|
||||
Comments, feedback via Mastodon: [Mike Lynch](https://old.mermaid.town/@fsvo)
|
||||
Comments, feedback via Mastodon: [Mike Lynch](https://aus.social/@mikelynch)
|
||||
|
||||
## Observable
|
||||
|
||||
@ -25,3 +21,46 @@ npm run dev
|
||||
|
||||
Then visit <http://localhost:3000> to preview your app.
|
||||
|
||||
For more, see <https://observablehq.com/framework/getting-started>.
|
||||
|
||||
## Project structure
|
||||
|
||||
A typical Framework project looks like this:
|
||||
|
||||
```ini
|
||||
.
|
||||
├─ src
|
||||
│ ├─ components
|
||||
│ │ └─ timeline.js # an importable module
|
||||
│ ├─ data
|
||||
│ │ ├─ launches.csv.js # a data loader
|
||||
│ │ └─ events.json # a static data file
|
||||
│ ├─ example-dashboard.md # a page
|
||||
│ ├─ example-report.md # another page
|
||||
│ └─ index.md # the home page
|
||||
├─ .gitignore
|
||||
├─ observablehq.config.js # the app config file
|
||||
├─ package.json
|
||||
└─ README.md
|
||||
```
|
||||
|
||||
**`src`** - This is the “source root” — where your source files live. Pages go here. Each page is a Markdown file. Observable Framework uses [file-based routing](https://observablehq.com/framework/project-structure#routing), which means that the name of the file controls where the page is served. You can create as many pages as you like. Use folders to organize your pages.
|
||||
|
||||
**`src/index.md`** - This is the home page for your app. You can have as many additional pages as you’d like, but you should always have a home page, too.
|
||||
|
||||
**`src/data`** - You can put [data loaders](https://observablehq.com/framework/data-loaders) or static data files anywhere in your source root, but we recommend putting them here.
|
||||
|
||||
**`src/components`** - You can put shared [JavaScript modules](https://observablehq.com/framework/imports) anywhere in your source root, but we recommend putting them here. This helps you pull code out of Markdown files and into JavaScript modules, making it easier to reuse code across pages, write tests and run linters, and even share code with vanilla web applications.
|
||||
|
||||
**`observablehq.config.js`** - This is the [app configuration](https://observablehq.com/framework/config) file, such as the pages and sections in the sidebar navigation, and the app’s title.
|
||||
|
||||
## Command reference
|
||||
|
||||
| Command | Description |
|
||||
| ----------------- | -------------------------------------------------------- |
|
||||
| `npm install` | Install or reinstall dependencies |
|
||||
| `npm run dev` | Start local preview server |
|
||||
| `npm run build` | Build your static site, generating `./dist` |
|
||||
| `npm run deploy` | Deploy your app to Observable |
|
||||
| `npm run clean` | Clear the local data loader cache |
|
||||
| `npm run observable` | Run commands like `observable help` |
|
||||
|
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
|
BIN
docs/sample.png
BIN
docs/sample.png
Binary file not shown.
Before Width: | Height: | Size: 508 KiB |
101
greyscale.txt
101
greyscale.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
|
1317
package-lock.json
generated
1317
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -10,17 +10,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@observablehq/framework": "^1.13.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"await-spawn": "^4.0.2",
|
||||
"d3": "^7.9.0",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-color-difference": "^0.1.3",
|
||||
"d3-dsv": "^3.0.1",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"lodash.shuffle": "^4.2.0",
|
||||
"random": "^5.1.1",
|
||||
"yargs": "^17.7.2"
|
||||
"d3-time-format": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^5.0.5"
|
||||
|
236
poptimal.js
236
poptimal.js
@ -1,236 +0,0 @@
|
||||
import { Resvg } from "@resvg/resvg-js";
|
||||
import { promises } from "fs";
|
||||
import { JSDOM } from "jsdom";
|
||||
import * as d3 from "d3";
|
||||
import yargs from "yargs/yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import spawn from "await-spawn";
|
||||
import random from "random";
|
||||
|
||||
const xmlns = "http://www.w3.org/2000/xmlns/";
|
||||
const xlinkns = "http://www.w3.org/1999/xlink";
|
||||
const svgns = "http://www.w3.org/2000/svg";
|
||||
|
||||
import {RADIUS_OPTS, RADIUS_DESC, DotMaker} from './src/components/dots.js';
|
||||
import {PALETTES} from './src/components/palettes.js';
|
||||
import {ColourNamer} from './src/components/colour_namer.js';
|
||||
|
||||
const CELL = 10;
|
||||
const MAG = 2;
|
||||
const WIDTH = 20;
|
||||
const HEIGHT = WIDTH;
|
||||
const VISIBLE_DOG = 1000;
|
||||
|
||||
function randomise_params() {
|
||||
const palette_name = random.choice(Array.from(PALETTES.keys()));
|
||||
const palette_fn = PALETTES.get(palette_name);
|
||||
const palette = palette_fn();
|
||||
const patterns = [1,2].map((n) => { return {
|
||||
i: n,
|
||||
colour: palette[n],
|
||||
m: random.choice([1, 2, 3, 4, 5]),
|
||||
n: random.choice([1, 2, 3, 4, 5]),
|
||||
f: random.choice(RADIUS_OPTS),
|
||||
r: random.float(0, 0.4),
|
||||
}});
|
||||
return {
|
||||
background: palette[0],
|
||||
palette: palette_name,
|
||||
patterns: patterns
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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 dot_desc = named_colours.slice(1);
|
||||
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 "";
|
||||
}
|
||||
|
||||
|
||||
function poptimal_svg(params) {
|
||||
const window = new JSDOM().window;
|
||||
const document = window.document;
|
||||
const container = d3.select(document.body).append("div");
|
||||
|
||||
const dm = new DotMaker(WIDTH);
|
||||
|
||||
const svg = container.append("svg")
|
||||
.attr("width", WIDTH * CELL * MAG)
|
||||
.attr("height", HEIGHT * CELL * MAG)
|
||||
.attr("viewBox", [ 0, 0, WIDTH, HEIGHT ]);
|
||||
|
||||
const background = svg.append("rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", WIDTH)
|
||||
.attr("height", WIDTH)
|
||||
.attr("fill", params.background);
|
||||
|
||||
|
||||
params.patterns.map((p) => {
|
||||
const dots = dm.dots(1 / p.m, p.n, false);
|
||||
const dots_g = svg.append("g")
|
||||
.attr("id", `dots${p.i}`);
|
||||
|
||||
dots_g.selectAll("circle")
|
||||
.data(dots)
|
||||
.join("circle")
|
||||
.attr("r", (d) => dm.radius(d, p.f, p.r))
|
||||
.attr("fill", p.colour)
|
||||
.attr("cx", (d) => d.x)
|
||||
.attr("cy", (d) => d.y);
|
||||
});
|
||||
|
||||
const node = svg.node();
|
||||
|
||||
node.setAttributeNS(xmlns, "xmlns", svgns);
|
||||
node.setAttributeNS(xmlns, "xmlns:xlink", xlinkns);
|
||||
const serializer = new window.XMLSerializer;
|
||||
return serializer.serializeToString(node);
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function post_image(image, alt_text, cf) {
|
||||
const status_url = `${cf.base_url}/api/v1/statuses`;
|
||||
const media_url = `${cf.base_url}/api/v1/media`;
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${cf.access_token}`,
|
||||
};
|
||||
const rawData = await promises.readFile(image);
|
||||
const blob = new Blob([rawData]);
|
||||
const fd = new FormData();
|
||||
fd.set('description', alt_text);
|
||||
fd.set('file', blob, image, 'text/png');
|
||||
const resp = await fetch(media_url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: fd
|
||||
});
|
||||
|
||||
const bodyjson = await resp.text();
|
||||
const response = JSON.parse(bodyjson);
|
||||
const media_id = response["id"];
|
||||
|
||||
headers['Accept'] = 'application/json';
|
||||
headers['Content-Type'] = 'application/json';
|
||||
const resp2 = await fetch(status_url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ media_ids: [ media_id ] })
|
||||
});
|
||||
const bodyjson2 = await resp2.text();
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function main() {
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.usage("Usage: -s SIZE -o output.png -c config.json")
|
||||
.default('s', 1200)
|
||||
.default('c', 'config.json').argv;
|
||||
|
||||
const cfjson = await promises.readFile(argv.c);
|
||||
const cf = JSON.parse(cfjson);
|
||||
|
||||
|
||||
|
||||
const fn = argv.o || String(Date.now()) + '.png';
|
||||
|
||||
const imgfile = cf['working_dir'] + '/' + fn;
|
||||
|
||||
const params = randomise_params();
|
||||
const colourf = params.palette === 'grayscale' ? cf['grayscale'] : cf['colour'];
|
||||
|
||||
const namer = new ColourNamer();
|
||||
await namer.load_colours(colourf);
|
||||
|
||||
|
||||
const svg = poptimal_svg(params);
|
||||
const opts = {
|
||||
background: 'rgba(255, 255, 255, 1.0)',
|
||||
fitTo: {
|
||||
mode: 'width',
|
||||
value: argv.s,
|
||||
},
|
||||
};
|
||||
|
||||
const resvg = new Resvg(svg, opts);
|
||||
const pngData = resvg.render();
|
||||
const pngBuffer = pngData.asPng();
|
||||
|
||||
|
||||
await promises.writeFile(imgfile, pngBuffer);
|
||||
|
||||
// generate the alt_text last to check the image file histogram
|
||||
// so we don't include obscured colours
|
||||
const hist = await get_histogram(imgfile);
|
||||
const alt_text = image_description(namer, params, hist);
|
||||
|
||||
console.log(alt_text);
|
||||
console.log(imgfile);
|
||||
if( cf['base_url'] ) {
|
||||
await post_image(imgfile, alt_text, cf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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}
|
92
src/components/controls.js
vendored
92
src/components/controls.js
vendored
@ -2,6 +2,90 @@
|
||||
|
||||
import * as Inputs from "npm:@observablehq/inputs";
|
||||
import random from "npm:random";
|
||||
import shuffle from "npm:lodash.shuffle";
|
||||
import * as d3 from "npm:d3-color";
|
||||
|
||||
const PALETTES = new Map([
|
||||
[ "random RGB", palette_random ],
|
||||
[ "grayscale", palette_grayscale ],
|
||||
[ "monochrome", palette_monochrome ],
|
||||
[ "one spot", palette_one_spot ],
|
||||
[ "triad", triad_saturated ],
|
||||
[ "triad pastel", triad_pastel ],
|
||||
[ "triad dusk", triad_dusk ],
|
||||
[ "RGB", palette_rgb ],
|
||||
[ "RBY", palette_rby ],
|
||||
[ "CMY", palette_cmy ],
|
||||
]);
|
||||
|
||||
// two spot
|
||||
// RGB
|
||||
// CMY
|
||||
// RYB
|
||||
// triad - full saturation
|
||||
// triad - pastel
|
||||
// trial - dusk
|
||||
// random HSV
|
||||
// random RGB
|
||||
|
||||
function palette_random() {
|
||||
const u = random.uniform(0, 255);
|
||||
return [1,2,3].map((x)=> d3.rgb(u(), u(), u()));
|
||||
}
|
||||
|
||||
|
||||
function palette_grayscale() {
|
||||
const u = random.uniform(0, 1);
|
||||
return [1,2,3].map((x)=> d3.hsl(0, 0, u()));
|
||||
}
|
||||
|
||||
function palette_monochrome() {
|
||||
const u = random.uniform(0, 1);
|
||||
const h = u() * 360;
|
||||
return [1,2,3].map((x)=> d3.hsl(h, u(), u()));
|
||||
}
|
||||
|
||||
function palette_one_spot() {
|
||||
const hue = random.uniform(0, 360);
|
||||
const cols = [ d3.color("white"), d3.color("black"), d3.hsl(hue(), 1, 0.5) ]
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
function triad_saturated() {
|
||||
return triad(1, 0.5);
|
||||
}
|
||||
|
||||
function triad_pastel() {
|
||||
return triad(0.6, 0.7);
|
||||
}
|
||||
|
||||
function triad_dusk() {
|
||||
return triad(1, 0.25);
|
||||
}
|
||||
|
||||
function triad(s, l) {
|
||||
const u = random.uniform(0, 360);
|
||||
const h1 = u();
|
||||
const h2 = (h1 + 120) % 360;
|
||||
const h3 = (h1 + 240) % 360;
|
||||
const cols = [ h1, h2, h3 ].map((h) => d3.hsl(h, s, l));
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
function palette_rgb() {
|
||||
const cols = [ d3.rgb(255, 0, 0), d3.rgb(0, 255, 0), d3.rgb(0, 0, 255) ];
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
function palette_rby() {
|
||||
const cols = [ d3.rgb(255, 0, 0), d3.rgb(255, 255, 0), d3.rgb(0, 0, 255) ];
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
function palette_cmy() {
|
||||
const cols = [ d3.rgb(0, 255, 255), d3.rgb(255, 255, 0), d3.rgb(255, 0, 255) ];
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
|
||||
class DotControls {
|
||||
@ -26,10 +110,8 @@ class DotControls {
|
||||
this.f.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
null_grid() {
|
||||
// set to zero to stop flashing when randomising all
|
||||
this.r.value = 0;
|
||||
this.r.dispatchEvent(new Event("input"));
|
||||
random_colours() {
|
||||
this.set_colour(random_colour());
|
||||
}
|
||||
|
||||
set_colour(c) {
|
||||
@ -40,4 +122,4 @@ class DotControls {
|
||||
|
||||
}
|
||||
|
||||
export { DotControls };
|
||||
export { DotControls, PALETTES };
|
@ -16,21 +16,6 @@ const RADIUS_OPTS = [
|
||||
"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",
|
||||
"in": "getting larger in the centre",
|
||||
"out": "getting larger at the edges",
|
||||
"noise": "of random sizes",
|
||||
}
|
||||
|
||||
function distance(dx, dy) {
|
||||
return Math.sqrt(dx ** 2 + dy ** 2);
|
||||
}
|
||||
@ -38,8 +23,8 @@ function distance(dx, dy) {
|
||||
function int_range(v1, v2) {
|
||||
const vs = [v1, v2];
|
||||
vs.sort((a, b) => a - b);
|
||||
const low = Math.floor(vs[0] - 1);
|
||||
const high = Math.ceil(vs[1] + 1);
|
||||
const low = Math.floor(vs[0]);
|
||||
const high = Math.ceil(vs[1]);
|
||||
return [...Array(high - low + 1).keys()].map((i) => i + low);
|
||||
}
|
||||
|
||||
@ -50,7 +35,7 @@ class DotMaker {
|
||||
this.cy = 0.5 * width;
|
||||
}
|
||||
|
||||
dots(m, n, clip) {
|
||||
dots(m, n) {
|
||||
if( m - n === 0 ) {
|
||||
return [];
|
||||
}
|
||||
@ -61,7 +46,7 @@ class DotMaker {
|
||||
js.map((j) => {
|
||||
const x = (j - m * i) / (m - n);
|
||||
const y = m * (x + i);
|
||||
if( !clip || (x > 0 && y > 0 && x < this.width && y < this.width) ) {
|
||||
if( x > 0 && y > 0 && x < this.width && y < this.width ) {
|
||||
ps.push({i:i, j:j, x:x, y:y});
|
||||
}
|
||||
});
|
||||
@ -107,6 +92,6 @@ class DotMaker {
|
||||
}
|
||||
}
|
||||
|
||||
export { RADIUS_OPTS, RADIUS_DESC, DotMaker };
|
||||
export { RADIUS_OPTS, DotMaker };
|
||||
|
||||
|
||||
|
@ -1,84 +0,0 @@
|
||||
|
||||
import * as resvg from 'npm:@resvg/resvg-wasm';
|
||||
|
||||
|
||||
const xmlns = "http://www.w3.org/2000/xmlns/";
|
||||
const xlinkns = "http://www.w3.org/1999/xlink";
|
||||
const svgns = "http://www.w3.org/2000/svg";
|
||||
|
||||
// adapted from the DOM.download method as per this issue:
|
||||
|
||||
// https://github.com/observablehq/framework/issues/906
|
||||
|
||||
|
||||
export function download(value, name = "untitled", label = "Save") {
|
||||
const a = document.createElement("a");
|
||||
const b = a.appendChild(document.createElement("button"));
|
||||
b.textContent = label;
|
||||
a.download = name;
|
||||
|
||||
async function reset() {
|
||||
await new Promise(requestAnimationFrame);
|
||||
URL.revokeObjectURL(a.href);
|
||||
a.removeAttribute("href");
|
||||
b.textContent = label;
|
||||
b.disabled = false;
|
||||
}
|
||||
|
||||
a.onclick = async event => {
|
||||
console.log("clicked download");
|
||||
b.disabled = true;
|
||||
if (a.href) {
|
||||
console.log(`already saved: ${a.href}`);
|
||||
return reset(); // Already saved.
|
||||
}
|
||||
b.textContent = "Saving…";
|
||||
try {
|
||||
console.log("awaiting value function");
|
||||
const object = await (typeof value === "function" ? value() : value);
|
||||
console.log(object);
|
||||
b.textContent = "Download";
|
||||
a.href = URL.createObjectURL(object); // eslint-disable-line require-atomic-updates
|
||||
console.log(`url = ${a.href}`);
|
||||
} catch (ignore) {
|
||||
b.textContent = label;
|
||||
}
|
||||
if (event.eventPhase) return reset(); // Already downloaded.
|
||||
b.disabled = false;
|
||||
};
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
|
||||
export function svg_to_string(svg) {
|
||||
svg = svg.cloneNode(true);
|
||||
svg.setAttributeNS(xmlns, "xmlns", svgns);
|
||||
svg.setAttributeNS(xmlns, "xmlns:xlink", xlinkns);
|
||||
const serializer = new window.XMLSerializer;
|
||||
return serializer.serializeToString(svg);
|
||||
}
|
||||
|
||||
|
||||
export function download_as_svg(svg) {
|
||||
const str = svg_to_string(svg);
|
||||
return new Blob([str], {type: "image/svg+xml"})
|
||||
}
|
||||
|
||||
export async function download_as_png (svg) {
|
||||
// The Wasm must be initialized first
|
||||
|
||||
const svgstr = svg_to_string(svg);
|
||||
|
||||
const opts = {
|
||||
fitTo: {
|
||||
mode: 'width', // If you need to change the size
|
||||
value: 1200,
|
||||
}
|
||||
};
|
||||
const resvgJS = new resvg.Resvg(svgstr, opts)
|
||||
const pngData = resvgJS.render(svgstr, opts)
|
||||
const pngBuffer = pngData.asPng();
|
||||
return new Blob([pngBuffer], { type: 'image/png' });
|
||||
}
|
||||
|
@ -1,81 +0,0 @@
|
||||
|
||||
import * as d3 from "d3-color";
|
||||
import shuffle from "lodash.shuffle";
|
||||
import random from "random";
|
||||
|
||||
|
||||
const PALETTES = new Map([
|
||||
[ "random RGB", palette_random ],
|
||||
[ "grayscale", palette_grayscale ],
|
||||
[ "monochrome", palette_monochrome ],
|
||||
[ "one spot", palette_one_spot ],
|
||||
[ "triad", triad_saturated ],
|
||||
[ "triad pastel", triad_pastel ],
|
||||
[ "triad dusk", triad_dusk ],
|
||||
[ "RGB", palette_rgb ],
|
||||
[ "RBY", palette_rby ],
|
||||
[ "CMY", palette_cmy ],
|
||||
]);
|
||||
|
||||
function palette_random() {
|
||||
const u = random.uniform(0, 255);
|
||||
return [1,2,3].map((x)=> d3.rgb(u(), u(), u()));
|
||||
}
|
||||
|
||||
|
||||
function palette_grayscale() {
|
||||
const u = random.uniform(0, 1);
|
||||
return [1,2,3].map((x)=> d3.hsl(0, 0, u()));
|
||||
}
|
||||
|
||||
function palette_monochrome() {
|
||||
const u = random.uniform(0, 1);
|
||||
const h = u() * 360;
|
||||
return [1,2,3].map((x)=> d3.hsl(h, u(), u()));
|
||||
}
|
||||
|
||||
function palette_one_spot() {
|
||||
const hue = random.uniform(0, 360);
|
||||
const cols = [ d3.color("white"), d3.color("black"), d3.hsl(hue(), 1, 0.5) ]
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
function triad_saturated() {
|
||||
return triad(1, 0.5);
|
||||
}
|
||||
|
||||
function triad_pastel() {
|
||||
return triad(0.6, 0.7);
|
||||
}
|
||||
|
||||
function triad_dusk() {
|
||||
return triad(1, 0.25);
|
||||
}
|
||||
|
||||
function triad(s, l) {
|
||||
const u = random.uniform(0, 360);
|
||||
const h1 = u();
|
||||
const h2 = (h1 + 120) % 360;
|
||||
const h3 = (h1 + 240) % 360;
|
||||
const cols = [ h1, h2, h3 ].map((h) => d3.hsl(h, s, l));
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
function palette_rgb() {
|
||||
const cols = [ d3.rgb(255, 0, 0), d3.rgb(0, 255, 0), d3.rgb(0, 0, 255) ];
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
function palette_rby() {
|
||||
const cols = [ d3.rgb(255, 0, 0), d3.rgb(255, 255, 0), d3.rgb(0, 0, 255) ];
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
function palette_cmy() {
|
||||
const cols = [ d3.rgb(0, 255, 255), d3.rgb(255, 255, 0), d3.rgb(255, 0, 255) ];
|
||||
return shuffle(cols);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { PALETTES }
|
109
src/index.md
109
src/index.md
@ -2,13 +2,9 @@
|
||||
toc: false
|
||||
---
|
||||
|
||||
|
||||
|
||||
<h1>poptimal</h1>
|
||||
|
||||
colourful generative patterns using [d3](https://d3js.org/) and [Observable Framework](https://observablehq.com/framework/)
|
||||
|
||||
<p>v1.1.2 | 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>
|
||||
<p>v1.0.2 | 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-2">
|
||||
|
||||
@ -17,13 +13,9 @@ colourful generative patterns using [d3](https://d3js.org/) and [Observable Fram
|
||||
```js
|
||||
|
||||
import {RADIUS_OPTS, DotMaker} from './components/dots.js';
|
||||
import {DotControls} from './components/controls.js';
|
||||
import {PALETTES} from './components/palettes.js';
|
||||
import {download, download_as_svg, download_as_png} from './components/download.js';
|
||||
import {PALETTES, DotControls} from './components/controls.js';
|
||||
import random from "npm:random";
|
||||
|
||||
import * as resvg from 'npm:@resvg/resvg-wasm';
|
||||
|
||||
const CELL = 10;
|
||||
const MAG = 2;
|
||||
const WIDTH = 20;
|
||||
@ -60,14 +52,8 @@ const randomise_palette = view(Inputs.button("Random", {label:"palette"}));
|
||||
const randomise_all = view(Inputs.button("Random", {label:"all"}));
|
||||
|
||||
|
||||
```
|
||||
|
||||
```js
|
||||
// in its own code block so that it doesn't slow down load for the rest
|
||||
// of the page
|
||||
await resvg.initWasm(fetch('https://unpkg.com/@resvg/resvg-wasm/index_bg.wasm'));
|
||||
|
||||
const wasm_init = "ready";
|
||||
```
|
||||
|
||||
|
||||
@ -82,13 +68,11 @@ ctrl2.random_grid();
|
||||
|
||||
```js
|
||||
randomise_all;
|
||||
const rpalette = random.choice(Array.from(PALETTES.keys()));
|
||||
ctrl1.null_grid();
|
||||
ctrl2.null_grid();
|
||||
palette_input.value = PALETTES.get(rpalette);
|
||||
palette_input.dispatchEvent(new Event("input"));
|
||||
ctrl1.random_grid();
|
||||
ctrl2.random_grid();
|
||||
const rpalette = random.choice(Array.from(PALETTES.keys()));
|
||||
palette_input.value = PALETTES.get(rpalette);
|
||||
palette_input.dispatchEvent(new Event("input"));
|
||||
|
||||
```
|
||||
|
||||
@ -118,14 +102,13 @@ if( palette_fn ) {
|
||||
|
||||
```js
|
||||
|
||||
const dots1 = dm.dots(1 / m1, n1, false);
|
||||
const dots2 = dm.dots(1 / m2, n2, false);
|
||||
const dots1 = dm.dots(1 / m1, n1);
|
||||
const dots2 = dm.dots(1 / m2, n2);
|
||||
|
||||
|
||||
```
|
||||
|
||||
```js
|
||||
// Set up the svg canvas
|
||||
|
||||
|
||||
const svg = d3.create("svg")
|
||||
@ -133,95 +116,43 @@ const svg = d3.create("svg")
|
||||
.attr("height", HEIGHT * CELL * MAG)
|
||||
.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
|
||||
// grid, not via the sliders
|
||||
|
||||
// see https://www.d3indepth.com/transitions/ and use enter / exit etc
|
||||
|
||||
// note
|
||||
|
||||
// do background as a select so that transitions work
|
||||
|
||||
const bg_g = svg.append("g")
|
||||
.attr("id", "background");
|
||||
|
||||
bg_g.selectAll("rect")
|
||||
.data( [ { bg: bg } ] )
|
||||
.join("rect")
|
||||
const background = svg.append("rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", WIDTH)
|
||||
.attr("height", WIDTH)
|
||||
.attr("fill", (d) => d.bg)
|
||||
;
|
||||
|
||||
.attr("fill", bg);
|
||||
|
||||
const dots_g1 = svg.append("g")
|
||||
.attr("id", "dots1")
|
||||
.attr("clip-path", "url(#clipRect)");
|
||||
.attr("id", "dots1");
|
||||
|
||||
dots_g1.selectAll("circle")
|
||||
.data(dots1)
|
||||
.join("circle")
|
||||
.attr("r", (d) => dm.radius(d, f1, r1))
|
||||
.attr("fill", fg1)
|
||||
.attr("cx", (d) => d.x)
|
||||
.attr("cy", (d) => d.y)
|
||||
.attr("fill", fg1);
|
||||
.attr("cy", (d) => d.y);
|
||||
|
||||
const dots_g2 = svg.append("g")
|
||||
.attr("id", "dots2")
|
||||
.attr("clip-path", "url(#clipRect)");
|
||||
.attr("id", "dots2");
|
||||
|
||||
dots_g2.selectAll("circle")
|
||||
.data(dots2)
|
||||
.join("circle")
|
||||
.attr("r", (d) => dm.radius(d, f2, r2))
|
||||
.attr("fill", fg2)
|
||||
.attr("cx", (d) => d.x)
|
||||
.attr("cy", (d) => d.y)
|
||||
.attr("fill", fg2);
|
||||
.attr("cy", (d) => d.y);
|
||||
|
||||
|
||||
|
||||
display(svg.node());
|
||||
|
||||
```
|
||||
|
||||
```js
|
||||
// separate code block for when I understand transitions better
|
||||
|
||||
dots_g1.selectAll("circle").attr("r", (d) => dm.radius(d, f1, r1));
|
||||
dots_g2.selectAll("circle").attr("r", (d) => dm.radius(d, f2, r2));
|
||||
|
||||
```
|
||||
|
||||
```js
|
||||
|
||||
display(download(() => {
|
||||
const thing = download_as_svg(svg.node())
|
||||
return thing;
|
||||
}, "poptimal.svg", "Save as SVG"));
|
||||
```
|
||||
|
||||
```js
|
||||
|
||||
await wasm_init;
|
||||
|
||||
display(download(() => {
|
||||
console.log("PNG value");
|
||||
const thing = download_as_png(svg.node())
|
||||
return thing;
|
||||
}, "poptimal.png", "Save as PNG"));
|
||||
|
||||
|
||||
```
|
||||
(PNGs made with <a href="https://github.com/thx/resvg-js">resvg-wasm</a> in-browser)
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user