Compare commits
No commits in common. "main" and "feature-downloads" have entirely different histories.
main
...
feature-do
|
@ -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
|
||||
|
|
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` |
|
||||
|
|
BIN
docs/sample.png
BIN
docs/sample.png
Binary file not shown.
Before Width: | Height: | Size: 508 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -10,15 +10,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@observablehq/framework": "^1.13.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"d3": "^7.9.0",
|
||||
"d3-color": "^3.1.0",
|
||||
"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"
|
||||
|
|
113
poptimal.js
113
poptimal.js
|
@ -1,113 +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 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, DotMaker} from './src/components/dots.js';
|
||||
import {PALETTES} from './src/components/palettes.js';
|
||||
|
||||
const CELL = 10;
|
||||
const MAG = 2;
|
||||
const WIDTH = 20;
|
||||
const HEIGHT = WIDTH;
|
||||
|
||||
function poptimal_svg() {
|
||||
const window = new JSDOM().window;
|
||||
const document = window.document;
|
||||
const container = d3.select(document.body).append("div");
|
||||
|
||||
const dm = new DotMaker(WIDTH);
|
||||
|
||||
const m1 = random.choice([1, 2, 3, 4, 5]);
|
||||
const n1 = random.choice([1, 2, 3, 4, 5]);
|
||||
const m2 = random.choice([1, 2, 3, 4, 5]);
|
||||
const n2 = random.choice([1, 2, 3, 4, 5]);
|
||||
const palette_fn = random.choice(Array.from(PALETTES.values()));
|
||||
const palette = palette_fn();
|
||||
const bg = palette[0];
|
||||
const fg1 = palette[1];
|
||||
const fg2 = palette[2];
|
||||
const f1 = random.choice(RADIUS_OPTS);
|
||||
const f2 = random.choice(RADIUS_OPTS);
|
||||
const r1 = random.float(0, 0.4);
|
||||
const r2 = random.float(0, 0.4);
|
||||
|
||||
const dots1 = dm.dots(1 / m1, n1);
|
||||
const dots2 = dm.dots(1 / m2, n2);
|
||||
|
||||
|
||||
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", bg);
|
||||
|
||||
const dots_g1 = svg.append("g")
|
||||
.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);
|
||||
|
||||
const dots_g2 = svg.append("g")
|
||||
.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);
|
||||
|
||||
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 main() {
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.usage("Usage: -w WIDTH -o OUTPUT_PNG")
|
||||
.default('w', 1200)
|
||||
.default('o', 'poptimal.png').argv;
|
||||
|
||||
const svg = poptimal_svg(argv.w);
|
||||
const opts = {
|
||||
background: 'rgba(255, 255, 255, 1.0)',
|
||||
fitTo: {
|
||||
mode: 'width',
|
||||
value: argv.w,
|
||||
},
|
||||
};
|
||||
const resvg = new Resvg(svg, opts)
|
||||
const pngData = resvg.render()
|
||||
const pngBuffer = pngData.asPng()
|
||||
|
||||
await promises.writeFile(argv.o, pngBuffer);
|
||||
}
|
||||
|
||||
|
||||
|
||||
main();
|
|
@ -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 };
|
|
@ -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"})
|
||||
}
|
||||
|
|
|
@ -1,78 +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 }
|
61
src/index.md
61
src/index.md
|
@ -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.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>
|
||||
<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"));
|
||||
|
||||
```
|
||||
|
||||
|
@ -125,7 +122,6 @@ const dots2 = dm.dots(1 / m2, n2);
|
|||
```
|
||||
|
||||
```js
|
||||
// Set up the svg canvas
|
||||
|
||||
|
||||
const svg = d3.create("svg")
|
||||
|
@ -133,29 +129,12 @@ const svg = d3.create("svg")
|
|||
.attr("height", HEIGHT * CELL * MAG)
|
||||
.attr("viewBox", [ 0, 0, WIDTH, 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");
|
||||
|
@ -163,9 +142,10 @@ const dots_g1 = svg.append("g")
|
|||
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");
|
||||
|
@ -173,22 +153,17 @@ const dots_g2 = svg.append("g")
|
|||
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(() => {
|
||||
|
@ -211,6 +186,8 @@ display(download(() => {
|
|||
```
|
||||
(PNGs made with <a href="https://github.com/thx/resvg-js">resvg-wasm</a> in-browser)
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
Loading…
Reference in New Issue