Skip to content
Snippets Groups Projects
app.js 10.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • import { line, curveLinear } from "d3-shape"
    
    import * as HTML from "./elements.js"
    
    import { connect } from "./room.js"
    
    
    const TEST_ROOM = "imperial"
    
    
    const {
      computeErasureIntervals,
      combineErasureIntervals,
    } = require("./erasure")
    
    // 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 * 10
    
    // function interpolatedCoordinate(start, end, length) {
    //   const dx = end[0] - start[0]
    //   const dy = end[1] - start[1]
    //   const dist = getDistance(start, end)
    //   const ratio = length / dist
    //   return [start[0] + dx * ratio, start[1] + dy * ratio]
    // }
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
    
    function eraseAt(x, y, room) {
      let mousePos = [x, y]
      room.getPaths().forEach((points, pathID) => {
    
        const t = computeErasureIntervals(points, mousePos, ERASERRADIUS)
        if (!TEST_ERASE_INTERVAL[pathID]) TEST_ERASE_INTERVAL[pathID] = {}
        TEST_ERASE_INTERVAL[pathID] = combineErasureIntervals(
          TEST_ERASE_INTERVAL[pathID],
          t,
        )
        room.dispatchEvent(
          new CustomEvent("addOrUpdatePath", {
            detail: { id: pathID, points },
          }),
        )
    
        points.forEach((point) => {
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
          const distanceToPoint = getDistance(mousePos, point)
          if (distanceToPoint <= ERASERRADIUS) {
    
            // room.erasePoint(pathID, i)
    
            // let prev, next
            // if (i > 0) {
            //   prev = i - 1
            // }
            // if (i < points.length - 1) {
            //   next = i + 1
            // }
            // if (prev !== undefined) {
            //   const interpolatedPoint = interpolatedCoordinate(
            //     point,
            //     points[prev],
            //     ERASERRADIUS,
            //   )
            //   room.insertIntoPath(pathID, prev, interpolatedPoint)
            // }
            // if (next !== undefined) {
            //   const interpolatedPoint = interpolatedCoordinate(
            //     point,
            //     points[next],
            //     ERASERRADIUS,
            //   )
            //   room.insertIntoPath(pathID, next, interpolatedPoint)
            // }
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
    const SVG_URL = "http://www.w3.org/2000/svg"
    
    const SVG = {
      create: {
        circle: () => SVG.create.elem("circle"),
        group: () => SVG.create.elem("g"),
        path: () => SVG.create.elem("path"),
        elem: (elemName) => document.createElementNS(SVG_URL, elemName),
      },
    }
    
    
    let TEST_ERASE_INTERVAL = {}
    
    function erasurePoints(pointA, pointB, [start, fin]) {
    
      if (start >= fin) return
      if (start <= 0) start = 0
      if (fin >= 1) fin = 1
    
      const [xa, ya] = pointA
      const [xb, yb] = pointB
    
      const dx = xb - xa
      const dy = yb - ya
    
      const pointOnLine = (percent) => [xa + percent * dx, ya + percent * dy]
      return [start, fin].map(pointOnLine)
    
    function getOrAddPathElem(pathElems, id) {
    
      let pathElem = pathElems.get(id)
    
      if (pathElem == null) {
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
        pathElem = SVG.create.group()
    
    
        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)")
    
    
        HTML.canvas.appendChild(pathElem)
    
        pathElems.set(id, pathElem)
    
    function generateSvgPoint(subPath) {
      const subpathElem = SVG.create.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)
    
      return subpathElem
    }
    
    function generateSvgPath(subPath) {
      const subpathElem = SVG.create.path()
      subpathElem.setAttribute("d", lineFn(subPath))
      return subpathElem
    }
    
    function generateSvgForSubpath(subPath) {
      return subPath.length === 1
        ? generateSvgPoint(subPath)
        : generateSvgPath(subPath)
    }
    
    function isInvalidPoint(point) {
      return point[0] === undefined
    }
    
    function isEndPoint(point) {
      return point[2] === "end"
    }
    
    function getEraseIntervals(pathID, pointID) {
      const eraseIntervalsForPath = TEST_ERASE_INTERVAL[pathID]
      if (!eraseIntervalsForPath) return undefined
      return eraseIntervalsForPath[pointID]
    }
    
    function generatePointsForPathElem(pathElem, pathID, dataPoints) {
      const points = dataPoints
    
      // Push a fake path split to generate the last path
    
      points.push([-1, -1, "end"])
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
    
      let subPath = []
      for (let i = 0; i < points.length; i++) {
        const point = points[i]
    
        if (isInvalidPoint(point)) {
    
        if (isEndPoint(point)) {
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
          // End of subpath
    
          if (subPath.length) {
            const subpathElem = generateSvgForSubpath(subPath)
    
            pathElem.appendChild(subpathElem)
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
        // Valid point inside a subpath
        subPath.push(point)
    
        const nextPoint = points[i + 1]
    
        const eraseIntervals = getEraseIntervals(pathID, i)
        if (!eraseIntervals || nextPoint === undefined) continue
    
    
        for (const eraseInterval of eraseIntervals) {
    
          if (!eraseInterval) continue
    
          const erasedIntervalBounds = erasurePoints(
            point,
            nextPoint,
            eraseInterval,
          )
          if (!(erasedIntervalBounds && erasedIntervalBounds.length)) continue
          const [endOfDrawnSegment, startOfNewSegment] = erasedIntervalBounds
    
          subPath.push(endOfDrawnSegment)
          const subpathElem = generateSvgPath(subPath)
          pathElem.appendChild(subpathElem)
    
          subPath = [startOfNewSegment]
    
    }
    
    const addOrUpdatePathElem = (pathElems, id, points) => {
      const pathElem = getOrAddPathElem(pathElems, id)
      if (!points.length) {
        return pathElem
      }
      generatePointsForPathElem(pathElem, id, points)
    
      return pathElem
    }
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
      return Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
    
    function setElemVisible(elem, visible = true) {
      if (!(elem && elem.style)) return
      elem.style.display = visible ? "block" : "none"
    }
    
    function showConnectedRoom(roomID) {
      HTML.connectedRoomID.textContent = roomID
      setElemVisible(HTML.connectedRoomInfoContainer)
    }
    
    function setBlankUIState() {
      while (HTML.canvas.children[1]) {
        HTML.canvas.removeChild(HTML.canvas.children[1])
    
      HTML.connectedPeers.innerHTML = "No peers are connected"
    
    }
    
    function handleRoomConnectClick() {
    
      const selectedRoomID = HTML.roomIDElem.value
    
      if (!selectedRoomID) return
    
      setBlankUIState()
    
      connectToARoom(selectedRoomID)
    }
    
    function handleRoomConnectionEstablished(room) {
      showConnectedRoom(room.name)
    
    
      HTML.userIDElem.value = room.ownID || ""
    
      room.addEventListener("allocateOwnID", ({ detail: id }) => {
    
        HTML.userIDElem.value = id
    
      })
    
      room.addEventListener("userJoin", ({ detail: id }) => {
    
        if (HTML.connectedPeers.children.length == 0) {
    
        const peerElem = document.createElement("li")
        peerElem.innerHTML = id
    
        HTML.connectedPeers.appendChild(peerElem)
    
      })
    
      room.addEventListener("userLeave", ({ detail: id }) => {
    
        for (const peerElem of HTML.connectedPeers.children) {
    
          if (peerElem.innerHTML == id) {
    
            HTML.connectedPeers.removeChild(peerElem)
    
    Giovanni Caruso's avatar
    Giovanni Caruso committed
          }
    
        if (HTML.connectedPeers.children.length == 0) {
    
          HTML.connectedPeers.innerHTML = "No peers are connected"
        }
    
      const pathElems = new Map()
    
    
      room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
    
        addOrUpdatePathElem(pathElems, id, points)
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
      let currentTool = tools.ERASER
    
      const pathIDsByPointerID = new Map()
    
      const canvasOnPointerEnter = (e) => {
        if (~e.buttons & 1) {
    
        const mousePos = [e.offsetX, e.offsetY]
    
        if (currentTool == tools.PEN) {
          pathIDsByPointerID.set(e.pointerId, room.addPath(mousePos))
        } else if (currentTool == tools.ERASER) {
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
          eraseAt(mousePos[0], mousePos[1], room)
    
      const canvasOnPointerLeave = (e) => {
        pathIDsByPointerID.delete(e.pointerId)
    
      const canvasOnPointerMove = (e) => {
        if (~e.buttons & 1) {
    
        const mousePos = [e.offsetX, e.offsetY]
    
        if (currentTool == tools.PEN) {
          room.extendPath(pathIDsByPointerID.get(e.pointerId), mousePos)
        } else if (currentTool == tools.ERASER) {
    
          eraseAt(e.offsetX, e.offsetY, room)
    
      const peerButtonOnClick = () => {
    
        const peerID = HTML.peerIDElem.value
    
        if (peerID == "") {
          return
        }
        room.inviteUser(peerID)
    
        HTML.peerIDElem.value = ""
    
      const penButtonOnClick = () => {
    
        HTML.penButton.classList.add("selected")
        HTML.eraserButton.classList.remove("selected")
    
      const eraserButtonOnClick = () => {
    
        HTML.penButton.classList.remove("selected")
        HTML.eraserButton.classList.add("selected")
    
      HTML.canvas.addEventListener("pointerdown", canvasOnPointerEnter)
      HTML.canvas.addEventListener("pointerenter", canvasOnPointerEnter)
      HTML.canvas.addEventListener("pointerup", canvasOnPointerLeave)
      HTML.canvas.addEventListener("pointerleave", canvasOnPointerLeave)
      HTML.canvas.addEventListener("pointermove", canvasOnPointerMove)
      HTML.penButton.addEventListener("click", penButtonOnClick)
      HTML.eraserButton.addEventListener("click", eraserButtonOnClick)
      HTML.peerButton.addEventListener("click", peerButtonOnClick)
    
      HTML.roomConnectButton.removeEventListener("click", handleRoomConnectClick)
    
      const roomConnectButtonOnClick = () => {
    
        const selectedRoomID = HTML.roomIDElem.value
    
        if (!selectedRoomID || selectedRoomID == room.name) {
          return
        }
    
        HTML.canvas.removeEventListener("pointerdown", canvasOnPointerEnter)
        HTML.canvas.removeEventListener("pointerenter", canvasOnPointerEnter)
        HTML.canvas.removeEventListener("pointerup", canvasOnPointerLeave)
        HTML.canvas.removeEventListener("pointerleave", canvasOnPointerLeave)
        HTML.canvas.removeEventListener("pointermove", canvasOnPointerMove)
    
        HTML.peerButton.removeEventListener("click", peerButtonOnClick)
        HTML.penButton.removeEventListener("click", penButtonOnClick)
        HTML.eraserButton.removeEventListener("click", eraserButtonOnClick)
    
        HTML.roomConnectButton.removeEventListener(
          "click",
          roomConnectButtonOnClick,
        )
    
        handleRoomConnectClick()
    
      HTML.roomConnectButton.addEventListener("click", roomConnectButtonOnClick)
    
    Yuriy Maksymets's avatar
    Yuriy Maksymets committed
    
      let pid = room.addPath([100, 100])
    
      room.extendPath(pid, [110, 105])
      room.extendPath(pid, [120, 100])
      room.extendPath(pid, [130, 105])
      room.extendPath(pid, [140, 100])
    
    }
    
    function handleRoomConnectionError(err) {
      alert(`Error connecting to a room:\n${err}`)
    }
    
    function connectToARoom(roomID) {
      connect(roomID)
        .then(handleRoomConnectionEstablished)
        .catch(handleRoomConnectionError)
    
    HTML.canvas.addEventListener("touchmove", (e) => e.preventDefault())
    
    
    HTML.roomConnectButton.addEventListener("click", handleRoomConnectClick, {
      once: true,
    
    connectToARoom(TEST_ROOM)