Published
Edited
Jan 4, 2021
7 stars
Insert cell
md`# Flow Fields Mug Vinyl Generator

TODO:
1. DONE - Establish the width and height of the mug
2. DONE - Generate a flow field to sample from. Effectively porting code over from the Flow Fields Christmas ornament generator
3. DONE - Generate lines on the flow field
4. DONE - Apply the Minkowski and segmenting technique
5. DONE - Do a test cut
6. Do a real cut on vinyl
7. Apply it to a mug`

Insert cell
html`<figure>
${await FileAttachment("7BC32E85-5F74-4C1A-A9B6-B8541B65DAFE.jpeg").image()}
<figcaption>First Test Cut</figcaption>
</figure>`
Insert cell
viewof widthInMM = html`<input type=range value=120 min=60 max=240 step=1>`
Insert cell
viewof heightInMM = html`<input type=range value=160 min=60 max=240 step=1>`
Insert cell
diameter = Math.PI * widthInMM
Insert cell
md`Mouth Width: ${widthInMM}mm, Height: ${heightInMM}mm, Diameter: ${Math.round(diameter)}mm`
Insert cell
viewof fieldScale = html`<input type=range value=1 min=0 max=3 step=0.05>`
Insert cell
Insert cell
viewof stepLength = html`<input type=range value=1 min=0.2 max=5 step=0.1>`
Insert cell
viewof maxLength = html`<input type=range value=50 min=10 max=350 step=1>`
Insert cell
offsetRadius = (minProximity - 1.25) / 2
Insert cell
viewof minProximity = html`<input type=range value=5.5 min=1 max=20 step=0.25>`
Insert cell
paths = {
const board = d3.quadtree()
.x(d => d.x)
.y(d => d.y)
.extent([diameter, heightInMM])
const tooCloseToExistingPoint = (next) => {
const proximity = dynamicProximity(next)
const nearest = board.find(next.x, next.y, proximity)
/*
if (nearest) {
console.log({ nearest: `${nearest.x} ${nearest.y}`, proximity })
}
*/
return (!nearest) ? false : true
}
const pathTracer = (init) => {
const result = []
result.push(init)

while (result.length < 100) {
const l = result[result.length - 1]
const fp = fieldQuadTree.find(l.x, l.y)

let next = {
x: l.x + Math.cos(fp.radian) * stepLength,
y: l.y + Math.sin(fp.radian) * stepLength
}
const isTooClose = (result.length && tooCloseToExistingPoint(next))
result.push(next)
if (isTooClose || Math.random() > 0.98 || result.length > maxLength) { break; }
if (!isPointInBounds(next)) {
// draw last point outside of bounds, but then stop
break;
}
}

return result
}
const minPathLength = 4
const paths = []
let failureCount = 0
let acceptanceThreshold = 75
/*
getFibonacciSpherePoints(2000, radius, false).map((p, i) => {
const l = convert3DPointToSphereCoordinate(p.x, p.y, p.z)
const q = sphereToFlat(l.lat, l.lng)
*/
d3.range(300000).forEach((i) => {
const q = { x: Math.random() * diameter, y: Math.random() * heightInMM }
if (!isPointInBounds(q)) { return }
const path = pathTracer(q)
//console.log(path)
const keep = path.length > acceptanceThreshold
if (keep) {
board.addAll(path) // add to points we're tracking
paths.push(path)
} else {
failureCount++
}
if (failureCount > 25) {
acceptanceThreshold = Math.max(3, acceptanceThreshold * 0.98)
failureCount = 0
}
})
return paths
}
Insert cell
isPointInBounds = ({ x, y }) => {
const margin = 2
return (x < margin || x > diameter - margin || y < margin || y > heightInMM - margin) ? false : true
}
Insert cell
Insert cell
dynamicProximity = {
const ease = d3.easePolyIn.exponent(2.5)
const fn = ({ x, y }) => {
return ease(1 - y / heightInMM) * 15 + minProximity
}
return fn
}
Insert cell
{
const svg = d3.create("svg")
.attr("viewBox", [0, 0, diameter, heightInMM])
.attr("width", width);
flowField.forEach(p => {
svg.append("polygon")
.attr("transform", `translate(${p.x}, ${p.y}) rotate(${p.radian / Math.PI * 180})`)
.attr("points", "-0.5,0 0.5,0 0,-1.5")
.attr("fill", d3.interpolateRainbow((p.radian + Math.PI) / (2 * Math.PI)))
})
return svg.node()
}
Insert cell
fieldDensity = 2
Insert cell
fieldQuadTree = {
const q = d3.quadtree()
.x(d => d.x)
.y(d => d.y)
.extent([diameter, heightInMM])
.addAll(flowField)
return q
}
Insert cell
flowField = {
const columns = Math.ceil(diameter / fieldDensity)
const rows = Math.ceil(heightInMM / fieldDensity)
const longerSideLength = (columns > rows) ? columns : rows
const points = []
let minNoise = Infinity
let maxNoise = -Infinity
d3.range(rows).forEach(row => {
d3.range(columns).forEach(col => {
const noise = perlin2.gen(col / longerSideLength * fieldScale, row / longerSideLength * fieldScale)
minNoise = (noise < minNoise) ? noise : minNoise
maxNoise = (noise > maxNoise) ? noise : maxNoise
points.push({
x: col * fieldDensity,
y: row * fieldDensity,
noise
})
})
})
const noiseToRadianScale = d3.scaleLinear()
.domain([minNoise, maxNoise])
.range([-Math.PI, Math.PI])
points.forEach(p => {
p.radian = noiseToRadianScale(p.noise)
})
return points
}
Insert cell
generateSegments = (pathString, increment = 3) => {
const pointAtPath = pointAtLengthQueryObj(pathString)
const segments = []
let location = 0
while (location < pointAtPath.pathLength) {
const count = 3 + ((Math.random() > 0.1) ? Math.pow(Math.ceil(Math.random() * 8), 2) : 0)
const segment = []
d3.range(count).forEach(i => {
const nextLocation = location + i * increment
if (nextLocation < pointAtPath.pathLength) {
segment.push(pointAtPath(location + i * increment))
}
})
if (segment.length > 1) {
segments.push(segment)
}
location += count * increment + 3 * offsetRadius
}
return segments
}
Insert cell
pointAtLengthQueryObj = (pathString) => {
const pathEl = DOM.element("svg:path", {
d: pathString
})
const fn = function(length) {
const svgPoint = pathEl.getPointAtLength(length)
return {
x: svgPoint.x,
y: svgPoint.y
}
}
fn.pathElement = pathEl
fn.pathLength = pathEl.getTotalLength()
return fn
}
Insert cell
import { minkowski } from "a4b4f6a0d08dc6f5"
Insert cell
height = width * 0.4
Insert cell
perlin2 = new tumult.Perlin2("tyler_hobbs_45")
Insert cell
tumult = require('https://unpkg.com/tumult/dist/tumult.min.js')
Insert cell
d3 = require("d3@6")
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