Public
Edited
Mar 11
2 forks
Importers
96 stars
Insert cell
Insert cell
Insert cell
Insert cell
array = n => {let A = new Array(n); for (let i = 0; i < n; i++) A[i] = i; return A}
Insert cell
array(10)
Insert cell
Insert cell
range = (start, end, step = 1) => {
const n = -~((end - start) / step), A = new Array(n)
for (let i = 0, k = start; i < n; i++, k += step) A[i] = k
return A
}
Insert cell
range(6, 12)
Insert cell
range(8, 16, 4)
Insert cell
range(8, 16, 5)
Insert cell
Insert cell
cut = (a, b, n) => array(n + 1).map(i => a + i * (b - a) / n)
Insert cell
cut(0, 10, 4)
Insert cell
cut(5, 25, 5)
Insert cell
cut(1, 35, 6)
Insert cell
cut2 = (a, b, n) => array(n).map(i => lerp(a, b, i/(n-1)))
Insert cell
cut2(5, 25, 5)
Insert cell
Insert cell
shiftLeft = (arr, n = 1) => shiftRight(arr, -n)
Insert cell
shiftLeft(array(10), 2)
Insert cell
shiftRight = (arr, n = 1) => {
const m = arr.length, copy = new Array(m)
n = ((n % m) + m) % m
for (let i = 0, k = m-n; k < m;) copy[i++] = arr[k++]
for (let i = n, k = 0; k < m-n;) copy[i++] = arr[k++]
return copy
}
Insert cell
shiftRight(array(10), 2)
Insert cell
Insert cell
shuffle = (arr, r = random) => {
const copy = [...arr] // create a copy of original array
for (let i = copy.length - 1; i; i --) {
const randomIndex = randInt(0, i + 1, r)
;[copy[i], copy[randomIndex]] = [copy[randomIndex], copy[i]] // swap
}
return copy
}
Insert cell
shuffle(array(10))
Insert cell
Insert cell
PI = Math.PI
Insert cell
TAU = Math.PI * 2
Insert cell
goldenRatio = (1 + Math.sqrt(5)) / 2
Insert cell
Insert cell
squareDist = (...args) => {
const len = args.length / 2
return array(len).reduce((acc, d, i) => acc + (args[i] - args[i + len]) ** 2, 0)
}
Insert cell
squareDist(0, 0, 0, 10, 10, 10)
Insert cell
dist = (...args) => squareDist(...args) ** .5
Insert cell
dist(0, 0, 0, 10, 10, 10)
Insert cell
manhattanDist = (...args) => {
const len = args.length / 2
return array(len).reduce((acc, d, i) => acc + Math.abs(args[i] - args[i + len]), 0)
}
Insert cell
manhattanDist(0, 0, 0, 10, 10, 10)
Insert cell
Insert cell
lerp = (a, b, amount, clamped = false) => {
const t = !clamped ? amount : clamp(amount, 0, 1)
return a * (1 - t) + b * t
}
Insert cell
lerp(27, 38, 0.5)
Insert cell
Insert cell
inverseLerp = (a, b, value, clamped = false) => {
let out = (value - a) / (b - a)
return !clamped ? out : clamp(out, 0, 1)
}
Insert cell
inverseLerp(10, 15, 17, true)
Insert cell
Insert cell
mix = (a, b, amount) => {
if (a.length === b.length) return a.map((d, i) => lerp(a[i], b[i], amount))
throw new Error('lengths do not match')
}
Insert cell
mix([2,4,6], [1, 9, 12], 0.5)
Insert cell
Insert cell
clamp = (a, min, max) => Math.max(Math.min(a, max), min)
Insert cell
clamp(23, 10, 20)
Insert cell
Insert cell
map = (n, a, b, c, d, clamped = false) => lerp(c, d, inverseLerp(a, b, n, clamped))
Insert cell
map(37, 28, 89, -10, 10)
Insert cell
map(5, 7, 16, 0, 10)
Insert cell
map(5, 7, 16, 0, 10, true)
Insert cell
Insert cell
modularDist = (value, mod) => Math.abs(value - mod * Math.round(value/mod))
Insert cell
modularDist((now/100000) * TAU, PI)
Insert cell
Insert cell
random = (a, b, r = Math.random) => {
if(Array.isArray(a)) return a[r() * a.length | 0]
if(!a && a !== 0) return r()
if(!b && b !== 0) return r() * a

if(a > b) [a, b] = [b, a] // swap values
return a + r() * (b - a)
}
Insert cell
random(5)
Insert cell
random(10, 50)
Insert cell
random([1, 3, 5])
Insert cell
Insert cell
randBool = (threshold = .5, r = random) => r() < threshold
Insert cell
randBool()
Insert cell
randBool(.7)
Insert cell
Insert cell
randInt = (a, b, r = random) => Math.floor(random(a, b, r))
Insert cell
randInt(10)
Insert cell
randInt(20, 50)
Insert cell
Insert cell
coin = (p = .5, r = random) => r() < p ? -1 : 1
Insert cell
Insert cell
Insert cell
// Standard Normal variate using Box-Muller transform
// adapted from https://stackoverflow.com/a/36481059
function gaussianRand(mean=0, stdev=1, r=random) {
const u = 1 - r() // Converting [0,1) to (0,1]
const v = r()
const z = Math.sqrt(-2 * Math.log(u)) * Math.cos(TAU * v)
// Transform to the desired mean and standard deviation:
return z * stdev + mean
}
Insert cell
gaussianRand()
Insert cell
Insert cell
Insert cell
Insert cell
power = 8
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
weightedRandom([ '🍌', '🍎', '🥕' ], [ 3, 7, 1 ])
Insert cell
Insert cell
PRNG = require('https://bundle.run/prng@0.0.1')
Insert cell
{
const prng = new PRNG(135)
return array(10).map(i => prng.rand(1, 13))
}
Insert cell
seededRandom = (seed) => {
const prng = new PRNG(seed)
return (a, b, r = prng.rand) => {
const n = 100000
if(b) return prng.rand(a * n, b * n) / n
if(a) return prng.rand(a * n) / n
return prng.rand(n) / n
}
}
Insert cell
{
const prng = seededRandom(135)
return array(10).map(i => prng(1, 13))
}
Insert cell
Insert cell
rnd = seed => {
let prng = new PRNG(seed)
const reset = seed => prng = new PRNG(seed)
const r = () => prng.rand(10e7)/10e7
const rnd = (a, b) => random(a, b, r)
const rndInt = (a, b) => randInt(a, b, r)
const rndBool = (a, b, n) => randBool(a, r)
const rndCoin = (a) => coin(a, r)
const gssnRnd = (a, b) => gaussianRand(a, b, r)
const rndExp = (a, b, n) => expRand(a, b, n, r)
const rndCirc = (a, b, n) => circleRand(a, b, r)
const wghtdRnd = (a, b, n) => weightedRandom(a, b, r)

return {
reset,
random: rnd,
randInt: rndInt,
randBool: rndBool,
coin: rndCoin,
gaussianRand: gssnRnd,
expRand: rndExp,
circleRand: rndCirc,
weightedRandom: wghtdRnd,
}
}
Insert cell
{
const r = rnd(135)
const {random, randInt, randBool, coin, expRand, circleRand, weightedRandom, gaussianRand} = r

let a = [
random(5, 15),
randInt(5, 15),
randBool(.9),
coin(.1),
gaussianRand(),
expRand(100, 200, 8),
circleRand(100, 200),
weightedRandom([ '🍌', '🍎', '🥕' ], [ 3, 7, 1 ])
]
r.reset(135)
let b = [
random(5, 15),
randInt(5, 15),
randBool(.9),
coin(.1),
gaussianRand(),
expRand(100, 200, 8),
circleRand(100, 200),
weightedRandom([ '🍌', '🍎', '🥕' ], [ 3, 7, 1 ])
]

return [a, b]
}
Insert cell
Insert cell
Insert cell
beat = (value, intensity = 2, freq = 1) => (Math.atan(Math.sin(value * TAU * freq) * intensity) + PI / 2) / PI
Insert cell
Insert cell
ease = (p, g) => !g ? 3 * p * p - 2 * p * p * p :
p < 0.5 ? 0.5 * Math.pow(2 * p, g) :
1 - 0.5 * Math.pow(2 * (1 - p), g)
Insert cell
Insert cell
softplus = (q, p) => {
const qq = q + p
if(qq <= 0) return 0
if(qq >= 2 * p) return qq - p
return 1 / (4 * p) * qq * qq
}
Insert cell
Insert cell
easing = require('d3-ease')
Insert cell
Insert cell
step = (x, edge) => x < edge ? 0 : 1
Insert cell
Insert cell
smoothstep = (x, edge0, edge1) => {
const t = clamp((x - edge0) / (edge1 - edge0), 0, 1)
return t * t * (3 - 2 * t)
}
Insert cell
Insert cell
linearstep = (t, begin, end) => clamp((t - begin) / (end - begin), 0, 1)
Insert cell
Insert cell
linearstepUpDown = (t, upBegin, upEnd, downBegin, downEnd) => linearstep(t, upBegin, upEnd) - linearstep(t, downBegin, downEnd)
Insert cell
Insert cell
stepUpDown = (t, begin, end) => step(t, begin) - step(t, end)
Insert cell
Insert cell
Insert cell
radToDeg = angle => angle * (360 / TAU )
Insert cell
Insert cell
degToRad = angle => angle * (TAU / 360)
Insert cell
Insert cell
modAngle = angle => angle - TAU * Math.floor(angle/TAU)
Insert cell
Insert cell
lerpAngle = (a, b, v) => {
let diff = modAngle(b - a)
if(diff > PI) diff = -modAngle(a - b)
return a + diff * v
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
svg`<svg width="400" viewBox="0 0 400 400" style="border: solid 1px black">
${(() => {
let arr = []
divideQuad(PVector(0, 0), PVector(400, 0), PVector(400, 400), PVector(0, 400), 5, arr, 1)
return arr.map(({A, B, C, D}) => `<path
fill="none"
stroke="black"
d="${[A, B, C, D].map(({x, y}, i) => `${i === 0 ? 'M' : 'L'}${x},${y}`).join(' ')}"
/>`).join('')
})()}
</svg>`
Insert cell
Insert cell
// let spline = new Spline(x1, y1, x2, y2, x3, y3, … xn, yn [, smoothness [, isClosed]])
class Spline {
constructor(...pts) {
pts = pts.flat()
let smoothness = 1
this.isClosed = false
if(pts.length % 2 == 1) {
smoothness = pts.pop()
}
else if(typeof pts[pts.length - 1] === 'boolean'){
this.isClosed = pts.pop()
smoothness = pts.pop()
}
this.pts = pts.reduce((acc, curr, i) => {
if(!acc[i/2|0]) acc[i/2|0] = {}
acc[i/2|0][['x', 'y'][i%2]] = curr
return acc
}, [])
this.centers = []
for(let i = 0; i < this.pts.length - (this.isClosed ? 0 : 1); i++) {
const {x: x1, y: y1} = this.pts[i % this.pts.length]
const {x: x2, y: y2} = this.pts[(i + 1) % this.pts.length]
this.centers[i % this.pts.length] = {
x: (x1 + x2) / 2,
y: (y1 + y2) / 2
}
}
this.ctrls = this.isClosed ? [] : [[this.pts[0], this.pts[0]]]
for(let i = this.isClosed ? 0 : 1; i < this.centers.length; i++) {
const pt = this.pts[i]
const c0 = this.centers[(this.centers.length + i - 1) % this.centers.length]
const c1 = this.centers[i]
const dx = (c0.x - c1.x) / 2
const dy = (c0.y - c1.y) / 2
this.ctrls[i] = [
{
x: pt.x + smoothness * dx,
y: pt.y + smoothness * dy
},
{
x: pt.x - smoothness * dx,
y: pt.y - smoothness * dy
}
]
}
if(!this.isClosed) {
this.ctrls.push([
this.pts[this.pts.length - 1],
this.pts[this.pts.length - 1]
])
}

this.d = `M${this.pts[0].x},${this.pts[0].y} ${this.centers.map((d, i) => `C${this.ctrls[i][1].x},${this.ctrls[i][1].y},${this.ctrls[(i + 1) % this.pts.length][0].x},${this.ctrls[(i + 1) % this.pts.length][0].y},${this.pts[(i + 1) % this.pts.length].x},${this.pts[(i + 1) % this.pts.length].y}`).join(' ')}`
this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
this.path.setAttribute('d', this.d)
this.pathLen = this.path.getTotalLength()
}

// drawPts, drawCtrls and drawCenter are color strings
drawSpline(ctx, drawPts = false, drawCtrls = false, drawCenters = false) {
const {pts, ctrls, centers} = this
ctx.beginPath()
ctx.moveTo(pts[0].x, pts[0].y)
for(let i = 0; i < centers.length; i ++) {
ctx.bezierCurveTo(
ctrls[i][1].x, ctrls[i][1].y,
ctrls[(i + 1) % pts.length][0].x, ctrls[(i + 1) % pts.length][0].y,
pts[(i + 1) % pts.length].x, pts[(i + 1) % pts.length].y
)
}
ctx.stroke()
if(drawPts) {
ctx.fillStyle = drawPts
pts.forEach(({x, y}) => ctx.square(x, y, 10))
}
if(drawCtrls) {
ctx.fillStyle = drawCtrls
ctx.strokeStyle = drawCtrls
ctx.lineWidth = 1
ctrls.forEach(([ctrl0, ctrl1]) => {
const {x: x0, y: y0} = ctrl0
const {x: x1, y: y1} = ctrl1
ctx.beginPath()
ctx.moveTo(x0, y0)
ctx.lineTo(x1, y1)
ctx.stroke()
ctx.square(x0, y0, 10)
ctx.square(x1, y1, 10)
})
}
if(drawCenters) {
centers.forEach((c, i) => {
ctx.strokeStyle = drawCenters
ctx.beginPath()
ctx.moveTo(pts[i].x, pts[i].y)
ctx.lineTo(pts[(i+1) % pts.length].x, pts[(i+1) % pts.length].y)
ctx.stroke()
ctx.fillStyle = drawCenters
ctx.square(c.x, c.y, 10)
})
}
}

getSVGPath() {
return this.d
}
getPtAt(t) {
return this.path.getPointAtLength(t * this.pathLen)
}
}
Insert cell
Insert cell
class Ray {
constructor(pos, dir) {
this.pos = pos.clone()
this.dir = dir.clone()
}
}
Insert cell
Insert cell
class Segment {
constructor(a, b) {
this.a = a.clone()
this.b = b.clone()
this.center = this.pointAt(0.5)
this.dx = this.a.distX(this.b)
this.dy = this.a.distY(this.b)
}
copy() {
return new Segment(this.a, this.b)
}
getLength() {
return PVector.sub(this.b, this.a).mag()
}
getAngle() {
return PVector.sub(this.b, this.a).angle2D()
}
pointAt(n) {
return PVector.lerp(this.a, this.b, n)
}
reduce(n) {
const ab = PVector.sub(this.b, this.a).setMag(n)
this.a.add(ab)
this.b.sub(ab)
this.center = this.pointAt(0.5)
this.dx = this.a.distX(this.b)
this.dy = this.a.distY(this.b)
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class Polygon {
// create a new Polygon from SVG path string
static fromPath(path, lengthBetweenPts = 5) {
const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path')
pathElement.setAttribute('d', path)
const pathLength = pathElement.getTotalLength()
const poly = new Polygon(...array(pathLength / lengthBetweenPts | 0).map(i => {
return PVector(pathElement.getPointAtLength(i * lengthBetweenPts))
}))
poly.originalPath = path
return poly
}

// Boolean operations using ClipperLib
// https://sourceforge.net/p/jsclipper/wiki/Home%206/
static intersection(p1, p2) {
const INTERSECTION = ClipperLib.ClipType.ctIntersection
return Polygon.boolOp(INTERSECTION, p1, p2)
}
static union(p1, p2) {
const UNION = ClipperLib.ClipType.ctUnion
return Polygon.boolOp(UNION, p1, p2)
}
static diff(p1, p2) {
const DIFF = ClipperLib.ClipType.ctDifference
return Polygon.boolOp(DIFF, p1, p2)
}
static xor(p1, p2) {
const XOR = ClipperLib.ClipType.ctXor
return Polygon.boolOp(XOR, p1, p2)
}
static boolOp(type, p1, p2) {
const polygonToClipperPath = p => p.pts.map(pt => ({ X: pt.x * 100, Y: pt.y * 100 }))
const subj_paths = [polygonToClipperPath(p1)]
const clip_paths = [polygonToClipperPath(p2)]

const clipper = new ClipperLib.Clipper();
const SUBJECT = ClipperLib.PolyType.ptSubject // 0
const CLIP = ClipperLib.PolyType.ptClip // 1
clipper.AddPaths(subj_paths, SUBJECT, true)
clipper.AddPaths(clip_paths, CLIP, true)

const result = [];

const EVEN_ODD = ClipperLib.PolyFillType.pftEvenOdd // 0
const NON_ZERO = ClipperLib.PolyFillType.pftNonZero // 1
const POSITIVE = ClipperLib.PolyFillType.pftPositive // 2
const NEGATIVE = ClipperLib.PolyFillType.pftNegative // 3
clipper.Execute(type, result, NON_ZERO, NON_ZERO)

return result.map(pts => new Polygon(...pts.map(pt => PVector(pt.X / 100, pt.Y / 100))))
}

// offset done using ClipperLib
// https://sourceforge.net/p/jsclipper/wiki/Home%206/#b-offsetting-paths
static offset(p, delta, type = 'round') {
// scale up coords since Clipper is using integer
const scale = 1000
let paths = [p.pts.map(pt => ({ X: pt.x * scale, Y: pt.y * scale }))]

// Simplifying
paths = ClipperLib.Clipper.SimplifyPolygons(paths, ClipperLib.PolyFillType.pftNonZero)

// Cleaning
var cleandelta = 0.1 // 0.1 should be the appropriate delta in different cases
paths = ClipperLib.JS.Clean(paths, cleandelta * scale)

// Create an instance of ClipperOffset object
var co = new ClipperLib.ClipperOffset()
const jointType = type === 'miter' ? ClipperLib.JoinType.jtMiter :
type === 'square' ? ClipperLib.JoinType.jtSquare :
ClipperLib.JoinType.jtRound
// Add paths
co.AddPaths(paths, jointType, ClipperLib.EndType.etClosedPolygon)
// Create an empty solution and execute the offset operation
let offsetted_paths = new ClipperLib.Paths()
co.Execute(offsetted_paths, delta * scale)

ClipperLib.JS.ScaleDownPaths(offsetted_paths, scale)
if(!(offsetted_paths[0] && offsetted_paths[0].length > 0)) return null
return new Polygon(...offsetted_paths[0].map(pt => PVector(pt.X, pt.Y)))
}

constructor(...pts) {
this.pts = pts.map(pt => pt.clone())
this.segments = pts.map((pt, i) => new Segment(pt, pts[(i + 1 ) % pts.length]))
this.center = new PVector(pts.reduce((acc, d) => PVector.add(acc, d), PVector())).div(pts.length)
}

// adapted from https://www.codeproject.com/tips/84226/is-a-point-inside-a-polygon
contains(pt) {
const pts = this.pts
let c = false
for (let i = 0, j = pts.length-1; i < pts.length; j = i++) {
if (
((pts[i].y > pt.y) != (pts[j].y > pt.y)) &&
(pt.x < (pts[j].x-pts[i].x) * (pt.y-pts[i].y) / (pts[j].y-pts[i].y) + pts[i].x)
) {
c = !c
}
}
return c
}

getBoundingBox() {
const xs = this.pts.map(pt => pt.x)
const ys = this.pts.map(pt => pt.y)
const minX = Math.min(...xs)
const maxX = Math.max(...xs)
const minY = Math.min(...ys)
const maxY = Math.max(...ys)
return {
origin: PVector(minX, minY),
width: maxX - minX,
height: maxY - minY
}
}
getHatchesParametric(originalAngle = PI/2, startSpacing = 1, endSpacing = 10, func = t => t, offset = 0, alternate = false) {
if(offset !== 0) return Polygon.offset(this, -offset).getHatchesParametric(originalAngle, startSpacing, endSpacing, func, 0, alternate)
//bounding box
const originalBB = this.getBoundingBox()
const bb = {
origin: originalBB.origin.clone().sub(Math.min(startSpacing, endSpacing) * 2),
width: originalBB.width + 4 * Math.min(startSpacing, endSpacing),
height: originalBB.height + 4 * Math.min(startSpacing, endSpacing)
}

// hatches
let angle = ((originalAngle % TAU) + TAU) % PI
if(angle > PI/2) angle -= PI
if(modularDist(angle, PI/2) < 0.00001) angle += 0.00001
const hatchDirOriginal = PVector.fromAngle(originalAngle).setMag(1000)
const hatchDir = PVector.fromAngle(angle).setMag(1000)

let startY, endY
let leftPoint, rightPoint

if(angle < 0) {
rightPoint = bb.origin.clone().add(PVector(bb.width, bb.height))
startY = bb.origin.y
endY = lineLineIntersection(
bb.origin,
bb.origin.clone().addY(bb.height),
rightPoint,
rightPoint.clone().sub(hatchDir)
).y
leftPoint = PVector(bb.origin.x, endY)
}
else {
rightPoint = bb.origin.clone().addX(bb.width)
startY = lineLineIntersection(
bb.origin,
bb.origin.clone().addY(bb.height),
rightPoint,
rightPoint.clone().sub(hatchDir)
).y
endY = bb.origin.y + bb.height
leftPoint = PVector(bb.origin.x, startY)
}
const hatches = []
for(let y = startY, stepY = startSpacing, i = 0; y < endY; y += stepY) {
const segA = new Segment(PVector(bb.origin.x, startY), PVector(bb.origin.x, endY))
const segB = new Segment(leftPoint, rightPoint)

const t = (y - startY) / (endY - startY)
const d = startSpacing + (endSpacing - startSpacing) * func(t)
const step = hatchDir.clone().rotateBy(PI/2).setMag(d)
const stepProjection = hatchDir.clone().mult(angle < 0 ? -1 : 1).add(step)
const stepIntersection = lineLineIntersection(segA.a, segA.b, PVector.add(bb.origin, step), PVector.add(bb.origin, stepProjection))
stepY = stepIntersection.y - bb.origin.y
const A = PVector(bb.origin.x, y)
const B = A.clone().add(hatchDir)
const ray = new Ray(A, hatchDir)

const intersections = this.segments.map(seg => raySegIntersection(ray, seg))
.filter(d => d)
.sort((a, b) => a.y > b.y ? 1 :
a.y < b.y ? -1 :
a.x > b.x ? 1 :
a.x < b.x ? -1 : 0)

if(intersections.length < 2) continue;
else {
if(alternate && i % 2) intersections.reverse()
const segments = []
for(let i = 0; i < intersections.length && intersections[i + 1]; i += 2) {
segments.push(new Segment(intersections[i], intersections[i + 1]))
}
hatches.push(segments)
i++
}
}

return hatches.filter(d => d).flat().filter(seg => this.contains(seg.pointAt(0.3)))
}

getHatches(originalAngle = PI/2, spacing = 2, offset = 0, alternate = false) {
return this.getHatchesParametric(originalAngle, spacing, spacing, t => t, offset, alternate)
}

getHatches2(originalAngle = PI/2, startSpacing = 1, endSpacing = 10, power = 1, offset = 0, alternate = false) {
return this.getHatchesParametric(originalAngle, startSpacing, endSpacing, t => t ** power, offset, alternate)
}
}
Insert cell
Insert cell
Insert cell
await visibility(), (() => {
const w = width
const h = 300
const originalAngle = random(-TAU, TAU)
const startSpacing = randInt(2, 10)
const endSpacing = randInt(30, 60)
const offset = 0
// polygon
const n = randInt(3, 30)
const p = new Polygon(...array(n).map(i => {
const angle = TAU / n * i
const x = w/2 + Math.cos(angle) * random(20, w/2)
const y = h/2 + Math.sin(angle) * random(20, h/2)
return PVector(x, y)
}))
//bounding box
const bb = p.getBoundingBox()

// hatches
let angle = ((originalAngle % TAU) + TAU) % PI
if(angle > PI/2) angle -= PI
const hatchDirOriginal = PVector.fromAngle(originalAngle).setMag(1000)
const hatchDir = PVector.fromAngle(angle).setMag(1000)
let startY, endY
let leftPoint, rightPoint
if(angle < 0) {
rightPoint = bb.origin.clone().add(PVector(bb.width, bb.height))
startY = bb.origin.y - 5
endY = lineLineIntersection(
bb.origin,
bb.origin.clone().addY(bb.height),
rightPoint,
rightPoint.clone().sub(hatchDir)
).y + 5
leftPoint = PVector(bb.origin.x, endY)
}
else {
rightPoint = bb.origin.clone().addX(bb.width)
startY = lineLineIntersection(
bb.origin,
bb.origin.clone().addY(bb.height),
rightPoint,
rightPoint.clone().sub(hatchDir)
).y - 5
endY = bb.origin.y + bb.height + 5
leftPoint = PVector(bb.origin.x, startY)
}
const segA = new Segment(PVector(bb.origin.x, startY), PVector(bb.origin.x, endY))
const segB = new Segment(leftPoint, rightPoint)
const step = hatchDir.clone().rotateBy(PI/2).setMag(Math.min(startSpacing, endSpacing))
const stepProjection = hatchDir.clone().mult(angle < 0 ? -1 : 1).add(step)
const stepIntersection = lineLineIntersection(segA.a, segA.b, PVector.add(bb.origin, step), PVector.add(bb.origin, stepProjection))
const stepY = stepIntersection.y - bb.origin.y

const hatches = [
...p.getHatchesParametric(originalAngle, startSpacing, endSpacing, t => t ** 3, offset, true),
]
return svg`<svg width="${w}" height="${h}">
${pathFromPolygon(p, 'fill="none" stroke="black" stroke-width="3"')}
<g transform="translate(${bb.origin.x}, ${bb.origin.y})">
<!-- bounding box origin -->
<circle r="5" fill="red" />
<!-- bounding box -->
<rect
stroke="red"
stroke-dasharray="3 5"
fill="none"
width="${bb.width}"
height="${bb.height}"
/>
<!-- hatches with orignal angle -->
<line x2="${hatchDirOriginal.x}" y2="${hatchDirOriginal.y}" stroke="cyan" stroke-dasharray="3" />
<!-- hatches with computed angle -->
<line x2="${hatchDir.x}" y2="${hatchDir.y}" stroke="red" />
<!-- hatches step -->
<line x2="${step.x}" y2="${step.y}" stroke="black" stroke-dasharray="3" />
<line x1="${step.x}" y1="${step.y}" x2="${stepProjection.x}" y2="${stepProjection.y}" stroke="black" stroke-dasharray="3" />
</g>
<line
x1="${segA.a.x}"
y1="${segA.a.y}"
x2="${segA.b.x}"
y2="${segA.b.y}"
stroke="blue"
/>
<line
x1="${segB.a.x}"
y1="${segB.a.y}"
x2="${segB.b.x}"
y2="${segB.b.y}"
stroke="blue"
stroke-dasharray="3 10"
/>
<circle cx="${segA.a.x}" cy="${segA.a.y}" r="5" fill="blue" />
<circle cx="${segA.b.x}" cy="${segA.b.y}" r="5" fill="blue" />

<circle cx="${stepIntersection.x}" cy="${stepIntersection.y}" r="5" fill="green" />

${linesFromHatches(hatches, 'opacity="0.3" stroke="black" stroke-width="3"')}

${pathFromHatches(hatches, 'opacity="0.5" stroke="blue" stroke-dasharray="5" fill="none"')}

<!-- Infos -->
<!--
<g transform="translate(15, 15)">
<text>Bounding box</text>
<text y="15">x: ${bb.origin.x | 0}</text>
<text y="30">y: ${bb.origin.y | 0}</text>
<text y="45">width: ${bb.width | 0}</text>
<text y="60">height: ${bb.height | 0}</text>
</g>

<g transform="translate(150, 15)">
<text>Hatches</text>
<text y="15">originalAngle: ${((originalAngle*100)|0)/100}</text>
<text y="30">angle: ${((angle*100)|0)/100}</text>
</g>
-->
</svg>`
})()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class HexGrid {
constructor(x, y, w, h, hexSize = 10, type = 'flat') {
this.x = x
this.y = y
this.w = w
this.h = h

// type can be either 'flat' or 'pointy'
this.type = type === 'flat' || type === 'pointy' ? type : 'flat'

this.hexHeight = Math.sqrt(3) * hexSize
this.hexWidth = 2 * hexSize
if(this.type === 'pointy') [this.hexWidth, this.hexHeight] = [this.hexHeight, this.hexWidth] // swap

this.hexPoints = this.getHexPoints()

this.nx = this.type === 'flat' ?
2 + (w / (this.hexWidth * 3/4)) :
2 + (w / (this.hexWidth * 1/2))

this.ny = this.type === 'flat' ?
2 + (h / (this.hexHeight * 1/2)) :
2 + (h / (this.hexHeight * 3/4))

this.centers = this.getCenters()
this.w = this.centers.sort((a, b) => b.x - a.x)[0].x - this.x
this.h = this.centers.sort((a, b) => b.y - a.x)[0].y - this.y
delete this.nx
delete this.ny
}

getHexPoints() {
if(this.type === 'flat') {
return [
{ x: this.hexWidth / 2, y: 0 },
{ x: this.hexWidth / 4, y: this.hexHeight / 2 },
{ x: -this.hexWidth / 4, y: this.hexHeight / 2 },
{ x: -this.hexWidth / 2, y: 0 },
{ x: -this.hexWidth / 4, y: -this.hexHeight / 2 },
{ x: this.hexWidth / 4, y: -this.hexHeight / 2 },
]
}
else {
return [
{ x: 0, y: -this.hexHeight / 2 },
{ x: this.hexWidth / 2, y: -this.hexHeight / 4 },
{ x: this.hexWidth / 2, y: this.hexHeight / 4 },
{ x: 0, y: this.hexHeight / 2 },
{ x: -this.hexWidth / 2, y: this.hexHeight / 4 },
{ x: -this.hexWidth / 2, y: -this.hexHeight / 4 },
]
}
}

getCenters() {
const centers = []
for(let i = 0; i < this.ny; i ++) {
let isOut = false
for(let j = 0; j < this.nx && !isOut; j ++) {
let x = this.x
let y = this.y
if(this.type === 'flat') {
x += j * this.hexWidth * 3/2 + (i % 2) * this.hexWidth * 3/4
y += i * this.hexHeight * 1/2
}
else {
x += j * this.hexWidth + (i % 2) * this.hexWidth * 1/2
y += i * this.hexHeight * 3/4
}
if(x - this.hexWidth/2 < this.x + this.w && y - this.hexHeight/2 < this.y + this.h) {
centers.push({ x, y })
}
else isOut = true
}
}

return centers
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more