Each SCanvas has a seeded pseudorandom number generator. The same seed produces the same sequence of values. Keep the seed to reproduce a sketch, or change it to try another variation.
The SCanvas methods below share this generator. The standalone noise functions use fixed noise patterns, independent of the sketch seed.
s.random() // uniform number in [0, 1)
s.uniformRandomInt({ from: 3, to: 6 }) // integer: 3, 4, 5 or 6
s.uniformRandomInt({ to: 6, inclusive: false }) // 0..5
s.randomPoint() // a random point on the canvas
s.randomAngle() // uniform in [0, 2π)
s.randomPolarity() // -1 or 1
s.uniformGridPoint({ minX: 0, maxX: 4, minY: 0, maxY: 4 }) // random integer points.background(220, 25, 12)
s.times(150, () => {
s.setFillColor(s.uniformRandomInt({ from: 150, to: 220 }), 80, 60, 0.8)
s.fill(
new Star({
at: s.randomPoint(),
r: 0.01 + 0.04 * s.random(),
n: s.uniformRandomInt({ from: 4, to: 7 }),
a: s.randomAngle(),
})
)
})sample picks one element from an array, samples picks n (with replacement), and shuffle reorders an array in place:
const palette = [200, 215, 340, 45]
s.sample(palette) // e.g. 340
s.samples(3, palette) // e.g. [45, 200, 45]
s.shuffle([...palette]) // e.g. [215, 45, 200, 340]s.background(0, 0, 96)
const hues = [200, 215, 340, 45]
s.forTiling({ n: 9, type: "square", margin: 0.05 }, ([x, y], [dX]) => {
s.setFillColor(s.sample(hues), 70, s.sample([45, 55, 65]))
s.fill(new Square({ at: [x + dX * 0.05, y + dX * 0.05], s: dX * 0.9 }))
})perturb({ at, magnitude }) offsets a point randomly by up to magnitude / 2 in each direction:
s.background(35, 40, 94)
s.lineWidth = 0.005
s.setStrokeColor(25, 60, 30)
s.forTiling({ n: 12, type: "square" }, ([x, y], [dX, dY]) => {
s.draw(
SimplePath.withPoints([
[x, y],
[x + dX, y],
[x + dX, y + dY],
[x, y + dY],
])
.close()
.transformLoopedPoints((pt) => s.perturb({ at: pt, magnitude: 0.02 }))
)
})For values that cluster around a mean or represent counts, use:
gaussian({ mean, sd }) — normally distributed values, which cluster around the meanpoisson(lambda) — non-negative integers with mean (and variance) lambda, good for "how many things here?" decisionss.background(0, 0, 12)
s.times(400, () => {
const x = s.gaussian({ mean: 0.5, sd: 0.12 })
const y = s.gaussian({ mean: 0.5, sd: 0.12 })
const d = v.distance([x, y], [0.5, 0.5])
s.setFillColor(200 + d * 300, 80, 60, 0.7)
s.fill(new Circle({ at: [x, y], r: 0.012 }))
})Also see proportionately and doProportion on the Iteration page for weighted random choices, and forPoissonDiskPoints for evenly spread random points.
perlin2(x, y) returns smooth 2D noise, approximately in [-1, 1]. Nearby inputs give similar values. Multiply coordinates by a larger factor for finer detail, or a smaller one for broader variation.
import { perlin2 } from "solandra"
s.background(0, 0, 96)
s.forTiling({ n: 30, type: "square" }, ([x, y], [dX], [cX, cY]) => {
const n = perlin2(x * 3, y * 3)
s.setFillColor(190 + n * 60, 70, 50)
s.withTranslation([cX, cY], () => {
s.withRotation(n * Math.PI, () => {
s.fill(
new Rect({ at: [0, 0], w: dX * 0.9, h: dX * 0.18, align: "center" })
)
})
})
})Use noise as an angle to guide lines through a flow field:
s.background(220, 40, 14)
s.lineWidth = 0.003
s.times(120, (i) => {
let pt = s.randomPoint()
const path = SimplePath.startAt(pt)
s.times(30, () => {
const a = perlin2(pt[0] * 2.5, pt[1] * 2.5) * Math.PI * 2
pt = v.add(pt, [0.008 * Math.cos(a), 0.008 * Math.sin(a)])
path.addPoint(pt)
})
s.setStrokeColor(170 + i, 70, 60, 0.7)
s.draw(path.chaiken({ n: 2 }))
})fbm2 (fractional Brownian motion) combines layers, or octaves, of perlin2. Each octave adds finer detail at a lower amplitude. The result is normalised to approximately [-1, 1].
octaves — how many layers (default 4); one octave is exactly perlin2persistence — amplitude multiplier per octave (default 0.5); higher values add stronger detaillacunarity — frequency multiplier per octave (default 2)import { fbm2 } from "solandra"
s.background(205, 45, 70)
s.forTiling({ n: 60, type: "square" }, ([x, y], [dX, dY]) => {
const n = fbm2(x * 3, y * 3, { octaves: 6, persistence: 0.55 })
s.setFillColor(210 - n * 30, 40 + n * 20, 55 + n * 45)
s.fill(new Rect({ at: [x, y], w: dX * 1.2, h: dY * 1.2 }))
})Compare a single octave with several octaves below:
s.background(30, 30, 12)
s.lineWidth = 0.004
s.range({ from: 0.1, to: 0.9, n: 24 }, (y) => {
const line = SimplePath.withPoints(
s.build(s.range, { from: 0, to: 1, n: 80 }, (x) => [
x,
y * s.meta.bottom + fbm2(x * 2.5, y * 2.5, { octaves: 5 }) * 0.07,
])
)
s.setStrokeColor(40, 70, 85, 0.8)
s.draw(line)
})perlin2 and fbm2 depend only on their coordinates and options. Changing the sketch seed does not change them.
curl2 returns (∂n/∂y, -∂n/∂x), estimated from the noise field. The underlying curl field is divergence-free, making it useful for swirling flows without sources or sinks.
It accepts the same octaves, persistence and lacunarity options as fbm2, with one octave by default. epsilon sets the derivative sampling step. The vector's length depends on how quickly the noise changes.
import { curl2, v } from "solandra"
s.background(220, 25, 12)
s.lineWidth = 0.002
s.forTiling({ n: 22, type: "square" }, (_, [dX], [cX, cY]) => {
const [uX, uY] = v.normalize(curl2(cX * 2.5, cY * 2.5))
s.setStrokeColor(190 + uX * 60, 60, 70)
s.draw(
SimplePath.withPoints([
[cX, cY],
[cX + uX * dX, cY + uY * dX],
])
)
})SimplePath.flowLine traces a vector field with fixed-length steps. Pass a function from position to direction, and use until to stop early, for example at the canvas edge.
s.background(215, 30, 10)
s.lineWidth = 0.003
s.times(120, () => {
const from = s.randomPoint()
s.setStrokeColor(180 + from[0] * 80, 70, 60, 0.7)
s.draw(
SimplePath.flowLine({
from,
field: ([x, y]) => curl2(x * 2.5, y * 2.5),
n: 120,
step: 0.005,
until: (at) => !s.inDrawing(at),
})
)
})flowLine accepts any vector field. This example traces spirals around the centre:
s.background(45, 30, 95)
s.lineWidth = 0.004
s.aroundCircle({ n: 24, r: 0.42 }, (from, i) => {
s.setStrokeColor(10 + i * 6, 65, 50, 0.8)
s.draw(
SimplePath.flowLine({
from,
// at right angles to the centre, so it circles rather than falls in
field: (at) => v.rotate(v.subtract(s.meta.center, at), Math.PI / 2.2),
n: 200,
step: 0.005,
})
)
})worley2 returns distances to feature points scattered across a grid. Use it for cellular textures such as cobbles or stained glass.
Coordinates use grid units: worley2(x * 8, y * 8) gives eight grid cells across the canvas. Distances are zero at feature points and increase away from them; they are not clamped to [0, 1].
s.forTiling({ n: 150, type: "square" }, ([x, y], [dX, dY]) => {
const d = worley2(x * 7, y * 7)
s.setFillColor(25 + d * 25, 55, 12 + d * 55)
s.fill(new Rect({ at: [x, y], w: dX, h: dY }))
})feature selects the nearest distance ("f1"), second-nearest ("f2") or their "difference". The difference is zero where two feature points are equally near, outlining cell boundaries:
s.background(205, 25, 96)
s.forTiling({ n: 200, type: "square" }, ([x, y], [dX, dY]) => {
const d = worley2(x * 5, y * 5, { feature: "difference" })
if (d > 0.14) return
s.setFillColor(215, 45, 20, 1 - d / 0.14)
s.fill(new Rect({ at: [x, y], w: dX, h: dY }))
})metric selects the distance measure: "euclidean" (default), "manhattan" or "chebyshev". jitter controls feature-point placement, from 0 (cell centres) to 1 (anywhere in each grid cell, the default).
s.background(175, 35, 12)
s.forTiling({ n: 160, type: "square" }, ([x, y], [dX, dY], at) => {
const d = worley2(at[0] * 9, at[1] * 9, { metric: "chebyshev", jitter: 0.35 })
s.setFillColor(170 + d * 40, 55, 15 + d * 65)
s.fill(new Rect({ at: [x, y], w: dX, h: dY }))
})worleyCell2 returns the nearest feature point, its grid coordinates, a stable id and the two nearest distances. Points nearest the same feature share its id; use that to give each cell a colour.
s.background(0, 0, 10)
s.forTiling({ n: 160, type: "square" }, ([x, y], [dX, dY], at) => {
const { id, f1, f2 } = worleyCell2(at[0] * 8, at[1] * 8, { jitter: 0.9 })
if (f2 - f1 < 0.04) return // grouting between the tiles
s.setFillColor(190 + (id % 100), 55, 30 + (id % 45))
s.fill(new Rect({ at: [x, y], w: dX, h: dY }))
})Pass a seed to the SCanvas constructor, or use this site's refresh control to try another. Within a sketch, reset the random generator explicitly:
s.resetRandomNumberGenerator(42) // restart the sequence from a known seedFor seeded randomness outside a sketch, use the exported generator:
import { RNG } from "solandra"
const rng = new RNG(42)
rng.number() // deterministic sequence of numbers in [0, 1)Solandra was made by James Porter.
Check out the GitHub page or install with npm i solandra