html,body{margin: 0;padding: 0;}
body{margin:0px; padding:0px;overflow:hidden;}
let positions, velocities, forces, masses, fps, maxSpeed;
let graphData = [];
const graphWidth = 200;
const graphHeight = 100;
let target;
let targetSet = false;
let dragging = false;

// Dracula theme colors
const colors = {
  background: "#282a36",
  currentLine: "#44475a",
  foreground: "#f8f8f2",
  comment: "#6272a4",
  cyan: "#8be9fd",
  green: "#50fa7b",
  orange: "#ffb86c",
  pink: "#ff79c6",
  purple: "#bd93f9",
  red: "#ff5555",
  yellow: "#f1fa8c"
};

function setup() {
  createCanvas(windowWidth, windowHeight);
  positions = [createVector(-100, -100)];
  velocities = [createVector(0, 0)];
  forces = [createVector(0, 0)];
  masses = [1];
  fps = 60;
  maxSpeed = 256;
  target = createVector(0, 0);
  targetSet = true;
}

function draw() {
  clear();
  background(colors.background);
  let i = width / 2;
  let o = height / 2;
  translate(i, o);

  stroke(colors.comment);
  line(-i, 0, i, 0);
  line(0, -o, 0, o);

  drawSubgrid(i, o, 50);

  for (let s = 0; s < positions.length; s++) {
    let e = positions[s].x + velocities[s].x;
    let t = positions[s].y + velocities[s].y;
    let n = positions[s].x + forces[s].x;
    let c = positions[s].y + forces[s].y;

    strokeWeight(1);
    stroke(colors.pink);
    drawArrow(positions[s].x, positions[s].y, e, t);
    drawCircleMarker(positions[s], 4 * sqrt(masses[s]), s);

    strokeWeight(2);
    stroke(colors.green);
    drawArrow(positions[s].x, positions[s].y, n, c);

    strokeWeight(0);
    fill(colors.foreground);
    drawLabel(
      positions[s].x,
      positions[s].y,
      `Posição (${positions[s].x.toPrecision(4)}, ${positions[s].y.toPrecision(
        4
      )})`,
      LEFT
    );
    drawLabel(
      e,
      t,
      `Velocidade (${velocities[s].x.toPrecision(4)}, ${velocities[
        s
      ].y.toPrecision(4)})`,
      LEFT
    );
    drawLabel(
      n,
      c,
      `Aceleração (${forces[s].x.toPrecision(4)}, ${forces[s].y.toPrecision(
        4
      )})`,
      LEFT
    );

    if (targetSet) {
      forces[s] = p5.Vector.sub(target, positions[s]).div(2);
    }
    let r = forces[s].copy().div(masses[s]).div(fps);
    velocities[s].add(r);

    if (velocities[s].mag() > maxSpeed) {
      velocities[s].setMag(maxSpeed);
    }

    positions[s].add(velocities[s].copy().div(fps));

    if (positions[s].x > i || positions[s].x < -i) {
      velocities[s].x = -velocities[s].x;
    }
    if (positions[s].y > o || positions[s].y < -o) {
      velocities[s].y = -velocities[s].y;
    }

    // Store data for the graph
    graphData.push({
      velocity: velocities[s].mag(),
      acceleration: forces[s].mag() / masses[s]
    });

    // Limit the graph data to the last 100 frames
    if (graphData.length > 100) {
      graphData.shift();
    }
  }

  // Draw target
  if (targetSet) {
    fill(colors.red);
    noStroke();
    ellipse(target.x, target.y, 10, 10);
  }

  updateTargetAppearance();

  // Draw the graph
  strokeWeight(1);
  drawGraph();
}

function drawSubgrid(hw, hh, gridSize) {
  stroke("#44475a");
  strokeWeight(0.5);

  for (let x = -hw + (hw % gridSize); x <= hw; x += gridSize) {
    line(x, -hh, x, hh);
  }

  for (let y = -hh + (hh % gridSize); y <= hh; y += gridSize) {
    line(-hw, y, hw, y);
  }
}

function updateTargetAppearance() {
  let i = width / 2;
  let o = height / 2;

  // Check hover state
  let d = dist(mouseX - i, mouseY - o, target.x, target.y);
  if (d < 20) {
    hover = true;
    if (dragging) {
      cursor("grabbing");
    } else {
      cursor("grab");
    }
  } else {
    hover = false;
    cursor("default");
  }

  // Draw target with hover effect
  if (targetSet) {
    if (hover || dragging) {
      fill(colors.yellow);
      ellipse(target.x, target.y, 20, 20); // Larger "Alvo" when hovered or dragged
    } else {
      fill(colors.red);
      ellipse(target.x, target.y, 10, 10); // Normal size "Alvo"
    }
  }
}

function mousePressed() {
  let i = width / 2;
  let o = height / 2;
  let d = dist(mouseX - i, mouseY - o, target.x, target.y);

  // Start dragging if near the target
  if (d < 10) {
    dragging = true;
    cursor("grabbing");
  }
}

function mouseDragged() {
  if (dragging) {
    let i = width / 2;
    let o = height / 2;
    target.set(mouseX - i, mouseY - o);
    cursor("grabbing");
  }
}

function mouseReleased() {
  if (dragging) {
    dragging = false; // Stop dragging when the mouse is released
    cursor("grab");
  }
}

function mouseClicked() {
  if (!dragging) {
    // Fixate target only if not dragging
    let i = width / 2;
    let o = height / 2;
    target.set(mouseX - i, mouseY - o);
    targetSet = true;
  }
}

function drawCircleMarker(i, o, s) {
  fill(s === 0 ? colors.cyan : colors.purple);
  noStroke();
  ellipse(i.x, i.y, 2 * o, 2 * o);
}

function drawArrow(i, o, s, e) {
  line(i, o, s, e);
  let t = createVector(s - i, e - o)
    .normalize()
    .mult(4);
  line(s, e, s - t.y - t.x, e + t.x - t.y);
  line(s, e, s + t.y - t.x, e - t.x - t.y);
}

function drawLabel(i, o, s, e = CENTER) {
  push();
  textFont("monospace");
  textSize(14);
  textAlign(e);
  if (e == LEFT) {
    i += 6;
  }
  if (e == RIGHT) {
    i -= 6;
  }
  text(s, i, o);
  pop();
}

function drawGraph() {
  push();
  translate(width / 2 - graphWidth - 20, -height / 2 + 20);

  // Draw graph background
  fill(0, 127);
  rect(0, 0, graphWidth, graphHeight);

  // Find max values for scaling
  let maxVelocity = 0;
  let maxAcceleration = 0;
  for (let data of graphData) {
    maxVelocity = max(max(maxVelocity, data.velocity), 25);
    maxAcceleration = max(max(maxAcceleration, data.acceleration), 25);
  }

  // Define thresholds for increasing resolution
  const velocityThreshold = 100;
  const accelerationThreshold = 50;

  // Calculate step sizes based on thresholds
  const velocityStep = maxVelocity > velocityThreshold ? 50 : 25;
  const accelerationStep = maxAcceleration > accelerationThreshold ? 25 : 10;

  // Draw y-axis labels and grid lines
  stroke(colors.comment);
  fill(colors.foreground);
  textAlign(RIGHT, CENTER);
  textSize(10);
  for (let i = 0; i <= maxVelocity; i += velocityStep) {
    const y = map(i, 0, maxVelocity, graphHeight, 0);
    line(0, y, graphWidth, y);
    text(i.toFixed(0), -5, y);
  }

  // Draw graph lines
  strokeWeight(1);
  for (let i = 0; i < graphData.length - 1; i++) {
    // Velocity line
    stroke(colors.pink);
    let x1 = map(i, 0, 100, 0, graphWidth);
    let y1 = map(graphData[i].velocity, 0, maxVelocity, graphHeight, 0);
    let x2 = map(i + 1, 0, 100, 0, graphWidth);
    let y2 = map(graphData[i + 1].velocity, 0, maxVelocity, graphHeight, 0);
    line(x1, y1, x2, y2);

    // Acceleration line
    stroke(colors.green);
    y1 = map(graphData[i].acceleration, 0, maxAcceleration, graphHeight, 0);
    y2 = map(graphData[i + 1].acceleration, 0, maxAcceleration, graphHeight, 0);
    line(x1, y1, x2, y2);
  }

  // Draw x-axis labels
  fill(colors.foreground);
  noStroke();
  textAlign(CENTER, TOP);
  text("0", 0, graphHeight + 5);
  text("50", graphWidth / 2, graphHeight + 5);
  text("100", graphWidth, graphHeight + 5);

  // Draw legend
  fill(colors.foreground);
  noStroke();
  textAlign(LEFT, TOP);
  text("Velocidade", 10, 10);
  text("Aceleração", 10, 30);
  stroke(colors.pink);
  line(80, 17, 100, 17);
  stroke(colors.green);
  line(80, 37, 100, 37);

  pop();
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.jsdelivr.net/npm/p5@latest/lib/p5.min.js