<canvas></canvas>
html,
body {
  height: 100%;
  text-align: center;
  margin: 0;
}

p {
  z-index: 2;
  position: absolute;
  top: 0;
  left: 0;
}
// get canvas 2D context object
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

const GLOBALS = {
  anchorX: 0,
  anchorY: 0,
  mapAnchor: { x: 0, y: 0 },
  click: { x: undefined, y: undefined },
  mouse: { x: undefined, y: undefined },
  mouseDown: { x: undefined, y: undefined },
  mouseUp: { x: undefined, y: undefined },
  distance: (a, b) =>
    Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)), //  formula: √ (x2 − x1)^2 + (y2 − y1)^2
  angle: (a, b) => Math.atan2(b.y - a.y, b.x - a.x) // formula: atan2(y2 - y1, x2 - x1)
  // Globally accessable helper functions for calculating angle and distance between two points
};
// use GLOBALS to keep track of mouse actions, and have them accessable by every sprite

const PROPS = [];

/* Our main character sprite */

class Player {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.radius = 30;
  }
  render() {
    //this.x += GLOBALS.charX / 10;
    //this.y += GLOBALS.charY / 10;

    let { x, y, radius } = this;
    ctx.beginPath();
    ctx.lineWidth = 2;
    ctx.strokeStyle = "red";
    ctx.arc(x, y, radius, 0, 2 * Math.PI);
    ctx.stroke();
  }
}

const CHARS = [];
let player = new Player(window.innerWidth / 2, window.innerHeight / 2);
CHARS.push(player);

// function for applying any initial settings
function init() {
  // apply a fullscreen fit
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;

  // register event listeners to store mouse actions and coordinates inside GLOBALS, so that they can be accessed by sprites that need them.

  window.addEventListener("click", (e) => {
    GLOBALS.mouseUp.x = GLOBALS.mouseDown.x = GLOBALS.mouse.x = GLOBALS.click.x =
      e.pageX;
    GLOBALS.mouseUp.y = GLOBALS.mouseDown.y = GLOBALS.mouse.y = GLOBALS.click.y =
      e.pageY;
  });

  function tStart(e) {
    GLOBALS.mouseUp.x = undefined;
    GLOBALS.mouseUp.y = undefined;
    GLOBALS.mouseDown.x = GLOBALS.mouse.x = e.pageX || e.touches[0]?.pageX;
    GLOBALS.mouseDown.y = GLOBALS.mouse.y = e.pageY || e.touches[0]?.pageY;
  }

  function tEnd(e) {
    GLOBALS.mouseDown.x = GLOBALS.mouse.x = undefined;
    GLOBALS.mouseDown.y = GLOBALS.mouse.y = undefined;
  }

  function tMove(e) {
    GLOBALS.mouse.x = e.pageX || e.touches[0]?.pageX;
    GLOBALS.mouse.y = e.pageY || e.touches[0]?.pageY;
  }
  // add listeners for both mobile and desktop (for demonstration purposes)

  window.addEventListener("touchstart", tStart);
  window.addEventListener("mousedown", tStart);
  window.addEventListener("touchend", tEnd);
  window.addEventListener("mouseup", tEnd);
  window.addEventListener("touchmove", tMove);
  window.addEventListener("mousemove", tMove);
}

function Pot(x, y) {
  x += GLOBALS.mapAnchor.x;
  y += GLOBALS.mapAnchor.y;

  ctx.globalAlpha = 1;
  // stem
  ctx.rect(x + 14, y + 20, 4, 60);
  ctx.stroke();
  ctx.fill();
  // flower head
  ctx.fillStyle = "white";
  ctx.lineWidth = 2;
  ctx.rect(x, y, 10, 10);
  ctx.rect(x + 22, y + 22, 10, 10);
  ctx.rect(x + 22, y, 10, 10);
  ctx.rect(x, y + 22, 10, 10);
  ctx.stroke();
  ctx.fill();
  ctx.beginPath();
  ctx.rect(x + 11, y - 4, 10, 10);
  ctx.rect(x - 4, y + 11, 10, 10);
  ctx.rect(x + 26, y + 11, 10, 10);
  ctx.rect(x + 11, y + 25, 10, 10);
  ctx.stroke();
  ctx.fill();
  ctx.beginPath();
  ctx.rect(x + 6, y + 6, 20, 20);
  ctx.stroke();
  ctx.fill();

  // pot
  ctx.beginPath();
  ctx.rect(x - 4, y + 90, 39, 22);
  ctx.stroke();
  ctx.fill();
  ctx.strokeRect(x - 7, y + 80, 45, 9);
  ctx.rect(x - 7, y + 80, 45, 9);
  ctx.fill();
}

// function for rendering background elements
function renderBackground() {
  // update map anchor..
  GLOBALS.mapAnchor.x -= GLOBALS.anchorX / 10;
  GLOBALS.mapAnchor.y -= GLOBALS.anchorY / 10;

  Pot(window.innerWidth / 2 + 50, window.innerHeight / 2);
  Pot(window.innerWidth / 2 - 100, window.innerHeight / 2 - 70);
}

// function for rendering prop objects in PROPS
function renderProps() {}

// function for rendering character objects in CHARS
function renderCharacters() {
  for (let i of CHARS) {
    i.render();
  }
}

/* CLASS FOR JOYSTICKS */
class Joystick {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.distance = { x: 0, y: 0 };
    this.angle = 0;
  }
  render() {
    // display distance property before every render

    let { x, y } = this;
    let { distance, angle } = GLOBALS;
    ctx.beginPath();
    ctx.lineWidth = 2;
    ctx.globalAlpha = 0.7;
    ctx.strokeStyle = "black";
    ctx.arc(x, y, 50, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.beginPath();
    // logic for keeping thumbstick inside the circle
    if (GLOBALS.mouseUp.x) {
      ctx.arc(x, y, 20, 0, 2 * Math.PI);
      this.distance.x = x - this.x;
      this.distance.y = y - this.y;
      GLOBALS.anchorX = x - this.x;
      GLOBALS.anchorY = y - this.y;
      this.angle = angle({ x: x, y: y }, this);
    } else if (
      distance(GLOBALS.mouse, this) < 50 &&
      distance(GLOBALS.mouseDown, this) < 50
    ) {
      ctx.arc(GLOBALS.mouse.x, GLOBALS.mouse.y, 30, 0, 2 * Math.PI);
      this.angle = angle(GLOBALS.mouse, this);
      this.distance.x = GLOBALS.mouse.x - this.x;
      this.distance.y = GLOBALS.mouse.y - this.y;
      GLOBALS.anchorX = GLOBALS.mouse.x - this.x;
      GLOBALS.anchorY = GLOBALS.mouse.y - this.y;
    } else if (
      distance(GLOBALS.mouse, this) > 50 &&
      distance(GLOBALS.mouseDown, this) < 50
    ) {
      let { x, y } = GLOBALS.mouse;
      let d = distance(GLOBALS.mouse, this) - 50,
        ok = false;
      while (ok === false) {
        if (x < this.x) {
          x++;
        } else {
          x--;
        }
        if (y < this.y) {
          y++;
        } else {
          y--;
        }
        if (distance({ x: x, y: y }, this) < 50) {
          ok = true;
        }
      }
      ctx.arc(x, y, 30, 0, 2 * Math.PI);
      this.distance.x = x - this.x;
      this.distance.y = y - this.y;
      GLOBALS.anchorX = x - this.x;
      GLOBALS.anchorY = y - this.y;
      this.angle = angle({ x: x, y: y }, this);
    } else {
      ctx.arc(x, y, 30, 0, 2 * Math.PI);
      this.distance.x = x - this.x;
      this.distance.y = y - this.y;
      GLOBALS.anchorX = x - this.x;
      GLOBALS.anchorY = y - this.y;
      this.angle = angle({ x: x, y: y }, this);
    }

    ctx.stroke();
    ctx.fillStyle = "gray";
    ctx.fill();
  }
}

// function for rendering onscreen controls
let stick = new Joystick(70, window.innerHeight - 70);
function renderControls() {
  stick.render();
}

// main function to be run for rendering frames
function startFrames() {
  // erase entire canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // render each type of entity in order, relative to layers
  renderBackground();
  renderProps();
  renderCharacters();
  renderControls();

  // rerun function (call next frame)
  window.requestAnimationFrame(startFrames);
}

init(); // initialize game settings
startFrames(); // start running frames

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.