Skip to content
Snippets Groups Projects
app.js 5.69 KiB
Newer Older
import { line, curveLinear } from "d3-shape"
import { HTML } from "./elements.js"
import { connect } from "./room.js"

const {
  canvas,
  connectedPeers,
  peerButton,
  peerIDElem,
  userIDElem,
  roomIDElem,
} = HTML
// TODO: switch to curve interpolation that respects mouse points based on velocity
const lineFn = line()
  .x((d) => d[0])
  .y((d) => d[1])
  .curve(curveLinear)

const tools = {
  PEN: "pen",
  ERASER: "eraser",
}

const STROKECOLOUR = "blue"
const STROKERADIUS = 2
const ERASERRADIUS = STROKERADIUS * 2

const pathElems = new Map()

const addOrUpdatePathElem = (id, points) => {
  let pathElem = pathElems.get(id)

  if (pathElem == null) {
    pathElem = document.createElementNS("http://www.w3.org/2000/svg", "g")

    pathElem.setAttribute("stroke", STROKECOLOUR)
    pathElem.setAttribute("stroke-width", STROKERADIUS * 2)
    pathElem.setAttribute("fill", "none")
    pathElem.setAttribute("pointer-events", "none")
    pathElem.setAttribute("marker-start", "url(#dot)")
    pathElem.setAttribute("marker-end", "url(#dot)")

    canvas.appendChild(pathElem)
    pathElems.set(id, pathElem)
Giovanni Caruso's avatar
Giovanni Caruso committed

  if (points.length == 0) {
    return pathElem
  // Push a fake path split to generate the last path
  points.push([-1, -1, false])
  for (let point of points) {
    if (point[0] === undefined) {
      continue
    if (point[2] === false) {
      if (subpath.length === 1) {
        let subpathElem = document.createElementNS(
          "http://www.w3.org/2000/svg",
          "circle",
        )

        subpathElem.setAttribute("stroke", "none")
        subpathElem.setAttribute("fill", STROKECOLOUR)
        subpathElem.setAttribute("cx", subpath[0][0])
        subpathElem.setAttribute("cy", subpath[0][1])
        subpathElem.setAttribute("r", STROKERADIUS)

        pathElem.appendChild(subpathElem)
      } else if (subpath.length > 0) {
        let subpathElem = document.createElementNS(
          "http://www.w3.org/2000/svg",
          "path",
        )

        subpathElem.setAttribute("d", lineFn(subpath))

        pathElem.appendChild(subpathElem)
      }
  return pathElem
}
const getDistance = (a, b) => {
  return Math.sqrt(
    (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]),
  )
}
function setElemVisible(elem, visible = true) {
  if (!(elem && elem.style)) return
  elem.style.display = visible ? "block" : "none"
}
function hideConnectedRoom() {
  setElemVisible(HTML.connectedRoomInfoContainer, false)
}
function showConnectedRoom(roomID) {
  HTML.connectedRoomID.textContent = roomID
  setElemVisible(HTML.connectedRoomInfoContainer)
}
function connectToARoom(roomID) {
  connect(roomID).then((room) => {
    showConnectedRoom(roomID)

    let userInput = false
    let currentTool = tools.PEN
    let currentPathID = null

    userIDElem.value = room.ownID || ""
    room.addEventListener("allocateOwnID", ({ detail: id }) => {
      userIDElem.value = id
    })

    room.addEventListener("userJoin", ({ detail: id }) => {
      const peerElem = document.createElement("li")
      peerElem.innerHTML = id
      connectedPeers.appendChild(peerElem)
    })

    room.addEventListener("userLeave", ({ detail: id }) => {
      for (const peerElem of connectedPeers.children) {
        if (peerElem.innerHTML == id) {
          connectedPeers.removeChild(peerElem)
        }
Giovanni Caruso's avatar
Giovanni Caruso committed
      }
    })

    room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
      addOrUpdatePathElem(id, points)
    })

    canvas.addEventListener("mousedown", (e) => {
      userInput = true

      let mouse = [e.offsetX, e.offsetY]

      if (currentTool === tools.PEN) {
        currentPathID = room.addPath(mouse)
      } else if (currentTool === tools.ERASER) {
        room.getPaths().forEach((points, pathID) => {
          points.forEach((point, i) => {
            if (getDistance(mouse, point) <= ERASERRADIUS) {
              room.erasePoint(pathID, i)
            }
          })
    canvas.addEventListener("mouseleave", () => {
      userInput = false
    })
    canvas.addEventListener("mouseup", () => {
      userInput = false
    })
    canvas.addEventListener("mousemove", (e) => {
      if (!userInput) {
        return
      }
      let mouse = [e.offsetX, e.offsetY]

      if (currentTool === tools.PEN) {
        room.extendPath(currentPathID, mouse)
      } else if (currentTool === tools.ERASER) {
        room.getPaths().forEach((points, pathID) => {
          points.forEach((point, i) => {
            if (getDistance(mouse, point) <= ERASERRADIUS) {
              room.erasePoint(pathID, i)
            }
          })
Giovanni Caruso's avatar
Giovanni Caruso committed
        })
    peerButton.addEventListener("click", () => {
      const peerID = peerIDElem.value
      if (peerID == "") {
        return
      }
      room.inviteUser(peerID)
      peerIDElem.value = ""
    })

    penButton.addEventListener("click", () => {
      currentTool = tools.PEN

      penButton.classList.add("selected")
      eraserButton.classList.remove("selected")
    })

    eraserButton.addEventListener("click", () => {
      currentTool = tools.ERASER

      penButton.classList.remove("selected")
      eraserButton.classList.add("selected")
    })

    HTML.roomConnectButton.addEventListener("click", () => {
      room = undefined
      const selectedRoomID = roomIDElem.value
      if (!selectedRoomID) return
      connectToARoom(selectedRoomID)
    })
HTML.roomConnectButton.addEventListener("click", () => {
  const selectedRoomID = roomIDElem.value
  if (!selectedRoomID) return
  connectToARoom(selectedRoomID)

hideConnectedRoom()
// connectToARoom("imperial")