The with... methods save canvas drawing state before running a callback and restore it afterwards. This scopes colours, line widths, transforms, clipping and blend modes. Drawn pixels remain on the canvas.
withContext scopes changes to the drawing state:
s.background(0, 0, 95)
s.setFillColor(215, 70, 45)
s.withContext(() => {
s.setFillColor(340, 80, 55) // only applies inside
s.fill(new Circle({ at: [0.3, 0.5], r: 0.15 }))
})
s.fill(new Circle({ at: [0.7, 0.5], r: 0.15 })) // back to bluewithTranslation(vector, cb), withRotation(angle, cb) and withScale([sX, sY], cb) transform coordinates inside a callback. Rotation and scaling use the origin. To rotate a shape in place, translate to its position, rotate, then draw at [0, 0]:
s.background(40, 30, 95)
s.forTiling(
{ n: 7, type: "square", margin: 0.05 },
(_pt, [dX], [cX, cY], i) => {
s.setFillColor(200 + i * 2, 60, 50)
s.withTranslation([cX, cY], () => {
s.withRotation(i * 0.15, () => {
s.fill(
new Rect({ at: [0, 0], w: dX * 0.7, h: dX * 0.7, align: "center" })
)
})
})
}
)Nest transforms to build recursive patterns:
s.background(230, 40, 12)
s.setStrokeColor(180, 70, 60)
s.lineWidth = 0.006
const branch = (depth) => {
if (depth === 0) return
s.draw(new Line([0, 0], [0, -0.22]))
s.withTranslation([0, -0.22], () => {
s.withScale([0.72, 0.72], () => {
s.withRotation(0.5, () => branch(depth - 1))
s.withRotation(-0.4, () => branch(depth - 1))
})
})
}
s.withTranslation([0.5, 0.95], () => branch(8))For an arbitrary affine transform there is withTransform({ hScale, hSkew, vSkew, vScale, dX, dY }, cb), mirroring the raw canvas transform matrix.
withClipping(shape, cb) restricts drawing to the area enclosed by a Traceable:
s.background(45, 30, 94)
s.withClipping(new Circle({ at: [0.5, 0.5], r: 0.4 }), () => {
s.background(215, 70, 25)
s.lineWidth = 0.012
s.times(40, (n) => {
s.setStrokeColor(180 + n * 3, 70, 60)
const y = 0.05 + n * 0.025
s.draw(
SimplePath.withPoints(
s.build(s.range, { from: -0.1, to: 1.1, n: 20 }, (x) => [
x,
y + 0.02 * Math.sin(x * 12 + n),
])
).chaiken({ n: 2 })
)
})
})withBlendMode(mode, cb) sets globalCompositeOperation for the callback. Modes include "multiply", "screen", "overlay", "lighten" and "difference".
s.background(0, 0, 98)
s.withBlendMode("multiply", () => {
const hues = [215, 340, 45]
s.times(3, (n) => {
s.setFillColor(hues[n], 80, 60)
s.fill(
new Circle({
at: [
0.5 + 0.12 * Math.cos((n * Math.PI * 2) / 3 - Math.PI / 2),
0.52 + 0.12 * Math.sin((n * Math.PI * 2) / 3 - Math.PI / 2),
],
r: 0.22,
})
)
})
})withSymmetry(config, cb) repeats a drawing with rotations or reflections around a centre:
type: "rotational" (the default) — n copies, evenly spaced around a full turntype: "mirror" — a reflected pair; axis is "vertical" (the default, flipping left to right) or "horizontal"type: "kaleidoscope" — n rotations, each drawn twice, once reflected, so 2n copiesat — the centre of symmetry; defaults to the canvas centreThe callback is given the index of the copy and whether that copy is a reflection.
s.background(255, 30, 12)
s.withSymmetry({ n: 12 }, (i) => {
s.setFillColor(280 + i * 4, 70, 55, 0.55)
s.fill(new Ellipse({ at: [0.5, 0.16], w: 0.14, h: 0.34 }))
})
s.setFillColor(45, 85, 65)
s.fill(new Circle({ at: s.meta.center, r: 0.06 }))Place the motif off the axis to see each rotation and its reflection:
s.background(230, 25, 12)
const [cX, cY] = s.meta.center
s.withSymmetry({ type: "kaleidoscope", n: 6 }, (i, reflected) => {
s.setFillColor(30 + i * 25, 75, 55, 0.5)
s.fill(new Ellipse({ at: [cX + 0.06, cY - 0.16], w: 0.13, h: 0.22 }))
s.setFillColor(reflected ? 195 : 45, 85, 60, 0.7)
s.fill(new Circle({ at: [cX + 0.025, cY - 0.25], r: 0.028 }))
})Each copy has its own saved drawing state. Transforms and styles in one callback do not affect later copies.
Nest helpers to combine their effects. This example clips, rotates and blends:
s.background(220, 40, 10)
s.withClipping(new RegularPolygon({ at: [0.5, 0.5], n: 6, r: 0.42 }), () => {
s.withBlendMode("screen", () => {
s.times(30, (n) => {
s.withTranslation([0.5, 0.5], () => {
s.withRotation(n * 0.4, () => {
s.setFillColor(180 + n * 6, 80, 50, 0.5)
s.fill(new Rect({ at: [0.05, -0.02], w: 0.4, h: 0.04 }))
})
})
})
})
})Solandra was made by James Porter.
Check out the GitHub page or install with npm i solandra