├── .gitignore ├── assets ├── images │ ├── line.jpg │ ├── pip.jpg │ ├── line-t.jpg │ ├── angle-offset.jpg │ ├── light-casting.jpg │ ├── offseted-rays.jpg │ ├── offseted-rays-solve.jpg │ └── Simple_raycasting_with_fisheye_correction.gif └── demos │ ├── demo1.js │ ├── demo14.js │ ├── demo11.js │ ├── demo4.js │ ├── demo3.js │ ├── demo2.js │ ├── demo13.js │ ├── demo6.js │ ├── demo12.js │ ├── demo5.js │ ├── demo7.js │ ├── demo8.js │ ├── demo9.js │ └── demo10.js ├── README.md ├── styles.css ├── LICENSE.md ├── cheatsheet.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /assets/images/line.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sszczep/ray-casting-in-2d-game-engines/HEAD/assets/images/line.jpg -------------------------------------------------------------------------------- /assets/images/pip.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sszczep/ray-casting-in-2d-game-engines/HEAD/assets/images/pip.jpg -------------------------------------------------------------------------------- /assets/images/line-t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sszczep/ray-casting-in-2d-game-engines/HEAD/assets/images/line-t.jpg -------------------------------------------------------------------------------- /assets/images/angle-offset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sszczep/ray-casting-in-2d-game-engines/HEAD/assets/images/angle-offset.jpg -------------------------------------------------------------------------------- /assets/images/light-casting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sszczep/ray-casting-in-2d-game-engines/HEAD/assets/images/light-casting.jpg -------------------------------------------------------------------------------- /assets/images/offseted-rays.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sszczep/ray-casting-in-2d-game-engines/HEAD/assets/images/offseted-rays.jpg -------------------------------------------------------------------------------- /assets/images/offseted-rays-solve.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sszczep/ray-casting-in-2d-game-engines/HEAD/assets/images/offseted-rays-solve.jpg -------------------------------------------------------------------------------- /assets/images/Simple_raycasting_with_fisheye_correction.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sszczep/ray-casting-in-2d-game-engines/HEAD/assets/images/Simple_raycasting_with_fisheye_correction.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ```diff 2 | @@ This project is not maintained anymore. @@ 3 | @@ Feel free to contact me directly in case of any questions. @@ 4 | ``` 5 | 6 | # Ray casting in 2D game engines 7 | Not that hard as you might have thought. 8 | 9 | This article has been updated and moved to https://sszczep.dev/blog/ray-casting-in-2d-game-engines/. 10 | 11 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/sszczep) 12 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .img-fluid { 2 | max-width: 100%; 3 | max-height: 300px; 4 | } 5 | 6 | .nav-link { 7 | margin: .1rem 0; 8 | } 9 | 10 | h1 { 11 | margin: 3rem 0; 12 | } 13 | 14 | h2 { 15 | margin: 2rem 0; 16 | } 17 | 18 | h3 { 19 | margin: 1rem 0; 20 | } 21 | 22 | h4 { 23 | margin: .5rem 0; 24 | } 25 | 26 | .MathJax.CtxtMenu_Attached_0 { 27 | overflow-x: auto; 28 | overflow-y: hidden; 29 | text-align: left !important; 30 | } 31 | 32 | @media (min-width: 992px) { 33 | #navbar { 34 | max-height: 100vh; 35 | overflow-y: auto; 36 | padding-right: 15px; 37 | } 38 | 39 | .nav-link { 40 | padding: 0; 41 | margin: .2rem 0; 42 | font-size: .9rem; 43 | } 44 | 45 | .nav-link.active { 46 | color: inherit !important; 47 | background-color: inherit !important; 48 | text-decoration: underline; 49 | } 50 | } 51 | 52 | @media print { 53 | #navbar { 54 | display: none; 55 | } 56 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 Sebastian Szczepański 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /assets/demos/demo1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo1'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const rayAnchor = { x: 0, y: 150 }; 11 | 12 | const lineSegments = [ 13 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 14 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 15 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 16 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 17 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 18 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 19 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 20 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 21 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 22 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 23 | ]; 24 | 25 | function getMousePosition(event) { 26 | const rect = canvas.getBoundingClientRect(); 27 | const scaleX = canvas.width / rect.width; 28 | const scaleY = canvas.height / rect.height; 29 | 30 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 31 | } 32 | 33 | function getIntersectionPoint(ray, segment) { 34 | const [A, B] = segment; 35 | const [C, D] = ray; 36 | 37 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 38 | 39 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 40 | if(s < 0 || s > 1) return null; 41 | 42 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 43 | if(r < 0) return null; 44 | 45 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y }; 46 | } 47 | 48 | function getIntersectionPoints(ray, segments) { 49 | return segments.map(segment => getIntersectionPoint(ray, segment)).filter(point => point !== null); 50 | } 51 | 52 | function clearCanvas() { 53 | ctx.fillStyle = 'white'; 54 | ctx.fillRect(0, 0, canvas.width, canvas.height); 55 | } 56 | 57 | function drawSegments(segments) { 58 | ctx.strokeStyle = 'black'; 59 | segments.forEach(segment => { 60 | ctx.beginPath(); 61 | ctx.moveTo(segment[0].x, segment[0].y); 62 | ctx.lineTo(segment[1].x, segment[1].y); 63 | ctx.stroke(); 64 | }); 65 | } 66 | 67 | function drawRay(ray) { 68 | ctx.strokeStyle = 'blue'; 69 | ctx.beginPath(); 70 | 71 | // Extending line segment for better ray visualization 72 | // Can be omitted in production code 73 | const x = ray[1].x + (ray[1].x - ray[0].x) * 1000; 74 | const y = ray[1].y + (ray[1].y - ray[0].y) * 1000; 75 | 76 | ctx.moveTo(ray[0].x, ray[0].y); 77 | ctx.lineTo(x, y); 78 | ctx.stroke(); 79 | } 80 | 81 | function drawIntersectionPoints(ray) { 82 | ctx.fillStyle = 'red'; 83 | getIntersectionPoints(ray, lineSegments).forEach(point => { 84 | ctx.beginPath(); 85 | ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI); 86 | ctx.fill(); 87 | }); 88 | } 89 | 90 | function draw(mousePos) { 91 | clearCanvas(); 92 | drawSegments(lineSegments); 93 | drawRay([rayAnchor, mousePos]); 94 | drawIntersectionPoints([rayAnchor, mousePos]); 95 | } 96 | 97 | window.addEventListener('mousemove', event => { 98 | const mousePos = getMousePosition(event); 99 | if( 100 | mousePos.x < 0 || mousePos.x > canvas.width 101 | || mousePos.y < 0 || mousePos.y > canvas.height 102 | ) return; 103 | 104 | draw(mousePos); 105 | }); 106 | 107 | draw({ x: canvas.width, y: canvas.height / 2 }); 108 | } -------------------------------------------------------------------------------- /assets/demos/demo14.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Important! 4 | // We extend ray to be at least canvas.width * Math.sqrt(2) px long, it ensures that the ray goes across the whole canvas. 5 | // Previously we were doing it only for graphical representation as it didn't matter for calculations, but now we need it for Bresenham's line algorithm. 6 | // You might want to slightly modify the algorithm to omit this necessity. 7 | 8 | { 9 | const canvas = document.getElementById('demo14'); 10 | const ctx = canvas.getContext('2d'); 11 | 12 | canvas.width = 600; 13 | canvas.height = 300; 14 | 15 | const rayAnchor = { x: 0, y: 0 }; 16 | 17 | const cellSize = 30; 18 | 19 | function getMousePosition(event) { 20 | const rect = canvas.getBoundingClientRect(); 21 | const scaleX = canvas.width / rect.width; 22 | const scaleY = canvas.height / rect.height; 23 | 24 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 25 | } 26 | 27 | function clearCanvas() { 28 | ctx.fillStyle = 'white'; 29 | ctx.fillRect(0, 0, canvas.width, canvas.height); 30 | } 31 | 32 | function drawCells() { 33 | ctx.strokeStyle = 'red'; 34 | for(let col = 0; col < Math.ceil(canvas.width / cellSize); col++) { 35 | const cellX = col * cellSize; 36 | 37 | for(let row = 0; row < Math.ceil(canvas.height / cellSize); row++) { 38 | const cellY = row * cellSize; 39 | 40 | ctx.strokeRect(cellX, cellY, cellSize, cellSize); 41 | } 42 | } 43 | } 44 | 45 | function extendRay(ray) { 46 | const len = Math.sqrt((ray[0].x - ray[0].y) ** 2 + (ray[0].y - ray[1].y) ** 2); 47 | const x = ray[1].x + (ray[1].x - ray[0].x) / len * canvas.width * 1.42; 48 | const y = ray[1].y + (ray[1].y - ray[0].y) / len * canvas.width * 1.42; 49 | return { x, y }; 50 | } 51 | 52 | function drawRay(ray) { 53 | ctx.lineWidth = 3; 54 | ctx.strokeStyle = 'blue'; 55 | ctx.beginPath(); 56 | ctx.moveTo(ray[0].x, ray[0].y); 57 | ctx.lineTo(ray[1].x, ray[1].y); 58 | ctx.stroke(); 59 | ctx.lineWidth = 1; 60 | } 61 | 62 | function visit(x, y) { 63 | const cellX = Math.floor(x / cellSize); 64 | const cellY = Math.floor(y / cellSize); 65 | 66 | ctx.strokeStyle = 'green'; 67 | ctx.lineWidth = 3; 68 | ctx.strokeRect(cellX * cellSize, cellY * cellSize, cellSize, cellSize); 69 | ctx.lineWidth = 1; 70 | } 71 | 72 | // Shamelessly stolen from https://playtechs.blogspot.com/2007/03/raytracing-on-grid.html 73 | function bresenham(x0, y0, x1, y1) { 74 | let dx = Math.abs(x1 - x0); 75 | let dy = Math.abs(y1 - y0); 76 | let x = x0; 77 | let y = y0; 78 | let n = 1 + dx + dy; 79 | const x_inc = (x1 > x0) ? 1 : -1; 80 | const y_inc = (y1 > y0) ? 1 : -1; 81 | let error = dx - dy; 82 | dx *= 2; 83 | dy *= 2; 84 | 85 | for (; n > 0; --n) { 86 | visit(x, y); 87 | 88 | if (error > 0) { 89 | x += x_inc; 90 | error -= dy; 91 | } else { 92 | y += y_inc; 93 | error += dx; 94 | } 95 | } 96 | } 97 | 98 | function draw(mousePos) { 99 | clearCanvas(); 100 | drawCells(); 101 | const extendedRayEnd = extendRay([rayAnchor, mousePos]); 102 | bresenham(rayAnchor.x, rayAnchor.y, extendedRayEnd.x, extendedRayEnd.y); 103 | drawRay([rayAnchor, extendedRayEnd]); 104 | } 105 | 106 | window.addEventListener('mousemove', event => { 107 | const mousePos = getMousePosition(event); 108 | if( 109 | mousePos.x < 0 || mousePos.x > canvas.width 110 | || mousePos.y < 0 || mousePos.y > canvas.height 111 | ) return; 112 | 113 | draw(mousePos); 114 | }); 115 | 116 | draw({ x: 600, y: 300 }); 117 | } -------------------------------------------------------------------------------- /assets/demos/demo11.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo11'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const rayAnchor = { x: 0, y: 150 }; 11 | 12 | const circles = [ 13 | { x: 100, y: 50, radius: 30 }, 14 | { x: 300, y: 150, radius: 50 }, 15 | { x: 400, y: 200, radius: 30 }, 16 | ]; 17 | 18 | function getMousePosition(event) { 19 | const rect = canvas.getBoundingClientRect(); 20 | const scaleX = canvas.width / rect.width; 21 | const scaleY = canvas.height / rect.height; 22 | 23 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 24 | } 25 | 26 | function getIntersectionPoint(ray, circle) { 27 | const [A, B] = ray; 28 | const C = { x: circle.x, y: circle.y }; 29 | const r = circle.radius; 30 | 31 | const a = (B.x - A.x) ** 2 + (B.y - A.y) ** 2; 32 | const b = 2 * ((B.x - A.x) * (A.x - C.x) + (B.y - A.y) * (A.y - C.y)); 33 | const c = (A.x - C.x) ** 2 + (A.y - C.y) ** 2 - r ** 2; 34 | const discriminant = b ** 2 - 4 * a * c; 35 | 36 | const result = []; 37 | 38 | if(discriminant === 0) { 39 | const t = -b / (2 * a); 40 | 41 | if(t >= 0) { 42 | const x = t * (B.x - A.x) + A.x; 43 | const y = t * (B.y - A.y) + A.y; 44 | 45 | result.push({ x, y }); 46 | } 47 | } else if(discriminant > 0) { 48 | const discriminantSqrt = Math.sqrt(discriminant); 49 | const t1 = (-b + discriminantSqrt) / (2 * a); 50 | const t2 = (-b - discriminantSqrt) / (2 * a); 51 | 52 | if(t1 >= 0) { 53 | const x = t1 * (B.x - A.x) + A.x; 54 | const y = t1 * (B.y - A.y) + A.y; 55 | 56 | result.push({ x, y }) 57 | } 58 | 59 | if(t2 >= 0) { 60 | const x = t2 * (B.x - A.x) + A.x; 61 | const y = t2 * (B.y - A.y) + A.y; 62 | 63 | result.push({ x, y }); 64 | } 65 | } 66 | 67 | return result; 68 | } 69 | 70 | function getIntersectionPoints(ray, circles) { 71 | return circles.flatMap(circle => getIntersectionPoint(ray, circle)); 72 | } 73 | 74 | function clearCanvas() { 75 | ctx.fillStyle = 'white'; 76 | ctx.fillRect(0, 0, canvas.width, canvas.height); 77 | } 78 | 79 | function drawCircles(circles) { 80 | ctx.strokeStyle = 'black'; 81 | circles.forEach(circle => { 82 | ctx.beginPath(); 83 | ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI); 84 | ctx.stroke(); 85 | }); 86 | } 87 | 88 | function drawRay(ray) { 89 | ctx.strokeStyle = 'blue'; 90 | ctx.beginPath(); 91 | 92 | // Extending line segment for better ray visualization 93 | // Can be omitted in production code 94 | const x = ray[1].x + (ray[1].x - ray[0].x) * 1000; 95 | const y = ray[1].y + (ray[1].y - ray[0].y) * 1000; 96 | 97 | ctx.moveTo(ray[0].x, ray[0].y); 98 | ctx.lineTo(x, y); 99 | ctx.stroke(); 100 | } 101 | 102 | function drawIntersectionPoints(ray, circles) { 103 | ctx.fillStyle = 'red'; 104 | getIntersectionPoints(ray, circles).forEach(point => { 105 | ctx.beginPath(); 106 | ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI); 107 | ctx.fill(); 108 | }); 109 | } 110 | 111 | function draw(mousePos) { 112 | clearCanvas(); 113 | drawCircles(circles); 114 | drawRay([rayAnchor, mousePos]); 115 | drawIntersectionPoints([rayAnchor, mousePos], circles); 116 | } 117 | 118 | window.addEventListener('mousemove', event => { 119 | const mousePos = getMousePosition(event); 120 | if( 121 | mousePos.x < 0 || mousePos.x > canvas.width 122 | || mousePos.y < 0 || mousePos.y > canvas.height 123 | ) return; 124 | 125 | draw(mousePos); 126 | }); 127 | 128 | draw({ x: canvas.width, y: canvas.height / 2 }); 129 | } -------------------------------------------------------------------------------- /assets/demos/demo4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo4'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const lineSegments = [ 11 | [{ x: 0, y: 0 }, { x: 600, y: 0 }], 12 | [{ x: 600, y: 0 }, { x: 600, y: 300 }], 13 | [{ x: 600, y: 300 }, { x: 0, y: 300 }], 14 | [{ x: 0, y: 300 }, { x: 0, y: 0 }], 15 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 16 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 17 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 18 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 19 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 20 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 21 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 22 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 23 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 24 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 25 | ]; 26 | 27 | // In production code, you might want to filter only unique vertices 28 | const vertices = lineSegments.reduce((vertices, segment) => { 29 | return [...vertices, ...segment]; 30 | }, []); 31 | 32 | function getMousePosition(event) { 33 | const rect = canvas.getBoundingClientRect(); 34 | const scaleX = canvas.width / rect.width; 35 | const scaleY = canvas.height / rect.height; 36 | 37 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 38 | } 39 | 40 | function getIntersectionPoint(ray, segment, smallestR) { 41 | const [A, B] = segment; 42 | const [C, D] = ray; 43 | 44 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 45 | 46 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 47 | if(r < 0) return null; 48 | if(smallestR !== null && smallestR < r) return null; 49 | 50 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 51 | if(s < 0 || s > 1) return null; 52 | 53 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y, r }; 54 | } 55 | 56 | function getClosestIntersectionPoint(ray, segments) { 57 | return segments.reduce((closest, segment) => { 58 | return getIntersectionPoint(ray, segment, closest ? closest.r : null) || closest; 59 | }, null); 60 | } 61 | 62 | function clearCanvas() { 63 | ctx.fillStyle = 'white'; 64 | ctx.fillRect(0, 0, canvas.width, canvas.height); 65 | } 66 | 67 | function drawSegments(segments) { 68 | ctx.strokeStyle = 'black'; 69 | segments.forEach(segment => { 70 | ctx.beginPath(); 71 | ctx.moveTo(segment[0].x, segment[0].y); 72 | ctx.lineTo(segment[1].x, segment[1].y); 73 | ctx.stroke(); 74 | }); 75 | } 76 | 77 | function drawRay(ray) { 78 | ctx.strokeStyle = 'blue'; 79 | ctx.beginPath(); 80 | 81 | ctx.moveTo(ray[0].x, ray[0].y); 82 | ctx.lineTo(ray[1].x, ray[1].y); 83 | ctx.stroke(); 84 | } 85 | 86 | function drawClosestIntersectionPoint(ray) { 87 | const closestPoint = getClosestIntersectionPoint(ray, lineSegments); 88 | if(closestPoint !== null) { 89 | ctx.fillStyle = 'red'; 90 | ctx.beginPath(); 91 | ctx.arc(closestPoint.x, closestPoint.y, 5, 0, 2 * Math.PI); 92 | ctx.fill(); 93 | } 94 | } 95 | 96 | function draw(mousePos) { 97 | clearCanvas(); 98 | drawSegments(lineSegments); 99 | 100 | vertices.forEach(vertex => { 101 | drawRay([mousePos, vertex]); 102 | drawClosestIntersectionPoint([mousePos, vertex]); 103 | }); 104 | } 105 | 106 | window.addEventListener('mousemove', event => { 107 | const mousePos = getMousePosition(event); 108 | if( 109 | mousePos.x < 0 || mousePos.x > canvas.width 110 | || mousePos.y < 0 || mousePos.y > canvas.height 111 | ) return; 112 | 113 | draw(mousePos); 114 | }); 115 | 116 | draw({ x: canvas.width / 2, y: canvas.height / 2 }); 117 | } -------------------------------------------------------------------------------- /assets/demos/demo3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo3'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const angleOffset = 12; 11 | 12 | const lineSegments = [ 13 | [{ x: 0, y: 0 }, { x: 600, y: 0 }], 14 | [{ x: 600, y: 0 }, { x: 600, y: 300 }], 15 | [{ x: 600, y: 300 }, { x: 0, y: 300 }], 16 | [{ x: 0, y: 300 }, { x: 0, y: 0 }], 17 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 18 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 19 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 20 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 21 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 22 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 23 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 24 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 25 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 26 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 27 | ]; 28 | 29 | function getMousePosition(event) { 30 | const rect = canvas.getBoundingClientRect(); 31 | const scaleX = canvas.width / rect.width; 32 | const scaleY = canvas.height / rect.height; 33 | 34 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 35 | } 36 | 37 | function getIntersectionPoint(ray, segment, smallestR) { 38 | const [A, B] = segment; 39 | const [C, D] = ray; 40 | 41 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 42 | 43 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 44 | if(r < 0) return null; 45 | if(smallestR !== null && smallestR < r) return null; 46 | 47 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 48 | if(s < 0 || s > 1) return null; 49 | 50 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y, r }; 51 | } 52 | 53 | function getClosestIntersectionPoint(ray, segments) { 54 | return segments.reduce((closest, segment) => { 55 | return getIntersectionPoint(ray, segment, closest ? closest.r : null) || closest; 56 | }, null); 57 | } 58 | 59 | function getAngleOffsetPoint(point, angle) { 60 | const dist = 1; 61 | 62 | return { 63 | x: point.x + dist * Math.sin(Math.PI / 180 * angle), 64 | y: point.y - dist * Math.cos(Math.PI / 180 * angle), 65 | }; 66 | } 67 | 68 | function clearCanvas() { 69 | ctx.fillStyle = 'white'; 70 | ctx.fillRect(0, 0, canvas.width, canvas.height); 71 | } 72 | 73 | function drawSegments(segments) { 74 | ctx.strokeStyle = 'black'; 75 | segments.forEach(segment => { 76 | ctx.beginPath(); 77 | ctx.moveTo(segment[0].x, segment[0].y); 78 | ctx.lineTo(segment[1].x, segment[1].y); 79 | ctx.stroke(); 80 | }); 81 | } 82 | 83 | function drawRay(ray) { 84 | ctx.strokeStyle = 'blue'; 85 | ctx.beginPath(); 86 | 87 | ctx.moveTo(ray[0].x, ray[0].y); 88 | ctx.lineTo(ray[1].x, ray[1].y); 89 | ctx.stroke(); 90 | } 91 | 92 | function drawClosestIntersectionPoint(closestPoint) { 93 | if(closestPoint !== null) { 94 | ctx.fillStyle = 'red'; 95 | ctx.beginPath(); 96 | ctx.arc(closestPoint.x, closestPoint.y, 5, 0, 2 * Math.PI); 97 | ctx.fill(); 98 | } 99 | } 100 | 101 | function draw(mousePos) { 102 | clearCanvas(); 103 | drawSegments(lineSegments); 104 | 105 | for(let angle = 0; angle < 360; angle += angleOffset) { 106 | const offsetPoint = getAngleOffsetPoint(mousePos, angle); 107 | const closestPoint = getClosestIntersectionPoint([mousePos, offsetPoint], lineSegments); 108 | drawRay([mousePos, closestPoint]); 109 | drawClosestIntersectionPoint(closestPoint); 110 | } 111 | } 112 | 113 | window.addEventListener('mousemove', event => { 114 | const mousePos = getMousePosition(event); 115 | if( 116 | mousePos.x < 0 || mousePos.x > canvas.width 117 | || mousePos.y < 0 || mousePos.y > canvas.height 118 | ) return; 119 | 120 | draw(mousePos); 121 | }); 122 | 123 | draw({ x: canvas.width / 2, y: canvas.height / 2 }); 124 | } -------------------------------------------------------------------------------- /assets/demos/demo2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo2'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const rayAnchor = { x: 0, y: 150 }; 11 | 12 | const lineSegments = [ 13 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 14 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 15 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 16 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 17 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 18 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 19 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 20 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 21 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 22 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 23 | ]; 24 | 25 | function getMousePosition(event) { 26 | const rect = canvas.getBoundingClientRect(); 27 | const scaleX = canvas.width / rect.width; 28 | const scaleY = canvas.height / rect.height; 29 | 30 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 31 | } 32 | 33 | function getIntersectionPoint(ray, segment, smallestR) { 34 | const [A, B] = segment; 35 | const [C, D] = ray; 36 | 37 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 38 | 39 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 40 | if(r < 0) return null; 41 | if(smallestR !== null && smallestR < r) return null; 42 | 43 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 44 | if(s < 0 || s > 1) return null; 45 | 46 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y, r }; 47 | } 48 | 49 | function getClosestIntersectionPoint(ray, segments) { 50 | return segments.reduce((closest, segment) => { 51 | return getIntersectionPoint(ray, segment, closest ? closest.r : null) || closest; 52 | }, null); 53 | } 54 | 55 | function getIntersectionPoints(ray, segments) { 56 | return segments.map(segment => getIntersectionPoint(ray, segment)).filter(point => point !== null); 57 | } 58 | 59 | function clearCanvas() { 60 | ctx.fillStyle = 'white'; 61 | ctx.fillRect(0, 0, canvas.width, canvas.height); 62 | } 63 | 64 | function drawSegments(segments) { 65 | ctx.strokeStyle = 'black'; 66 | segments.forEach(segment => { 67 | ctx.beginPath(); 68 | ctx.moveTo(segment[0].x, segment[0].y); 69 | ctx.lineTo(segment[1].x, segment[1].y); 70 | ctx.stroke(); 71 | }); 72 | } 73 | 74 | function drawRay(ray) { 75 | ctx.strokeStyle = 'blue'; 76 | ctx.beginPath(); 77 | 78 | // Extending line segment for better ray visualization 79 | // Can be omitted in production code 80 | const x = ray[1].x + (ray[1].x - ray[0].x) * 1000; 81 | const y = ray[1].y + (ray[1].y - ray[0].y) * 1000; 82 | 83 | ctx.moveTo(ray[0].x, ray[0].y); 84 | ctx.lineTo(x, y); 85 | ctx.stroke(); 86 | } 87 | 88 | function drawIntersectionPoints(ray) { 89 | ctx.fillStyle = 'grey'; 90 | getIntersectionPoints(ray, lineSegments).forEach(point => { 91 | ctx.beginPath(); 92 | ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI); 93 | ctx.fill(); 94 | }); 95 | } 96 | 97 | function drawClosestIntersectionPoint(ray) { 98 | const closestPoint = getClosestIntersectionPoint(ray, lineSegments); 99 | if(closestPoint !== null) { 100 | ctx.fillStyle = 'red'; 101 | ctx.beginPath(); 102 | ctx.arc(closestPoint.x, closestPoint.y, 5, 0, 2 * Math.PI); 103 | ctx.fill(); 104 | } 105 | } 106 | 107 | function draw(mousePos) { 108 | clearCanvas(); 109 | drawSegments(lineSegments); 110 | drawRay([rayAnchor, mousePos]); 111 | drawIntersectionPoints([rayAnchor, mousePos]); 112 | drawClosestIntersectionPoint([rayAnchor, mousePos]); 113 | } 114 | 115 | window.addEventListener('mousemove', event => { 116 | const mousePos = getMousePosition(event); 117 | if( 118 | mousePos.x < 0 || mousePos.x > canvas.width 119 | || mousePos.y < 0 || mousePos.y > canvas.height 120 | ) return; 121 | 122 | draw(mousePos); 123 | }); 124 | 125 | draw({ x: canvas.width, y: canvas.height / 2 }); 126 | } -------------------------------------------------------------------------------- /assets/demos/demo13.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo13'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const lineSegments = [ 11 | [{ x: 0, y: 0 }, { x: 600, y: 0 }], 12 | [{ x: 600, y: 0 }, { x: 600, y: 300 }], 13 | [{ x: 600, y: 300 }, { x: 0, y: 300 }], 14 | [{ x: 0, y: 300 }, { x: 0, y: 0 }], 15 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 16 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 17 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 18 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 19 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 20 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 21 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 22 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 23 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 24 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 25 | ]; 26 | 27 | const cellSize = 150; 28 | const cells = []; 29 | 30 | // We need to check if a line segment intersects with the cell. 31 | // For simplicity, I decided to use bounding boxes. 32 | // It might not be the most accurate solution but it gets the job done. 33 | // You might want to check for line-segment - cell intersection instead. 34 | for(let col = 0; col < Math.ceil(canvas.width / cellSize); col++) { 35 | cells.push([]); 36 | 37 | for(let row = 0; row < Math.ceil(canvas.height / cellSize); row++) { 38 | const cell = lineSegments.filter(segment => { 39 | const cellX = col * cellSize; 40 | const cellY = row * cellSize; 41 | 42 | const [A, B] = segment; 43 | const width = Math.abs(A.x - B.x); 44 | const height = Math.abs(A.y - B.y); 45 | const x = Math.min(A.x, B.x); 46 | const y = Math.min(A.y, B.y); 47 | 48 | return ( 49 | // If line-segment inside a cell 50 | ( 51 | x >= cellX && x + width <= cellX + cellSize 52 | && y >= cellY && y + height <= cellY + cellSize 53 | ) 54 | // If bounding boxes intersect 55 | || ( 56 | cellX < x + width 57 | && cellX + cellSize > x 58 | && cellY < y + height 59 | && cellY + height > y 60 | ) 61 | ); 62 | }); 63 | 64 | cells[col].push(cell); 65 | } 66 | } 67 | 68 | function getMousePosition(event) { 69 | const rect = canvas.getBoundingClientRect(); 70 | const scaleX = canvas.width / rect.width; 71 | const scaleY = canvas.height / rect.height; 72 | 73 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 74 | } 75 | 76 | function clearCanvas() { 77 | ctx.fillStyle = 'white'; 78 | ctx.fillRect(0, 0, canvas.width, canvas.height); 79 | } 80 | 81 | function drawSegments(segments) { 82 | ctx.strokeStyle = 'black'; 83 | segments.forEach(segment => { 84 | ctx.beginPath(); 85 | ctx.moveTo(segment[0].x, segment[0].y); 86 | ctx.lineTo(segment[1].x, segment[1].y); 87 | ctx.stroke(); 88 | }); 89 | } 90 | 91 | function drawCells() { 92 | ctx.strokeStyle = 'red'; 93 | for(let col = 0; col < Math.ceil(canvas.width / cellSize); col++) { 94 | const cellX = col * cellSize; 95 | 96 | for(let row = 0; row < Math.ceil(canvas.height / cellSize); row++) { 97 | const cellY = row * cellSize; 98 | 99 | ctx.strokeRect(cellX, cellY, cellSize, cellSize); 100 | 101 | ctx.fillStyle = 'black'; 102 | ctx.font = '16px serif'; 103 | ctx.fillText(`(${cellX},${cellY})`, cellX + 5, cellY + 21); 104 | } 105 | } 106 | } 107 | 108 | function drawActiveCell(mousePos) { 109 | const x = Math.floor(mousePos.x / cellSize); 110 | const y = Math.floor(mousePos.y / cellSize); 111 | 112 | ctx.strokeStyle = 'green'; 113 | ctx.lineWidth = 3; 114 | 115 | ctx.strokeRect(x * cellSize, y * cellSize, cellSize, cellSize); 116 | 117 | ctx.beginPath(); 118 | cells[x][y].forEach(segment => { 119 | const [A, B] = segment; 120 | ctx.moveTo(A.x, A.y); 121 | ctx.lineTo(B.x, B.y); 122 | }); 123 | ctx.stroke(); 124 | 125 | ctx.lineWidth = 1; 126 | } 127 | 128 | function draw(mousePos) { 129 | clearCanvas(); 130 | drawSegments(lineSegments); 131 | drawCells(); 132 | drawActiveCell(mousePos); 133 | } 134 | 135 | window.addEventListener('mousemove', event => { 136 | const mousePos = getMousePosition(event); 137 | if( 138 | mousePos.x < 0 || mousePos.x > canvas.width 139 | || mousePos.y < 0 || mousePos.y > canvas.height 140 | ) return; 141 | 142 | draw(mousePos); 143 | }); 144 | 145 | draw({ x: 0, y: 0 }); 146 | } -------------------------------------------------------------------------------- /assets/demos/demo6.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo6'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const lineSegments = [ 11 | [{ x: 0, y: 0 }, { x: 600, y: 0 }], 12 | [{ x: 600, y: 0 }, { x: 600, y: 300 }], 13 | [{ x: 600, y: 300 }, { x: 0, y: 300 }], 14 | [{ x: 0, y: 300 }, { x: 0, y: 0 }], 15 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 16 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 17 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 18 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 19 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 20 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 21 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 22 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 23 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 24 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 25 | ]; 26 | 27 | // In production code, you might want to filter only unique vertices 28 | const vertices = lineSegments.reduce((vertices, segment) => { 29 | return [...vertices, ...segment]; 30 | }, []); 31 | 32 | function getMousePosition(event) { 33 | const rect = canvas.getBoundingClientRect(); 34 | const scaleX = canvas.width / rect.width; 35 | const scaleY = canvas.height / rect.height; 36 | 37 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 38 | } 39 | 40 | function getIntersectionPoint(ray, segment, smallestR) { 41 | const [A, B] = segment; 42 | const [C, D] = ray; 43 | 44 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 45 | 46 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 47 | if(r < 0) return null; 48 | if(smallestR !== null && smallestR < r) return null; 49 | 50 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 51 | if(s < 0 || s > 1) return null; 52 | 53 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y, r }; 54 | } 55 | 56 | function getClosestIntersectionPoint(ray, segments) { 57 | return segments.reduce((closest, segment) => { 58 | return getIntersectionPoint(ray, segment, closest ? closest.r : null) || closest; 59 | }, null); 60 | } 61 | 62 | function sortIntersectionPointsByAngle(anchor, points) { 63 | return points.sort((P1, P2) => Math.atan2(P1.y - anchor.y, P1.x - anchor.x) - Math.atan2(P2.y - anchor.y, P2.x - anchor.x)); 64 | } 65 | 66 | function clearCanvas() { 67 | ctx.fillStyle = 'lightgrey'; 68 | ctx.fillRect(0, 0, canvas.width, canvas.height); 69 | } 70 | 71 | function drawSegments(segments) { 72 | ctx.strokeStyle = 'black'; 73 | segments.forEach(segment => { 74 | ctx.beginPath(); 75 | ctx.moveTo(segment[0].x, segment[0].y); 76 | ctx.lineTo(segment[1].x, segment[1].y); 77 | ctx.stroke(); 78 | }); 79 | } 80 | 81 | function drawRay(ray) { 82 | ctx.strokeStyle = 'blue'; 83 | ctx.beginPath(); 84 | 85 | ctx.moveTo(ray[0].x, ray[0].y); 86 | ctx.lineTo(ray[1].x, ray[1].y); 87 | ctx.stroke(); 88 | } 89 | 90 | function drawClosestIntersectionPoint(closestPoint) { 91 | if(closestPoint !== null) { 92 | ctx.fillStyle = 'red'; 93 | ctx.beginPath(); 94 | ctx.arc(closestPoint.x, closestPoint.y, 5, 0, 2 * Math.PI); 95 | ctx.fill(); 96 | } 97 | } 98 | 99 | function drawVisibleArea(sortedIntersectionPoints) { 100 | ctx.fillStyle = 'white'; 101 | ctx.beginPath(); 102 | ctx.moveTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 103 | sortedIntersectionPoints.slice(1).forEach(point => { 104 | ctx.lineTo(point.x, point.y); 105 | }); 106 | ctx.lineTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 107 | ctx.fill(); 108 | } 109 | 110 | function draw(mousePos) { 111 | clearCanvas(); 112 | 113 | const intersectionPoints = []; 114 | vertices.forEach(vertex => { 115 | const closestPoint = getClosestIntersectionPoint([mousePos, vertex], lineSegments); 116 | if(closestPoint !== null) intersectionPoints.push(closestPoint); 117 | }); 118 | 119 | const sortedIntersectionPoints = sortIntersectionPointsByAngle(mousePos, intersectionPoints); 120 | 121 | drawVisibleArea(sortedIntersectionPoints); 122 | drawSegments(lineSegments); 123 | intersectionPoints.forEach(intersectionPoint => { 124 | drawRay([mousePos, intersectionPoint]); 125 | drawClosestIntersectionPoint(intersectionPoint); 126 | }); 127 | } 128 | 129 | window.addEventListener('mousemove', event => { 130 | const mousePos = getMousePosition(event); 131 | if( 132 | mousePos.x < 0 || mousePos.x > canvas.width 133 | || mousePos.y < 0 || mousePos.y > canvas.height 134 | ) return; 135 | 136 | draw(mousePos); 137 | }); 138 | 139 | draw({ x: canvas.width / 2, y: canvas.height / 2 }); 140 | } -------------------------------------------------------------------------------- /assets/demos/demo12.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo12'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const circles = [ 11 | { x: 100, y: 50, radius: 30 }, 12 | { x: 300, y: 150, radius: 50 }, 13 | { x: 400, y: 200, radius: 30 }, 14 | ]; 15 | 16 | function getMousePosition(event) { 17 | const rect = canvas.getBoundingClientRect(); 18 | const scaleX = canvas.width / rect.width; 19 | const scaleY = canvas.height / rect.height; 20 | 21 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 22 | } 23 | 24 | function getTangents(rayAnchor, circle) { 25 | const A = { x: rayAnchor.x - circle.x, y: rayAnchor.y - circle.y }; 26 | const r = circle.radius; 27 | 28 | const result = []; 29 | 30 | if(A.x !== 0 && A.y !== 0) { 31 | const a = A.x ** 2 + A.y ** 2; 32 | const b = -2 * r ** 2 * A.y; 33 | const c = r ** 4 - r ** 2 * A.x ** 2; 34 | const discriminant = b ** 2 - 4 * a * c; 35 | 36 | if(discriminant === 0) { 37 | const y = -b / 2 * A; 38 | const x = (r ** 2 - y * A.y) / A.x; 39 | 40 | result.push({ x: x + circle.x, y: y + circle.y }); 41 | } else if(discriminant > 0) { 42 | const discriminantSqrt = Math.sqrt(discriminant); 43 | const y1 = (-b + discriminantSqrt) / (2 * a); 44 | const y2 = (-b - discriminantSqrt) / (2 * a); 45 | const x1 = (r ** 2 - y1 * A.y) / A.x; 46 | const x2 = (r ** 2 - y2 * A.y) / A.x; 47 | 48 | result.push({ x: x1 + circle.x, y: y1 + circle.y }); 49 | result.push({ x: x2 + circle.x, y: y2 + circle.y }); 50 | } 51 | } else if(A.x === 0 && A.y !== 0) { 52 | const a = A.y ** 2; 53 | const b = 0; 54 | const c = r ** 4 - r ** 2 * A.y ** 2; 55 | const discriminant = b ** 2 - 4 * a * c; 56 | 57 | if(discriminant === 0) { 58 | const x = -b / 2 * A; 59 | const y = r ** 2 / A.y; 60 | 61 | result.push({ x: x + circle.x, y: y + circle.y }); 62 | } else if(discriminant > 0) { 63 | const discriminantSqrt = Math.sqrt(discriminant); 64 | const x1 = (-b + discriminantSqrt) / (2 * a); 65 | const x2 = (-b - discriminantSqrt) / (2 * a); 66 | const y1 = r ** 2 / A.y; 67 | const y2 = r ** 2 / A.y; 68 | 69 | result.push({ x: x1 + circle.x, y: y1 + circle.y }); 70 | result.push({ x: x2 + circle.x, y: y2 + circle.y }); 71 | } 72 | } else if(A.x !== 0 && A.y === 0) { 73 | const a = A.x ** 2; 74 | const b = 0; 75 | const c = r ** 4 - r ** 2 * A.x ** 2; 76 | const discriminant = b ** 2 - 4 * a * c; 77 | 78 | if(discriminant === 0) { 79 | const y = -b / 2 * A; 80 | const x = r ** 2 / A.x; 81 | 82 | result.push({ x: x + circle.x, y: y + circle.y }); 83 | } else if(discriminant > 0) { 84 | const discriminantSqrt = Math.sqrt(discriminant); 85 | const y1 = (-b + discriminantSqrt) / (2 * a); 86 | const y2 = (-b - discriminantSqrt) / (2 * a); 87 | const x1 = r ** 2 / A.x; 88 | const x2 = r ** 2 / A.x; 89 | 90 | result.push({ x: x1 + circle.x, y: y1 + circle.y }); 91 | result.push({ x: x2 + circle.x, y: y2 + circle.y }); 92 | } 93 | } 94 | 95 | return result; 96 | } 97 | 98 | function getAllTangents(rayAnchor, circles) { 99 | return circles.flatMap(circle => getTangents(rayAnchor, circle)); 100 | } 101 | 102 | function clearCanvas() { 103 | ctx.fillStyle = 'white'; 104 | ctx.fillRect(0, 0, canvas.width, canvas.height); 105 | } 106 | 107 | function drawCircles(circles) { 108 | ctx.strokeStyle = 'black'; 109 | circles.forEach(circle => { 110 | ctx.beginPath(); 111 | ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI); 112 | ctx.stroke(); 113 | }); 114 | } 115 | 116 | function drawTangentsAndPoints(anchorPoint, circles) { 117 | ctx.fillStyle = 'red'; 118 | ctx.strokeStyle = 'blue'; 119 | getAllTangents(anchorPoint, circles).forEach(point => { 120 | ctx.beginPath(); 121 | ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI); 122 | ctx.fill(); 123 | 124 | ctx.beginPath(); 125 | ctx.moveTo(anchorPoint.x, anchorPoint.y); 126 | ctx.lineTo(point.x, point.y); 127 | ctx.stroke(); 128 | }); 129 | } 130 | 131 | function draw(mousePos) { 132 | clearCanvas(); 133 | drawCircles(circles); 134 | drawTangentsAndPoints(mousePos, circles); 135 | } 136 | 137 | window.addEventListener('mousemove', event => { 138 | const mousePos = getMousePosition(event); 139 | if( 140 | mousePos.x < 0 || mousePos.x > canvas.width 141 | || mousePos.y < 0 || mousePos.y > canvas.height 142 | ) return; 143 | 144 | draw(mousePos); 145 | }); 146 | 147 | draw({ x: canvas.width, y: canvas.height / 2 }); 148 | } -------------------------------------------------------------------------------- /assets/demos/demo5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo5'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const angleOffset = 12; 11 | 12 | const lineSegments = [ 13 | [{ x: 0, y: 0 }, { x: 600, y: 0 }], 14 | [{ x: 600, y: 0 }, { x: 600, y: 300 }], 15 | [{ x: 600, y: 300 }, { x: 0, y: 300 }], 16 | [{ x: 0, y: 300 }, { x: 0, y: 0 }], 17 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 18 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 19 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 20 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 21 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 22 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 23 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 24 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 25 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 26 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 27 | ]; 28 | 29 | function getMousePosition(event) { 30 | const rect = canvas.getBoundingClientRect(); 31 | const scaleX = canvas.width / rect.width; 32 | const scaleY = canvas.height / rect.height; 33 | 34 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 35 | } 36 | 37 | function getIntersectionPoint(ray, segment, smallestR) { 38 | const [A, B] = segment; 39 | const [C, D] = ray; 40 | 41 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 42 | 43 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 44 | if(r < 0) return null; 45 | if(smallestR !== null && smallestR < r) return null; 46 | 47 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 48 | if(s < 0 || s > 1) return null; 49 | 50 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y, r }; 51 | } 52 | 53 | function getClosestIntersectionPoint(ray, segments) { 54 | return segments.reduce((closest, segment) => { 55 | return getIntersectionPoint(ray, segment, closest ? closest.r : null) || closest; 56 | }, null); 57 | } 58 | 59 | function sortIntersectionPointsByAngle(anchor, points) { 60 | return points.sort((P1, P2) => Math.atan2(P1.y - anchor.y, P1.x - anchor.x) - Math.atan2(P2.y - anchor.y, P2.x - anchor.x)); 61 | } 62 | 63 | function getAngleOffsetPoint(point, angle) { 64 | const dist = 1; 65 | return { 66 | x: point.x + dist * Math.sin(Math.PI / 180 * angle), 67 | y: point.y - dist * Math.cos(Math.PI / 180 * angle), 68 | }; 69 | } 70 | 71 | function clearCanvas() { 72 | ctx.fillStyle = 'lightgrey'; 73 | ctx.fillRect(0, 0, canvas.width, canvas.height); 74 | } 75 | 76 | function drawSegments(segments) { 77 | ctx.strokeStyle = 'black'; 78 | segments.forEach(segment => { 79 | ctx.beginPath(); 80 | ctx.moveTo(segment[0].x, segment[0].y); 81 | ctx.lineTo(segment[1].x, segment[1].y); 82 | ctx.stroke(); 83 | }); 84 | } 85 | 86 | function drawRay(ray) { 87 | ctx.strokeStyle = 'blue'; 88 | ctx.beginPath(); 89 | 90 | ctx.moveTo(ray[0].x, ray[0].y); 91 | ctx.lineTo(ray[1].x, ray[1].y); 92 | ctx.stroke(); 93 | } 94 | 95 | function drawClosestIntersectionPoint(closestPoint) { 96 | if(closestPoint !== null) { 97 | ctx.fillStyle = 'red'; 98 | ctx.beginPath(); 99 | ctx.arc(closestPoint.x, closestPoint.y, 5, 0, 2 * Math.PI); 100 | ctx.fill(); 101 | } 102 | } 103 | 104 | function drawVisibleArea(sortedIntersectionPoints) { 105 | ctx.fillStyle = 'white'; 106 | ctx.beginPath(); 107 | ctx.moveTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 108 | sortedIntersectionPoints.slice(1).forEach(point => { 109 | ctx.lineTo(point.x, point.y); 110 | }); 111 | ctx.lineTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 112 | ctx.fill(); 113 | } 114 | 115 | function draw(mousePos) { 116 | clearCanvas(); 117 | 118 | const intersectionPoints = []; 119 | for(let angle = 0; angle < 360; angle += angleOffset) { 120 | const offsetPoint = getAngleOffsetPoint(mousePos, angle); 121 | const closestPoint = getClosestIntersectionPoint([mousePos, offsetPoint], lineSegments); 122 | 123 | if(closestPoint !== null) intersectionPoints.push(closestPoint); 124 | } 125 | 126 | const sortedIntersectionPoints = sortIntersectionPointsByAngle(mousePos, intersectionPoints); 127 | 128 | drawVisibleArea(sortedIntersectionPoints); 129 | drawSegments(lineSegments); 130 | intersectionPoints.forEach(intersectionPoint => { 131 | drawRay([mousePos, intersectionPoint]); 132 | drawClosestIntersectionPoint(intersectionPoint); 133 | }); 134 | } 135 | 136 | window.addEventListener('mousemove', event => { 137 | const mousePos = getMousePosition(event); 138 | if( 139 | mousePos.x < 0 || mousePos.x > canvas.width 140 | || mousePos.y < 0 || mousePos.y > canvas.height 141 | ) return; 142 | 143 | draw(mousePos); 144 | }); 145 | 146 | draw({ x: canvas.width / 2, y: canvas.height / 2 }); 147 | } -------------------------------------------------------------------------------- /assets/demos/demo7.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo7'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const mainAngleOffset = 12; 11 | const angleOffset = 0.00001; 12 | 13 | const lineSegments = [ 14 | [{ x: 0, y: 0 }, { x: 600, y: 0 }], 15 | [{ x: 600, y: 0 }, { x: 600, y: 300 }], 16 | [{ x: 600, y: 300 }, { x: 0, y: 300 }], 17 | [{ x: 0, y: 300 }, { x: 0, y: 0 }], 18 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 19 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 20 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 21 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 22 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 23 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 24 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 25 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 26 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 27 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 28 | ]; 29 | 30 | function getMousePosition(event) { 31 | const rect = canvas.getBoundingClientRect(); 32 | const scaleX = canvas.width / rect.width; 33 | const scaleY = canvas.height / rect.height; 34 | 35 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 36 | } 37 | 38 | function getIntersectionPoint(ray, segment, smallestR) { 39 | const [A, B] = segment; 40 | const [C, D] = ray; 41 | 42 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 43 | 44 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 45 | if(r < 0) return null; 46 | if(smallestR !== null && smallestR < r) return null; 47 | 48 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 49 | if(s < 0 || s > 1) return null; 50 | 51 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y, r }; 52 | } 53 | 54 | function getClosestIntersectionPoint(ray, segments) { 55 | return segments.reduce((closest, segment) => { 56 | return getIntersectionPoint(ray, segment, closest ? closest.r : null) || closest; 57 | }, null); 58 | } 59 | 60 | function sortIntersectionPointsByAngle(anchor, points) { 61 | return points.sort((P1, P2) => Math.atan2(P1.y - anchor.y, P1.x - anchor.x) - Math.atan2(P2.y - anchor.y, P2.x - anchor.x)); 62 | } 63 | 64 | function getAngleOffsetPoint(point, angle) { 65 | const dist = 1; 66 | return { 67 | x: point.x + dist * Math.sin(Math.PI / 180 * angle), 68 | y: point.y - dist * Math.cos(Math.PI / 180 * angle), 69 | }; 70 | } 71 | 72 | function clearCanvas() { 73 | ctx.fillStyle = 'lightgrey'; 74 | ctx.fillRect(0, 0, canvas.width, canvas.height); 75 | } 76 | 77 | function drawSegments(segments) { 78 | ctx.strokeStyle = 'black'; 79 | segments.forEach(segment => { 80 | ctx.beginPath(); 81 | ctx.moveTo(segment[0].x, segment[0].y); 82 | ctx.lineTo(segment[1].x, segment[1].y); 83 | ctx.stroke(); 84 | }); 85 | } 86 | 87 | function drawRay(ray) { 88 | ctx.strokeStyle = 'blue'; 89 | ctx.beginPath(); 90 | 91 | ctx.moveTo(ray[0].x, ray[0].y); 92 | ctx.lineTo(ray[1].x, ray[1].y); 93 | ctx.stroke(); 94 | } 95 | 96 | function drawExtraRay(ray) { 97 | ctx.strokeStyle = 'red'; 98 | ctx.beginPath(); 99 | 100 | ctx.moveTo(ray[0].x, ray[0].y); 101 | ctx.lineTo(ray[1].x, ray[1].y); 102 | ctx.stroke(); 103 | } 104 | 105 | function drawClosestIntersectionPoint(closestPoint) { 106 | if(closestPoint !== null) { 107 | ctx.fillStyle = 'red'; 108 | ctx.beginPath(); 109 | ctx.arc(closestPoint.x, closestPoint.y, 5, 0, 2 * Math.PI); 110 | ctx.fill(); 111 | } 112 | } 113 | 114 | function drawVisibleArea(sortedIntersectionPoints) { 115 | ctx.fillStyle = 'white'; 116 | ctx.beginPath(); 117 | ctx.moveTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 118 | sortedIntersectionPoints.slice(1).forEach(point => { 119 | ctx.lineTo(point.x, point.y); 120 | }); 121 | ctx.lineTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 122 | ctx.fill(); 123 | } 124 | 125 | function draw(mousePos) { 126 | clearCanvas(); 127 | 128 | const intersectionPoints = []; 129 | const extraIntersectionPoints = []; // Hold those points separately for better visualization 130 | for(let angle = 0; angle < 360; angle += mainAngleOffset) { 131 | const offsetPoint = getAngleOffsetPoint(mousePos, angle); 132 | const extraOffsetPoint1 = getAngleOffsetPoint(mousePos, angle - angleOffset); 133 | const extraOffsetPoint2 = getAngleOffsetPoint(mousePos, angle + angleOffset); 134 | const closestPoint = getClosestIntersectionPoint([mousePos, offsetPoint], lineSegments); 135 | const extraClosestPoint1 = getClosestIntersectionPoint([mousePos, extraOffsetPoint1], lineSegments); 136 | const extraClosestPoint2 = getClosestIntersectionPoint([mousePos, extraOffsetPoint2], lineSegments); 137 | 138 | if(closestPoint !== null) intersectionPoints.push(closestPoint); 139 | if(extraClosestPoint1 !== null) extraIntersectionPoints.push(extraClosestPoint1); 140 | if(extraClosestPoint2 !== null) extraIntersectionPoints.push(extraClosestPoint2); 141 | } 142 | 143 | const sortedIntersectionPoints = sortIntersectionPointsByAngle(mousePos, [...intersectionPoints, ...extraIntersectionPoints]); 144 | 145 | drawVisibleArea(sortedIntersectionPoints); 146 | drawSegments(lineSegments); 147 | extraIntersectionPoints.forEach(intersectionPoint => { 148 | drawExtraRay([mousePos, intersectionPoint]); 149 | drawClosestIntersectionPoint(intersectionPoint); 150 | }); 151 | intersectionPoints.forEach(intersectionPoint => { 152 | drawRay([mousePos, intersectionPoint]); 153 | drawClosestIntersectionPoint(intersectionPoint); 154 | }); 155 | } 156 | 157 | window.addEventListener('mousemove', event => { 158 | const mousePos = getMousePosition(event); 159 | if( 160 | mousePos.x < 0 || mousePos.x > canvas.width 161 | || mousePos.y < 0 || mousePos.y > canvas.height 162 | ) return; 163 | 164 | draw(mousePos); 165 | }); 166 | 167 | draw({ x: canvas.width / 2, y: canvas.height / 2 }); 168 | } -------------------------------------------------------------------------------- /assets/demos/demo8.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo8'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const angleOffset = 0.00001; 11 | 12 | const lineSegments = [ 13 | [{ x: 0, y: 0 }, { x: 600, y: 0 }], 14 | [{ x: 600, y: 0 }, { x: 600, y: 300 }], 15 | [{ x: 600, y: 300 }, { x: 0, y: 300 }], 16 | [{ x: 0, y: 300 }, { x: 0, y: 0 }], 17 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 18 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 19 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 20 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 21 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 22 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 23 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 24 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 25 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 26 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 27 | ]; 28 | 29 | // In production code, you might want to filter only unique vertices 30 | const vertices = lineSegments.reduce((vertices, segment) => { 31 | return [...vertices, ...segment]; 32 | }, []); 33 | 34 | function getMousePosition(event) { 35 | const rect = canvas.getBoundingClientRect(); 36 | const scaleX = canvas.width / rect.width; 37 | const scaleY = canvas.height / rect.height; 38 | 39 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 40 | } 41 | 42 | function getIntersectionPoint(ray, segment, smallestR) { 43 | const [A, B] = segment; 44 | const [C, D] = ray; 45 | 46 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 47 | 48 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 49 | if(r < 0) return null; 50 | if(smallestR !== null && smallestR < r) return null; 51 | 52 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 53 | if(s < 0 || s > 1) return null; 54 | 55 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y, r }; 56 | } 57 | 58 | function getClosestIntersectionPoint(ray, segments) { 59 | return segments.reduce((closest, segment) => { 60 | return getIntersectionPoint(ray, segment, closest ? closest.r : null) || closest; 61 | }, null); 62 | } 63 | 64 | function sortIntersectionPointsByAngle(anchor, points) { 65 | return points.sort((P1, P2) => Math.atan2(P1.y - anchor.y, P1.x - anchor.x) - Math.atan2(P2.y - anchor.y, P2.x - anchor.x)); 66 | } 67 | 68 | function getOffsettedRayPoint(ray, angle) { 69 | return { 70 | x: (ray[1].x - ray[0].x) * Math.cos(angle) - (ray[1].y - ray[0].y) * Math.sin(angle) + ray[0].x, 71 | y: (ray[1].y - ray[0].y) * Math.cos(angle) + (ray[1].x - ray[0].x) * Math.sin(angle) + ray[0].y, 72 | }; 73 | } 74 | 75 | function clearCanvas() { 76 | ctx.fillStyle = 'lightgrey'; 77 | ctx.fillRect(0, 0, canvas.width, canvas.height); 78 | } 79 | 80 | function drawSegments(segments) { 81 | ctx.strokeStyle = 'black'; 82 | segments.forEach(segment => { 83 | ctx.beginPath(); 84 | ctx.moveTo(segment[0].x, segment[0].y); 85 | ctx.lineTo(segment[1].x, segment[1].y); 86 | ctx.stroke(); 87 | }); 88 | } 89 | 90 | function drawRay(ray) { 91 | ctx.strokeStyle = 'blue'; 92 | ctx.beginPath(); 93 | 94 | ctx.moveTo(ray[0].x, ray[0].y); 95 | ctx.lineTo(ray[1].x, ray[1].y); 96 | ctx.stroke(); 97 | } 98 | 99 | function drawExtraRay(ray) { 100 | ctx.strokeStyle = 'red'; 101 | ctx.beginPath(); 102 | 103 | ctx.moveTo(ray[0].x, ray[0].y); 104 | ctx.lineTo(ray[1].x, ray[1].y); 105 | ctx.stroke(); 106 | } 107 | 108 | function drawClosestIntersectionPoint(closestPoint) { 109 | if(closestPoint !== null) { 110 | ctx.fillStyle = 'red'; 111 | ctx.beginPath(); 112 | ctx.arc(closestPoint.x, closestPoint.y, 5, 0, 2 * Math.PI); 113 | ctx.fill(); 114 | } 115 | } 116 | 117 | function drawVisibleArea(sortedIntersectionPoints) { 118 | ctx.fillStyle = 'white'; 119 | ctx.beginPath(); 120 | ctx.moveTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 121 | sortedIntersectionPoints.slice(1).forEach(point => { 122 | ctx.lineTo(point.x, point.y); 123 | }); 124 | ctx.lineTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 125 | ctx.fill(); 126 | } 127 | 128 | function draw(mousePos) { 129 | clearCanvas(); 130 | 131 | const intersectionPoints = []; 132 | const extraIntersectionPoints = []; // Hold those points separately for better visualization 133 | vertices.forEach(vertex => { 134 | const extraOffsetPoint1 = getOffsettedRayPoint([mousePos, vertex], -angleOffset); 135 | const extraOffsetPoint2 = getOffsettedRayPoint([mousePos, vertex], angleOffset); 136 | const closestPoint = getClosestIntersectionPoint([mousePos, vertex], lineSegments); 137 | const extraClosestPoint1 = getClosestIntersectionPoint([mousePos, extraOffsetPoint1], lineSegments); 138 | const extraClosestPoint2 = getClosestIntersectionPoint([mousePos, extraOffsetPoint2], lineSegments); 139 | 140 | if(closestPoint !== null) intersectionPoints.push(closestPoint); 141 | if(extraClosestPoint1 !== null) extraIntersectionPoints.push(extraClosestPoint1); 142 | if(extraClosestPoint2 !== null) extraIntersectionPoints.push(extraClosestPoint2); 143 | }); 144 | 145 | const sortedIntersectionPoints = sortIntersectionPointsByAngle(mousePos, [...intersectionPoints, ...extraIntersectionPoints]); 146 | 147 | drawVisibleArea(sortedIntersectionPoints); 148 | drawSegments(lineSegments); 149 | extraIntersectionPoints.forEach(intersectionPoint => { 150 | drawExtraRay([mousePos, intersectionPoint]); 151 | drawClosestIntersectionPoint(intersectionPoint); 152 | }); 153 | intersectionPoints.forEach(intersectionPoint => { 154 | drawRay([mousePos, intersectionPoint]); 155 | drawClosestIntersectionPoint(intersectionPoint); 156 | }); 157 | } 158 | 159 | window.addEventListener('mousemove', event => { 160 | const mousePos = getMousePosition(event); 161 | if( 162 | mousePos.x < 0 || mousePos.x > canvas.width 163 | || mousePos.y < 0 || mousePos.y > canvas.height 164 | ) return; 165 | 166 | draw(mousePos); 167 | }); 168 | 169 | draw({ x: canvas.width / 2, y: canvas.height / 2 }); 170 | } -------------------------------------------------------------------------------- /assets/demos/demo9.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo9'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const angleOffset = 0.00001; 11 | 12 | const visibilityRadius = 100; 13 | 14 | const lineSegments = [ 15 | [{ x: 0, y: 0 }, { x: 600, y: 0 }], 16 | [{ x: 600, y: 0 }, { x: 600, y: 300 }], 17 | [{ x: 600, y: 300 }, { x: 0, y: 300 }], 18 | [{ x: 0, y: 300 }, { x: 0, y: 0 }], 19 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 20 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 21 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 22 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 23 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 24 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 25 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 26 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 27 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 28 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 29 | ]; 30 | 31 | // In production code, you might want to filter only unique vertices 32 | const vertices = lineSegments.reduce((vertices, segment) => { 33 | return [...vertices, ...segment]; 34 | }, []); 35 | 36 | function getMousePosition(event) { 37 | const rect = canvas.getBoundingClientRect(); 38 | const scaleX = canvas.width / rect.width; 39 | const scaleY = canvas.height / rect.height; 40 | 41 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 42 | } 43 | 44 | function getIntersectionPoint(ray, segment, smallestR) { 45 | const [A, B] = segment; 46 | const [C, D] = ray; 47 | 48 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 49 | 50 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 51 | if(r < 0) return null; 52 | if(smallestR !== null && smallestR < r) return null; 53 | 54 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 55 | if(s < 0 || s > 1) return null; 56 | 57 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y, r }; 58 | } 59 | 60 | function getClosestIntersectionPoint(ray, segments) { 61 | return segments.reduce((closest, segment) => { 62 | return getIntersectionPoint(ray, segment, closest ? closest.r : null) || closest; 63 | }, null); 64 | } 65 | 66 | function sortIntersectionPointsByAngle(anchor, points) { 67 | return points.sort((P1, P2) => Math.atan2(P1.y - anchor.y, P1.x - anchor.x) - Math.atan2(P2.y - anchor.y, P2.x - anchor.x)); 68 | } 69 | 70 | function getOffsettedRayPoint(ray, angle) { 71 | return { 72 | x: (ray[1].x - ray[0].x) * Math.cos(angle) - (ray[1].y - ray[0].y) * Math.sin(angle) + ray[0].x, 73 | y: (ray[1].y - ray[0].y) * Math.cos(angle) + (ray[1].x - ray[0].x) * Math.sin(angle) + ray[0].y, 74 | }; 75 | } 76 | 77 | function clearCanvas() { 78 | ctx.fillStyle = 'lightgrey'; 79 | ctx.fillRect(0, 0, canvas.width, canvas.height); 80 | } 81 | 82 | function drawSegments(segments) { 83 | ctx.strokeStyle = 'black'; 84 | segments.forEach(segment => { 85 | ctx.beginPath(); 86 | ctx.moveTo(segment[0].x, segment[0].y); 87 | ctx.lineTo(segment[1].x, segment[1].y); 88 | ctx.stroke(); 89 | }); 90 | } 91 | 92 | function drawRay(ray) { 93 | ctx.strokeStyle = 'blue'; 94 | ctx.beginPath(); 95 | 96 | ctx.moveTo(ray[0].x, ray[0].y); 97 | ctx.lineTo(ray[1].x, ray[1].y); 98 | ctx.stroke(); 99 | } 100 | 101 | function drawExtraRay(ray) { 102 | ctx.strokeStyle = 'red'; 103 | ctx.beginPath(); 104 | 105 | ctx.moveTo(ray[0].x, ray[0].y); 106 | ctx.lineTo(ray[1].x, ray[1].y); 107 | ctx.stroke(); 108 | } 109 | 110 | function drawClosestIntersectionPoint(closestPoint) { 111 | if(closestPoint !== null) { 112 | ctx.fillStyle = 'red'; 113 | ctx.beginPath(); 114 | ctx.arc(closestPoint.x, closestPoint.y, 5, 0, 2 * Math.PI); 115 | ctx.fill(); 116 | } 117 | } 118 | 119 | function drawVisibleArea(sortedIntersectionPoints) { 120 | ctx.fillStyle = 'white'; 121 | ctx.beginPath(); 122 | ctx.moveTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 123 | sortedIntersectionPoints.slice(1).forEach(point => { 124 | ctx.lineTo(point.x, point.y); 125 | }); 126 | ctx.lineTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 127 | ctx.fill(); 128 | } 129 | 130 | function draw(mousePos) { 131 | clearCanvas(); 132 | 133 | const intersectionPoints = []; 134 | const extraIntersectionPoints = []; // Hold those points separately for better visualization 135 | vertices.forEach(vertex => { 136 | const extraOffsetPoint1 = getOffsettedRayPoint([mousePos, vertex], -angleOffset); 137 | const extraOffsetPoint2 = getOffsettedRayPoint([mousePos, vertex], angleOffset); 138 | const closestPoint = getClosestIntersectionPoint([mousePos, vertex], lineSegments); 139 | const extraClosestPoint1 = getClosestIntersectionPoint([mousePos, extraOffsetPoint1], lineSegments); 140 | const extraClosestPoint2 = getClosestIntersectionPoint([mousePos, extraOffsetPoint2], lineSegments); 141 | 142 | if(closestPoint !== null) intersectionPoints.push(closestPoint); 143 | if(extraClosestPoint1 !== null) extraIntersectionPoints.push(extraClosestPoint1); 144 | if(extraClosestPoint2 !== null) extraIntersectionPoints.push(extraClosestPoint2); 145 | }); 146 | 147 | const sortedIntersectionPoints = sortIntersectionPointsByAngle(mousePos, [...intersectionPoints, ...extraIntersectionPoints]); 148 | 149 | ctx.save(); 150 | ctx.beginPath(); 151 | ctx.arc(mousePos.x, mousePos.y, visibilityRadius, 0, 2 * Math.PI); 152 | ctx.clip(); 153 | drawVisibleArea(sortedIntersectionPoints); 154 | ctx.restore(); 155 | 156 | drawSegments(lineSegments); 157 | extraIntersectionPoints.forEach(intersectionPoint => { 158 | drawExtraRay([mousePos, intersectionPoint]); 159 | drawClosestIntersectionPoint(intersectionPoint); 160 | }); 161 | intersectionPoints.forEach(intersectionPoint => { 162 | drawRay([mousePos, intersectionPoint]); 163 | drawClosestIntersectionPoint(intersectionPoint); 164 | }); 165 | } 166 | 167 | window.addEventListener('mousemove', event => { 168 | const mousePos = getMousePosition(event); 169 | if( 170 | mousePos.x < 0 || mousePos.x > canvas.width 171 | || mousePos.y < 0 || mousePos.y > canvas.height 172 | ) return; 173 | 174 | draw(mousePos); 175 | }); 176 | 177 | draw({ x: canvas.width / 2, y: canvas.height / 2 }); 178 | } -------------------------------------------------------------------------------- /assets/demos/demo10.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const canvas = document.getElementById('demo10'); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | canvas.width = 600; 8 | canvas.height = 300; 9 | 10 | const angleOffset = 0.00001; 11 | 12 | const flashlightDistance = 150; 13 | const flashlightArc = Math.PI / 3; 14 | const flashlightAnchor = { x: 300, y: 150 }; 15 | 16 | const lineSegments = [ 17 | [{ x: 0, y: 0 }, { x: 600, y: 0 }], 18 | [{ x: 600, y: 0 }, { x: 600, y: 300 }], 19 | [{ x: 600, y: 300 }, { x: 0, y: 300 }], 20 | [{ x: 0, y: 300 }, { x: 0, y: 0 }], 21 | [{ x: 200, y: 50 }, { x: 250, y: 50 }], 22 | [{ x: 200, y: 50 }, { x: 200, y: 100 }], 23 | [{ x: 400, y: 50 }, { x: 350, y: 50 }], 24 | [{ x: 400, y: 50 }, { x: 400, y: 100 }], 25 | [{ x: 200, y: 250 }, { x: 250, y: 250 }], 26 | [{ x: 200, y: 250 }, { x: 200, y: 200 }], 27 | [{ x: 400, y: 250 }, { x: 350, y: 250 }], 28 | [{ x: 400, y: 250 }, { x: 400, y: 200 }], 29 | [{ x: 100, y: 50 }, { x: 100, y: 250 }], 30 | [{ x: 500, y: 50 }, { x: 500, y: 250 }], 31 | ]; 32 | 33 | // In production code, you might want to filter only unique vertices 34 | const vertices = lineSegments.reduce((vertices, segment) => { 35 | return [...vertices, ...segment]; 36 | }, []); 37 | 38 | function getMousePosition(event) { 39 | const rect = canvas.getBoundingClientRect(); 40 | const scaleX = canvas.width / rect.width; 41 | const scaleY = canvas.height / rect.height; 42 | 43 | return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; 44 | } 45 | 46 | function getIntersectionPoint(ray, segment, smallestR) { 47 | const [A, B] = segment; 48 | const [C, D] = ray; 49 | 50 | const denominator = (D.x - C.x) * (B.y - A.y) - (B.x - A.x) * (D.y - C.y); 51 | 52 | const r = ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) / denominator; 53 | if(r < 0) return null; 54 | if(smallestR !== null && smallestR < r) return null; 55 | 56 | const s = ((A.x - C.x) * (D.y - C.y) - (D.x - C.x) * (A.y - C.y)) / denominator; 57 | if(s < 0 || s > 1) return null; 58 | 59 | return { x: s * (B.x - A.x) + A.x, y: s * (B.y - A.y) + A.y, r }; 60 | } 61 | 62 | function getClosestIntersectionPoint(ray, segments) { 63 | return segments.reduce((closest, segment) => { 64 | return getIntersectionPoint(ray, segment, closest ? closest.r : null) || closest; 65 | }, null); 66 | } 67 | 68 | function sortIntersectionPointsByAngle(anchor, points) { 69 | return points.sort((P1, P2) => Math.atan2(P1.y - anchor.y, P1.x - anchor.x) - Math.atan2(P2.y - anchor.y, P2.x - anchor.x)); 70 | } 71 | 72 | function getOffsettedRayPoint(ray, angle) { 73 | return { 74 | x: (ray[1].x - ray[0].x) * Math.cos(angle) - (ray[1].y - ray[0].y) * Math.sin(angle) + ray[0].x, 75 | y: (ray[1].y - ray[0].y) * Math.cos(angle) + (ray[1].x - ray[0].x) * Math.sin(angle) + ray[0].y, 76 | }; 77 | } 78 | 79 | function clearCanvas() { 80 | ctx.fillStyle = 'lightgrey'; 81 | ctx.fillRect(0, 0, canvas.width, canvas.height); 82 | } 83 | 84 | function drawSegments(segments) { 85 | ctx.strokeStyle = 'black'; 86 | segments.forEach(segment => { 87 | ctx.beginPath(); 88 | ctx.moveTo(segment[0].x, segment[0].y); 89 | ctx.lineTo(segment[1].x, segment[1].y); 90 | ctx.stroke(); 91 | }); 92 | } 93 | 94 | function drawRay(ray) { 95 | ctx.strokeStyle = 'blue'; 96 | ctx.beginPath(); 97 | 98 | ctx.moveTo(ray[0].x, ray[0].y); 99 | ctx.lineTo(ray[1].x, ray[1].y); 100 | ctx.stroke(); 101 | } 102 | 103 | function drawExtraRay(ray) { 104 | ctx.strokeStyle = 'red'; 105 | ctx.beginPath(); 106 | 107 | ctx.moveTo(ray[0].x, ray[0].y); 108 | ctx.lineTo(ray[1].x, ray[1].y); 109 | ctx.stroke(); 110 | } 111 | 112 | function drawClosestIntersectionPoint(closestPoint) { 113 | if(closestPoint !== null) { 114 | ctx.fillStyle = 'red'; 115 | ctx.beginPath(); 116 | ctx.arc(closestPoint.x, closestPoint.y, 5, 0, 2 * Math.PI); 117 | ctx.fill(); 118 | } 119 | } 120 | 121 | function drawVisibleArea(sortedIntersectionPoints) { 122 | ctx.fillStyle = 'white'; 123 | ctx.beginPath(); 124 | ctx.moveTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 125 | sortedIntersectionPoints.slice(1).forEach(point => { 126 | ctx.lineTo(point.x, point.y); 127 | }); 128 | ctx.lineTo(sortedIntersectionPoints[0].x, sortedIntersectionPoints[0].y); 129 | ctx.fill(); 130 | } 131 | 132 | function draw(mousePos) { 133 | clearCanvas(); 134 | 135 | const intersectionPoints = []; 136 | const extraIntersectionPoints = []; // Hold those points separately for better visualization 137 | vertices.forEach(vertex => { 138 | const extraOffsetPoint1 = getOffsettedRayPoint([flashlightAnchor, vertex], -angleOffset); 139 | const extraOffsetPoint2 = getOffsettedRayPoint([flashlightAnchor, vertex], angleOffset); 140 | const closestPoint = getClosestIntersectionPoint([flashlightAnchor, vertex], lineSegments); 141 | const extraClosestPoint1 = getClosestIntersectionPoint([flashlightAnchor, extraOffsetPoint1], lineSegments); 142 | const extraClosestPoint2 = getClosestIntersectionPoint([flashlightAnchor, extraOffsetPoint2], lineSegments); 143 | 144 | if(closestPoint !== null) intersectionPoints.push(closestPoint); 145 | if(extraClosestPoint1 !== null) extraIntersectionPoints.push(extraClosestPoint1); 146 | if(extraClosestPoint2 !== null) extraIntersectionPoints.push(extraClosestPoint2); 147 | }); 148 | 149 | const sortedIntersectionPoints = sortIntersectionPointsByAngle(flashlightAnchor, [...intersectionPoints, ...extraIntersectionPoints]); 150 | 151 | ctx.save(); 152 | ctx.beginPath(); 153 | ctx.moveTo(300, 150); 154 | ctx.arc(300, 150, flashlightDistance, Math.atan2(mousePos.y - 150, mousePos.x - 300) - flashlightArc / 2, Math.atan2(mousePos.y - 150, mousePos.x - 300) + flashlightArc / 2); 155 | ctx.clip(); 156 | drawVisibleArea(sortedIntersectionPoints); 157 | ctx.restore(); 158 | 159 | drawSegments(lineSegments); 160 | extraIntersectionPoints.forEach(intersectionPoint => { 161 | drawExtraRay([flashlightAnchor, intersectionPoint]); 162 | drawClosestIntersectionPoint(intersectionPoint); 163 | }); 164 | intersectionPoints.forEach(intersectionPoint => { 165 | drawRay([flashlightAnchor, intersectionPoint]); 166 | drawClosestIntersectionPoint(intersectionPoint); 167 | }); 168 | } 169 | 170 | window.addEventListener('mousemove', event => { 171 | const mousePos = getMousePosition(event); 172 | if( 173 | mousePos.x < 0 || mousePos.x > canvas.width 174 | || mousePos.y < 0 || mousePos.y > canvas.height 175 | ) return; 176 | 177 | draw(mousePos); 178 | }); 179 | 180 | draw({ x: canvas.width / 2, y: canvas.height / 2 }); 181 | } -------------------------------------------------------------------------------- /cheatsheet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 30 | 31 | 32 | 33 | 34 | 35 | 46 | 47 | Ray casting in 2D game engines - cheatsheet 48 | 49 | 50 |
51 |

Ray casting in 2D game engines

52 |

ultimate cheatsheet

53 | 54 |
55 |
56 |
57 |

Point-in-polygon problem

58 |

59 | PIP problem asks whether a given point lies inside, outside, or on the boundary of a polygon. Using the ray casting algorithm, we can count how many times the point intersects the edges of the polygon. If the number of the intersections is even, the point is on the outside of the polygon. If the number of the intersections is odd, the point is on the inside or on the boundary of a polygon. 60 |

61 |
62 |
63 |
64 |
65 |

Line-segment - ray intersection point

66 |

67 | Assume that point \(P\) is the intersection point of line-segment defined by points \(A\) and \(B\), and ray defined by points \(C\) and \(D\). Point \(P\) is then expressed by set of two equations: 68 | \[ 69 | \left\{\begin{align} 70 | P &= s(B - A) + A\text {, where } 0 \leq s \leq 1 \text{, and} \\ 71 | P &= r(D - C) + C\text {, where } r \geq 0 \text{.} 72 | \end{align}\right. 73 | \] 74 | 75 | \[ 76 | r = \tfrac{(B_x - A_x)(C_y - A_y) - (C_x - A_x)(B_y - A_y)}{(D_x - C_x)(B_y - A_y) - (B_x - A_x)(D_y - C_y)} 77 | \] 78 | 79 | \[ 80 | s = \tfrac{(A_x - C_x)(D_y - C_y) - (D_x - C_x)(A_y - C_y)}{(D_x - C_x)(B_y - A_y) - (B_x - A_x)(D_y - C_y)} 81 | \] 82 | 83 | Having \(s\) and \(r\) calculated, we can calculate \(P\) using one of the equations from the set. 84 |

85 |
86 |
87 |
88 |
89 |

Illuminating the visible area

90 |
    91 |
  1. Cast rays on all vertices
  2. 92 |
  3. For every cast ray, cast two additional rays offseted by small angle
  4. 93 |
  5. Sort intersection points
  6. 94 |
  7. Draw a visibility polygon by connecting sorted intersection points
  8. 95 |
96 |
Casting slighly offsetted rays
97 |

98 | Consider ray \(AB\) starting at \(A = (A_x, A_y)\) going through \(B = (B_x, B_y)\). We want to find such point \(C = (C_x, C_y)\) that ray \(AC\) would be rotated by \(\phi\) with \(A\) being the origin point. \(C\) coordinates would be as follow: 99 | \[ 100 | \left\{\begin{align} 101 | C_x &= (B_x - A_x)cos(\phi) - (B_y - A_y)\sin(\phi) + A_x \\ 102 | C_y &= (B_y - A_y)cos(\phi) + (B_x - A_x)\sin(\phi) + A_y 103 | \end{align}\right. 104 | \] 105 |

106 |
Sorting intersection points
107 |

108 | \[ 109 | P_1 > P_2 \leftrightarrow atan2(P_{1_y} - A_y, P_{1_x} - A_x) > (P_{2_y} - A_y, P_{2_x} - A_x) 110 | \] 111 |

112 |
113 |
114 |
115 |
116 |

Circle - ray intersection point

117 |

118 | Let \(P\) be an intersection point, \(A\) a ray's anchor point, \(B\) a point on the ray, \(C\) a circle's center point and \(r\) a circle's radius. 119 | 120 | \[ 121 | \left\{\begin{align} 122 | a &= (B_x - A_x) ^ 2 + (B_y - A_y) ^ 2 \\ 123 | b &= 2((B_x - A_x)(A_x - C_x) + (B_y - A_y)(A_y - C_y)) \\ 124 | c &= (A_x - C_x) ^ 2 + (A_y - C_y) ^ 2 - r ^ 2 \\ 125 | \Delta &= b ^ 2 - 4ac 126 | \end{align}\right. 127 | \] 128 |

129 |
    130 |
  • \(\Delta < 0\): ray does not intersect the circle,
  • 131 |
  • \(\Delta = 0 \): ray intersects the circle in one point (tangent),
  • 132 |
  • \(\Delta > 0 \): ray intersects the circle in two points.
  • 133 |
134 |
135 |

136 | Only if \(\Delta = 0\): 137 | 138 | \[ 139 | \begin{align} 140 | t = \tfrac{-b}{2a} 141 | \end{align} 142 | \] 143 | 144 | Only if \(t \geq 0\): 145 | 146 | \[ 147 | \left\{\begin{align} 148 | P_x &= t(B_x - A_x) + A_x \\ 149 | P_y &= t(B_y - A_y) + A_y 150 | \end{align}\right. 151 | \] 152 |

153 |

154 | Only if \(\Delta > 0\): 155 | 156 | \[ 157 | \left\{\begin{align} 158 | t_1 &= \tfrac{-b -\sqrt{\Delta}}{2a} \\ 159 | t_2 &= \tfrac{-b +\sqrt{\Delta}}{2a} \\ 160 | \end{align}\right. 161 | \] 162 | 163 | Only if \(t_i \geq 0\): 164 | 165 | \[ 166 | \left\{\begin{align} 167 | P_{i_x} &= t_i(B_x - A_x) + A_x \\ 168 | P_{i_y} &= t_i(B_y - A_y) + A_y 169 | \end{align}\right. 170 | \] 171 |

172 |
173 |
174 |
175 |
176 |
177 |

Spatial hashmaps

178 |

179 | Using spatial hashmaps allows to divide the play area into smaller cells (of predefined size). Each cell consists of a list containing our shapes (most likely line-segments). If a single shape spans across multiple cells, it will be included in all of them.
180 |
181 | We can use them for things such as implementing viewports, visibility circles and flashlights. 182 |

183 |
184 |
185 |
186 |
187 |

Bresenham-based supercover line algorithm

188 |

189 | Using spatial hashmaps and modified Bresenham's line algorithm, we can traverse the grid in an efficient manner making as few checks as required. The algorithm should stop when the first cell with an intersection point is found.
190 |
191 | You can read more about Bresenham's line algorithm here, and its modified version here. 192 |

193 |
194 |
195 |
196 |
197 |

Casting rays on circles

198 |

199 | Let \(P_i\) be tangent points, \(A\) a ray's anchor point, \(C\) a circle's center point and \(r\) a circle's radius.
200 | We will also be moving all points using translation vector \(\overrightarrow{v} = (-C_x, -C_y)\): \(X^\prime = X + \overrightarrow{v}\), so the circle's center is at \((0,0)\).
201 |
202 | Applies to all cases below: 203 |

204 |
    205 |
  • \(\Delta < 0\): there are no tangent points (ray's anchor point is in the circle),
  • 206 |
  • \(\Delta = 0 \): there is only one tangent point (ray's anchor point),
  • 207 |
  • \(\Delta > 0 \): there are two tangent points.
  • 208 |
209 |
210 |
211 |

212 | Case #1 (\(A_x ^ \prime \neq 0 \land A_y ^ \prime \neq 0\)):
213 | \[ 214 | \left\{\begin{align} 215 | a &= A_x ^{\prime ^ 2} + A_y ^{\prime ^ 2} \\ 216 | b &= - 2r ^ 2 A_y ^ \prime \\ 217 | c &= r ^ 4 - r ^ 2 A_x ^ {\prime ^ 2} \\ 218 | \Delta &= b ^ 2 - 4ac 219 | \end{align}\right. 220 | \] 221 |

222 |
223 |

224 | Only if \(\Delta = 0\): 225 | 226 | \[ 227 | \left\{\begin{align} 228 | P_y ^ \prime &= \tfrac{-b}{2a} \\ 229 | P_x ^ \prime &= \tfrac{r ^ 2 - P_y ^ \prime A_y ^ \prime }{A_x ^ \prime} 230 | \end{align}\right. 231 | \] 232 | 233 | \[ 234 | \left\{\begin{align} 235 | P_x &= P_x ^ \prime + C_x \\ 236 | P_y &= P_y ^ \prime + C_y 237 | \end{align}\right. 238 | \] 239 |

240 |

241 | Only if \(\Delta > 0\): 242 | 243 | \[ 244 | \left\{\begin{align} 245 | P_{1_y} ^ \prime &= \tfrac{-b -\sqrt{\Delta}}{2a} \\ 246 | P_{2_y} ^ \prime &= \tfrac{-b +\sqrt{\Delta}}{2a} \\ 247 | P_{i_x} ^ \prime &= \tfrac{r ^ 2 - P_{i_y} ^ \prime A_y ^ \prime }{A_x ^ \prime} 248 | \end{align}\right. 249 | \] 250 | 251 | \[ 252 | \left\{\begin{align} 253 | P_{i_x} &= P_{i_x} ^ \prime + C_x \\ 254 | P_{i_y} &= P_{i_y} ^ \prime + C_y 255 | \end{align}\right. 256 | \] 257 |

258 |
259 |
260 |
261 |

262 | Case #2 (\(A_x ^ \prime = 0 \land A_y ^ \prime \neq 0\)):
263 | \[ 264 | \left\{\begin{align} 265 | a &= A_y ^ {\prime ^ 2} \\ 266 | b &= 0 \\ 267 | c &= r ^ 4 - r ^ 2 A_y ^ {\prime ^ 2} \\ 268 | \Delta &= b ^ 2 - 4ac 269 | \end{align}\right. 270 | \] 271 |

272 |
273 |

274 | Only if \(\Delta = 0\): 275 | 276 | \[ 277 | \left\{\begin{align} 278 | P_x ^ \prime &= \tfrac{-b}{2a} \\ 279 | P_y ^ \prime &= \tfrac{r ^ 2}{A_y ^ \prime} 280 | \end{align}\right. 281 | \] 282 | 283 | \[ 284 | \left\{\begin{align} 285 | P_x &= P_x ^ \prime + C_x \\ 286 | P_y &= P_y ^ \prime + C_y 287 | \end{align}\right. 288 | \] 289 |

290 |

291 | Only if \(\Delta > 0\): 292 | 293 | \[ 294 | \left\{\begin{align} 295 | P_{1_x} ^ \prime &= \tfrac{-b -\sqrt{\Delta}}{2a} \\ 296 | P_{2_x} ^ \prime &= \tfrac{-b +\sqrt{\Delta}}{2a} \\ 297 | P_{i_y} ^ \prime &= \tfrac{r ^ 2}{A_y ^ \prime} 298 | \end{align}\right. 299 | \] 300 | 301 | \[ 302 | \left\{\begin{align} 303 | P_{i_x} &= P_{i_x} ^ \prime + C_x \\ 304 | P_{i_y} &= P_{i_y} ^ \prime + C_y 305 | \end{align}\right. 306 | \] 307 |

308 |
309 |
310 |
311 |

312 | Case #3 (\(A_x ^ \prime \neq 0 \land A_y ^ \prime = 0\)):
313 | \[ 314 | \left\{\begin{align} 315 | a &= A_x ^ {\prime ^ 2} \\ 316 | b &= 0 \\ 317 | c &= r ^ 4 - r ^ 2 A_x ^ {\prime ^ 2} \\ 318 | \Delta &= b ^ 2 - 4ac 319 | \end{align}\right. 320 | \] 321 |

322 |
323 |

324 | Only if \(\Delta = 0\): 325 | 326 | \[ 327 | \left\{\begin{align} 328 | P_y ^ \prime &= \tfrac{-b}{2a} \\ 329 | P_x ^ \prime &= \tfrac{r ^ 2}{A_x ^ \prime} 330 | \end{align}\right. 331 | \] 332 | 333 | \[ 334 | \left\{\begin{align} 335 | P_x &= P_x ^ \prime + C_x \\ 336 | P_y &= P_y ^ \prime + C_y 337 | \end{align}\right. 338 | \] 339 |

340 |

341 | Only if \(\Delta > 0\): 342 | 343 | \[ 344 | \left\{\begin{align} 345 | P_{1_y} ^ \prime &= \tfrac{-b -\sqrt{\Delta}}{2a} \\ 346 | P_{2_y} ^ \prime &= \tfrac{-b +\sqrt{\Delta}}{2a} \\ 347 | P_{i_x} ^ \prime &= \tfrac{r ^ 2}{A_x ^ \prime} 348 | \end{align}\right. 349 | \] 350 | 351 | \[ 352 | \left\{\begin{align} 353 | P_{i_x} &= P_{i_x} ^ \prime + C_x \\ 354 | P_{i_y} &= P_{i_y} ^ \prime + C_y 355 | \end{align}\right. 356 | \] 357 |

358 |
359 |
360 |
361 |

362 | Case #4 (\(A_x ^ \prime = A_y ^ \prime = 0\)):
363 |
364 | No tangent points (\(A = C\)). 365 |

366 |
367 |
368 |
369 |
370 |
371 |
372 | 373 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Ray casting in 2D game engines 38 | 39 | 40 |
41 |

Ray casting in 2D game engines

42 |
43 | 90 |
91 |

Introduction

92 |

93 | In my opinion, ray casting is a beautiful concept that is not that hard to grasp, but the quality resources are rare. We will learn the math behind it so you can implement it in your future projects with ease. I will try to make it as comprehensible as possible, explain all the caveats and issues you may stumble upon. We will also talk about optimization and how can spatial hash maps significantly help you. I will also provide some basic live examples for you to try out. Please note that demos were written to be as simple as possible, do not expect enterprise-grade code - we only learn about the concept, not the implementation. 94 |

95 | 96 |

What is ray casting?

97 |
98 |
99 |

100 | Ray casting is the most basic of many computer graphics rendering algorithms that use the geometric algorithm of ray tracing. Ray tracing-based rendering algorithms operate in image order to render three-dimensional scenes to two-dimensional images... 101 | The idea behind ray casting is to trace rays from the eye, one per pixel, and find the closest object blocking the path of that ray – think of an image as a screen-door, with each square in the screen being a pixel. This is then the object the eye sees through that pixel. 102 |

103 |
104 | 107 |
108 |

109 | Well, it does not tell us much, does it? Let me simplify that. Ray casting is a fundamental and popular technique used to determine the visibility of specific objects (polygons) by tracing rays from the eye (for instance, player’s hero) on every pixel (well, not quite in our case - more on that later) and finding the nearest intersections with objects. 110 |

111 | 112 |

Where might ray casting be used?

113 |

114 | Ray casting has many uses, especially in three-dimensional space. I outlined three, in my opinion, the most important, which are commonly used in 2D game engines: 115 |

116 | 117 |

Creating 3D perspective in a 2D map

118 |

119 | The most well-known game that used this technique is Wolfenstein 3D. Rays were traced to determine the closest objects, and their distance from the player position was used to appropriately scale them. 120 |

121 |
122 | Simple raycasting with fisheye correction 123 |
Simple raycasting with fisheye correction by Lucas Vieira
124 |
125 | 126 |

Point-in-polygon problem

127 |

128 | PIP problem asks whether a given point lies inside, outside, or on the boundary of a polygon. Using the ray casting algorithm, we can count how many times the point intersects the edges of the polygon. If the number of the intersections is even, the point is on the outside of the polygon. If the number of the intersections is odd, the point is on the inside or on the boundary of a polygon. 129 |

130 |
131 | Point-in-polygon problem using ray casting 132 |
Point-in-polygon problem using ray casting
133 |
134 | 135 |

Object visibility and light casting

136 |

137 | This is the problem we will specifically tackle in this article - determining which objects are visible by the player and illuminating the visible area. 138 |

139 |
140 | Object visibility and light casting 141 |
Object visibility and light casting
142 |
143 | 144 |

Object visibility and light casting in 2D games

145 |

146 | In this section we will go through basics such as calculating intersection points, casting rays, sorting intersection points to illuminate the visible area, and few words about circles. I will provide interactive demos at each stage so you do not get lost and see the results immediately. 147 |

148 | 149 |

Line-segment - ray intersection point

150 |

It is all about those intersection points. Let's learn step by step how to find them and how to use them. We will derive a line parametric equation first, calculate intersection points and finally learn how to determine which one of them is the closest efficiently.

151 | 152 |

Deriving line parametric equation

153 |

154 | Let us talk about lines and their parametric equation first. 155 |

156 |
157 | Simple line example 158 |
Simple line example
159 |
160 |

161 | We can express vector \(\overrightarrow{AP}\) by the following equation: \(\overrightarrow{AP} = t\overrightarrow{AB}\), where \(t\) is an equation parameter defining how much do we stretch (\(|t| > 1\)) or shrink (\(|t| < 1\)) and if we flip the direction (\(t < 0\)) of vector \(\overrightarrow{AP}\) in relation to vector \(\overrightarrow{AB}\). 162 |

163 |

164 | Let me show you few examples: 165 |

166 |
167 | Line parametric equation parameter example 168 |
Line parametric equation parameter example
169 |
170 |

171 | As you might have noticed, we can use \(t\) as a scale factor: \(|AP| = |t||AB|\). 172 | We will use that fact when determining the closest intersection point. 173 |

174 |

175 | Now we can easily see, that if we want the point to be contained on: 176 |

177 |
    178 |
  • line: \(t \in \mathbb{R}\),
  • 179 |
  • ray: \(t \geq 0\),
  • 180 |
  • line-segment: \(0 \leq t \leq 1\).
  • 181 |
182 |

183 | Having all of that sorted out, we can finally derive the parametric equation:
184 | \[ 185 | \begin{align} 186 | \overrightarrow{AP} &= t \overrightarrow{AB} \\ 187 | P - A &= t (B - A) \\ 188 | P &= t(B - A) + A \\ 189 | \end{align} 190 | \] 191 |

192 |

193 | If you still do not know what happens here, I can recommend an awesome short lecture by Norm Prokup: Parametrizing a Line Segment - Concept. 194 |

195 | 196 |

Calculating the intersection point

197 |

198 | Assume that point \(P\) is the intersection point of line-segment defined by points \(A\) and \(B\), and ray defined by points \(C\) and \(D\). Point \(P\) is then expressed by set of two equations: 199 | \[ 200 | \left\{\begin{align} 201 | P &= s(B - A) + A\text {, where } 0 \leq s \leq 1 \text{, and} \\ 202 | P &= r(D - C) + C\text {, where } r \geq 0 \text{.} 203 | \end{align}\right. 204 | \] 205 |

206 |

207 | Solve for \(s\) and \(r\): 208 | \[ 209 | \left\{\begin{align} 210 | s(B_x - A_x) + A_x &= r(D_x - C_x) + C_x \Rightarrow s = \tfrac{r(D_x - C_x) + (C_x - A_x)}{B_x - A_x} \\ 211 | s(B_y - A_y) + A_y &= r(D_y - C_y) + C_y \Rightarrow r = \tfrac{s(B_y - A_y) + (A_y - C_y)}{D_y - C_y} 212 | \end{align}\right. 213 | \] 214 |

215 |
216 |
217 |

218 | Substitute \(s\) into the second equation: 219 | \[ 220 | \begin{align} 221 | &\tfrac{r(D_x - C_x) + (C_x - A_x)}{B_x - A_x} (B_y - A_y) + A_y = r(D_y - C_y) + C_y \\[10pt] 222 | &r \tfrac{(D_x - C_x)(B_y - A_y)}{B_x - A_x} + \tfrac{(C_x - A_x)(B_y - A_y)}{B_x - A_x} + A_y = r(D_y - C_y) + C_y \\[10pt] 223 | &r (\tfrac{(D_x - C_x)(B_y - A_y)}{B_x - A_x} - (D_y - C_y)) = (C_y - A_y) - \tfrac{(C_x - A_x)(B_y - A_y)}{B_x - A_x} \\[10pt] 224 | &r \tfrac{(D_x - C_x)(B_y - A_y) - (D_y - C_y)(B_x - A_x)}{B_x - A_x} = \tfrac{(C_y - A_y)(B_x - A_x) - (C_x - A_x)(B_y - A_y)}{B_x - A_x} \\[10pt] 225 | &r = \tfrac{(C_y - A_y)(B_x - A_x) - (C_x - A_x)(B_y - A_y)}{(D_x - C_x)(B_y - A_y) - (D_y - C_y)(B_x - A_x)} \\[10pt] 226 | &r = \tfrac{(B_x - A_x)(C_y - A_y) - (C_x - A_x)(B_y - A_y)}{(D_x - C_x)(B_y - A_y) - (B_x - A_x)(D_y - C_y)} \\[10pt] 227 | \end{align} 228 | \] 229 |

230 |
231 |
232 |

233 | Substitute \(r\) into the first equation: 234 | \[ 235 | \begin{align} 236 | &s(B_x - A_x) + A_x = \tfrac{s(B_y - A_y) + (A_y - C_y)}{D_y - C_y} (D_x - C_x) + C_x \\[10pt] 237 | &s(B_x - A_x) + A_x = s \tfrac{(B_y - A_y)(D_x - C_x)}{D_y - C_y} + \tfrac{(A_y - C_y)(D_x - C_x)}{D_y - C_y} + C_x \\[10pt] 238 | &s (\tfrac{(B_y - A_y)(D_x - C_x)}{D_y - C_y} - (B_x - A_x)) = (A_x - C_x) - \tfrac{(A_y - C_y)(D_x - C_x)}{D_y - C_y} \\[10pt] 239 | &s \tfrac{(B_y - A_y)(D_x - C_x) - (B_x - A_x)(D_y - C_y)}{D_y - C_y} = \tfrac{(A_x - C_x)(D_y - C_y) - (A_y - C_y)(D_x - C_x)}{D_y - C_y} \\[10pt] 240 | &s = \tfrac{(A_x - C_x)(D_y - C_y) - (A_y - C_y)(D_x - C_x)}{(B_y - A_y)(D_x - C_x) - (B_x - A_x)(D_y - C_y)} \\[10pt] 241 | &s = \tfrac{(A_x - C_x)(D_y - C_y) - (D_x - C_x)(A_y - C_y)}{(D_x - C_x)(B_y - A_y) - (B_x - A_x)(D_y - C_y)} \\[10pt] 242 | \end{align} 243 | \] 244 |

245 |
246 |
247 |

248 | Having \(s\) and \(r\) calculated, we can calculate \(P\) using one of the equations from the set. 249 |

250 |
251 | 252 |
Demo 1 - all intersection points
253 |
254 | 255 |

Finding the closest intersection point

256 |

257 | We only need the closest intersection point to properly draw the visible area. The naive solution would be to calculate distances between the ray starting point and intersection points using the Pythagorean Theorem: \(\sqrt{(C_x - P_x) ^ 2 + (C_y - P_y) ^ 2}\). Remember the line equation parameter, though? I have already mentioned that we can use it as a scale factor. As we want to compare distances on ray, we can check for the smallest \(r\) parameter value: \(|\overrightarrow{CP_1}| < |\overrightarrow{CP_2}| \Leftrightarrow |r_1| < |r_2|\). 258 |

259 |
260 | 261 |
Demo 2 - the closest intersection point
262 |
263 | 264 |

Casting rays

265 |

266 | In the following section we will go through two different ways rays might be cast. We will compare them and mention their pros and cons. 267 |

268 | 269 |

Casting rays by offset angle

270 |

271 | First way is to cast rays in all directions by specified offset angle. For instance, we could cast 30 rays in total offseted by \(12^\circ\). Let's see how to generate all those rays first. 272 |

273 |

274 | Let \(P_1 = (x_1, y_1)\) be an anchor point of our rays, and let \(P_2 = (x_2, y_2)\) be a some point on a line going through \(P_1\) at angle \(\phi\): 275 |

276 |
277 | Line from given point at some angle 278 |
Line from given point at some angle
279 |
280 |

281 | We can define \(x_2\) as \(x_1 + dx\) and \(y_2\) as \(y_1 - dy\) (our y-axis is inverted hence why the minus sign).
282 | Now we need to derive formulas for \(dx\) and \(dy\): 283 | \[ 284 | \left\{\begin{align} 285 | \sin(\phi) &= \tfrac{dy}{\mathit{dist}} \Rightarrow dy = \mathit{dist} * \sin(\phi) \\ 286 | \cos(\phi) &= \tfrac{dx}{\mathit{dist}} \Rightarrow dx = \mathit{dist} * \cos(\phi) 287 | \end{align}\right. 288 | \] 289 | \(\mathit{dist}\) in our case has an arbitrary (greater than 0) value (we are looking for any point on the line), so we are safe to assume \(\mathit{dist} = 1\) to simplify the calculations. Having it all considered, \(P_2 = (x_1 + \sin(\phi), y_1 - \cos(\phi))\), where \(\phi\) is our angle offset. 290 |

291 |
292 | 293 |
Demo 3 - casting rays by offset angle
294 |
295 | 296 |

Casting rays on vertices

297 |

298 | Casting rays on vertices is most likely your go-to solution. Instead of casting rays in all directions, we can simply cast them on our polygons' vertices. Depending on the number of vertices, we can save computing power by not casting useless rays. In the next sections you will see how does it impact animation smoothness and also learn how to further optimize the whole process. 299 |

300 |
301 | 302 |
Demo 4 - casting rays on vertices
303 |
304 | 305 |

Illuminating the visible area

306 |

307 | This is where the real fun begins. We will illuminate the visible area by filling a giant polygon. 308 |

309 | 310 |

Sorting intersection points

311 |

312 | To correctly order vertices to build a proper polygon, we need to sort them by angle first. We will use \(atan2(y, x)\) function for that. You can read more about it here. 313 |

314 |

315 | Let's compare both ray casting methods: 316 |

317 |
318 |
319 | 320 |
Demo 5 - casting rays by offset angle (filled visible area)
321 |
322 |
323 | 324 |
Demo 6 - casting rays on vertices (filled visible area)
325 |
326 |
327 |

328 | Both of them looks glitchy, jumpy and inaccurate. Let's take a closer look why. 329 |

330 | 331 |

Casting slightly offseted rays

332 |

333 | Notice what happens when rays are cast directly on vertices - they should go beyond that vertex but we are getting only the closest intersection point: 334 |

335 |
336 | Rays on vertices problem 337 |
Rays on vertices problem
338 |
339 |

340 | The most common solution is casting two extra rays offseted by small angle (in both directions) for every cast ray. Consider ray \(AB\) starting at \(A = (A_x, A_y)\) going through \(B = (B_x, B_y)\). We want to find such point \(C = (C_x, C_y)\) that ray \(AC\) would be rotated by \(\phi\) with \(A\) being the origin point. \(C\) coordinates would be as follow: 341 | \[ 342 | \left\{\begin{align} 343 | C_x &= (B_x - A_x)\cos(\phi) - (B_y - A_y)\sin(\phi) + A_x \\ 344 | C_y &= (B_y - A_y)\cos(\phi) + (B_x - A_x)\sin(\phi) + A_y 345 | \end{align}\right. 346 | \] 347 | Note that we do not need to do it for the first method - simply add or substract the offset from the angle we are calculating from.
348 | For further explanation you can check this article. 349 |

350 |
351 | Rays on vertices problem - solve 352 |
Rays on vertices problem - solve
353 |
354 |

355 | Let's see how the illumination behaves after changes: 356 |

357 |
358 |
359 | 360 |
Demo 7 - casting rays by offset angle (filled visible area with extra rays)
361 |
362 |
363 | 364 |
Demo 8 - casting rays on vertices (filled visible area with extra rays)
365 |
366 |
367 |

368 | It did not help much for the first method. We could decrease angle offset (hence increase number of rays) but the result will still be poor. On the other hand, the second method looks very smooth and accurate. From now on, we will not be talking about the first method anymore. 369 |

370 | 371 |

Visibility circle and flashlight

372 |

373 | We may want to somehow limit player's visibility. We can achieve that by creating a clipping region of desired shape. In the demos below, I used a CanvasRenderingContext2D.clip() method. 374 |

375 |
376 |
377 | 378 |
Demo 9 - visibility circle
379 |
380 |
381 | 382 |
Demo 10 - flashlight
383 |
384 |
385 |

386 | As you might have noticed, there is no optimization whatsoever - we are still calculating all the intersection points. We will get back to it once we learn about spatial hashmaps. 387 |

388 | 389 |

What about circles?

390 |

391 | I have rarely seen a real use-case scenario for ray casting on circles. Please treat this section as an extra where I only briefly talk about it. I will provide you with equations and basic demos, the rest is up to you. 392 |

393 | 394 |

Circle - ray intersection point

395 |

396 | Let \(P\) be an intersection point, \(A\) a ray's anchor point, \(B\) a point on the ray, \(C\) a circle's center point and \(r\) a circle's radius. 397 | \[ 398 | \left\{\begin{align} 399 | P_x &= t(B_x - A_x) + A_x \\ 400 | P_y &= t(B_y - A_y) + A_y \\ 401 | r ^ 2 &= (P_x - C_x) ^ 2 + (P_y - C_y) ^ 2 402 | \end{align}\right. 403 | \] 404 | 405 | Solve for \(t\): 406 | 407 | \[ 408 | \begin{align} 409 | r ^ 2 &= P_x ^ 2 - 2P_xC_x + C_x ^ 2 + P_y ^ 2 - 2P_yC_y + C_y ^ 2 \\ 410 | r ^ 2 &= (t(B_x - A_x) + A_x) ^ 2 - 2(t(B_x - A_x) + A_x)C_x + C_x ^ 2 \\ 411 | &+ (t(B_y - A_y) + A_y) ^ 2 - 2(t(B_y - A_y) + A_y)C_y + C_y ^ 2 \\ 412 | r ^ 2 &= t ^ 2 (B_x - A_x) ^ 2 + 2t(B_x - A_x)A_x + A_x ^ 2 - 2t(B_x - A_x)C_x - 2A_xC_x + C_x ^ 2 \\ 413 | &+ t ^ 2 (B_y - A_y) ^ 2 + 2t(B_y - A_y)A_y + A_y ^ 2 - 2t(B_y - A_y)C_y - 2A_yC_y + C_y ^ 2 \\ 414 | 0 &= t ^ 2 ((B_x - A_x) ^ 2 + (B_y - A_y) ^ 2) \\ 415 | &+ 2t((B_x - A_x)A_x - (B_x - A_x)C_x + (B_y - A_y)A_y - (B_y - A_y)C_y) \\ 416 | &+ A_x ^ 2 - 2A_xC_x + C_x ^ 2 + A_y ^ 2 - 2A_yC_y + C_y ^ 2 - r ^ 2 \\ 417 | 0 &= t ^ 2 ((B_x - A_x) ^ 2 + (B_y - A_y) ^ 2) \\ 418 | &+ 2t((B_x - A_x)(A_x - C_x) + (B_y - A_y)(A_y - C_y)) \\ 419 | &+ (A_x - C_x) ^ 2 + (A_y - C_y) ^ 2 - r ^ 2 420 | 421 | \end{align} 422 | \] 423 | 424 | Solve the quadratic equation: 425 | 426 | \[ 427 | \left\{\begin{align} 428 | a &= (B_x - A_x) ^ 2 + (B_y - A_y) ^ 2 \\ 429 | b &= 2((B_x - A_x)(A_x - C_x) + (B_y - A_y)(A_y - C_y)) \\ 430 | c &= (A_x - C_x) ^ 2 + (A_y - C_y) ^ 2 - r ^ 2 \\ 431 | \Delta &= b ^ 2 - 4ac 432 | \end{align}\right. 433 | \] 434 |

435 |
    436 |
  • \(\Delta < 0\): ray does not intersect the circle,
  • 437 |
  • \(\Delta = 0 \): ray intersects the circle in one point (tangent),
  • 438 |
  • \(\Delta > 0 \): ray intersects the circle in two points.
  • 439 |
440 |
441 |

442 | Only if \(\Delta = 0\): 443 | 444 | \[ 445 | \begin{align} 446 | t = \tfrac{-b}{2a} 447 | \end{align} 448 | \] 449 | 450 | Only if \(t \geq 0\): 451 | 452 | \[ 453 | \left\{\begin{align} 454 | P_x &= t(B_x - A_x) + A_x \\ 455 | P_y &= t(B_y - A_y) + A_y 456 | \end{align}\right. 457 | \] 458 |

459 |

460 | Only if \(\Delta > 0\): 461 | 462 | \[ 463 | \left\{\begin{align} 464 | t_1 &= \tfrac{-b -\sqrt{\Delta}}{2a} \\ 465 | t_2 &= \tfrac{-b +\sqrt{\Delta}}{2a} \\ 466 | \end{align}\right. 467 | \] 468 | 469 | Only if \(t_i \geq 0\): 470 | 471 | \[ 472 | \left\{\begin{align} 473 | P_{i_x} &= t_i(B_x - A_x) + A_x \\ 474 | P_{i_y} &= t_i(B_y - A_y) + A_y 475 | \end{align}\right. 476 | \] 477 |

478 |
479 | 480 |
481 | 482 |
Demo 11 - circle - ray instersection
483 |
484 | 485 |

Casting rays on circles

486 |

487 | We need to find two tangent lines to a given circle passing through a ray anchor point.
488 | Let \(P_i\) be tangent points, \(A\) a ray's anchor point, \(C\) a circle's center point and \(r\) a circle's radius.
489 | We will also be moving all points using translation vector \(\overrightarrow{v} = (-C_x, -C_y)\): \(X^\prime = X + \overrightarrow{v}\), so the circle's center is at \((0,0)\).
490 | 491 |
492 | 493 | From the circle equation: 494 | \[ 495 | P_x ^ {\prime ^ 2} + P_y ^ {\prime ^ 2} = r ^ 2 496 | \] 497 | 498 | From the perpendicular line to the circle's radius: 499 | 500 | \[ 501 | \begin{align} 502 | & \tfrac{P_y ^ \prime}{P_x ^ \prime} = - \tfrac{1}{\tfrac{P_y ^ \prime - A_y ^ \prime}{P_x ^ \prime - A_x ^ \prime}} \\ 503 | & \tfrac{P_y ^ \prime}{P_x ^ \prime} = - \tfrac{P_x ^ \prime - A_x ^ \prime}{P_y ^ \prime - A_y ^ \prime} \\ 504 | & - P_y ^ {\prime ^ 2} + P_y ^ \prime A_y ^ \prime = P_x ^ {\prime ^ 2} - P_x ^ \prime A_x ^ \prime \\ 505 | & P_x ^ {\prime ^ 2} + P_y ^ {\prime ^ 2} = P_x ^ \prime A_x ^ \prime + P_y ^ \prime A_y ^ \prime 506 | \end{align} 507 | \] 508 | 509 | Solve the equation system: 510 | 511 | \[ 512 | \left\{\begin{align} 513 | & P_x ^ {\prime ^ 2} + P_y ^ {\prime ^ 2} = r ^ 2 \\ 514 | & P_x ^ {\prime ^ 2} + P_y ^ {\prime ^ 2} = P_x ^ \prime A_x ^ \prime + P_y ^ \prime A_y ^ \prime 515 | \end{align}\right. 516 | \] 517 | 518 | \[ 519 | \begin{align} 520 | & r ^ 2 = P_x ^ \prime A_x ^ \prime + P_y ^ \prime A_y ^ \prime \\ 521 | & P_x ^ \prime = \tfrac{r ^ 2 - P_y ^ \prime A_y ^ \prime }{A_x ^ \prime} \text{, where } A_x ^ \prime \neq 0 522 | \end{align} 523 | \] 524 | 525 | Substitute \(P_x ^ \prime\) into the first equation: 526 | 527 | \[ 528 | \begin{align} 529 | & (\tfrac{r ^ 2 - P_y ^ \prime A_y ^ \prime }{A_x ^ \prime}) ^ 2 + P_y ^ {\prime ^ 2} = r ^ 2 \\ 530 | & \tfrac{r ^ 4 - 2 r ^ 2 P_y ^ \prime A_y ^ \prime + (P_y ^ \prime A_y ^ \prime) ^ 2}{A_x ^{\prime ^ 2}} + P_y ^ {\prime ^ 2} = r ^ 2 \\ 531 | & r ^ 4 - 2 r ^ 2 P_y ^ \prime A_y ^ \prime + (P_y ^ \prime A_y ^ \prime) ^ 2 + P_y ^ {\prime ^ 2} A_x ^{\prime ^ 2} = r ^ 2 A_x ^{\prime ^ 2} \\ 532 | & P_y ^ {\prime ^ 2} (A_x ^{\prime ^ 2} + A_y ^{\prime ^ 2}) - P_y ^ \prime 2r ^ 2 A_y ^ \prime + r ^ 4 - r ^ 2 A_x ^ {\prime ^ 2} = 0 533 | \end{align} 534 | \] 535 | 536 | Solve the quadratic equation: 537 | 538 | \[ 539 | \left\{\begin{align} 540 | a &= A_x ^{\prime ^ 2} + A_y ^{\prime ^ 2} \\ 541 | b &= - 2r ^ 2 A_y ^ \prime \\ 542 | c &= r ^ 4 - r ^ 2 A_x ^ {\prime ^ 2} \\ 543 | \Delta &= b ^ 2 - 4ac 544 | \end{align}\right. 545 | \] 546 |

547 |
    548 |
  • \(\Delta < 0\): there are no tangent points (ray's anchor point is in the circle),
  • 549 |
  • \(\Delta = 0 \): there is only one tangent point (ray's anchor point),
  • 550 |
  • \(\Delta > 0 \): there are two tangent points.
  • 551 |
552 |
553 |

554 | Only if \(\Delta = 0\): 555 | 556 | \[ 557 | \left\{\begin{align} 558 | P_y ^ \prime &= \tfrac{-b}{2a} \\ 559 | P_x ^ \prime &= \tfrac{r ^ 2 - P_y ^ \prime A_y ^ \prime }{A_x ^ \prime} 560 | \end{align}\right. 561 | \] 562 | 563 | \[ 564 | \left\{\begin{align} 565 | P_x &= P_x ^ \prime + C_x \\ 566 | P_y &= P_y ^ \prime + C_y 567 | \end{align}\right. 568 | \] 569 |

570 |

571 | Only if \(\Delta > 0\): 572 | 573 | \[ 574 | \left\{\begin{align} 575 | P_{1_y} ^ \prime &= \tfrac{-b -\sqrt{\Delta}}{2a} \\ 576 | P_{2_y} ^ \prime &= \tfrac{-b +\sqrt{\Delta}}{2a} \\ 577 | P_{i_x} ^ \prime &= \tfrac{r ^ 2 - P_{i_y} ^ \prime A_y ^ \prime }{A_x ^ \prime} 578 | \end{align}\right. 579 | \] 580 | 581 | \[ 582 | \left\{\begin{align} 583 | P_{i_x} &= P_{i_x} ^ \prime + C_x \\ 584 | P_{i_y} &= P_{i_y} ^ \prime + C_y 585 | \end{align}\right. 586 | \] 587 |

588 |
589 |

590 | There is still one issue - it will not work if \(A_x ^ \prime = 0\).
591 | Take a closer look at the following equation: \(r ^ 2 = P_x ^ \prime A_x ^ \prime + P_y ^ \prime A_y ^ \prime\).
592 | If \(A_x ^ \prime = 0\), the equation becomes: \(r ^ 2 = P_y ^ \prime A_y ^ \prime\) - we cannot substitute \(P_x ^ \prime\) anymore.
593 | Here are the steps for solving the equation system once that happens: 594 |

595 |

596 | Solve the equation system: 597 | 598 | \[ 599 | \left\{\begin{align} 600 | & P_x ^ {\prime ^ 2} + P_y ^ {\prime ^ 2} = r ^ 2 \\ 601 | & P_x ^ {\prime ^ 2} + P_y ^ {\prime ^ 2} = P_x ^ \prime A_x ^ \prime + P_y ^ \prime A_y ^ \prime \\ 602 | & A_x ^ \prime = 0 603 | \end{align}\right. 604 | \] 605 | 606 | \[ 607 | \begin{align} 608 | & r ^ 2 = P_y ^ \prime A_y ^ \prime \\ 609 | & P_y ^ \prime = \tfrac{r ^ 2}{A_y ^ \prime} \text{, where } A_y ^ \prime \neq 0 610 | \end{align} 611 | \] 612 | 613 | Substitute \(P_y ^ \prime\) into the first equation: 614 | 615 | \[ 616 | \begin{align} 617 | & P_x ^ {\prime ^ 2} + (\tfrac{r ^ 2}{A_y ^ \prime}) ^ 2 = r ^ 2 \\ 618 | & P_x ^ {\prime ^ 2} + \tfrac{r ^ 4}{A_y ^ {\prime ^ 2}} = r ^ 2 \\ 619 | & P_x ^ {\prime ^ 2} A_y ^ {\prime ^ 2} + r ^ 4 = r ^ 2 A_y ^ {\prime ^ 2} \\ 620 | & P_x ^ {\prime ^ 2} A_y ^ {\prime ^ 2} + r ^ 4 - r ^ 2 A_y ^ {\prime ^ 2} = 0 621 | \end{align} 622 | \] 623 | 624 | Solve the quadratic equation: 625 | 626 | \[ 627 | \left\{\begin{align} 628 | a &= A_y ^ {\prime ^ 2} \\ 629 | b &= 0 \\ 630 | c &= r ^ 4 - r ^ 2 A_y ^ {\prime ^ 2} \\ 631 | \Delta &= b ^ 2 - 4ac 632 | \end{align}\right. 633 | \] 634 |

635 |
    636 |
  • \(\Delta < 0\): there are no tangent points (ray's anchor point is in the circle),
  • 637 |
  • \(\Delta = 0 \): there is only one tangent point (ray's anchor point),
  • 638 |
  • \(\Delta > 0 \): there are two tangent points.
  • 639 |
640 |
641 |

642 | Only if \(\Delta = 0\): 643 | 644 | \[ 645 | \left\{\begin{align} 646 | P_x ^ \prime &= \tfrac{-b}{2a} \\ 647 | P_y ^ \prime &= \tfrac{r ^ 2}{A_y ^ \prime} 648 | \end{align}\right. 649 | \] 650 | 651 | \[ 652 | \left\{\begin{align} 653 | P_x &= P_x ^ \prime + C_x \\ 654 | P_y &= P_y ^ \prime + C_y 655 | \end{align}\right. 656 | \] 657 |

658 |

659 | Only if \(\Delta > 0\): 660 | 661 | \[ 662 | \left\{\begin{align} 663 | P_{1_x} ^ \prime &= \tfrac{-b -\sqrt{\Delta}}{2a} \\ 664 | P_{2_x} ^ \prime &= \tfrac{-b +\sqrt{\Delta}}{2a} \\ 665 | P_{i_y} ^ \prime &= \tfrac{r ^ 2}{A_y ^ \prime} 666 | \end{align}\right. 667 | \] 668 | 669 | \[ 670 | \left\{\begin{align} 671 | P_{i_x} &= P_{i_x} ^ \prime + C_x \\ 672 | P_{i_y} &= P_{i_y} ^ \prime + C_y 673 | \end{align}\right. 674 | \] 675 |

676 |
677 |

678 | Note that we can do the same for \(A_y ^ \prime = 0\), however, it is optional.
679 | If \(A_x ^ \prime = A_y ^ \prime = 0\), there are no solutions. 680 |

681 |
682 | 683 |
Demo 12 - casting rays on circles
684 |
685 | 686 |

Few optimization techniques

687 |

688 | In this section, we will discuss the usage of spatial hashmaps and modified Bresenham's line algorithm. I will not go into implementation details as they are commonly available on the Internet. I will, however, provide you with some demos to try these out and come up with your own ideas on where to use them. 689 |

690 | 691 |

Spatial hashmaps

692 |

693 | Currently, we are drawing all polygons and calculating all intersection points, but it is pretty much useless. In most cases, our play area will be much bigger than the viewport. By using spatial hashmaps, we can quickly determine which polygons are visible to the player and perform adequate calculations.
694 |
695 | Basically, we want to divide the play area into smaller cells (of predefined size). Each cell consists of a list containing our shapes (most likely line-segments). If a single shape spans across multiple cells, it will be included in all of them.
696 |
697 | The name of this technique suggests using hashmaps (eg. cells would be named something like X+":"+Y), but in our case, using a simple 2D array might be more desired.
698 |
699 | Here is a simple demo showing how it might work: 700 |

701 |
702 | 703 |
Demo 13 - spatial hashmaps
704 |
705 |

706 | We can use them for viewports as mentioned previously, and also visibility circles and flashlights. 707 |

708 | 709 |

Bresenham-based supercover line algorithm

710 |

711 | Well, it is high time we did something with finding the closest intersection points. Using spatial hashmaps and modified Bresenham's line algorithm, we can traverse the grid in an efficient manner making as few checks as required. The algorithm should stop when the first cell with an intersection point is found.
712 |
713 | You can read more about Bresenham's line algorithm here, and its modified version here. 714 |

715 |
716 | 717 |
Demo 14 - Bresenham-based supercover line algorithm
718 |
719 | 720 |

Everything has an end

721 | Sorry, unfortunately, I mean this article and not rays. 722 |

723 | I think I have covered everything to help you get started. I will try my best to expand it in the future if I spot something needing an additional explanation. I have also created a little cheatsheet for you to quickly recap some of the topics and derived formulas.
724 |
725 | If you enjoyed the read be sure to star and watch this repository on GitHub. You might also want to follow me for more content like that in the future. Also do not forget to create an issue if one of the topics seems to be poorly explained or you have other suggestions.
726 |
727 | Stay tuned for more! 728 |

729 |
730 |
731 |
732 | 733 | --------------------------------------------------------------------------------