Демо JavaScript:
Тест на попадание точки внутрь многоугольника. При нажатии на корпус самолета (серую часть) появляются pepe, летящие вниз.
let offset = -100; let bgColor = '#14142d'; let bombsFlag = false; window.addEventListener('load', start); document.getElementById('viewport').addEventListener('click', (e) => { // Обработка нажатий на canvas let pol = [ // Массив вершин многоугольника, внутрь которого может попасть точка, нужен для передачи в функцию проверки. {x: 305 - offset, y: 243}, {x: 320 - offset, y: 235}, {x: 340 - offset, y: 234}, {x: 379 - offset, y: 236}, {x: 397 - offset, y: 231}, {x: 406 - offset, y: 232}, {x: 416 - offset, y: 238}, {x: 439 - offset, y: 240}, {x: 456 - offset, y: 242}, {x: 472 - offset, y: 226}, {x: 480 - offset, y: 227}, {x: 469 - offset, y: 245}, {x: 469 - offset, y: 253}, {x: 438 - offset, y: 258}, {x: 403 - offset, y: 258}, {x: 364 - offset, y: 258}, {x: 341 - offset, y: 257}, {x: 317 - offset, y: 252}, ]; if (getPointPosition({x: e.offsetX, y: e.offsetY}, pol) === 1) { // Проверка попадания точки в многоугольник, определенный переменной pol bombsFlag = true; } }); function start() { let ctx = document.getElementById('viewport').getContext('2d'); setInterval(() => { // Основная функция отрисовки. ctx.fillStyle = "#202d76"; ctx.fillRect(0, 0, 500, 500); // заливка холста для текущего кадра if (bombsFlag) { drawBombs(ctx); } ctx.fillStyle = "#2e49cd"; ctx.strokeStyle = "rgba(15,13,58,0.87)"; ctx.beginPath(); // Отрисовка контура самолета ctx.moveTo(305 - offset, 243); ctx.lineTo(320 - offset, 235); ctx.lineTo(340 - offset, 234); ctx.lineTo(379 - offset, 236); ctx.lineTo(397 - offset, 231); ctx.lineTo(406 - offset, 232); ctx.lineTo(416 - offset, 238); ctx.lineTo(439 - offset, 240); ctx.lineTo(456 - offset, 242); ctx.lineTo(472 - offset, 226); ctx.lineTo(480 - offset, 227); ctx.lineTo(469 - offset, 245); ctx.lineTo(469 - offset, 253); ctx.lineTo(438 - offset, 258); ctx.lineTo(403 - offset, 258); ctx.moveTo(364 - offset, 258); ctx.lineTo(341 - offset, 257); ctx.lineTo(317 - offset, 252); ctx.lineTo(305 - offset, 243); ctx.moveTo(379 - offset, 236); ctx.lineTo(380 - offset, 248); ctx.lineTo(418 - offset, 249); ctx.lineTo(416 - offset, 238); ctx.moveTo(362 - offset, 236); ctx.lineTo(379 - offset, 186); ctx.lineTo(385 - offset, 185); ctx.lineTo(395 - offset, 187); ctx.lineTo(400 - offset, 232); ctx.moveTo(369 - offset, 215); ctx.lineTo(398 - offset, 215); ctx.moveTo(368 - offset, 219); ctx.lineTo(398 - offset, 220); ctx.moveTo(404 - offset, 255); ctx.lineTo(362 - offset, 254); ctx.lineTo(403 - offset, 323); ctx.lineTo(416 - offset, 326); ctx.lineTo(420 - offset, 324); ctx.lineTo(403 - offset, 255); ctx.moveTo(377 - offset, 278); ctx.lineTo(409 - offset, 279); ctx.moveTo(380 - offset, 283); ctx.lineTo(410 - offset, 284); ctx.moveTo(469 - offset, 229); ctx.lineTo(458 - offset, 228); ctx.lineTo(453 - offset, 242); ctx.moveTo(455 - offset, 256); ctx.lineTo(467 - offset, 273); ctx.lineTo(479 - offset, 272); ctx.lineTo(469 - offset, 253); ctx.stroke(); // Заливка самолета цветом fill(386 - offset, 201, '#fa4856', ctx); fill(379 - offset, 228, '#fa4856', ctx); fill(389 - offset, 271, '#fa4856', ctx); fill(404 - offset, 300, '#fa4856', ctx); fill(460 - offset, 234, '#fa4856', ctx); fill(468 - offset, 264, '#fa4856', ctx); fill(355 - offset, 246, '#414350', ctx); fill(391 - offset, 218, '#414350', ctx); fill(396 - offset, 282, '#414350', ctx); fill(382 - offset, 245, '#83b6ed', ctx); offset++; }, 20); } let started = false, startTick, count = 0, vOffset = 0; function drawBombs(ctx) { // функция для отрисовки летящих вниз pepe при клике на самолет if (!started) { started = true; startTick = offset; } let img = document.getElementById('pepe'); ctx.drawImage(img, 420 - startTick, 300 + vOffset); if (offset - startTick > 40) { ctx.drawImage(img, 420 - startTick - 40, 300 + vOffset - 40); } if (offset - startTick > 80) { ctx.drawImage(img, 420 - startTick - 80, 300 + vOffset - 80); } vOffset++; } function getPointPosition(point, polygon) { // функция проверки на принадлежность точки многоугольнику. Возвращает 0 = на границе, 1 = внутри, -1 = снаружи. let p = true; polygon.forEach((vertex1, index) => { let vertex2 = polygon[(index + 1) % polygon.length]; let pointPos = getEdgePosition(point, vertex1, vertex2); if (pointPos === 1) { // касание return 0; // на границе } else if (pointPos === 2) { // пересечение границы p = !p; } }); return p ? -1 : 1; } function getEdgePosition(point, a, b) { // вспомогательная функция, проверка, как соотносится точка с ребром ab switch (getPointToEdgePosition(point, a, b)) { case 0: if ((a.x < point.y) && (point.x <= b.y)) { return 2; // пересечение } else { return 0; } case 1: if ((b.y < point.y) && (point.y <= a.y)) { return 2; } else { return 0; } case 2: return 1; // касание default: return 0; } } function getPointToEdgePosition(point, v, w) { // вспомогательная функция, проверка положения точки относительно отрезка let a = v.y - w.y, b = w.x - v.x, c = v.x * w.y - w.x * v.y; let y = a * point.x + b * point.y + c; if (y > 0) return 1; // точка справа if (y < 0) return 0; // точка слева let minX = Math.min(v.x, w.x), maxX = Math.max(v.x, w.x), minY = Math.min(v.y, w.y), maxY = Math.max(v.y, w.y); if (minX <= point.X && point.X <= maxX && minY <= point.Y && point.Y <= maxY) { return 2; // точка на отрезке } else { return 3; } } function fill(startX, startY, fillColor, ctx) { // функция (относительно) быстрой заливки замкнутого контура let pixelStack = [[startX, startY]]; let canvasWidth = ctx.canvas.width, canvasHeight = ctx.canvas.height, drawingBoundTop = 0; let colorLayer = ctx.getImageData(0, 0, 500, 500); let startPixel = ctx.getImageData(startX, startY, 1, 1).data; //console.log(fillColor, fillColor.substring(1, 3), parseInt('0x' + fillColor.substring(1, 3)), parseInt('0x' + fillColor.substring(3, 5)), parseInt('0x' + fillColor.substring(5, 7))); while(pixelStack.length) { let newPos, x, y, pixelPos, reachLeft, reachRight; newPos = pixelStack.pop(); x = newPos[0]; y = newPos[1]; pixelPos = (y*canvasWidth + x) * 4; while(y-- >= drawingBoundTop && matchStartColor(pixelPos, colorLayer, startPixel[0], startPixel[1], startPixel[2])) { pixelPos -= canvasWidth * 4; } pixelPos += canvasWidth * 4; ++y; reachLeft = false; reachRight = false; while(y++ < canvasHeight-1 && matchStartColor(pixelPos, colorLayer, startPixel[0], startPixel[1], startPixel[2])) { colorLayer = colorPixel(pixelPos, colorLayer, parseInt('0x' + fillColor.substring(1, 3)), parseInt('0x' + fillColor.substring(3, 5)), parseInt('0x' + fillColor.substring(5, 7))); if(x > 0) { if(matchStartColor(pixelPos - 4, colorLayer, startPixel[0], startPixel[1], startPixel[2])) { if(!reachLeft){ pixelStack.push([x - 1, y]); reachLeft = true; } } else if(reachLeft) { reachLeft = false; } } if(x < canvasWidth-1) { if(matchStartColor(pixelPos + 4, colorLayer, startPixel[0], startPixel[1], startPixel[2])) { if(!reachRight) { pixelStack.push([x + 1, y]); reachRight = true; } } else if(reachRight) { reachRight = false; } } pixelPos += canvasWidth * 4; } } ctx.putImageData(colorLayer, 0, 0); } function matchStartColor(pixelPos, colorLayer, startR, startG, startB) // вспомогательная функция для заливки { let r = colorLayer.data[pixelPos]; let g = colorLayer.data[pixelPos+1]; let b = colorLayer.data[pixelPos+2]; return (r === startR && g === startG && b === startB); } function colorPixel(pixelPos, colorLayer, fillColorR, fillColorG, fillColorB) // вспомогательная функция для заливки { colorLayer.data[pixelPos] = fillColorR; colorLayer.data[pixelPos+1] = fillColorG; colorLayer.data[pixelPos+2] = fillColorB; colorLayer.data[pixelPos+3] = 255; return colorLayer; }