xxxxxxxxxx
/*
Here's a submission to this week's #WCCChallenge theme, SNAKES!
I'm using this as an opportunity to try out the p5 2.0 beta!
More info here: https://github.com/processing/p5.js/releases/tag/v2.0.0-beta.1
In terms of new features, this is using:
- A stroke shader that changes the stroke width variably
- splineVertex(), the updated version of curveVertex(), where it actually
draws the curve through your first and last point without having to
duplicate points! (Closed smooth curves with endShape(CLOSE) work too, even
though I didn't use them here.)
*/
let snakePts
let margin = 100
let startTime
let snakeShader
let bg
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL);
makeSnake()
snakeShader = baseStrokeShader().modify({
'StrokeVertex getObjectInputs': `(StrokeVertex inputs) {
inputs.weight *= 0.85 + 0.15*sin((inputs.position.x + inputs.position.y) * 0.05);
return inputs;
}`
})
}
let distCache = {}
function getDist(a, b) {
if (a.x+a.y > b.x+b.y) {
[a,b] = [b,a];
}
const key = `${a.x.toFixed(2)},${a.y.toFixed(2)}-${b.x.toFixed(2)},${b.y.toFixed(2)}`
if (key in distCache) {
return distCache[key]
}
const d = a.dist(b)
distCache[key] = d
return d
}
function makeSnake() {
const pts = []
for (let i = 0; i < 300; i++) {
pts.push(createVector(random(-width/2+margin, width/2-margin), random(-height/2+margin, height/2-margin)))
}
distCache = {}
// "We have traveling salesman at home"
// So I want to connect all the points into one line. Ideally
// this would be the shortest path connecting all of them, but
// that's HARD. So instead I'm just connecting the closest pairs
// greedily until I have one line instead of trying to find the
// global minimum. Good enough!
const candidates = pts.map(p => [p])
while (candidates.length > 1) {
const string = candidates.shift()
const dists = candidates.map((other) => {
const dists = [
getDist(string.at(0), other.at(0)),
getDist(string.at(0), other.at(-1)),
getDist(string.at(-1), other.at(0)),
getDist(string.at(-1), other.at(-1)),
]
const minDist = min(dists)
return { minDist, dists }
})
const minDist = min(dists.map(d => d.minDist))
const idx = dists.map(d => d.minDist).indexOf(minDist)
const [other] = candidates.splice(idx, 1)
const distIdx = dists[idx].dists.indexOf(minDist)
if (distIdx === 0) {
candidates.push([other.reverse(), string])
} else if (distIdx === 1) {
candidates.push([other, string])
} else if (distIdx === 2) {
candidates.push([string, other])
} else {
candidates.push([string, other.reverse()])
}
}
snakePts = candidates[0]
startTime = millis()
bg = random([
'#F2DF7F',
'#F0C0CA',
'#C9E6EB',
'#D5D5F9'
])
}
function mouseClicked() {
makeSnake()
}
function draw() {
const t = millis() - startTime
background(bg)
push()
strokeWeight(30)
stroke('#24AC96')
curveDetail(0.1)
noFill()
strokeShader(snakeShader);
beginShape()
let lastX, lastY
for (const [i, { x, y }] of snakePts.entries()) {
const len = i/(snakePts.length-1)
const sx = x + 5*sin(i+t*0.01)
const sy = map(t, len * 4000, len * 4000 + 1000, -height*0.8, y, true) + 5*sin(i*0.9+t*0.01)
splineVertex(sx, sy)
lastX = sx
lastY = sy
// splineVertex(x, y)
}
endShape()
pop()
push()
strokeWeight(4)
stroke(0)
point(lastX, lastY)
pop()
push()
noStroke()
fill(bg)
const tan = snakePts.at(-1).copy().sub(snakePts.at(-2)).normalize()
translate(lastX, lastY)
rotate(atan2(tan.y, tan.x))
translate(0, 5)
triangle(0, 0, 20, -8, 20, 8)
pop()
}