{"id":20660962,"url":"https://github.com/liatemplates/collaborativedrawing","last_synced_at":"2026-05-29T08:04:24.623Z","repository":{"id":251888193,"uuid":"838478392","full_name":"LiaTemplates/CollaborativeDrawing","owner":"LiaTemplates","description":"Adds collaborative drawings (whiteboards) to LiaScript classrooms","archived":false,"fork":false,"pushed_at":"2024-08-06T09:16:30.000Z","size":8,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-10T04:28:33.950Z","etag":null,"topics":["collaboration","liascript","liascript-template","markdown","oer","whiteboard"],"latest_commit_sha":null,"homepage":"https://liascript.github.io/course/?https://raw.githubusercontent.com/LiaTemplates/CollaborativeDrawing/main/README.md","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"cc0-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LiaTemplates.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-05T18:11:54.000Z","updated_at":"2024-08-06T09:18:04.000Z","dependencies_parsed_at":"2024-08-06T11:23:57.196Z","dependency_job_id":null,"html_url":"https://github.com/LiaTemplates/CollaborativeDrawing","commit_stats":null,"previous_names":["liatemplates/collaborativedrawing"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/LiaTemplates/CollaborativeDrawing","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LiaTemplates%2FCollaborativeDrawing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LiaTemplates%2FCollaborativeDrawing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LiaTemplates%2FCollaborativeDrawing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LiaTemplates%2FCollaborativeDrawing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LiaTemplates","download_url":"https://codeload.github.com/LiaTemplates/CollaborativeDrawing/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LiaTemplates%2FCollaborativeDrawing/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33642320,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["collaboration","liascript","liascript-template","markdown","oer","whiteboard"],"created_at":"2024-11-16T19:06:38.753Z","updated_at":"2026-05-29T08:04:24.609Z","avatar_url":"https://github.com/LiaTemplates.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!--\nauthor:  André Dietrich\n\nemail:   LiaScript@web.de\n\nversion: 0.0.1\n\ncomment: This is a simple example for a collaborative drawing tool, that can be\n         used in classrooms. It is based on the idea of a shared whiteboard,\n         where students can draw together.\n\npersistent: true\n\n@onload\nwindow.getRandomColor = function() {\n  return `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)})`\n}\n\nwindow.isSubSet = function(A, B) {\n  return [...A].every(element =\u003e B.has(element))\n}\n@end\n\n@Collaborative.lines: @Collaborative.lines_(@uid,@0,@1,@2)\n\n@Collaborative.lines_\n\u003cscript run-once=\"true\"\u003e\nconst canvas = document.getElementById(\"canvas_@0\");\nconst ctx = canvas.getContext(\"2d\");\nconst color = window.getRandomColor();\nconst dots = new Set();\nlet drawing = false;\nlet lastX = 0;\nlet lastY = 0;\n\nfunction publish() {\n  if (LIA.classroom.connected) {\n    LIA.classroom.publish(\"dots_@0\", JSON.stringify(Array.from(dots)));\n  }\n}\n\nfunction getPos(event) {\n  const rect = canvas.getBoundingClientRect();\n  const scaleX = canvas.width / rect.width;\n  const scaleY = canvas.height / rect.height;\n  let clientX, clientY;\n\n  if (event.touches) {\n    clientX = event.touches[0].clientX;\n    clientY = event.touches[0].clientY;\n  } else {\n    clientX = event.clientX;\n    clientY = event.clientY;\n  }\n\n  return {\n    x: (clientX - rect.left) * scaleX,\n    y: (clientY - rect.top) * scaleY\n  };\n}\n\nfunction drawLine(x1, y1, x2, y2, color) {\n  ctx.beginPath();\n  ctx.moveTo(x1, y1);\n  ctx.lineTo(x2, y2);\n  ctx.strokeStyle = color;\n  ctx.lineWidth = 4;\n  ctx.stroke();\n}\n\nfunction redrawDots() {\n  ctx.clearRect(0, 0, canvas.width, canvas.height);\n  dots.forEach(dotString =\u003e {\n    const dot = JSON.parse(dotString);\n    drawLine(dot.lastX, dot.lastY, dot.x, dot.y, dot.color);\n  });\n}\n\nfunction startDrawing(event) {\n  drawing = true;\n  const { x, y } = getPos(event);\n  lastX = x;\n  lastY = y;\n  dots.add(JSON.stringify({ x, y, color }));\n  publish();\n}\n\nfunction draw(event) {\n  if (!drawing) return;\n  const { x, y } = getPos(event);\n  dots.add(JSON.stringify({ x, y, lastX, lastY, color }));\n  publish();\n  drawLine(lastX, lastY, x, y, color);\n  lastX = x;\n  lastY = y;\n  if (event.touches) event.preventDefault(); // Prevent scrolling on touch devices\n}\n\nfunction stopDrawing() {\n  drawing = false;\n}\n\ncanvas.addEventListener('mousedown', startDrawing);\ncanvas.addEventListener('mousemove', draw);\ncanvas.addEventListener('mouseup', stopDrawing);\ncanvas.addEventListener('mouseout', stopDrawing);\n\ncanvas.addEventListener('touchstart', startDrawing);\ncanvas.addEventListener('touchmove', draw);\ncanvas.addEventListener('touchend', stopDrawing);\ncanvas.addEventListener('touchcancel', stopDrawing);\n\nLIA.classroom.on(\"connect\", () =\u003e {\n  setTimeout(() =\u003e {\n    console.log(\"connected\");\n    LIA.classroom.publish(\"join_@0\", null);\n  }, 1000);\n});\n\nLIA.classroom.subscribe(\"dots_@0\", (message) =\u003e {\n  const receivedDots = new Set(JSON.parse(message));\n  const allDots = new Set([...dots, ...receivedDots]);\n\n  if (!window.isSubSet(dots, receivedDots)) {\n    receivedDots.forEach(dot =\u003e dots.add(dot));\n    publish();\n  } else {\n    receivedDots.forEach(dot =\u003e dots.add(dot));\n  }\n\n  redrawDots();\n});\n\nLIA.classroom.subscribe(\"join_@0\", publish);\n\nconsole.log(\"painting on canvas_@0\");\n\u003c/script\u003e\n\u003ccanvas\n  id=\"canvas_@0\"\n  width=\"@1\"\n  height=\"@2\"\n  style=\"border: 1px solid black; width: 100%; background: url('@3') center/cover no-repeat;\"\u003e\n\u003c/canvas\u003e\n@end\n\n@Collaborative.dots: @Collaborative.dots_(@uid,@0,@1,@2)\n\n@Collaborative.dots_\n\u003cscript run-once=\"true\"\u003e\nconst canvas = document.getElementById(\"canvas_@0\");\nconst ctx = canvas.getContext(\"2d\");\nconst color = window.getRandomColor();\nconst dots = new Set();\n\nfunction publish() {\n  if (LIA.classroom.connected) {\n    LIA.classroom.publish(\"dots_@0\", JSON.stringify(Array.from(dots)));\n  }\n}\n\nfunction getPos(event) {\n  const rect = canvas.getBoundingClientRect();\n  const scaleX = canvas.width / rect.width;\n  const scaleY = canvas.height / rect.height;\n  let clientX, clientY;\n\n  if (event.touches) {\n    clientX = event.touches[0].clientX;\n    clientY = event.touches[0].clientY;\n  } else {\n    clientX = event.clientX;\n    clientY = event.clientY;\n  }\n\n  return {\n    x: (clientX - rect.left) * scaleX,\n    y: (clientY - rect.top) * scaleY\n  };\n}\n\nfunction drawDot(x, y, color) {\n  ctx.beginPath();\n  ctx.arc(x, y, 5, 0, 2 * Math.PI);\n  ctx.fillStyle = color;\n  ctx.fill();\n}\n\nfunction redrawDots() {\n  ctx.clearRect(0, 0, canvas.width, canvas.height);\n  dots.forEach(dotString =\u003e {\n    const dot = JSON.parse(dotString);\n    drawDot(dot.x, dot.y, dot.color);\n  });\n}\n\nfunction handleClick(event) {\n  const { x, y } = getPos(event);\n  dots.add(JSON.stringify({ x, y, color }));\n  publish();\n  drawDot(x, y, color);\n}\n\ncanvas.addEventListener('click', handleClick);\ncanvas.addEventListener('touchstart', function(event) {\n  handleClick(event);\n  event.preventDefault(); // Prevent scrolling on touch devices\n});\n\nLIA.classroom.on(\"connect\", () =\u003e {\n  setTimeout(function() {\n    console.log(\"connected\");\n    LIA.classroom.publish(\"join_@0\", null);\n  }, 1000);\n});\n\nLIA.classroom.subscribe(\"dots_@0\", (message) =\u003e {\n  const receivedDots = new Set(JSON.parse(message));\n  const allDots = new Set([...dots, ...receivedDots]);\n\n  if (!window.isSubSet(dots, receivedDots)) {\n    receivedDots.forEach(dot =\u003e dots.add(dot));\n    publish();\n  } else {\n    receivedDots.forEach(dot =\u003e dots.add(dot));\n  }\n\n  redrawDots();\n});\n\nLIA.classroom.subscribe(\"join_@0\", (message) =\u003e {\n  publish();\n});\n\nconsole.log(\"painting on canvas_@0\");\n\u003c/script\u003e\n\u003ccanvas\n  id=\"canvas_@0\"\n  width=\"@1\"\n  height=\"@2\"\n  style=\"border: 1px solid black; width: 100%; background: url('@3') center/cover no-repeat;\"\u003e\n\u003c/canvas\u003e\n@end\n\n--\u003e\n\n# Collaborative Drawing\n\n    --{{0}}--\nThis is a simple example for a collaborative drawing tool, that can be used in classrooms.\nIt is based on the idea of a shared whiteboard, where students can draw together and uses the LiaScript publish-subscribe mechanism to synchronize the drawings.\nCurrently it has support for 2 macros, one allows to draw lines and the other one to draw dots.\nBoth macros can be used with or without a background image.\n\n**Try it on LiaScript:**\n\nhttps://liascript.github.io/course/?https://raw.githubusercontent.com/LiaTemplates/CollaborativeDrawing/main/README.md\n\nSee the project on Github:\n\nhttps://github.com/LiaTemplates/CollaborativeDrawing\n\n    --{{1}}--\nThere are three ways to use this template.\nThe easiest way is to use the import statement and the url of the raw text-file of the master branch or any other branch or version.\nBut you can also copy the required functionality directly into the header of your Markdown document, see therefor the last slide.\nAnd of course, you could also clone this project and change it, as you wish.\n\n      {{1}}\n\n- Load the macros via and set the persistence flag to true.\n  Persistent will guarantee, that your drawings will be updated, even if you are on another slide.\n\n  ```text\n  import: https://raw.githubusercontent.com/LiaTemplates/CollaborativeDrawing/main/README.md\n\n  persistent: true\n  ```\n\n  or use the tagged version:\n\n  ```text\n  import: https://raw.githubusercontent.com/LiaTemplates/CollaborativeDrawing/0.0.1/README.md\n\n  persistent: true\n  ```\n\n- Copy the definitions into your Project\n\n- Clone this repository on GitHub\n\n## `@Collaborative.lines`\n\n    --{{0}}--\nThis adds a new canvas to your document, where you have to define the width and height, whereby the image is always scaled to the maximum available width.\nFor every connected user a new color will be generated randomly, so that you can see who is drawing what.\n\n`@Collaborative.lines(width,height)`\n\n    --{{1}}--\nThe following example shows a canvas with a width of 640 and a height of 320 pixels.\n\n      {{1}}\n@Collaborative.lines(640,320)\n\n### With background image\n\n    --{{0}}--\nAs a third and optional parameter, you can also add a background image to the canvas.\n\n`@Collaborative.lines(width,height,url)`\n\n`@Collaborative.lines(640,320,https://upload.wikimedia.org/wikipedia/commons/thumb/7/7e/Pieter_Brueghel_the_Elder_-_The_Dutch_Proverbs_-_Google_Art_Project.jpg/1280px-Pieter_Brueghel_the_Elder_-_The_Dutch_Proverbs_-_Google_Art_Project.jpg)`\n\n@[Collaborative.lines(640,320)](https://upload.wikimedia.org/wikipedia/commons/thumb/7/7e/Pieter_Brueghel_the_Elder_-_The_Dutch_Proverbs_-_Google_Art_Project.jpg/1280px-Pieter_Brueghel_the_Elder_-_The_Dutch_Proverbs_-_Google_Art_Project.jpg)\n\n    --{{1}}--\nYou can use also the alternative link-style for the background image.\nThis type of link is preferred, if you are dealing with relative paths.\n\n      {{1}}\n``` markdown\n@[Collaborative.lines(640,320)](./img/example.jpg)\n```\n\n## `@Collaborative.dots`\n\n    --{{0}}--\nThis is a simplification to the previous macro, where you can only draw dots, with click or touch events.\nOn a white canvas this might not be very useful, but if you add a background image, you can use it to mark certain points.\nAlso here you can apply the link style macro syntax, where the last parameter is the url of the background image.\n\n`@Collaborative.dots(width,height)`\n\n`@[Collaborative.dots(1000,500)](url)`\n\n---\n\n@[Collaborative.dots(640,320)](https://upload.wikimedia.org/wikipedia/commons/3/31/A_large_blank_world_map_with_oceans_marked_in_blue-edited.png)\n\n## Implementation\n\n    --{{0}}--\nThe implementation consists of three parts.\nThe first is simply a definition of some helper functions, that are used in the following two macros.\n\n      {{0}}\n``` html\npersistent: true\n\n@onload\nwindow.getRandomColor = function() {\n  return `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)})`\n}\n\nwindow.isSubSet = function(A, B) {\n  return [...A].every(element =\u003e B.has(element))\n}\n@end\n```\n\n    --{{1}}--\nCollaborative drawing of lines defines two macros, where the first calls the second one passes all parameters, but also adds `@uid` to generate a unique id, which is then used to separate the different canvases and api calls.\nThus, a canvas is created with the size of the given parameters and a background image, if provided.\nThe script access this canvas and adds event listeners for mouse and touch events, which are used to draw lines on the canvas.\nThe drawing is synchronized with the other users via the LiaScript publish-subscribe mechanism and by using a set, which stores all the dots that are drawn, which are then send to the other users.\nIn this case, this can be seen as a very simple form of a \"Conflict-free Replicated Data Type\" (CRDT), where the order of the operations does not matter.\n\n      {{1}}\n``` html\n@Collaborative.lines: @Collaborative.lines_(@uid,@0,@1,@2)\n\n@Collaborative.lines_\n\u003cscript run-once=\"true\"\u003e\nconst canvas = document.getElementById(\"canvas_@0\");\nconst ctx = canvas.getContext(\"2d\");\nconst color = window.getRandomColor();\nconst dots = new Set();\nlet drawing = false;\nlet lastX = 0;\nlet lastY = 0;\n\nfunction publish() {\n  if (LIA.classroom.connected) {\n    LIA.classroom.publish(\"dots_@0\", JSON.stringify(Array.from(dots)));\n  }\n}\n\nfunction getPos(event) {\n  const rect = canvas.getBoundingClientRect();\n  const scaleX = canvas.width / rect.width;\n  const scaleY = canvas.height / rect.height;\n  let clientX, clientY;\n\n  if (event.touches) {\n    clientX = event.touches[0].clientX;\n    clientY = event.touches[0].clientY;\n  } else {\n    clientX = event.clientX;\n    clientY = event.clientY;\n  }\n\n  return {\n    x: (clientX - rect.left) * scaleX,\n    y: (clientY - rect.top) * scaleY\n  };\n}\n\nfunction drawLine(x1, y1, x2, y2, color) {\n  ctx.beginPath();\n  ctx.moveTo(x1, y1);\n  ctx.lineTo(x2, y2);\n  ctx.strokeStyle = color;\n  ctx.lineWidth = 4;\n  ctx.stroke();\n}\n\nfunction redrawDots() {\n  ctx.clearRect(0, 0, canvas.width, canvas.height);\n  dots.forEach(dotString =\u003e {\n    const dot = JSON.parse(dotString);\n    drawLine(dot.lastX, dot.lastY, dot.x, dot.y, dot.color);\n  });\n}\n\nfunction startDrawing(event) {\n  drawing = true;\n  const { x, y } = getPos(event);\n  lastX = x;\n  lastY = y;\n  dots.add(JSON.stringify({ x, y, color }));\n  publish();\n}\n\nfunction draw(event) {\n  if (!drawing) return;\n  const { x, y } = getPos(event);\n  dots.add(JSON.stringify({ x, y, lastX, lastY, color }));\n  publish();\n  drawLine(lastX, lastY, x, y, color);\n  lastX = x;\n  lastY = y;\n  if (event.touches) event.preventDefault(); // Prevent scrolling on touch devices\n}\n\nfunction stopDrawing() {\n  drawing = false;\n}\n\ncanvas.addEventListener('mousedown', startDrawing);\ncanvas.addEventListener('mousemove', draw);\ncanvas.addEventListener('mouseup', stopDrawing);\ncanvas.addEventListener('mouseout', stopDrawing);\n\ncanvas.addEventListener('touchstart', startDrawing);\ncanvas.addEventListener('touchmove', draw);\ncanvas.addEventListener('touchend', stopDrawing);\ncanvas.addEventListener('touchcancel', stopDrawing);\n\nLIA.classroom.on(\"connect\", () =\u003e {\n  setTimeout(() =\u003e {\n    console.log(\"connected\");\n    LIA.classroom.publish(\"join_@0\", null);\n  }, 1000);\n});\n\nLIA.classroom.subscribe(\"dots_@0\", (message) =\u003e {\n  const receivedDots = new Set(JSON.parse(message));\n  const allDots = new Set([...dots, ...receivedDots]);\n\n  if (!window.isSubSet(dots, receivedDots)) {\n    receivedDots.forEach(dot =\u003e dots.add(dot));\n    publish();\n  } else {\n    receivedDots.forEach(dot =\u003e dots.add(dot));\n  }\n\n  redrawDots();\n});\n\nLIA.classroom.subscribe(\"join_@0\", publish);\n\nconsole.log(\"painting on canvas_@0\");\n\u003c/script\u003e\n\u003ccanvas\n  id=\"canvas_@0\"\n  width=\"@1\"\n  height=\"@2\"\n  style=\"border: 1px solid black; width: 100%; background: url('@3') center/cover no-repeat;\"\u003e\n\u003c/canvas\u003e\n@end\n```\n\n    --{{2}}--\nThe second macro is a simplification of the first one, where only dots can be drawn.\nThe script is almost the same, but the drawing function is different and only draws dots on the canvas.\n\n      {{2}}\n``` html\n@Collaborative.dots: @Collaborative.dots_(@uid,@0,@1,@2)\n\n@Collaborative.dots_\n\u003cscript run-once=\"true\"\u003e\nconst canvas = document.getElementById(\"canvas_@0\");\nconst ctx = canvas.getContext(\"2d\");\nconst color = window.getRandomColor();\nconst dots = new Set();\n\nfunction publish() {\n  if (LIA.classroom.connected) {\n    LIA.classroom.publish(\"dots_@0\", JSON.stringify(Array.from(dots)));\n  }\n}\n\nfunction getPos(event) {\n  const rect = canvas.getBoundingClientRect();\n  const scaleX = canvas.width / rect.width;\n  const scaleY = canvas.height / rect.height;\n  let clientX, clientY;\n\n  if (event.touches) {\n    clientX = event.touches[0].clientX;\n    clientY = event.touches[0].clientY;\n  } else {\n    clientX = event.clientX;\n    clientY = event.clientY;\n  }\n\n  return {\n    x: (clientX - rect.left) * scaleX,\n    y: (clientY - rect.top) * scaleY\n  };\n}\n\nfunction drawDot(x, y, color) {\n  ctx.beginPath();\n  ctx.arc(x, y, 5, 0, 2 * Math.PI);\n  ctx.fillStyle = color;\n  ctx.fill();\n}\n\nfunction redrawDots() {\n  ctx.clearRect(0, 0, canvas.width, canvas.height);\n  dots.forEach(dotString =\u003e {\n    const dot = JSON.parse(dotString);\n    drawDot(dot.x, dot.y, dot.color);\n  });\n}\n\nfunction handleClick(event) {\n  const { x, y } = getPos(event);\n  dots.add(JSON.stringify({ x, y, color }));\n  publish();\n  drawDot(x, y, color);\n}\n\ncanvas.addEventListener('click', handleClick);\ncanvas.addEventListener('touchstart', function(event) {\n  handleClick(event);\n  event.preventDefault(); // Prevent scrolling on touch devices\n});\n\nLIA.classroom.on(\"connect\", () =\u003e {\n  setTimeout(function() {\n    console.log(\"connected\");\n    LIA.classroom.publish(\"join_@0\", null);\n  }, 1000);\n});\n\nLIA.classroom.subscribe(\"dots_@0\", (message) =\u003e {\n  const receivedDots = new Set(JSON.parse(message));\n  const allDots = new Set([...dots, ...receivedDots]);\n\n  if (!window.isSubSet(dots, receivedDots)) {\n    receivedDots.forEach(dot =\u003e dots.add(dot));\n    publish();\n  } else {\n    receivedDots.forEach(dot =\u003e dots.add(dot));\n  }\n\n  redrawDots();\n});\n\nLIA.classroom.subscribe(\"join_@0\", (message) =\u003e {\n  publish();\n});\n\nconsole.log(\"painting on canvas_@0\");\n\u003c/script\u003e\n\u003ccanvas\n  id=\"canvas_@0\"\n  width=\"@1\"\n  height=\"@2\"\n  style=\"border: 1px solid black; width: 100%; background: url('@3') center/cover no-repeat;\"\u003e\n\u003c/canvas\u003e\n@end\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fliatemplates%2Fcollaborativedrawing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fliatemplates%2Fcollaborativedrawing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fliatemplates%2Fcollaborativedrawing/lists"}