Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hlgr/drawing-app
  • sweng-group-15/drawing-app
2 results
Show changes
Showing with 2486 additions and 744 deletions
File added
This diff is collapsed.
import "array-flat-polyfill"
// Local canvas rendering. // Local canvas rendering.
// Emit input events and receive draw calls seperately - these must be piped // Emit input events and receive draw calls seperately - these must be piped
// together externally if desired. // together externally if desired.
import { line, curveCatmullRom } from "d3-shape" import { line, curveCatmullRom, curveLinear } from "d3-shape"
import { canvas } from "./elements.js" import { canvas } from "./elements.js"
...@@ -10,46 +12,27 @@ const SVG_URL = "http://www.w3.org/2000/svg" ...@@ -10,46 +12,27 @@ const SVG_URL = "http://www.w3.org/2000/svg"
import * as HTML from "./elements.js" import * as HTML from "./elements.js"
// TODO: look at paper.js which has path.smooth() and curve.getPart() export const LAST_RECOGNIZED_PATH_ID = "LSP"
// TODO: look at snap.svg which has path.getTotalLength(), path.subpath() and Snap.closestPoint()
// TODO: look at curve interpolation that respects mouse points based on velocity
const curve = curveCatmullRom.alpha(1.0) const curve = curveCatmullRom.alpha(1.0)
const smoothLine = line() const smoothLine = line()
.x((d) => d[0]) .x((d) => d[0])
.y((d) => d[1]) .y((d) => d[1])
.curve(curve) .curve(curve)
const straightLine = line()
.x((d) => d[0])
.y((d) => d[1])
.curve(curveLinear)
const pathGroupElems = new Map() const pathGroupElems = new Map()
const MAX_POINT_DISTANCE = 5
const MAX_RADIUS_DELTA = 0.05 const MAX_RADIUS_DELTA = 0.05
// Interpolate a path so that: // Interpolate a path so that:
// - The distance between two adjacent points is capped at MAX_POINT_DISTANCE.
// - The radius delta between two adjacent points is capped at // - The radius delta between two adjacent points is capped at
// MAX_RADIUS_DELTA // MAX_RADIUS_DELTA.
// If paths are too choppy, try decreasing these constants. // If paths are too choppy, try decreasing these constants.
const smoothPath = ([...path]) => { const smoothPath = ([...path]) => {
// Apply MAX_POINT_DISTANCE.
for (let i = 1; i < path.length; i++) {
const dx = path[i][0] - path[i - 1][0]
const dy = path[i][1] - path[i - 1][1]
const dw = path[i][2] - path[i - 1][2]
const distance = Math.hypot(dx, dy)
const segmentsToSplit = Math.ceil(distance / MAX_POINT_DISTANCE)
const newPoints = []
for (let j = 1; j < segmentsToSplit; j++) {
newPoints.push([
path[i - 1][0] + (dx / segmentsToSplit) * j,
path[i - 1][1] + (dy / segmentsToSplit) * j,
path[i - 1][2] + (dw / segmentsToSplit) * j,
path[i - 1][3],
])
}
path.splice(i, 0, ...newPoints)
i += newPoints.length
}
// Apply MAX_RADIUS_DELTA. // Apply MAX_RADIUS_DELTA.
for (let i = 1; i < path.length; i++) { for (let i = 1; i < path.length; i++) {
const dx = path[i][0] - path[i - 1][0] const dx = path[i][0] - path[i - 1][0]
...@@ -101,26 +84,14 @@ const ensurePathGroupElem = (id) => { ...@@ -101,26 +84,14 @@ const ensurePathGroupElem = (id) => {
return groupElem return groupElem
} }
const renderPoint = (point) => { const renderSubpath = (subpath, pathSmooth) => {
const circleElem = createSvgElem("circle")
circleElem.setAttribute("stroke", "none")
circleElem.setAttribute("fill", point[3])
circleElem.setAttribute("cx", point[0])
circleElem.setAttribute("cy", point[1])
circleElem.setAttribute("r", point[2])
return circleElem
}
const renderSubpath = (subpath) => {
if (subpath.length == 1) {
return renderPoint(subpath[0])
}
const pathElem = createSvgElem("path") const pathElem = createSvgElem("path")
pathElem.setAttribute("stroke", subpath[0][3]) pathElem.setAttribute("stroke", subpath[0][3])
pathElem.setAttribute("stroke-width", subpath[0][2] * 2) pathElem.setAttribute("stroke-width", subpath[0][2] * 2)
pathElem.setAttribute("d", smoothLine(subpath)) pathElem.setAttribute(
"d",
pathSmooth ? smoothLine(subpath) : straightLine(subpath),
)
return pathElem return pathElem
} }
...@@ -223,10 +194,57 @@ export const splitOnPressures = ([...path]) => { ...@@ -223,10 +194,57 @@ export const splitOnPressures = ([...path]) => {
} }
export const renderPath = (id, points, pathEraseIntervals) => { export const renderPath = (id, points, pathEraseIntervals) => {
let rectShapeStartIndex = -1
const pointsWithIntervals = Object.keys(pathEraseIntervals).length
// Rect recognition hint shape: pure rect with no erasure
if (pointsWithIntervals == 0 && points.length == 5) {
rectShapeStartIndex = 0
// Recognised rect shape: rect after completely erased raw data
} else if (pointsWithIntervals > 0 && points.length > 5) {
// Check that the preceding raw data is completely erased
for (let i = 0; i < points.length - 5; i++) {
if (
!pathEraseIntervals[i] ||
pathEraseIntervals[i].length != 1 ||
pathEraseIntervals[i][0][0] != 0 ||
pathEraseIntervals[i][0][1] != 1
) {
break
}
}
rectShapeStartIndex = points.length - 5
}
// Only draw the path smooth if it is not a recognised rect shape, i.e. the last five points form a cycle
const pathSmooth = !(
rectShapeStartIndex >= 0 &&
points[rectShapeStartIndex][0] == points[rectShapeStartIndex + 4][0] &&
points[rectShapeStartIndex][1] == points[rectShapeStartIndex + 4][1] &&
points[rectShapeStartIndex][2] == points[rectShapeStartIndex + 4][2] &&
points[rectShapeStartIndex][3] == points[rectShapeStartIndex + 4][3]
)
let subpaths = applyErasureIntervals(points, pathEraseIntervals) let subpaths = applyErasureIntervals(points, pathEraseIntervals)
if (subpaths.length < 1 && id != LAST_RECOGNIZED_PATH_ID) {
const pathGroupElem = pathGroupElems.get(id)
if (pathGroupElem) {
pathGroupElems.delete(id)
HTML.canvas.removeChild(pathGroupElem)
}
return
}
subpaths = subpaths.map(smoothPath) subpaths = subpaths.map(smoothPath)
subpaths = subpaths.flatMap(splitOnPressures) subpaths = subpaths.flatMap(splitOnPressures)
const subpathElems = subpaths.map((subpath) => renderSubpath(subpath)) const subpathElems = subpaths.map((subpath) =>
renderSubpath(subpath, pathSmooth),
)
const pathGroupElem = ensurePathGroupElem(id) const pathGroupElem = ensurePathGroupElem(id)
subpathElems.forEach((subpathElem) => pathGroupElem.appendChild(subpathElem)) subpathElems.forEach((subpathElem) => pathGroupElem.appendChild(subpathElem))
} }
...@@ -237,12 +255,13 @@ export const clear = () => { ...@@ -237,12 +255,13 @@ export const clear = () => {
} }
// Necessary since buttons property is non standard on iOS versions < 13.2 // Necessary since buttons property is non standard on iOS versions < 13.2
const isValidPointerEvent = (e) => { const isValidPointerEvent = (e, name) => {
if (name === "strokeend") return true
return e.buttons & 1 || e.pointerType === "touch" return e.buttons & 1 || e.pointerType === "touch"
} }
const dispatchPointerEvent = (name) => (e) => { const dispatchPointerEvent = (name) => (e) => {
if (isValidPointerEvent(e)) { if (isValidPointerEvent(e, name)) {
input.dispatchEvent(new CustomEvent(name, { detail: e })) input.dispatchEvent(new CustomEvent(name, { detail: e }))
} }
} }
...@@ -251,6 +270,8 @@ const dispatchPointerEvent = (name) => (e) => { ...@@ -251,6 +270,8 @@ const dispatchPointerEvent = (name) => (e) => {
canvas.addEventListener("pointerdown", dispatchPointerEvent("strokestart")) canvas.addEventListener("pointerdown", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerenter", dispatchPointerEvent("strokestart")) canvas.addEventListener("pointerenter", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerup", dispatchPointerEvent("strokeend")) canvas.addEventListener("pointerup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("onmouseup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("mouseup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointerleave", dispatchPointerEvent("strokeend")) canvas.addEventListener("pointerleave", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointermove", dispatchPointerEvent("strokemove")) canvas.addEventListener("pointermove", dispatchPointerEvent("strokemove"))
canvas.addEventListener("touchmove", (e) => e.preventDefault()) canvas.addEventListener("touchmove", (e) => e.preventDefault())
export const getUserID = jest.fn((uid) => uid) export const userID = { uuid: null }
export const getPeerHandle = jest.fn((/*uid*/) => undefined)
export const getPeerFootprint = jest.fn((/*uid*/) =>
Promise.resolve(Date.now()))
export const sendListener = { callback: null } export const sendListener = { callback: null }
export const send = jest.fn((uid, channel, message) => {
if (sendListener.callback) {
sendListener.callback(uid, channel, message)
}
})
const sendMockClear = send.mockClear
send.mockClear = () => {
sendListener.callback = null
sendMockClear()
}
export const broadcastListener = { callback: null } export const broadcastListener = { callback: null }
export const broadcast = jest.fn((channel, message) => {
if (broadcastListener.callback) {
broadcastListener.callback(channel, message)
}
})
const broadcastMockClear = broadcast.mockClear
broadcast.mockClear = () => {
broadcastListener.callback = null
broadcastMockClear()
}
export const terminatePeer = jest.fn()
export const destructor = jest.fn(() => eventListeners.clear())
const eventListeners = new Map() const eventListeners = new Map()
export const getEventListener = (room, event) => export const getEventListener = (room, event) =>
eventListeners.get(`${room}:${event}`) eventListeners.get(`${room}:${event}`)
export const addEventListener = jest.fn((room, event, callback) =>
eventListeners.set(`${room}:${event}`, callback),
)
const addEventListenerMockClear = addEventListener.mockClear
addEventListener.mockClear = () => {
eventListeners.clear()
addEventListenerMockClear()
}
const MockConnection = jest.fn().mockImplementation(({ room }) => { class MockConnection {
setTimeout( constructor({ room }) {
() => this.room = room
getEventListener(room, "roomJoined") &&
getEventListener(room, "roomJoined")(), setTimeout(
0, () =>
) getEventListener(room, "roomJoined") &&
getEventListener(room, "roomJoined")(),
0,
)
}
getUserID() {
return userID.uuid
}
getPeerHandle(/*uid*/) {
return undefined
}
return { getPeerFootprint(/*uid*/) {
getUserID: () => getUserID(room), return Promise.resolve(Date.now())
getPeerHandle,
getPeerFootprint,
send,
broadcast,
terminatePeer,
destructor,
addEventListener: (event, callback) =>
addEventListener(room, event, callback),
} }
})
send(uid, channel, message) {
if (sendListener.callback) {
sendListener.callback(uid, channel, message)
}
}
broadcast(channel, message) {
if (broadcastListener.callback) {
broadcastListener.callback(channel, message)
}
}
terminatePeer() {
// Twiddle thumbs
}
destructor() {
sendListener.callback = null
broadcastListener.callback = null
eventListeners.clear()
}
addEventListener(event, callback) {
eventListeners.set(`${this.room}:${event}`, callback)
}
}
export default MockConnection export default MockConnection
This diff is collapsed.
Subproject commit 139ab6e2cc9d6f0501fa958f3c813df0fcc81310
export const userIDElem = document.getElementById("user-id")
export const peerIDElem = document.getElementById("peer-id")
export const peerButton = document.getElementById("peer-connect")
export const connectedPeers = document.getElementById("connected-peers") export const connectedPeers = document.getElementById("connected-peers")
export const overallStatusIcon = document.getElementById("overall-status-icon") export const overallStatusIcon = document.getElementById("overall-status-icon")
...@@ -12,6 +8,12 @@ export const overallStatusIconImage = document.getElementById( ...@@ -12,6 +8,12 @@ export const overallStatusIconImage = document.getElementById(
export const canvas = document.getElementById("canvas") export const canvas = document.getElementById("canvas")
export const penButton = document.getElementById("pen-tool") export const penButton = document.getElementById("pen-tool")
export const eraserButton = document.getElementById("eraser-tool") export const eraserButton = document.getElementById("eraser-tool")
export const recognitionModeButton = document.getElementById("recognition-mode")
export const draggingToolButton = document.getElementById("dragging-tool")
export const canvasCenterToolButton = document.getElementById("canvas-center")
export const fastUndoButton = document.getElementById("fast-undo-tool")
export const undoButton = document.getElementById("undo-tool")
export const roomIDElem = document.getElementById("room-id") export const roomIDElem = document.getElementById("room-id")
export const roomConnectButton = document.getElementById("room-connect") export const roomConnectButton = document.getElementById("room-connect")
...@@ -25,8 +27,9 @@ export const closeButton = document.querySelectorAll(".close") ...@@ -25,8 +27,9 @@ export const closeButton = document.querySelectorAll(".close")
export const palette = document.getElementById("palette") export const palette = document.getElementById("palette")
export const rectangle = document.getElementById("rectangle") export const rectangle = document.getElementById("rectangle")
export const wheel = document.getElementById("wheel") export const wheel = document.getElementById("wheel")
export const picker = document.getElementById("other-colours") export const strokeColorPicker = document.getElementById("other-colours")
export const slider = document.getElementById("range") export const strokeRadiusSlider = document.getElementById("range")
export const output = document.getElementById("value") export const output = document.getElementById("value")
export const labelColours = document.getElementById("colours") export const labelColours = document.getElementById("colours")
export const userInfo = document.getElementById("user-avatar") export const userInfo = document.getElementById("user-avatar")
export const topPanel = document.getElementById("top-panel")
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Subproject commit 04ea4e5081059faffdebae65c92da25fa98f996d Subproject commit 216be42b2a6cc5632d427cf222c789d34026a274
Subproject commit 60d95bad97b43bb73dda1ef86b605b1114ae1294
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.