Compare commits

..

No commits in common. "main" and "feature-downloads" have entirely different histories.

14 changed files with 302 additions and 1872 deletions

View File

@ -1,9 +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

View File

@ -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/)
![Two overlapping grids of purple and lime dots on a teal background](https://git.tilde.town/bombinans/poptimal/raw/branch/main/docs/sample.png)
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 youd 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 apps 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` |

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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();

View File

@ -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}

View File

@ -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 };

View File

@ -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 };

View File

@ -61,6 +61,7 @@ export function svg_to_string(svg) {
export function download_as_svg(svg) {
console.log("HEY download_as_svg");
const str = svg_to_string(svg);
return new Blob([str], {type: "image/svg+xml"})
}
@ -73,7 +74,7 @@ export async function download_as_png (svg) {
const opts = {
fitTo: {
mode: 'width', // If you need to change the size
value: 1200,
value: 400,
}
};
const resvgJS = new resvg.Resvg(svgstr, opts)

View File

@ -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 }

View File

@ -6,9 +6,7 @@ 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.1.0 | 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,8 +15,7 @@ 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 {PALETTES, DotControls} from './components/controls.js';
import {download, download_as_svg, download_as_png} from './components/download.js';
import random from "npm:random";
@ -60,6 +57,8 @@ const randomise_palette = view(Inputs.button("Random", {label:"palette"}));
const randomise_all = view(Inputs.button("Random", {label:"all"}));
```
```js
@ -82,13 +81,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 +115,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,73 +129,41 @@ 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(() => {
@ -222,6 +186,8 @@ display(download(() => {
```
(PNGs made with <a href="https://github.com/thx/resvg-js">resvg-wasm</a> in-browser)
</div>
<style>