Skip to content

Sling

Started off by simulating a rope, ended up attaching a "stone" to it to make a classical sling.

Stay still and click somewhere in the left end to pick up a stone, then spin to throw it at the target.

A target hangs in midair in front of a foggy landscape. Large numbers loom in the background. On the left, a sling is readied to shoot the target.

/* 2019.19 - "sling" */

let rope = Array(10).fill().map((_, i) => ({x: 0, y: i, vx: 0, vy: 0}))
let stones = []
let primed = false
let gravity = 1
let limitX = 230
let target = {x: 500, y: 200, d: 50, w: 0, bull: false, high: false}
let targetC = 0
let stoneC = 0
let bests = []
let version = 3

function setup() {
  createCanvas(900, 600);
  cursor("grab")
  bests = getItem("4aVBVWfeC-bests") || bests
  if (!bests[0] || bests[0] < version) {
    bests = [version]
  }
}

function draw() {
  let mist = color(154, 245, 245)
  let grass = color(59, 160, 50)
  background(mist);
  
  noStroke()
  for (let i = 0; i < 5; i ++) {
    fill(lerpColor(mist, grass, (i + 1) / 5))
    if (i == 0 && bests[targetC + 1]) {
      textSize(200)
      textStyle(BOLD)
      textAlign(LEFT, BOTTOM)
      text(bests[targetC + 1], width / 2, 320 + pow(i / 5, 1) * 150 + 100 * noise(5 * 0.2 / 2, 0) + 60)
    }
    if (i == 1) {
      textSize(200)
      textStyle(BOLD)
      textAlign(bests[targetC + 1] ? RIGHT : CENTER, BOTTOM)
      text(stoneC, width / 2, 320 + pow(i / 5, 1.2) * 150 + 100 * noise(5 * 0.2 / 2, i * 3) + 60)
    }
    beginShape()
    vertex(0, height)
    vertex(0, 220 + sqrt(i / 5) * 100 + 100 * noise(0, i * 3))
    for (let j = 0; j <= 5; j ++) {
      splineVertex(j * width / 5, 320 + pow(i / 5, 1) * 150 + 100 * noise(j * 0.2, i * 3))
    }
    vertex(width, 320 + pow(i / 5, 1.5) * 150 + 100 * noise(5 * 0.2, i * 3))
    vertex(width, height)
    endShape()
  }
  
  let pin = {x: (constrain(mouseX, 0, width)), y: constrain(mouseY, 0, height)}
  if (pin.x > limitX) {
    pin.x = log(pin.x - limitX) * 2 + limitX
  }
  
  noStroke()
  if (pin.x > limitX) {
    fill(250, 200, 100, constrain(mouseX - limitX, 0, 160))
    beginShape()
    vertex(0, 0)
    vertex(limitX, 0)
    splineVertex(pin.x, pin.y)
    vertex(limitX, height)
    vertex(0, height)
    endShape()
  }
  
  for (let i = 0; i < rope.length; i ++) {
    rope[i].vx *= 0.9
    rope[i].vy *= 0.9
    for (let s of [-1, 1]) {
      let other = rope[i + s] || pin
      let dx = other.x - rope[i].x
      let dy = other.y - rope[i].y
      let d = max(mag(dx, dy), 1)
      let l = (i + s / 2 == (rope.length - 1) / 2 ? 21 : 3)
      let x = (d - l) * 0.58
      rope[i].vx += x * dx/d
      rope[i].vy += x * dy/d
    }
  }
  for (let i = 0; i < rope.length; i ++) {
    rope[i].x += rope[i].vx
    rope[i].y += rope[i].vy
    rope[i].vy += gravity
  }
  noFill()
  stroke(0)
  beginShape()
  vertex(pin.x, pin.y)
  for (let i = 1; i < rope.length; i ++) {
    splineVertex(rope[i - 1].x, rope[i - 1].y, rope[i].x, rope[i].y)
    // circle(rope[i].x, rope[i].y, 2)
  }
  vertex(pin.x, pin.y)
  endShape()
  let si = rope.length / 2 - 1
  let s = {
    x: (rope[si].x + rope[si + 1].x) / 2,
    y: (rope[si].y + rope[si + 1].y) / 2,
    vx: (rope[si].vx + rope[si + 1].vx) / 2,
    vy: (rope[si].vy + rope[si + 1].vy) / 2,
  }
  if (mouseIsPressed && (primed || mag(s.vy, s.vx) < 6)) {
    primed = true
    cursor("grabbing")
  } else if (primed) {
    stones.push(s)
    if (s.vx > 6 || mag(s.vy, s.vx) > 12 || s.x > 200) {
      stoneC ++
    }
    primed = false
    cursor("grab")
  }
  fill(0)
  noStroke()
  if (primed) {
    circle(s.x, s.y, 13)
  }
  for (let i = 0; i < stones.length; i ++) {
    let steps = min(mag(stones[i].vx, stones[i].vy) / 10, 7)
    for (let s = 0; s < steps; s ++) {
      stones[i].x += stones[i].vx/steps
      stones[i].y += stones[i].vy/steps
      let d = dist(stones[i].x, stones[i].y, target.x, target.y)
      if (d < target.d / 2 + 5) {
        target.w = 1
        if (stones[i].vy > 17.5) {
          target.high = true
        }
        if (d < target.d / 6) {
          target.bull = true
        }
      }
    }
    stones[i].vx *= 0.99
    stones[i].vy *= 0.99
    stones[i].vy += gravity
    fill(0)
    circle(stones[i].x, stones[i].y, 13)
    let tx = stones[i].x
    let ty = stones[i].y
    if (stones[i].y < 0) {
      ty = log(1-stones[i].y) * 2
    }
    if (stones[i].x < 0) {
      tx = log(1-stones[i].x) * 2
    }
    if (stones[i].x > width) {
      tx = width - log(stones[i].x - width + 1) * 2
    }
    if (stones[i].y > height) {
      ty = height - log(stones[i].y - height + 1) * 2
      fill(0, map(stones[i].y, height, height * 2, 100, 0))
      stones[i].vx *= 0.7
    }
    if (tx != stones[i].x || ty != stones[i].y) {
      push()
      translate(tx, ty)
      rotate(atan2(ty - stones[i].y, tx - stones[i].x))
      triangle(0, 0, 5, 5, 5, -5)
      pop()
    }
  }
  stroke(0)
  fill(250, 0, 0)
  if (target.w) {
    target.w ++
    textStyle(NORMAL)
    textSize(10)
    textAlign(CENTER, BOTTOM)
    let score = `${(targetC + 1)}/${stoneC}`
    if (target.high) {
      score = `high shot!\n${score}`
    }
    if (target.bull) {
      score = `bullseye!\n${score}`
    }
    if ((targetC + 1) == stoneC) {
      score = `${score}\nperfect!!`
    } else if ((targetC + 1) * 2 >= stoneC) {
      score = `${score}\ngood!`
    } else if ((targetC + 1) * 4 >= stoneC) {
      score = `${score}\nnice!`
    }
    if (bests[targetC + 1] > stoneC) {
      score = `${score}\nnew best!`
    } 
    text(score, target.x, target.y - target.d / 2 - target.w / 3)
    if (target.w > 50) {
      target = {
        d: 50 * pow(0.97, targetC + random()),
        x: random(600, 900) - target.d,
        y: random(target.d, 400-target.d),
        w: 0,
        bull: false,
        high: false
      }
      targetC ++
      bests[targetC] = bests[targetC] < stoneC ? bests[targetC] : stoneC
      storeItem("4aVBVWfeC-bests", bests)
    }
    fill(0, 255, 0)
  }
  let bounce = sin(frameCount / 7) / 3
  circle(target.x, target.y + bounce, target.d)
  push()
  fill(255)
  circle(target.x, target.y + bounce, target.d / 3 * 2)
  pop()
  circle(target.x, target.y + bounce, target.d / 3)
  textStyle(BOLD)
  noStroke()
  fill(255)
  textSize(floor(target.d / 3) - 1)
  textAlign(CENTER, CENTER)
  text(targetC + 1, target.x, target.y)
  let j = 0;
  for (let i = 0; i < stones.length; i ++) {
    if ((stones[i].y < height * 2 || stones[i].vy < 0)) {
      stones[j] = stones[i]
      j ++
    }
  }
  stones.splice(j, stones.length - j)
  
}

(Originally seen at https://editor.p5js.org/bojidar-bg/sketches/4aVBVWfeC)

Experiments tagged p5 (57/85)

Experiments tagged game (5/11)

Experiments on this site (57/85)