Public
Edited
May 10, 2020
Fork of Utilities
Insert cell
Insert cell
Insert cell
array = n => new Array(n).fill(0).map((d, i) => i)
Insert cell
array(10).map(i => i * 2)
Insert cell
Insert cell
shuffle = arr => {
let tmpArray = [ ... arr ] // create a copy of original array
for (let i = tmpArray.length - 1; i; i --) {
let randomIndex = randInt(i + 1);
[tmpArray[i], tmpArray[randomIndex]] = [tmpArray[randomIndex], tmpArray[i]] // swap
}
return tmpArray
}
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
lerp = (a, b, amount) => a + (b - a) * amount
Insert cell
lerp(27, 38, 0.5)
Insert cell
Insert cell
mix = (a, b, amount) => {
return a.length === b.length ? a.map((d, i) => lerp(a[i], b[i], amount)) : 'lengths not matching'
}
Insert cell
mix([2,4,6], [1, 9, 12], 0.5)
Insert cell
Insert cell
clamp = (a, min, max) => a < min ? min : a > max ? max : a
Insert cell
clamp(23, 10, 20)
Insert cell
Insert cell
map = (n, a, b, c, d, clamped = false) => {
return clamped ? clamp(lerp(c, d, (n - a) / (b - a)), c, d) : lerp(c, d, (n - a) / (b - a))
}
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
random = (a, b) => {
if(!a && a !== 0) return Math.random()
if(!b && b !== 0) return Math.random() * a

if(a > b) [a, b] = [b, a] // swap values
return a + Math.random() * (b - a)
}
Insert cell
random(3, 47)
Insert cell
randInt = (a, b) => ~~random(a, b)
Insert cell
randInt(3, 47)
Insert cell
Insert cell
prng = i => Math.sin(i * 9999) % 1
Insert cell
prng(1)
Insert cell
coin = () => random() < .5 ? -1 : 1
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// based on https://observablehq.com/@mattdesl/heartbeat-function
beat = (value, intensity = 2, frequency = 1) => {
return (Math.atan(Math.sin(value * Math.PI * frequency * 2) * intensity) + Math.PI / 2) / Math.PI
}
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
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
step = (x, edge) => x < edge ? 0 : 1
Insert cell
smoothstep = (x, edge0, edge1) => {
const t = clamp((x - edge0) / (edge1 - edge0), 0, 1)
return t * t * (3 - 2 * t)
}
Insert cell
linearstep = (begin, end, t) => clamp((t - begin) / (end - begin), 0, 1)
Insert cell
linearstepUpDown = (upBegin, upEnd, downBegin, downEnd, t) => {
return linearstep(upBegin, upEnd, t) - linearstep(downBegin, downEnd, t)
}
Insert cell
stepUpDown = (begin, end, t) => step(begin, t) - step(end, t)
Insert cell
Insert cell
Insert cell
class Ray {
constructor(pos, dir) {
this.pos = pos.clone()
this.dir = dir.clone()
}
}
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)
}
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)
}
}
Insert cell
Insert cell
Insert cell
Insert cell
{
const w = 200
// const ray = new Ray(PVector.random(PVector(w, w)), PVector.random2D().setMag(1000))
// const seg = new Segment(PVector.random(PVector(w, w)), PVector.random(PVector(w, w)))
const ray = new Ray(PVector(10, w/2), PVector(w,0))
const seg = new Segment(PVector(50, w/2), PVector(w/2 + 50, 0))
const intersection = raySegIntersection(ray, seg)
return svg`<svg width="${w}" height="${w}">
<rect x="0" y="0" width="${w}" height="${w}" stroke="red" fill="none"/>

<circle cx="${ray.pos.x}" cy="${ray.pos.y}" fill="green" stroke="none" r="3" />
<polyline fill="none" stroke="green" stroke-dasharray="3 3" points="${ray.pos.x} ${ray.pos.y} ${ray.pos.x + ray.dir.x} ${ray.pos.y + ray.dir.y}" />

<circle cx="${seg.a.x}" cy="${seg.a.y}" fill="blue" stroke="none" r="3" />
<circle cx="${seg.b.x}" cy="${seg.b.y}" fill="blue" stroke="none" r="3" />
<polyline fill="none" stroke="blue" points="${seg.a.x} ${seg.a.y} ${seg.b.x} ${seg.b.y}" />
${intersection ? `<circle fill="red" stroke="none" cx="${intersection.x}" cy="${intersection.y}" r="3" />`: ''}
</svg>`
}
Insert cell
Insert cell
{
const w = 200
const s1 = new Segment(PVector.random(PVector(w, w)), PVector.random(PVector(w, w)))
const s2 = new Segment(PVector.random(PVector(w, w)), PVector.random(PVector(w, w)))
const intersection = segSegIntersection(s1, s2)
return svg`<svg width="${w}" height="${w}">
<rect x="0" y="0" width="${w}" height="${w}" stroke="red" fill="none"/>

<circle cx="${s1.a.x}" cy="${s1.a.y}" fill="green" stroke="none" r="3" />
<circle cx="${s1.b.x}" cy="${s1.b.y}" fill="green" stroke="none" r="3" />
<polyline fill="none" stroke="green" points="${s1.a.x} ${s1.a.y} ${s1.b.x} ${s1.b.y}" />

<circle cx="${s2.a.x}" cy="${s2.a.y}" fill="blue" stroke="none" r="3" />
<circle cx="${s2.b.x}" cy="${s2.b.y}" fill="blue" stroke="none" r="3" />
<polyline fill="none" stroke="blue" points="${s2.a.x} ${s2.a.y} ${s2.b.x} ${s2.b.y}" />
${intersection ? `<circle fill="red" stroke="none" cx="${intersection.x}" cy="${intersection.y}" r="3" />`: ''}
</svg>`
}
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()
return new Polygon(...array(pathLength / lengthBetweenPts | 0).map(i => {
return PVector(pathElement.getPointAtLength(i * lengthBetweenPts))
}))
}
// 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))))
}
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
}
}

getHatches(originalAngle = PI/2, spacing = 2, alternate = false) {
//bounding box
const originalBB = this.getBoundingBox()
const bb = {
origin: originalBB.origin.clone().sub(spacing * 2),
width: originalBB.width + 4 * spacing,
height: originalBB.height + 4 * spacing
}

// hashes
let angle = ((originalAngle % TAU) + TAU) % PI
if(angle > PI/2) angle -= PI
if(angle === 0 || angle === PI / 2) 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 = lineLineIntersect(
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 = lineLineIntersect(
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 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(spacing)
const stepProjection = hatchDir.clone().mult(angle < 0 ? -1 : 1).add(step)
const stepIntersection = lineLineIntersect(segA.a, segA.b, PVector.add(bb.origin, step), PVector.add(bb.origin, stepProjection))
const stepY = stepIntersection.y - bb.origin.y

const hatches = array(((endY - startY) / stepY | 0) + 1).map(i => {
const A = PVector(bb.origin.x, startY + i * stepY)
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) return null
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]))
}
return segments
}).filter(d => d).flat().filter(seg => this.contains(seg.pointAt(0.3)))
return hatches
}
}
Insert cell
Insert cell
pathFromPolygon = p => `${p.pts.map((pt, i) => `${i === 0 ? 'M' : 'L'}${pt.x} ${pt.y}`).join(' ')} Z`
Insert cell
pathFromHatches = hatches => `${hatches.map((seg, i) => `${i === 0 ? 'M' : 'L'}${seg.a.x} ${seg.a.y} L${seg.b.x} ${seg.b.y}`).join(' ')}`
Insert cell
hatchMultiplePolygons = (polys = [], originalAngle = 0, spacing = 2, alternate = false) => {
const boundingPoly = new Polygon(...polys.map(p => p.pts).flat())
//bounding box
const originalBB = boundingPoly.getBoundingBox()
const bb = {
origin: originalBB.origin.clone().sub(spacing * 2),
width: originalBB.width + 4 * spacing,
height: originalBB.height + 4 * spacing
}

// hashes
let angle = ((originalAngle % TAU) + TAU) % PI
if(angle > PI/2) angle -= PI
if(angle === 0 || angle === PI / 2) angle += 0.0001
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 = lineLineIntersect(
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 = lineLineIntersect(
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 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(spacing)
const stepProjection = hatchDir.clone().mult(angle < 0 ? -1 : 1).add(step)
const stepIntersection = lineLineIntersect(segA.a, segA.b, PVector.add(bb.origin, step), PVector.add(bb.origin, stepProjection))
const stepY = stepIntersection.y - bb.origin.y

const hatches = array(((endY - startY) / stepY | 0) + 1).map(i => {
const A = PVector(bb.origin.x, startY + i * stepY)
const B = A.clone().add(hatchDir)
const ray = new Ray(A, hatchDir)

const intersections = polys.map(p => p.segments).flat().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) return null

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]))
}
return segments
}).filter(d => d).flat()//.filter(seg => this.contains(seg.pointAt(0.3)))

return hatches
}
Insert cell
{
const w = width
const h = 400
const center = PVector(w/2, h/2)
const p1 = new Polygon(...array(12).map(i => PVector(Math.cos(TAU/12 * i)*h/2, Math.sin(TAU/12 * i)*h/2).add(center)))
const p2 = new Polygon(...array(12).map(i => PVector(Math.cos(TAU/12 * i)*(h/2 - 50), Math.sin(TAU/12 * i)*(h/2 - 50)).add(center)))
const p3 = new Polygon(...array(12).map(i => PVector(Math.cos(TAU/12 * i)*(h/2 - 150), Math.sin(TAU/12 * i)*(h/2 - 150)).add(center)))
const hatches = hatchMultiplePolygons([p1, p2, p3])
return svg`<svg width="${w}" height="${h}">
<path fill="none" stroke="red" stroke-width="4" d="${pathFromPolygon(p1)}" />
<path fill="none" stroke="blue" stroke-width="4" d="${pathFromPolygon(p2)}" />
<path fill="none" stroke="green" stroke-width="4" d="${pathFromPolygon(p3)}" />
${hatches.map(seg => `<line stroke="blue" x1="${seg.a.x}" y1="${seg.a.y}" x2="${seg.b.x}" y2="${seg.b.y}" />`).join('\n')}
</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

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