AI Inverse Kinematics Tentacle - Snake Chain Animation Mesmerizing snake-like chain that follows yo

This sketch creates a mesmerizing snake-like tentacle made of 15 connected segments that smoothly chases the mouse cursor using inverse kinematics. The tentacle wobbles organically as it moves, colored in a gradient from purple at the head to cyan at the tail, with a glowing circular tip.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the tentacle snappier — Increasing followSpeed makes the target catch up to the mouse almost instantly, removing the laggy, elastic feel.
  2. Exaggerate the snake wobble — Boosting wobbleAmount makes each segment sway much more dramatically, giving a wilder, more serpentine motion.
  3. Grow a longer tentacle — Increasing numSegments adds more links to the chain, making it noticeably longer and more sinuous on screen.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch draws a 15-segment tentacle that stretches and curls across the screen, always reaching toward your mouse cursor with fluid, snake-like motion. Each segment is drawn as a thick line whose color and thickness gradually shift from a glowing purple head to a thin cyan tail, and the whole chain sways with layered sine-wave wobbles that make it feel alive rather than robotic. The visual effect relies on inverse kinematics (IK), trigonometry with atan2/sin/cos, linear interpolation with lerp and lerpColor, and a low-pass filter to smooth the mouse-following motion.

The code is split into small, focused functions: setup() initializes the segment arrays and colors, draw() computes a wobbly, smoothed target position each frame, updateTentacle() propagates that target backward through the chain, dragSegment() does the actual IK math for one segment, and drawTentacle() renders the results with a color and thickness gradient. Studying this file teaches you how to build a follow-the-leader chain animation, how atan2 lets you point one thing at another, and how combining several sine waves at different frequencies creates convincing organic movement.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, sizes each segment based on screen dimensions, initializes all 15 segments stacked at the center of the screen, and defines the purple-to-cyan color gradient.
  2. Every frame, draw() clears the background, calculates a small circular 'wobble' offset using cos/sin of frameCount, and smoothly interpolates the tentacle's target position toward the mouse (plus wobble) using lerp - this creates the laggy, fluid following motion.
  3. updateTentacle() then walks through all 15 segments in order, and for each one calls dragSegment() to solve the inverse kinematics so the segment's end reaches its target and its base is computed backward from there.
  4. dragSegment() uses atan2() to find the angle from each segment's base to its target, adds a sine-wave 'wobble' offset (with a phase shift per segment) to fake organic snake motion, then repositions the base at a fixed segLength distance from the end along that angle.
  5. After the base of one segment is calculated, it becomes the new target for the next segment in the chain, so the wobble and follow motion ripple down the tentacle like a wave.
  6. Finally drawTentacle() renders the segments from tail to head (so the head overlaps on top), interpolating color and stroke weight along the chain with lerpColor and lerp, then draws a glowing circle at the head tip.

🎓 Concepts You'll Learn

Inverse kinematics (IK)Trigonometry with atan2/sin/cosLinear interpolation (lerp/lerpColor)Chained/propagated stateLow-pass filtering for smoothingAnimation loop with frameCount

📝 Code Breakdown

setup()

setup() runs once at the start and is the natural place to size your canvas and populate any arrays your animation depends on, like the per-segment position arrays used here.

function setup() {
  createCanvas(windowWidth, windowHeight);

  segLength = min(width, height) / (numSegments + 3);

  // Initialize all segments at center
  for (let i = 0; i < numSegments; i++) {
    baseX[i] = width / 2;
    baseY[i] = height / 2;
    endX[i] = width / 2;
    endY[i] = height / 2;
    angles[i] = 0;
  }

  // Gradient: purple (head) → cyan (tail)
  headColor = color(180, 80, 255);
  tailColor = color(0, 255, 255);

  strokeCap(ROUND);

  // Start target in the center
  targetX = width / 2;
  targetY = height / 2;
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Initialize Segments for (let i = 0; i < numSegments; i++) {

Places every segment's base and end point at the center of the screen so the tentacle starts as a single point before unfurling.

createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window.
segLength = min(width, height) / (numSegments + 3);
Calculates how long each segment should be based on screen size, so the whole tentacle fits nicely regardless of window dimensions.
baseX[i] = width / 2;
Sets the starting x position of this segment's base to the horizontal center of the screen.
angles[i] = 0;
Initializes the segment's angle to zero radians (pointing right) before any motion begins.
headColor = color(180, 80, 255);
Defines the purple color used at the head end of the gradient.
tailColor = color(0, 255, 255);
Defines the cyan color used at the tail end of the gradient.
strokeCap(ROUND);
Makes the ends of each line segment rounded instead of squared off, so the tentacle looks smooth and continuous rather than jointed.
targetX = width / 2;
Sets the initial IK target to the center of the screen, matching the segments' starting position.

draw()

draw() runs continuously (about 60 times per second) and is where you should place any code that changes over time - here it computes a moving target and asks the IK chain to update and redraw toward it every frame.

🔬 These two lines create the target's idle drifting motion. What happens if you make the second multiplier match the first (both 1) instead of 1.3? What if you push it to 3?

  const wobbleX = cos(time) * wobbleRadius;
  const wobbleY = sin(time * 1.3) * wobbleRadius;
function draw() {
  background(5, 3, 20); // dark, slightly bluish background

  const time = frameCount * 0.03;

  // Small organic wobble applied to the *target* itself
  const wobbleRadius = segLength * 0.3;
  const wobbleX = cos(time) * wobbleRadius;
  const wobbleY = sin(time * 1.3) * wobbleRadius;

  // Smoothly follow the mouse (low-pass filter)
  const followSpeed = 0.15;
  const desiredX = mouseX + wobbleX;
  const desiredY = mouseY + wobbleY;

  targetX = lerp(targetX, desiredX, followSpeed);
  targetY = lerp(targetY, desiredY, followSpeed);

  // Update IK chain so that the first segment's end follows targetX/Y
  updateTentacle(targetX, targetY);

  // Draw segments from tail → head so head is on top
  drawTentacle();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Target Wobble Offset const wobbleX = cos(time) * wobbleRadius;

Creates a small circular drifting motion for the target using out-of-phase cosine and sine waves.

calculation Smoothed Target Position targetX = lerp(targetX, desiredX, followSpeed);

Applies a low-pass filter so the target eases toward the mouse instead of snapping instantly, giving the tentacle inertia.

background(5, 3, 20); // dark, slightly bluish background
Repaints the whole canvas each frame with a near-black navy color, erasing the previous frame so the tentacle appears to move rather than leaving trails.
const time = frameCount * 0.03;
Converts the frame counter into a slowly increasing time value used to drive the wobble's sine/cosine waves.
const wobbleRadius = segLength * 0.3;
Scales the wobble's size relative to segment length so it looks proportional at any screen size.
const wobbleX = cos(time) * wobbleRadius;
Computes a horizontal offset that oscillates over time, part of a circular drifting motion.
const wobbleY = sin(time * 1.3) * wobbleRadius;
Computes a vertical offset using a slightly different frequency (1.3x) so the wobble traces an elliptical, non-repeating loop rather than a simple circle.
const followSpeed = 0.15;
Sets how much of the distance to the desired position is closed each frame - a fraction between 0 and 1.
const desiredX = mouseX + wobbleX;
Combines the raw mouse position with the wobble offset to get where the target 'wants' to be.
targetX = lerp(targetX, desiredX, followSpeed);
Moves the actual target 15% of the way toward the desired position this frame, smoothing sudden mouse movements into fluid motion.
updateTentacle(targetX, targetY);
Passes the smoothed target into the IK chain so every segment recalculates its position to reach toward it.
drawTentacle();
Renders all the segments using their freshly updated positions.

updateTentacle()

This function demonstrates the core idea of a kinematic chain: solving one link's position and feeding the result as input to the next. This pattern is used in skeletal animation, robotics arms, and rope/chain simulations.

🔬 This loop chains segments together by passing each base position forward. What do you think would happen visually if you looped in reverse, from numSegments-1 down to 0, feeding the target from the tail instead of the head?

  for (let i = 0; i < numSegments; i++) {
    dragSegment(i, currentTargetX, currentTargetY);

    // The base of this segment becomes the target for the next one
    currentTargetX = baseX[i];
    currentTargetY = baseY[i];
  }
function updateTentacle(tx, ty) {
  // Head segment follows the (smoothed + wobbly) target
  let currentTargetX = tx;
  let currentTargetY = ty;

  for (let i = 0; i < numSegments; i++) {
    dragSegment(i, currentTargetX, currentTargetY);

    // The base of this segment becomes the target for the next one
    currentTargetX = baseX[i];
    currentTargetY = baseY[i];
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Propagate Target Down the Chain for (let i = 0; i < numSegments; i++) {

Solves each segment's IK in order, then hands off its computed base position as the target for the next segment, creating the follow-the-leader chain effect.

let currentTargetX = tx;
Starts with the smoothed mouse target as the position the very first (head) segment should reach.
for (let i = 0; i < numSegments; i++) {
Loops through every segment in order from head (index 0) to tail (last index).
dragSegment(i, currentTargetX, currentTargetY);
Solves the IK for segment i so its end touches the current target.
currentTargetX = baseX[i];
After solving segment i, its newly computed base becomes the target the next segment (i+1) must reach - this is what makes the chain follow smoothly link by link.

dragSegment()

This is the heart of the inverse kinematics solve: given a fixed segment length and a desired endpoint, it computes the required angle and base position. atan2 is the key p5.js/JS math function that makes this possible for any direction.

🔬 The '+ i * 0.5' offsets each segment's wobble timing. What happens if you change 0.5 to 2? To 0 (so every segment wobbles perfectly in sync)?

  const wobblePhase = frameCount * 0.08 + i * 0.5;
  const wobbleAmount = 0.35; // radians (~20 degrees)
  a += sin(wobblePhase) * wobbleAmount;
function dragSegment(i, tx, ty) {
  const dx = tx - baseX[i];
  const dy = ty - baseY[i];

  let a = atan2(dy, dx);

  // Per-segment wavy oscillation (snake-like)
  const wobblePhase = frameCount * 0.08 + i * 0.5;
  const wobbleAmount = 0.35; // radians (~20 degrees)
  a += sin(wobblePhase) * wobbleAmount;

  angles[i] = a;

  // The end of this segment should be exactly at (tx, ty)
  endX[i] = tx;
  endY[i] = ty;

  // Compute base position from end position and angle
  baseX[i] = tx - cos(a) * segLength;
  baseY[i] = ty - sin(a) * segLength;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Angle Toward Target let a = atan2(dy, dx);

Finds the angle pointing from the segment's current base toward its target using atan2, which correctly handles all directions.

calculation Add Per-Segment Wobble a += sin(wobblePhase) * wobbleAmount;

Offsets the angle with a sine wave whose phase depends on the segment index, so segments oscillate slightly out of sync, producing a slithering snake look.

const dx = tx - baseX[i];
Finds the horizontal distance from this segment's base to its target.
let a = atan2(dy, dx);
Calculates the angle (in radians) from the base toward the target - atan2 handles all directions correctly, unlike a plain division and atan.
const wobblePhase = frameCount * 0.08 + i * 0.5;
Creates a wave value that changes over time (frameCount) and is offset per segment (i * 0.5), so each segment wobbles slightly out of phase with its neighbors.
a += sin(wobblePhase) * wobbleAmount;
Adds a small oscillating adjustment to the angle, making the segment sway rather than pointing in a perfectly straight line to the target.
angles[i] = a;
Stores the final computed angle for this segment (not directly used elsewhere in this sketch, but useful for debugging or extension).
endX[i] = tx;
Pins this segment's end point exactly at its target position - this is the core IK constraint being enforced.
baseX[i] = tx - cos(a) * segLength;
Works backward from the end point along the computed angle to place the base exactly segLength pixels away, keeping every segment a fixed length.

drawTentacle()

This function shows how lerp() and lerpColor() let you create smooth gradients of any numeric or color property across a series of shapes, a technique useful for any chain, particle system, or gradient effect.

🔬 This block controls color and thickness along the chain. What happens visually if you change lerp(18, 4, t) to lerp(4, 18, t), making the tail the thick end instead of the head?

    const c = lerpColor(headColor, tailColor, t);
    stroke(c);

    // Thicker at head, thinner at tail
    const sw = lerp(18, 4, t);
    strokeWeight(sw);
function drawTentacle() {
  noFill();

  for (let i = numSegments - 1; i >= 0; i--) {
    const t = i / (numSegments - 1); // 0 = head, 1 = tail

    // Gradient from headColor (purple) to tailColor (cyan)
    const c = lerpColor(headColor, tailColor, t);
    stroke(c);

    // Thicker at head, thinner at tail
    const sw = lerp(18, 4, t);
    strokeWeight(sw);

    // Draw each segment as a line from base to end
    // line(): https://p5js.org/reference/#/p5/line
    line(baseX[i], baseY[i], endX[i], endY[i]);
  }

  // Glowing head "tip"
  noStroke();
  fill(headColor);
  const headRadius = 24;
  circle(endX[0], endY[0], headRadius); // circle(): https://p5js.org/reference/#/p5/circle
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Draw Tail-to-Head for (let i = numSegments - 1; i >= 0; i--) {

Iterates backwards so the tail segments are drawn first and the head segment is drawn last, ensuring the head visually overlaps the rest of the chain.

calculation Color and Thickness Gradient const c = lerpColor(headColor, tailColor, t);

Blends between the head and tail colors/thicknesses based on how far along the chain segment i is.

noFill();
Ensures shapes drawn (the lines) have no fill, only a stroke color, since line() only uses stroke anyway but this keeps drawing state clean.
for (let i = numSegments - 1; i >= 0; i--) {
Loops backward from the last segment (tail) to the first (head) so later-drawn shapes (the head) appear on top.
const t = i / (numSegments - 1); // 0 = head, 1 = tail
Normalizes the segment index into a 0-to-1 range, where 0 is the head and 1 is the tail, used to interpolate color and thickness.
const c = lerpColor(headColor, tailColor, t);
Blends smoothly between the purple head color and cyan tail color based on position t along the chain.
const sw = lerp(18, 4, t);
Interpolates the stroke weight from 18 pixels thick at the head down to 4 pixels thin at the tail.
line(baseX[i], baseY[i], endX[i], endY[i]);
Draws this segment as a straight line from its base point to its end point, using the current stroke color and weight.
fill(headColor);
Sets the fill color for the glowing tip circle to match the head's purple color.
circle(endX[0], endY[0], headRadius);
Draws a filled circle at the very tip of the first segment (the head), acting as a glowing 'eye' or tip for the tentacle.

windowResized()

This built-in p5.js function automatically runs whenever the browser window is resized, letting you keep responsive sketches correctly scaled and centered instead of distorted or cut off.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);

  // Recompute segment length to fit new window
  segLength = min(width, height) / (numSegments + 3);

  // Re-center tentacle so resize doesn’t break it
  for (let i = 0; i < numSegments; i++) {
    baseX[i] = width / 2;
    baseY[i] = height / 2;
    endX[i] = width / 2;
    endY[i] = height / 2;
    angles[i] = 0;
  }

  targetX = width / 2;
  targetY = height / 2;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Re-center Segments for (let i = 0; i < numSegments; i++) {

Resets every segment back to the new center point so resizing the browser window doesn't leave the tentacle stretched or off-screen.

resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window's new dimensions whenever it changes.
segLength = min(width, height) / (numSegments + 3);
Recalculates the segment length so the tentacle scales proportionally to the new window size.
baseX[i] = width / 2;
Resets each segment's base back to the new center, avoiding a broken or stretched tentacle after resizing.
targetX = width / 2;
Resets the IK target to the new center as well, keeping everything in sync.

📦 Key Variables

numSegments number

The total number of linked segments that make up the tentacle chain.

let numSegments = 15;
segLength number

The fixed length of each segment, calculated from screen size so the tentacle fits the window.

let segLength;
baseX / baseY array

Parallel arrays storing the starting (base) point of each segment.

let baseX = [];
endX / endY array

Parallel arrays storing the ending point of each segment, which is constrained to sit exactly at its target.

let endX = [];
angles array

Stores the computed angle (in radians) for each segment, used during the IK calculation.

let angles = [];
headColor object

The p5.Color used at the head end of the gradient (purple).

let headColor;
tailColor object

The p5.Color used at the tail end of the gradient (cyan).

let tailColor;
targetX / targetY number

The smoothed position the tentacle's head is currently chasing, blended between the mouse position and a wobble offset.

let targetX;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE dragSegment()

The angles[] array is computed and stored every frame but never actually read anywhere else in the sketch.

💡 Either remove the unused angles array to simplify the code, or make use of it (e.g. for debugging visualization) to justify keeping it.

BUG drawTentacle()

If numSegments is ever set to 1, the expression i / (numSegments - 1) divides by zero, producing NaN and breaking the gradient.

💡 Guard against numSegments <= 1, e.g. const t = numSegments > 1 ? i / (numSegments - 1) : 0;

FEATURE draw()

The tentacle only reacts to the mouse; on touch devices there is no equivalent interaction defined.

💡 Add touchMoved() to update mouseX/mouseY-equivalent target using touches[0].x and touches[0].y for mobile support.

STYLE global scope

Segment data is spread across five parallel arrays (baseX, baseY, endX, endY, angles) instead of a single array of segment objects.

💡 Consider using an array of objects like segments[i] = {baseX, baseY, endX, endY, angle} to make the code easier to extend and read.

🔄 Code Flow

Code flow showing setup, draw, updatetentacle, dragsegment, drawtentacle, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> wobble-calc[wobble-calc] draw --> smooth-follow[smooth-follow] draw --> chain-propagation[chain-propagation] chain-propagation --> angle-calc[angle-calc] angle-calc --> wobble-add[wobble-add] draw --> tail-to-head-loop[tail-to-head-loop] tail-to-head-loop --> gradient-calc[gradient-calc] setup --> init-loop[init-loop] windowresized --> resize-reset-loop[resize-reset-loop] click setup href "#fn-setup" click draw href "#fn-draw" click init-loop href "#sub-init-loop" click wobble-calc href "#sub-wobble-calc" click smooth-follow href "#sub-smooth-follow" click chain-propagation href "#sub-chain-propagation" click angle-calc href "#sub-angle-calc" click wobble-add href "#sub-wobble-add" click tail-to-head-loop href "#sub-tail-to-head-loop" click gradient-calc href "#sub-gradient-calc" click windowresized href "#fn-windowresized" click resize-reset-loop href "#sub-resize-reset-loop"

❓ Frequently Asked Questions

What visual effect does the AI Inverse Kinematics Tentacle sketch create?

The sketch visually creates a mesmerizing snake-like chain of 15 connected segments that smoothly follows the user's mouse movements, creating a fluid and organic animation.

How can users interact with the AI Inverse Kinematics Tentacle sketch?

Users can interact by moving their mouse, which causes the tentacle to follow the cursor in a smooth and dynamic manner, enhanced by a subtle wobble effect.

What creative coding technique is demonstrated in the AI Inverse Kinematics Tentacle sketch?

This sketch demonstrates the concept of inverse kinematics, allowing a chain of segments to dynamically adjust their angles to follow a target point, simulating natural movement.

Preview

AI Inverse Kinematics Tentacle - Snake Chain Animation Mesmerizing snake-like chain that follows yo - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Inverse Kinematics Tentacle - Snake Chain Animation Mesmerizing snake-like chain that follows yo - Code flow showing setup, draw, updatetentacle, dragsegment, drawtentacle, windowresized
Code Flow Diagram