Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active September 27, 2023 02:02
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save mattdesl/3675c85a72075557dbb6b9e3e04a53d9 to your computer and use it in GitHub Desktop.
Save mattdesl/3675c85a72075557dbb6b9e3e04a53d9 to your computer and use it in GitHub Desktop.
/**
* lessons → lerp → map to a new range
* —
*
* Map a number within 0.0 to 1.0 to a new range, such as 20.0 to 50.0.
* For example, scaling a parameter from a start to end value.
*/
const canvasSketch = require('canvas-sketch');
const { lerp } = require('canvas-sketch-util/math');
const settings = {
dimensions: [ 512, 512 ],
animate: true,
duration: 5
};
const rect = (context, x, y, width, height, color) => {
context.fillStyle = color;
context.fillRect(x, y, width, height);
};
const circle = (context, x, y, radius, color, lineWidth) => {
context.strokeStyle = context.fillStyle = color;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2, false);
context.lineWidth = lineWidth;
if (lineWidth != null) context.stroke();
else context.fill();
};
const progress = (context, time, width, height, margin = 0) => {
context.fillStyle = 'white';
context.fillRect(
margin * 2,
height - margin * 2,
(width - margin * 4) * time,
4
);
};
const sketch = ({ width, height }) => {
const margin = 25;
return props => {
// Destructure a few props we need
const { context, width, height, playhead } = props;
// Fill off-white background
rect(context, 0, 0, width, height, 'hsl(0, 0%, 98%)');
// Fill color foreground with padding
rect(context, margin, margin, width - margin * 2, height - margin * 2, '#e5b5b5');
// Draw this scene
draw(props);
// Also draw a timeline at the bottom
progress(context, playhead, width, height, margin);
};
function draw ({ context, width, height, playhead, deltaTime }) {
// Get our 0..1 range value
const t = playhead;
// Interpolate from start=20 to end=50 radius using t
const radius = lerp(20, 50, t);
// Interpolate from start=20 to end=10 line width using t
const lineWidth = lerp(20, 10, t);
// Draw circle
circle(context, width / 2, height / 2, radius, 'white', lineWidth);
}
};
canvasSketch(sketch, settings);
/**
* lessons → lerp → animate between points
* —
*
* Animate between two points using linear interpolation.
*/
const canvasSketch = require('canvas-sketch');
const { lerp } = require('canvas-sketch-util/math');
const settings = {
dimensions: [ 512, 512 ],
animate: true,
duration: 3
};
const rect = (context, x, y, width, height, color) => {
context.fillStyle = color;
context.fillRect(x, y, width, height);
};
const circle = (context, x, y, radius, color, lineWidth) => {
context.strokeStyle = context.fillStyle = color;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2, false);
context.lineWidth = lineWidth;
if (lineWidth != null) context.stroke();
else context.fill();
};
const progress = (context, time, width, height, margin = 0) => {
context.fillStyle = 'white';
context.fillRect(
margin * 2,
height - margin * 2,
(width - margin * 4) * time,
4
);
};
const sketch = () => {
const margin = 25;
return (props) => {
// Destructure a few props we need
const { context, width, height, playhead } = props;
// Fill off-white background
rect(context, 0, 0, width, height, 'hsl(0, 0%, 98%)');
// Fill color foreground with padding
rect(context, margin, margin, width - margin * 2, height - margin * 2, '#e5b5b5');
// Draw this scene
draw(props);
// Also draw a timeline at the bottom
progress(context, playhead, width, height, margin);
};
function draw ({ context, width, height, playhead }) {
// Chosoe size of circle & stroke
const lineWidth = 4;
const radius = 20;
// Choose a start and end point somewhere in our canvas
const start = [ width * 0.33, height * 0.33 ];
const end = [ width * 0.66, height * 0.66 ];
// Draw the start and end point
circle(context, start[0], start[1], radius, 'white', lineWidth);
circle(context, end[0], end[1], radius, 'white', lineWidth);
// Choose a 't' value between 0..1, in this case the loop playhead
const t = playhead;
// Interpolate the x dimension from start X to end X, using t
const x = lerp(start[0], end[0], t);
// Now interpolate the y dimension
const y = lerp(start[1], end[1], t);
// Now we have our new point in between the start and end
const point = [ x, y ];
// And draw it
circle(context, point[0], point[1], radius / 2, 'white');
}
};
canvasSketch(sketch, settings);
/**
* lessons → lerp → spring toward target
* —
*
* Move in two dimensions toward a target using linear interpolation.
* This produces a bit of a 'spring' effect when the target is far away.
*/
const canvasSketch = require('canvas-sketch');
const { lerp } = require('canvas-sketch-util/math');
const settings = {
dimensions: [ 512, 512 ],
animate: true,
duration: 5
};
const rect = (context, x, y, width, height, color) => {
context.fillStyle = color;
context.fillRect(x, y, width, height);
};
const circle = (context, x, y, radius, color, lineWidth) => {
context.strokeStyle = context.fillStyle = color;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2, false);
context.lineWidth = lineWidth;
if (lineWidth != null) context.stroke();
else context.fill();
};
const progress = (context, time, width, height, margin = 0) => {
context.fillStyle = 'white';
context.fillRect(
margin * 2,
height - margin * 2,
(width - margin * 4) * time,
4
);
};
const sketch = ({ width, height }) => {
const targets = [
[ width * 0.25, height * 0.5 ],
[ width * 0.25, height * 0.25 ],
[ width * 0.75, height * 0.75 ],
[ width * 0.5, height * 0.5 ]
];
const point = [ width * 0.5, height * 0.5 ];
const margin = 25;
return props => {
// Destructure a few props we need
const { context, width, height, playhead } = props;
// Fill off-white background
rect(context, 0, 0, width, height, 'hsl(0, 0%, 98%)');
// Fill color foreground with padding
rect(context, margin, margin, width - margin * 2, height - margin * 2, '#e5b5b5');
// Draw this scene
draw(props);
// Also draw a timeline at the bottom
progress(context, playhead, width, height, margin);
};
function draw ({ context, width, height, playhead, deltaTime }) {
// Chosoe size of circle & stroke
const lineWidth = 4;
const radius = 20;
// Choose one of the N targets based on loop time
const targetIndex = Math.floor(playhead * targets.length);
const target = targets[targetIndex];
// Draw the start and end point
circle(context, target[0], target[1], radius, 'white', lineWidth);
// Determine a rate at which we will step forward each frame,
// making it dependent on the time elapsed since last frame
const rate = 4 * deltaTime;
// Interpolate toward the target point at this rate
point[0] = lerp(point[0], target[0], rate);
point[1] = lerp(point[1], target[1], rate);
// Draw current point
circle(context, point[0], point[1], radius / 2, 'white');
}
};
canvasSketch(sketch, settings);
/**
* lessons → lerpArray → animate between colors
* —
*
* Animate between two HSL colors using a cubic easing function
* and perceptual color space (CIELAB).
*
* You can read about color interpolation here:
* https://howaboutanorange.com/blog/2011/08/10/color_interpolation/
*
* You can visualize the cubic bezier easing function using this tool:
* http://cubic-bezier.com/?#.85,0,.15,1
*/
const canvasSketch = require('canvas-sketch');
const { lerpArray } = require('canvas-sketch-util/math');
const colorSpace = require('color-space');
const BezierEasing = require('bezier-easing');
const settings = {
dimensions: [ 512, 512 ],
animate: true,
duration: 3
};
const rect = (context, x, y, width, height, color) => {
context.fillStyle = color;
context.fillRect(x, y, width, height);
};
const circle = (context, x, y, radius, color, lineWidth) => {
context.strokeStyle = context.fillStyle = color;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2, false);
context.lineWidth = lineWidth;
if (lineWidth != null) context.stroke();
else context.fill();
};
const progress = (context, time, width, height, margin = 0) => {
context.fillStyle = 'white';
context.fillRect(
margin * 2,
height - margin * 2,
(width - margin * 4) * time,
4
);
};
const sketch = ({ width, height }) => {
const margin = 25;
// A sharp in-out easing to snap forward without staying
// in the in-between frames for too long.
const ease = new BezierEasing(0.85, 0, 0.15, 1.0);
return props => {
// Destructure a few props we need
const { context, width, height, playhead } = props;
// Fill off-white background
rect(context, 0, 0, width, height, 'hsl(0, 0%, 98%)');
// Fill color foreground with padding
rect(context, margin, margin, width - margin * 2, height - margin * 2, '#e5b5b5');
// Draw this scene
draw(props);
// Also draw a timeline at the bottom
progress(context, playhead, width, height, margin);
};
function draw ({ context, width, height, playhead, deltaTime }) {
const radius = 80;
// Get our 0..1 range value
let t = playhead;
// Ease the value so we spend less time with in-between colors
t = ease(t);
// Choose a start and end color, let's use HSL here for fun
const color0HSL = [ 0, 50, 50 ];
const color1HSL = [ 200, 30, 40 ];
// Convert both colors to a perceptual color space
// Can also try others like 'xyz', 'hsl', 'hsluv', 'lch'
// In some cases this will help reduce in-between hue/saturation shifts
const space = 'lab';
const color0Conv = colorSpace.hsl[space](color0HSL);
const color1Conv = colorSpace.hsl[space](color1HSL);
// Interpolate within this new color space from A to B color
const colorConv = lerpArray(color0Conv, color1Conv, t);
// Now convert to regular RGB for our canvas
const [ R, G, B ] = colorSpace[space].rgb(colorConv);
// Create a CSS color string with R, G, B components
const color = `rgb(${R.toFixed(6)}, ${G.toFixed(6)}, ${B.toFixed(6)})`;
// Draw circle with this color
circle(context, width / 2, height / 2, radius, color);
}
};
canvasSketch(sketch, settings);
// Linear Interpolation
// Also known as "lerp" or "mix"
function lerp (start, end, t) {
return start * (1 - t) + end * t;
}
// Examples:
lerp(0, 100, 0.5); // 50
lerp(20, 80, 0); // 20
lerp(30, 5, 1); // 5
lerp(-1, 1, 0.5); // 0
lerp(0.5, 1, 0.5); // 0.75
const { lerpArray } = require('canvas-sketch-util/math');
// Select a random point along the line segment A-B
const A = [ 20, 30 ];
const B = [ 40, 70 ];
const t = Math.random();
const point = lerpArray(A, B, t);
@jas7i
Copy link

jas7i commented Sep 27, 2023

Linear interpolation is a really handy function for creative coding, animations, game dev, and generative art.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment