thi-ng / umbrella

⛱ Broadly scoped ecosystem & mono-repository of 199 TypeScript projects (and ~180 examples) for general purpose, functional, data driven development
https://thi.ng
Apache License 2.0
3.4k stars 150 forks source link

voronoi example #99

Closed nkint closed 5 years ago

nkint commented 5 years ago

Hi! After discussed recently on Discord I've done some test with voronoi and delanuy. The feeling is so good that I'd like to make a PR with a small example (see code below) to replicate the Mike Bostock and Mario visual images:

This could have some interesting feature as learning by example / tutorial:

What do you think?

A base skelton should be this (without splines and with only canvas):

import * as tx from '@thi.ng/transducers'
import * as g from '@thi.ng/geom'
import * as gsc from '@thi.ng/geom-subdiv-curve'
import { clearDOM, renderOnce } from '@thi.ng/hdom'
import { canvas } from '@thi.ng/hdom-canvas'
import { DVMesh } from '@thi.ng/geom-voronoi'
import { cartesian2 } from '@thi.ng/vectors'
import { TAU } from '@thi.ng/math'
import { simplify } from '@thi.ng/geom-resample'

const colors = ['#00429d', '#2e59a8', '#4771b2', '#5d8abd', '#73a2c6', '#8abccf']

const width = 600
const height = 600
const radius = (width / 2) * 0.8
const angle = 0
const center = [width / 2, height / 2]

export function sketch() {
  const points = [
    ...tx.iterator1(
      tx.map(index => cartesian2([], [radius, index * TAU + angle], center)),
      tx.normRange(10, false),
    ),
  ]

  const mesh = new DVMesh()
  mesh.addKeys(points, 0.01)
  const bounds = [[0, 0], [width, 0], [width, height], [0, height]]
  const cells = mesh.voronoi(bounds)

  const cellsSimplified = [...tx.iterator1(tx.map(cell => simplify(cell, 0.1, true)), cells)]

  const kernels = [gsc.SUBDIV_CHAIKIN_CLOSED, gsc.SUBDIV_CUBIC_CLOSED]
  const cellsSubdividedKernels = [
    ...tx.iterator1(
      tx.map(([[index, kernel], cell]) => ({
        pts: gsc.subdivide(cell, kernel, 4),
        color: colors[index],
      })),
      tx.permutations(tx.iterator1(tx.indexed(), kernels), cellsSimplified),
    ),
  ]

  renderOnce([
    'div.ma2',
    ['h1', 'Voronoi'],
    [
      canvas,
      { width, height },
      g.points(points, { size: 4, shape: 'circle', fill: '#ED474A' }),
      ...cellsSimplified.map(cell =>
        g.polygon(cell, { fill: 'none', stroke: '#ED474A', 'stroke-width': 1.5 }),
      ),
      ...tx.iterator1(
        tx.map(({ pts, color }) =>
          g.polygon(pts, { fill: 'none', stroke: color, 'stroke-width': 1.5 }),
        ),
        cellsSubdividedKernels,
      ),
    ],
  ])
}

if (process.env.NODE_ENV !== 'production') {
  const hot = (<any>module).hot
  hot &&
    hot.dispose(() => {
      clearDOM(document.getElementById('app'))
    })
}
postspectacular commented 5 years ago

Hey @nkint - by all means, I'd be totally happy about any more examples - can never have enough :) As for nice graphics: As an interim step prior to having a proper website for this project, I want create screenshots of all existing examples to help better navigate them. Since a lot of them aren't precisely eye candy, any more visual ones would be amazing!

The geom-voronoi example actually has two examples, they only still need to be added as such. See here:

As for the spline package and other geom-* packages, these are all just "low level" functions for the main geom package which combines their functionality. So I'd always recommend using this one first...

Some references:

import * as g from "@thi.ng/geom";
import * as sd from "@thi.ng/geom-subdiv-curve";
import * as tx from "@thi.ng/transducers";
import * as v from "@thi.ng/vectors";

const a = g.polygon([[0, 0], [100, 0], [50, 86]]);

// subdivide polygon (2 iterations)
// btw. there're also other subdiv types than cubic
// https://github.com/thi-ng/umbrella/blob/master/packages/geom-subdiv-curve/src/api.ts#L21

const a2 = g.subdivCurve(a, sd.SUBDIV_CUBIC_CLOSED, 2);
// Polygon {
//   points: [
//     [ 23.4375, 13.4375 ],
//     [ 34.375, 5.375 ],
//     [ 50, 2.6875 ],
//     [ 65.625, 5.375 ],
//     [ 76.5625, 13.4375 ],
//     [ 78.125, 26.875 ],
//     [ 72.65625, 41.65625 ],
//     [ 62.5, 53.75 ],
//     [ 50, 59.125 ],
//     [ 37.5, 53.75 ],
//     [ 27.34375, 41.65625 ],
//     [ 21.875, 26.875 ]
//   ],
//   attribs: {}
// }

// create SVG
console.log(g.asSvg(g.svgDoc({}, a2)))
// <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="56.25" height="56.44" viewBox="21.88 2.69 56.25 56.44"><polygon points="23.44,13.44 34.38,5.38 50.00,2.69 65.63,5.38 76.56,13.44 78.13,26.88 72.66,41.66 62.50,53.75 50.00,59.13 37.50,53.75 27.34,41.66 21.88,26.88"/></svg>

// convert a polygon to a path of cubic spline segments
const polyAsCubicPath = (poly, t = 0.5) => {
    // transform each poly edge a->b
    // wrap() is needed because we need to connect last seg w/ first later
    const segments = tx.wrap(
        tx.mapcat(([a,b])=> [v.mixN([], a, b, 0.5), b], g.edges(poly)),
        1,
        false
    );

    let first = true;
    const path = g.pathBuilder();
    // re-org segments such that we get [midEdgeA, origPolyVertex, midEdgeB]
    for (let s of tx.partition(3, 2, segments)) {
        if (first) {
            path.moveTo(s[0]);
            first = false;
        }
        // create cubic spline to midpoint of next edge
        // using interpolated control points
        path.cubicTo(
            v.mixN([], s[0], s[1], t),
            v.mixN([], s[1], s[2], 1 - t),
            s[2]
        );
    }
    return path.current();
};

// output as SVG
console.log(
    g.asSvg(
        g.svgDoc(
            // there seems to be a bug with computing cubic bounding box
            // so need to specify viewBox directly... (I will create another issue)
            {viewBox: "0 0 100 100", fill: "none"},
            ...tx.map(
                (t) => g.withAttribs(polyAsCubicPath(a, t), { stroke: [t, t, t, 1] }),
                tx.normRange(4, false)
            )
        )
    )
);

This last output will create this below using different t values to adjust curve tightness. The inner triangle is t=0, i.e. all control points coincide with the mid points of the original poly. The original poly is enclosing all of these curves...

Screen Shot 2019-07-10 at 3 23 33 AM

Btw. I will add the above function to the geom package soon...

postspectacular commented 5 years ago

Another thing... This code of yours:

const points = [
  ...tx.iterator1(
    tx.map(index => cartesian2([], [radius, index * TAU + angle], center)),
    tx.normRange(10, false),
  ),
]

Can be simplified with the geom package like so:

const points = g.vertices(g.circle(center, radius), 10)
postspectacular commented 5 years ago

@nkint also wanted to point out that you can use any convex polygon as clipping boundary for the voronoi, e.g. use a hexagon:

const bounds = g.vertices(g.circle([width / 2, height / 2], width / 2), 6);
postspectacular commented 5 years ago

Just added another version of that above polyToCubicPath function, which creates a curve going through the original polygon vertices (rather than through the edge midpoints as in the other function). To compare, in the image below the blue curves are created with the version from above, the red ones are computed with the new function. Original polygon in black.

Screen Shot 2019-07-10 at 9 07 13 PM

The new function computes tangents (and symmetric control points) for each polygon vertex (perpendicular to the corner bisector at each vertex) and in the version shown above, these tangent are scaled relative to the length of their parent poly edge. Alternatively, there's a flag to use uniform scaling only:

Screen Shot 2019-07-10 at 9 16 06 PM