diff --git a/challenge #6/egorbwork/CloudCircle.js b/challenge #6/egorbwork/CloudCircle.js new file mode 100644 index 0000000..06ce66f --- /dev/null +++ b/challenge #6/egorbwork/CloudCircle.js @@ -0,0 +1,109 @@ +class CloudCircle { + /** + * @param {Point} centerPoint + * @param {number} radius + */ + constructor(centerPoint, radius) { + this.centerPoint = centerPoint; + this.radius = radius; + this.tangents = []; + this.tangentCircles = []; + } + + /** + * @param {CloudTangent} tangent + */ + addTangent(tangent) { + this.tangents.push(tangent); + } + + /** + * source: http://2000clicks.com/mathhelp/GeometryConicSectionCircleIntersection.aspx + * @param {CloudCircle} circle + */ + calculateCloudTangentsForCircle(circle) { + let distanceBetweenCircles = this.centerPoint.getDistanceFor(circle.centerPoint); + let areaOfCirclesTriangle = 0.25 * Math.sqrt( + ((this.radius + circle.radius) ** 2 - distanceBetweenCircles ** 2) + * (distanceBetweenCircles ** 2 - (this.radius - circle.radius) ** 2) + ); + let firstTangentPoint = new Point( + 0.5 * (circle.centerPoint.coordinateX + this.centerPoint.coordinateX) + + 0.5 * (circle.centerPoint.coordinateX - this.centerPoint.coordinateX) + * (this.radius ** 2 - circle.radius ** 2) / distanceBetweenCircles ** 2 + + 2 * (circle.centerPoint.coordinateY - this.centerPoint.coordinateY) * areaOfCirclesTriangle + / distanceBetweenCircles ** 2, + 0.5 * (circle.centerPoint.coordinateY + this.centerPoint.coordinateY) + + 0.5 * (circle.centerPoint.coordinateY - this.centerPoint.coordinateY) + * (this.radius ** 2 - circle.radius ** 2) / distanceBetweenCircles ** 2 + - 2 * (circle.centerPoint.coordinateX - this.centerPoint.coordinateX) * areaOfCirclesTriangle + / distanceBetweenCircles ** 2 + ); + let secondTangentPoint = new Point( + 0.5 * (circle.centerPoint.coordinateX + this.centerPoint.coordinateX) + + 0.5 * (circle.centerPoint.coordinateX - this.centerPoint.coordinateX) + * (this.radius ** 2 - circle.radius ** 2) / distanceBetweenCircles ** 2 + - 2 * (circle.centerPoint.coordinateY - this.centerPoint.coordinateY) * areaOfCirclesTriangle + / distanceBetweenCircles ** 2, + 0.5 * (circle.centerPoint.coordinateY + this.centerPoint.coordinateY) + + 0.5 * (circle.centerPoint.coordinateY - this.centerPoint.coordinateY) + * (this.radius ** 2 - circle.radius ** 2) / distanceBetweenCircles ** 2 + + 2 * (circle.centerPoint.coordinateX - this.centerPoint.coordinateX) * areaOfCirclesTriangle + / distanceBetweenCircles ** 2 + ); + let firstTangent = new CloudTangent(firstTangentPoint, this, circle); + this.addTangent(firstTangent) + circle.addTangent(firstTangent); + let secondTangent = new CloudTangent(secondTangentPoint, this, circle); + this.addTangent(secondTangent) + circle.addTangent(secondTangent); + this.tangentCircles.push(circle); + circle.tangentCircles.push(this); + } + + /** + * @param {CloudCircle} circle + * @returns {boolean} + */ + isTangentCircle(circle) { + let distanceBetweenCircles = this.centerPoint.getDistanceFor(circle.centerPoint); + return distanceBetweenCircles < (this.radius + circle.radius) && !this.isCircleInside(circle); + } + + /** + * @param {Point} point + * @returns {boolean} + */ + isPointInsideOfCircle(point) { + let distanceTillPoint = point.getDistanceFor(this.centerPoint); + return distanceTillPoint <= this.radius; + } + + /** + * @returns {Array} + */ + getUsedTangents() { + return this.tangents.filter( + tangent => tangent.usageStatus + ); + } + + /** + * @param {CloudCircle} circle + * @returns {boolean} + */ + isCircleInside(circle) { + return this.isPointInsideOfCircle(circle.centerPoint) + && this.radius > circle.radius + && (circle.centerPoint.getDistanceFor(this.centerPoint) + circle.radius) < this.radius; + } + + /** + * source: https://gamedev.stackexchange.com/questions/33709/get-angle-in-radians-given-a-point-on-a-circle + * @param {Point} point + * @returns {number} + */ + calculateArcAngleForPoint(point) { + return Math.atan2(point.coordinateY - this.centerPoint.coordinateY, point.coordinateX - this.centerPoint.coordinateX); + } +} diff --git a/challenge #6/egorbwork/CloudDrawingEngine.js b/challenge #6/egorbwork/CloudDrawingEngine.js new file mode 100644 index 0000000..60a1ebc --- /dev/null +++ b/challenge #6/egorbwork/CloudDrawingEngine.js @@ -0,0 +1,203 @@ +class CloudDrawingEngine { + /** + * @param {CanvasRenderingContext2D} drawingContext + * @param {number} drawingContext + */ + constructor(drawingContext, lineWidth) { + this.drawingContext = drawingContext; + this.drawingContext.lineWidth = lineWidth; + this.drawingContext.strokeStyle = 'black'; + } + + /** + * @param {Array} + */ + drawCloud(cloud) { + this.prepareCloud(cloud); + this.drawingContext.clearRect(0, 0, 5000, 5000); + for(let circle of cloud.sort(this.compareCircleByUsedTangents)) { + let currentTangents = circle.getUsedTangents(); + if (currentTangents.length > 2) { + for (let tangentsGroup of this.getGroupedTangents(currentTangents)) { + let {first: firstTangent, second: secondTangent} = tangentsGroup; + this.drawingContext.beginPath(); + this.drawingContext.arc( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY, + circle.radius, + // Full circle visibility for Testing + // 0, 2 * Math.PI, + circle.calculateArcAngleForPoint(firstTangent.point), + circle.calculateArcAngleForPoint(secondTangent.point), + this.getArcClockWiseStatusForTangent(firstTangent, circle, cloud) + ); + firstTangent.addDrawnWith(secondTangent); + secondTangent.addDrawnWith(firstTangent); + this.drawingContext.stroke(); + // Tangent Points drawing for testing + // this.drawingContext.beginPath(); + // this.drawingContext.fillStyle = 'red'; + // this.drawingContext.fillRect(firstTangent.point.coordinateX, firstTangent.point.coordinateY, 5, 5); + // this.drawingContext.fillRect(secondTangent.point.coordinateX, secondTangent.point.coordinateY, 5, 5); + } + } else { + let [firstTangent, secondTangent] = currentTangents; + this.drawingContext.beginPath(); + this.drawingContext.arc( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY, + circle.radius, + // Full circle visibility for Testing + // 0, 2 * Math.PI, + circle.calculateArcAngleForPoint(firstTangent.point), + circle.calculateArcAngleForPoint(secondTangent.point), + this.getArcClockWiseStatusForTangent(firstTangent, circle, cloud) + ); + firstTangent.addDrawnWith(secondTangent); + secondTangent.addDrawnWith(firstTangent); + this.drawingContext.stroke(); + // Tangent Points drawing for testing + // this.drawingContext.beginPath(); + // this.drawingContext.fillStyle = 'red'; + // this.drawingContext.fillRect(firstTangent.point.coordinateX, firstTangent.point.coordinateY, 5, 5); + // this.drawingContext.fillRect(secondTangent.point.coordinateX, secondTangent.point.coordinateY, 5, 5); + } + } + + } + + /** + * @param {Array} + */ + prepareCloud(cloud) { + let preparedList = []; + for (let circle of cloud) { + preparedList.push(circle); + // Calculate tangents + let remainingCircles = cloud.filter(remainingCircle => !preparedList.includes(remainingCircle)); + for (let remainingCircle of remainingCircles) { + if (circle.isTangentCircle(remainingCircle)) { + circle.calculateCloudTangentsForCircle(remainingCircle); + } + } + // Mark not used in cloud tangents + for (let tangent of circle.tangents) { + for (let remainingCircle of cloud) { + if (remainingCircle !== tangent.firstCircle + && remainingCircle !== tangent.secondCircle + && remainingCircle.isPointInsideOfCircle(tangent.point) + ) { + tangent.setUsed(false); + } + } + } + } + } + + /** + * @param {CloudTangent} tangent + * @param {CloudCircle} circle + * @param {Array} circles + * @returns {boolean} + */ + getArcClockWiseStatusForTangent(tangent, circle, circles) { + let testPoint = this.getClockWiseTestPoint(tangent, circle); + for (let testCircle of circles) { + if (testCircle === circle) { + continue; + } else if(testCircle.isPointInsideOfCircle(testPoint)) { + return false; + } + } + return true; + } + + /** + * @param {CloudTangent} tangent + * @param {CloudTangent} secondTangent + * @param {CloudCircle} circle + * @returns {Point} + */ + getClockWiseTestPoint(tangent, circle) { + let modifier = 1; + if (tangent.point.coordinateX > circle.centerPoint.coordinateX + && tangent.point.coordinateY >= circle.centerPoint.coordinateY + ) { + return new Point( + tangent.point.coordinateX + ( + tangent.point.coordinateY === circle.centerPoint.coordinateY + ? (-modifier) : modifier + ), + tangent.point.coordinateY - modifier + ); + } else if (tangent.point.coordinateX >= circle.centerPoint.coordinateX + && tangent.point.coordinateY < circle.centerPoint.coordinateY + ) { + return new Point( + tangent.point.coordinateX - modifier, + tangent.point.coordinateY + ( + tangent.point.coordinateX === circle.centerPoint.coordinateX + ? modifier : (-modifier) + ) + ); + } else if (tangent.point.coordinateX < circle.centerPoint.coordinateX + && tangent.point.coordinateY <= circle.centerPoint.coordinateY + ) { + return new Point( + tangent.point.coordinateX + ( + tangent.point.coordinateY === circle.centerPoint.coordinateY + ? modifier : (-modifier) + ), + tangent.point.coordinateY + modifier + ); + } else { + return new Point( + tangent.point.coordinateX + modifier, + tangent.point.coordinateY + ( + tangent.point.coordinateX === circle.centerPoint.coordinateX + ? (-modifier) : modifier + ) + ); + } + } + + /** + * @param {Array} tangents + */ + * getGroupedTangents(tangents) { + let usedTangents = []; + let remainingTangents = tangents.slice(0); + for (let tangent of tangents) { + remainingTangents.splice(remainingTangents.indexOf(tangent), 1); + if (tangent.drawnWith.length === 2 || usedTangents.includes(tangent)) { + continue; + } + let availableTangent; + let minimumDistance; + for (let comparableTangent of remainingTangents) { + if (comparableTangent.drawnWith.length === 2 || comparableTangent.drawnWith.includes(tangent) + || usedTangents.includes(comparableTangent) + ) { + continue; + } + let distanceBetweenTangentPoints = tangent.point.getDistanceFor(comparableTangent.point); + if (!minimumDistance || minimumDistance > distanceBetweenTangentPoints) { + minimumDistance = distanceBetweenTangentPoints; + availableTangent = comparableTangent; + } + } + usedTangents.push(availableTangent); + + yield {first: tangent, second: availableTangent}; + } + } + + /** + * @param {CloudCircle} firstCircle + * @param {CloudCircle} secondCircle + * @returns {number} + */ + compareCircleByUsedTangents(firstCircle, secondCircle) { + return firstCircle.getUsedTangents().length - secondCircle.getUsedTangents().length; + } +} diff --git a/challenge #6/egorbwork/CloudGeneratorBuilder.js b/challenge #6/egorbwork/CloudGeneratorBuilder.js new file mode 100644 index 0000000..9f10573 --- /dev/null +++ b/challenge #6/egorbwork/CloudGeneratorBuilder.js @@ -0,0 +1,215 @@ +class CloudGeneratorBuilder { + /** + * @param {number} radiusMinimum + * @param {number} radiusMaximum + * @param {number} lineWidthConstraint + */ + constructor(radiusMinimum, radiusMaximum, lineWidthConstraint) { + this.radiusMinimum = radiusMinimum; + this.radiusMaximum = radiusMaximum; + this.coordinatesRandomGenerator = this.getCoordinatesRandomGenerator(); + this.numberOfCircles = 2; + this.lineWidthConstraint = lineWidthConstraint; + } + + setNumberOfCircles(numberOfCircles) { + this.numberOfCircles = numberOfCircles; + } + + /** + * @returns {number} + */ + getRandomRadius() { + return this.getRandomNumber(this.radiusMinimum, this.radiusMaximum / (2 * this.numberOfCircles)); + } + + /** + * @param {number} minimum + * @param {number} maximum + * @returns {number} + */ + getRandomNumber(minimum, maximum) { + return Math.floor(Math.random() * (maximum - minimum)) + minimum; + } + + /** + * @param {number} minimum + * @param {number} maximum + */ + * getCoordinatesRandomGenerator() { + let minimum = 400; + let maximum = 800; + while (true) { + ({minimum, maximum} = yield this.getRandomNumber(minimum, maximum) || {minimum, maximum}); + } + } + + * getCloudGenerator() { + do { + let cloud = []; + for (let counter = 0; counter < this.numberOfCircles; counter++) { + let circle; + do { + circle = this.generateCircleForCloud(cloud); + } while (!this.isValidCircleForCloud(circle, cloud)); + cloud.push(circle) + } + yield cloud; + } while(true); + } + + /** + * @param {Array} cloud + * @returns {CloudCircle} + */ + generateCircleForCloud(cloud) { + let radius = this.getRandomRadius(); + let coordinateX = this.coordinatesRandomGenerator.next( + { + minimum: cloud.reduce( + (minimum, circle) => { + return minimum < (circle.centerPoint.coordinateX - circle.radius) + ? minimum + : circle.centerPoint.coordinateX - circle.radius; + } + , 400 + ) - 2 * radius + this.lineWidthConstraint * 2, + maximum: cloud.reduce( + (maximum, circle) => { + return maximum > (circle.centerPoint.coordinateX + circle.radius) + ? maximum + : circle.centerPoint.coordinateX + circle.radius; + } + , 800 + ) + 2 * radius - this.lineWidthConstraint * 2 + } + ).value; + let coordinateY = this.coordinatesRandomGenerator.next( + { + minimum: cloud.reduce( + (minimum, circle) => { + return minimum < (circle.centerPoint.coordinateY - circle.radius) + ? minimum + : circle.centerPoint.coordinateY - circle.radius; + } + , 400 + ) - 2 * radius + this.lineWidthConstraint * 2, + maximum: cloud.reduce( + (maximum, circle) => { + return maximum > (circle.centerPoint.coordinateY + circle.radius) + ? maximum + : circle.centerPoint.coordinateY + circle.radius; + } + , 800 + ) + 2 * radius - this.lineWidthConstraint * 2 + } + ).value; + let centerPoint = new Point(coordinateX, coordinateY); + + return new CloudCircle(centerPoint, radius); + } + + /** + * @param {CloudCircle} circle + * @param {Array} cloud + * @returns {boolean} + */ + isValidCircleForCloud(circle, cloud) { + // For empty cloud circle tangent status is true + let tangentStatus = cloud.length === 0; + for (let currentCircle of cloud) { + if (currentCircle.centerPoint.isSamePoint(circle.centerPoint)) { + return false; + } + if (currentCircle.isCircleInside(circle)) { + return false; + } + if (currentCircle.isTangentCircle(circle)) { + tangentStatus = true; + } + } + let insideCloudStatus = this.isCirclePointsInsideOfExistingCloud(circle, cloud); + let cloudCirclesInsideStatus = this.isCirclesPointsInsideOfExtendedCloud(circle, cloud); + return tangentStatus && !insideCloudStatus && !cloudCirclesInsideStatus; + } + + /** + * @param {CloudCircle} circle + * @param {Array} cloud + * @returns {boolean} + */ + isCirclesPointsInsideOfExtendedCloud(circle, cloud) { + let extendedCloud = cloud.slice(0); + extendedCloud.push(circle); + for (let currentCircle of extendedCloud) { + let remainingCloud = extendedCloud.filter(remainingCircle => remainingCircle !== currentCircle); + if (this.isCirclePointsInsideOfExistingCloud(currentCircle, remainingCloud)) { + return true; + } + } + return false; + } + + /** + * @param {CloudCircle} circle + * @param {Array} cloud + * @returns {boolean} + */ + isCirclePointsInsideOfExistingCloud(circle, cloud) { + let northPoint = new Point( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY - circle.radius + ); + let southPoint = new Point( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY + circle.radius + ); + let eastPoint = new Point( + circle.centerPoint.coordinateX + circle.radius, + circle.centerPoint.coordinateY + ); + let westPoint = new Point( + circle.centerPoint.coordinateX - circle.radius, + circle.centerPoint.coordinateY + ); + // Not accurate but better than nothing + let northEastPoint = new Point( + circle.centerPoint.coordinateX + circle.radius / Math.sqrt(2), + circle.centerPoint.coordinateY - circle.radius / Math.sqrt(2) + ); + let northWestPoint = new Point( + circle.centerPoint.coordinateX - circle.radius / Math.sqrt(2), + circle.centerPoint.coordinateY - circle.radius / Math.sqrt(2) + ); + let southEastPoint = new Point( + circle.centerPoint.coordinateX + circle.radius / Math.sqrt(2), + circle.centerPoint.coordinateY + circle.radius / Math.sqrt(2) + ); + let southWestPoint = new Point( + circle.centerPoint.coordinateX - circle.radius / Math.sqrt(2), + circle.centerPoint.coordinateY + circle.radius / Math.sqrt(2) + ); + + return this.isPointInsideOfExistingCloud(northPoint, cloud) + && this.isPointInsideOfExistingCloud(southPoint, cloud) + && this.isPointInsideOfExistingCloud(eastPoint, cloud) + && this.isPointInsideOfExistingCloud(westPoint, cloud) + && this.isPointInsideOfExistingCloud(northEastPoint, cloud) + && this.isPointInsideOfExistingCloud(northWestPoint, cloud) + && this.isPointInsideOfExistingCloud(southEastPoint, cloud) + && this.isPointInsideOfExistingCloud(southWestPoint, cloud); + } + + /** + * @param {Point} point + * @param {Array} cloud + * @returns {boolean} + */ + isPointInsideOfExistingCloud(point, cloud) { + let isPointInsideOfCloud = false; + for (let currentCircle of cloud) { + isPointInsideOfCloud = isPointInsideOfCloud || currentCircle.isPointInsideOfCircle(point); + } + return isPointInsideOfCloud; + } +} diff --git a/challenge #6/egorbwork/CloudTangent.js b/challenge #6/egorbwork/CloudTangent.js new file mode 100644 index 0000000..8e3b753 --- /dev/null +++ b/challenge #6/egorbwork/CloudTangent.js @@ -0,0 +1,28 @@ +class CloudTangent { + /** + * @param {Point} point + * @param {CloudCircle} firstCircle + * @param {CloudCircle} secondCircle + */ + constructor(point, firstCircle, secondCircle) { + this.point = point; + this.firstCircle = firstCircle; + this.secondCircle = secondCircle; + this.usageStatus = true; + this.drawnWith = []; + } + + /** + * @param {boolean} status + */ + setUsed(status) { + this.usageStatus = status; + } + + /** + * @param {CloudTangent} tangent + */ + addDrawnWith(tangent) { + this.drawnWith.push(tangent); + } +} diff --git a/challenge #6/egorbwork/Point.js b/challenge #6/egorbwork/Point.js new file mode 100644 index 0000000..50a4729 --- /dev/null +++ b/challenge #6/egorbwork/Point.js @@ -0,0 +1,28 @@ +class Point { + /** + * @param {number} coordinateX + * @param {number} coordinateY + */ + constructor(coordinateX, coordinateY) { + this.coordinateX = coordinateX; + this.coordinateY = coordinateY; + } + + /** + * @param {Point} point + * @returns {number} + */ + getDistanceFor(point) { + return Math.sqrt( + (point.coordinateX - this.coordinateX) ** 2 + (point.coordinateY - this.coordinateY) ** 2 + ); + } + + /** + * @param {Point} point + * @returns {boolean} + */ + isSamePoint(point) { + return this.coordinateX === point.coordinateX && this.coordinateY === point.coordinateY; + } +} diff --git a/challenge #6/egorbwork/application.js b/challenge #6/egorbwork/application.js new file mode 100644 index 0000000..8f4098b --- /dev/null +++ b/challenge #6/egorbwork/application.js @@ -0,0 +1,23 @@ +let lineWidth = 5; +let cloudGeneratorBuilder = new CloudGeneratorBuilder(10, 1000, lineWidth); +let cloudGenerator = cloudGeneratorBuilder.getCloudGenerator(); +cloudGeneratorBuilder.setNumberOfCircles(8); +document.addEventListener("DOMContentLoaded", function() { + let cloud = cloudGenerator.next().value; + // Test data: + // let a = new CloudCircle(new Point(200, 200), 70); + // let b = new CloudCircle(new Point(100, 100), 100); + // let c = new CloudCircle(new Point(250, 250), 100); + // let e = new CloudCircle(new Point(300, 300), 80); + // let f = new CloudCircle(new Point(300, 240), 90); + // + // let testCloud = [ + // a, + // b, + // c, + // e, + // f + // ]; + let drawingEngine = new CloudDrawingEngine(document.getElementById('cloud').getContext('2d'), lineWidth); + drawingEngine.drawCloud(cloud); +}); diff --git a/challenge #6/egorbwork/cloud.html b/challenge #6/egorbwork/cloud.html new file mode 100644 index 0000000..6a99794 --- /dev/null +++ b/challenge #6/egorbwork/cloud.html @@ -0,0 +1,22 @@ + + + + + Cloud Generator + + + + + + + + +
+ +
+ To be able to see content, please use a modern version of Chrome or Firefox! +
+
+
+ + diff --git a/challenge #6/egorbwork/rocket-sience-solution/CloudCircle.js b/challenge #6/egorbwork/rocket-sience-solution/CloudCircle.js new file mode 100644 index 0000000..28db4b9 --- /dev/null +++ b/challenge #6/egorbwork/rocket-sience-solution/CloudCircle.js @@ -0,0 +1,86 @@ +class CloudCircle { + /** + * @param {Point} centerPoint + * @param {number} radius + */ + constructor(centerPoint, radius) { + this.centerPoint = centerPoint; + this.radius = radius; + } + + /** + * @param {CloudCircle} circle + * @returns {boolean} + */ + isTangentCircle(circle) { + let distanceBetweenCircles = this.centerPoint.getDistanceFor(circle.centerPoint); + return distanceBetweenCircles < (this.radius + circle.radius) && !this.isCircleInside(circle); + } + + /** + * @param {Point} point + * @returns {boolean} + */ + isPointInsideOfCircle(point) { + let distanceTillPoint = point.getDistanceFor(this.centerPoint); + return distanceTillPoint <= this.radius; + } + + /** + * @param {CloudCircle} circle + * @returns {boolean} + */ + isCircleInside(circle) { + return this.isPointInsideOfCircle(circle.centerPoint) + && this.radius > circle.radius + && (circle.centerPoint.getDistanceFor(this.centerPoint) + circle.radius) < this.radius; + } + + /** + * source: http://2000clicks.com/mathhelp/GeometryConicSectionCircleIntersection.aspx + * @param {CloudCircle} circle + * @returns {CloudTangentCase} + */ + calculateCloudTangentCaseForCircle(circle) { + let distanceBetweenCircles = this.centerPoint.getDistanceFor(circle.centerPoint); + let areaOfCirclesTriangle = 0.25 * Math.sqrt( + ((this.radius + circle.radius) ** 2 - distanceBetweenCircles ** 2) + * (distanceBetweenCircles ** 2 - (this.radius - circle.radius) ** 2) + ); + let firstTangentPoint = new Point( + 0.5 * (circle.centerPoint.coordinateX + this.centerPoint.coordinateX) + + 0.5 * (circle.centerPoint.coordinateX - this.centerPoint.coordinateX) + * (this.radius ** 2 - circle.radius ** 2) / distanceBetweenCircles ** 2 + + 2 * (circle.centerPoint.coordinateY - this.centerPoint.coordinateY) * areaOfCirclesTriangle + / distanceBetweenCircles ** 2, + 0.5 * (circle.centerPoint.coordinateY + this.centerPoint.coordinateY) + + 0.5 * (circle.centerPoint.coordinateY - this.centerPoint.coordinateY) + * (this.radius ** 2 - circle.radius ** 2) / distanceBetweenCircles ** 2 + - 2 * (circle.centerPoint.coordinateX - this.centerPoint.coordinateX) * areaOfCirclesTriangle + / distanceBetweenCircles ** 2 + ); + let secondTangentPoint = new Point( + 0.5 * (circle.centerPoint.coordinateX + this.centerPoint.coordinateX) + + 0.5 * (circle.centerPoint.coordinateX - this.centerPoint.coordinateX) + * (this.radius ** 2 - circle.radius ** 2) / distanceBetweenCircles ** 2 + - 2 * (circle.centerPoint.coordinateY - this.centerPoint.coordinateY) * areaOfCirclesTriangle + / distanceBetweenCircles ** 2, + 0.5 * (circle.centerPoint.coordinateY + this.centerPoint.coordinateY) + + 0.5 * (circle.centerPoint.coordinateY - this.centerPoint.coordinateY) + * (this.radius ** 2 - circle.radius ** 2) / distanceBetweenCircles ** 2 + + 2 * (circle.centerPoint.coordinateX - this.centerPoint.coordinateX) * areaOfCirclesTriangle + / distanceBetweenCircles ** 2 + ); + + return new CloudTangentCase(firstTangentPoint, secondTangentPoint, this, circle); + } + + /** + * source: https://gamedev.stackexchange.com/questions/33709/get-angle-in-radians-given-a-point-on-a-circle + * @param {Point} point + * @returns {number} + */ + calculateArcAngleForPoint(point) { + return Math.atan2(point.coordinateY - this.centerPoint.coordinateY, point.coordinateX - this.centerPoint.coordinateX); + } +} diff --git a/challenge #6/egorbwork/rocket-sience-solution/CloudDrawingEngine.js b/challenge #6/egorbwork/rocket-sience-solution/CloudDrawingEngine.js new file mode 100644 index 0000000..2fda8c6 --- /dev/null +++ b/challenge #6/egorbwork/rocket-sience-solution/CloudDrawingEngine.js @@ -0,0 +1,107 @@ +class CloudDrawingEngine { + /** + * @param {CanvasRenderingContext2D} drawingContext + * @param {number} lineWidth + */ + constructor(drawingContext, lineWidth) { + this.drawingContext = drawingContext; + this.drawingContext.lineWidth = lineWidth; + this.drawingContext.fillStyle = 'white'; + } + + /** + * @param {Array} cloud + */ + drawCloud(cloud) { + this.drawCloudScheme(cloud); + this.removeOverlappingParts(cloud); + } + + /** + * Clear canvas and Draw scheme with all circles + * @param cloud + */ + drawCloudScheme(cloud) { + this.drawingContext.clearRect(0, 0, 5000, 5000); + this.drawScheme(cloud); + } + + /** + * For each circle in cloud draw + * @param cloud + */ + drawScheme(cloud) { + this.drawingContext.strokeStyle = 'black'; + for (let circle of cloud) { + this.drawingContext.beginPath(); + this.drawingContext.arc( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY, + circle.radius, + 0, + 2 * Math.PI + ); + this.drawingContext.stroke(); + } + } + + /** + * Draw at overlapping parts arcs to cover them with white. + * @param {Array} cloud + */ + removeOverlappingParts(cloud) { + this.drawingContext.strokeStyle = 'white'; + // Strange But the white is one point less at drawing + this.drawingContext.lineWidth = this.drawingContext.lineWidth + 1; + // A bit improves images, and multiple number of elements is still ugly + let deltaRadians = 0.000005; + let tangentCases = this.generateTangentCases(cloud); + for (let tangentCase of tangentCases) { + // First circle + this.drawingContext.beginPath(); + this.drawingContext.arc( + tangentCase.firstCircle.centerPoint.coordinateX, + tangentCase.firstCircle.centerPoint.coordinateY, + tangentCase.firstCircle.radius, + tangentCase.firstCircle.calculateArcAngleForPoint(tangentCase.firstPoint) + deltaRadians, + tangentCase.firstCircle.calculateArcAngleForPoint(tangentCase.secondPoint) - deltaRadians, + tangentCase.firstCircleTangentOrientation + ); + this.drawingContext.stroke(); + + // Second circle + this.drawingContext.beginPath(); + this.drawingContext.arc( + tangentCase.secondCircle.centerPoint.coordinateX, + tangentCase.secondCircle.centerPoint.coordinateY, + tangentCase.secondCircle.radius, + tangentCase.secondCircle.calculateArcAngleForPoint(tangentCase.firstPoint) + deltaRadians, + tangentCase.secondCircle.calculateArcAngleForPoint(tangentCase.secondPoint) - deltaRadians, + tangentCase.secondCircleTangentOrientation + ); + this.drawingContext.stroke(); + } + this.drawingContext.lineWidth = this.drawingContext.lineWidth - 1; + } + + /** + * @param {Array} cloud + * @returns {Array} + */ + generateTangentCases(cloud) { + let tangentCases = []; + let processedList = []; + for (let circle of cloud) { + processedList.push(circle); + // Calculate tangents + let remainingCircles = cloud.filter(remainingCircle => !processedList.includes(remainingCircle)); + for (let remainingCircle of remainingCircles) { + if (circle.isTangentCircle(remainingCircle)) { + tangentCases.push(circle.calculateCloudTangentCaseForCircle(remainingCircle)); + } + } + } + + return tangentCases; + } +} diff --git a/challenge #6/egorbwork/rocket-sience-solution/CloudGeneratorBuilder.js b/challenge #6/egorbwork/rocket-sience-solution/CloudGeneratorBuilder.js new file mode 100644 index 0000000..a2d3da9 --- /dev/null +++ b/challenge #6/egorbwork/rocket-sience-solution/CloudGeneratorBuilder.js @@ -0,0 +1,233 @@ +class CloudGeneratorBuilder { + /** + * @param {number} radiusMinimum + * @param {number} radiusMaximum + * @param {number} lineWidthConstraint + */ + constructor(radiusMinimum, radiusMaximum, lineWidthConstraint) { + this.radiusMinimum = radiusMinimum; + this.radiusMaximum = radiusMaximum; + this.coordinatesRandomGenerator = this.getCoordinatesRandomGenerator(); + this.numberOfCircles = 2; + this.lineWidthConstraint = lineWidthConstraint; + } + + setNumberOfCircles(numberOfCircles) { + this.numberOfCircles = numberOfCircles; + } + + /** + * @returns {number} + */ + getRandomRadius() { + return this.getRandomNumber(this.radiusMinimum, this.radiusMaximum / this.numberOfCircles); + } + + /** + * @param {number} minimum + * @param {number} maximum + * @returns {number} + */ + getRandomNumber(minimum, maximum) { + return Math.floor(Math.random() * (maximum - minimum)) + minimum; + } + + /** + * @param {number} minimum + * @param {number} maximum + */ + * getCoordinatesRandomGenerator() { + let minimum = 400; + let maximum = 900; + while (true) { + ({minimum, maximum} = yield this.getRandomNumber(minimum, maximum) || {minimum, maximum}); + } + } + + /** + * @returns {iterator} + */ + * getCloudGenerator() { + do { + let cloud = []; + for (let counter = 0; counter < this.numberOfCircles; counter++) { + let circle; + do { + circle = this.generateCircleForCloud(cloud); + } while (!this.isValidCircleForCloud(circle, cloud)); + cloud.push(circle) + } + yield cloud; + } while(true); + } + + /** + * @param {Array} cloud + * @returns {CloudCircle} + */ + generateCircleForCloud(cloud) { + let radius = this.getRandomRadius(); + let coordinateX = this.coordinatesRandomGenerator.next( + { + minimum: cloud.reduce( + (minimum, circle) => { + return minimum < (circle.centerPoint.coordinateX - circle.radius) + ? minimum + : circle.centerPoint.coordinateX - circle.radius; + } + , 400 + ) - 2 * radius + this.lineWidthConstraint * 2, + maximum: cloud.reduce( + (maximum, circle) => { + return maximum > (circle.centerPoint.coordinateX + circle.radius) + ? maximum + : circle.centerPoint.coordinateX + circle.radius; + } + , 800 + ) + 2 * radius - this.lineWidthConstraint * 2 + } + ).value; + let coordinateY = this.coordinatesRandomGenerator.next( + { + minimum: cloud.reduce( + (minimum, circle) => { + return minimum < (circle.centerPoint.coordinateY - circle.radius) + ? minimum + : circle.centerPoint.coordinateY - circle.radius; + } + , 400 + ) - 2 * radius + this.lineWidthConstraint * 2, + maximum: cloud.reduce( + (maximum, circle) => { + return maximum > (circle.centerPoint.coordinateY + circle.radius) + ? maximum + : circle.centerPoint.coordinateY + circle.radius; + } + , 800 + ) + 2 * radius - this.lineWidthConstraint * 2 + } + ).value; + let centerPoint = new Point(coordinateX, coordinateY); + + return new CloudCircle(centerPoint, radius); + } + + /** + * @param {CloudCircle} circle + * @param {Array} cloud + * @returns {boolean} + */ + isValidCircleForCloud(circle, cloud) { + // For empty cloud circle tangent status is true + let tangentStatus = cloud.length === 0; + for (let currentCircle of cloud) { + if (currentCircle.centerPoint.isSamePoint(circle.centerPoint)) { + return false; + } + if (currentCircle.isCircleInside(circle)) { + return false; + } + if (currentCircle.isTangentCircle(circle)) { + tangentStatus = true; + } + } + let insideCloudStatus = this.isCirclePointsInsideOfExistingCloud(circle, cloud); + let cloudCirclesInsideStatus = this.isCirclesPointsInsideOfExtendedCloud(circle, cloud); + return tangentStatus && !(insideCloudStatus || cloudCirclesInsideStatus); + } + + /** + * @param {CloudCircle} circle + * @param {Array} cloud + * @returns {boolean} + */ + isCirclesPointsInsideOfExtendedCloud(circle, cloud) { + let extendedCloud = cloud.slice(0); + extendedCloud.push(circle); + for (let currentCircle of extendedCloud) { + let remainingCloud = extendedCloud.filter(remainingCircle => remainingCircle !== currentCircle); + if (this.isCirclePointsInsideOfExistingCloud(currentCircle, remainingCloud)) { + return true; + } + } + return false; + } + + /** + * @param {CloudCircle} circle + * @param {Array} cloud + * @returns {boolean} + */ + isCirclePointsInsideOfExistingCloud(circle, cloud) { + // Can be used only radius but than some of the cases will look like overlapping from line Width + // Line width is doubled to be sure that it looks fine with simplified version + let deltaRadius = circle.radius- this.lineWidthConstraint / 2 - 1; + let northPoint = new Point( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY - deltaRadius + ); + let southPoint = new Point( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY + deltaRadius + ); + let eastPoint = new Point( + circle.centerPoint.coordinateX + deltaRadius, + circle.centerPoint.coordinateY + ); + let westPoint = new Point( + circle.centerPoint.coordinateX - deltaRadius, + circle.centerPoint.coordinateY + ); + // Not accurate but better than nothing + // Using for point coordinates calculation angle of 45 degrees + let sin45 = Math.sin(this.convertDegreesToRadians(45)); + let cos45 = Math.cos(this.convertDegreesToRadians(45)); + + let northEastPoint = new Point( + circle.centerPoint.coordinateX + deltaRadius * cos45, + circle.centerPoint.coordinateY - deltaRadius * sin45 + ); + let northWestPoint = new Point( + circle.centerPoint.coordinateX - deltaRadius * cos45, + circle.centerPoint.coordinateY - deltaRadius * sin45 + ); + let southEastPoint = new Point( + circle.centerPoint.coordinateX + deltaRadius * cos45, + circle.centerPoint.coordinateY + deltaRadius * sin45 + ); + let southWestPoint = new Point( + circle.centerPoint.coordinateX - deltaRadius * cos45, + circle.centerPoint.coordinateY + deltaRadius * sin45 + ); + + return this.isPointInsideOfExistingCloud(northPoint, cloud) + && this.isPointInsideOfExistingCloud(southPoint, cloud) + && this.isPointInsideOfExistingCloud(eastPoint, cloud) + && this.isPointInsideOfExistingCloud(westPoint, cloud) + && this.isPointInsideOfExistingCloud(northEastPoint, cloud) + && this.isPointInsideOfExistingCloud(northWestPoint, cloud) + && this.isPointInsideOfExistingCloud(southEastPoint, cloud) + && this.isPointInsideOfExistingCloud(southWestPoint, cloud); + } + + /** + * @param {Point} point + * @param {Array} cloud + * @returns {boolean} + */ + isPointInsideOfExistingCloud(point, cloud) { + let isPointInsideOfCloud = false; + for (let currentCircle of cloud) { + isPointInsideOfCloud = isPointInsideOfCloud || currentCircle.isPointInsideOfCircle(point); + } + return isPointInsideOfCloud; + } + + /** + * @param {number} degrees + * @returns {number} + */ + convertDegreesToRadians(degrees) { + return (Math.PI / 180) * degrees; + } +} diff --git a/challenge #6/egorbwork/rocket-sience-solution/CloudTangentCase.js b/challenge #6/egorbwork/rocket-sience-solution/CloudTangentCase.js new file mode 100644 index 0000000..df55082 --- /dev/null +++ b/challenge #6/egorbwork/rocket-sience-solution/CloudTangentCase.js @@ -0,0 +1,67 @@ +class CloudTangentCase { + /** + * @param {Point} firstPoint + * @param {Point} secondPoint + * @param {CloudCircle} firstCircle + * @param {CloudCircle} secondCircle + */ + constructor(firstPoint, secondPoint, firstCircle, secondCircle) { + // Order the tangent points from bottom to top and left to right + if ((firstPoint.coordinateX > secondPoint.coordinateX) + || (firstPoint.coordinateX === secondPoint.coordinateX && firstPoint.coordinateY < secondPoint.coordinateY) + ) { + this.firstPoint = secondPoint; + this.secondPoint = firstPoint; + } else { + this.firstPoint = firstPoint; + this.secondPoint = secondPoint; + } + // Order the circles vice versa from right to left + if (firstCircle.centerPoint.coordinateX > secondCircle.centerPoint.coordinateX) { + this.firstCircle = firstCircle; + this.secondCircle = secondCircle; + } else { + this.firstCircle = secondCircle; + this.secondCircle = firstCircle; + } + this.firstCircleTangentOrientation = false; + this.secondCircleTangentOrientation = false; + this.calculateOverlapOrientations(); + } + + /** + * Calculates when the overlapped part of tangent circles should be covered clockwise and anti-clockwise + */ + calculateOverlapOrientations() { + let firstTestPoint = this.calculateTestPointForCircle(this.firstCircle); + // For case of the same coordinateX the anti-clockwise should be applied for negated case + this.firstCircleTangentOrientation = (this.firstPoint.coordinateX !== this.secondPoint.coordinateX + && this.secondCircle.isPointInsideOfCircle(firstTestPoint)) || (this.firstPoint.coordinateX === this.secondPoint.coordinateX + && !this.secondCircle.isPointInsideOfCircle(firstTestPoint)); + this.secondCircleTangentOrientation = !this.firstCircleTangentOrientation; + } + + /** + * Generates a test point for checking orientation of the arc + * @param {CloudCircle} circle + * @returns {Point} + */ + calculateTestPointForCircle(circle) { + let testPositionX, testPositionY; + if (this.firstPoint.coordinateX !== this.secondPoint.coordinateX) { + testPositionX = (this.firstPoint.coordinateX + this.secondPoint.coordinateX) / 2; + let relativeY = Math.sqrt( + circle.radius ** 2 - (testPositionX - circle.centerPoint.coordinateX) ** 2 + ); + testPositionY = circle.centerPoint.coordinateY + relativeY; + } else { + testPositionY = (this.firstPoint.coordinateY + this.secondPoint.coordinateY) / 2; + let relativeX = Math.sqrt( + circle.radius ** 2 - (testPositionY - circle.centerPoint.coordinateY) ** 2 + ); + testPositionX = circle.centerPoint.coordinateX - relativeX; + } + + return new Point(testPositionX, testPositionY); + } +} diff --git a/challenge #6/egorbwork/rocket-sience-solution/Point.js b/challenge #6/egorbwork/rocket-sience-solution/Point.js new file mode 100644 index 0000000..50a4729 --- /dev/null +++ b/challenge #6/egorbwork/rocket-sience-solution/Point.js @@ -0,0 +1,28 @@ +class Point { + /** + * @param {number} coordinateX + * @param {number} coordinateY + */ + constructor(coordinateX, coordinateY) { + this.coordinateX = coordinateX; + this.coordinateY = coordinateY; + } + + /** + * @param {Point} point + * @returns {number} + */ + getDistanceFor(point) { + return Math.sqrt( + (point.coordinateX - this.coordinateX) ** 2 + (point.coordinateY - this.coordinateY) ** 2 + ); + } + + /** + * @param {Point} point + * @returns {boolean} + */ + isSamePoint(point) { + return this.coordinateX === point.coordinateX && this.coordinateY === point.coordinateY; + } +} diff --git a/challenge #6/egorbwork/rocket-sience-solution/application.js b/challenge #6/egorbwork/rocket-sience-solution/application.js new file mode 100644 index 0000000..1c376aa --- /dev/null +++ b/challenge #6/egorbwork/rocket-sience-solution/application.js @@ -0,0 +1,53 @@ +/** + * More correct solution, but looks a bit ugly and have sometimes same problems with overlapping + */ + +let lineWidth = 5; +let cloudGeneratorBuilder = new CloudGeneratorBuilder(10, 800, lineWidth); +let cloudGenerator = cloudGeneratorBuilder.getCloudGenerator(); +let scheme = false; +let drawingEngine, cloud; + +document.addEventListener("DOMContentLoaded", function() { + cloudGeneratorBuilder.setNumberOfCircles(8); + drawingEngine = new CloudDrawingEngine(document.getElementById('cloud').getContext('2d'), lineWidth); + cloud = cloudGenerator.next().value; + drawingEngine.drawCloud(cloud); + + addSchemeButtonHandler(); + addRedrawButtonHandler(); +}); + +function addSchemeButtonHandler() { + let schemeButton = document.getElementById('scheme'); + + schemeButton.addEventListener('click', () => { + let alternativeText = schemeButton.getAttribute('data-text'); + schemeButton.setAttribute('data-text', schemeButton.textContent); + schemeButton.textContent = alternativeText; + scheme = !scheme; + if (scheme) { + drawingEngine.drawScheme(cloud); + } else { + drawingEngine.drawCloud(cloud); + } + }); +} + +function addRedrawButtonHandler() { + let redrawButton = document.getElementById('redraw'); + let inputNumber = document.getElementById('numberOfCircles'); + + redrawButton.addEventListener('click', () => { + let numberOfCircles = inputNumber.value; + cloudGeneratorBuilder.setNumberOfCircles(numberOfCircles); + cloud = cloudGenerator.next().value; + if (scheme) { + drawingEngine.drawCloudScheme(cloud); + } else { + drawingEngine.drawCloud(cloud); + } + }); +} + + diff --git a/challenge #6/egorbwork/rocket-sience-solution/cloud.html b/challenge #6/egorbwork/rocket-sience-solution/cloud.html new file mode 100644 index 0000000..33be206 --- /dev/null +++ b/challenge #6/egorbwork/rocket-sience-solution/cloud.html @@ -0,0 +1,28 @@ + + + + + Cloud Generator + + + + + + + + +
+ + + + +
+
+ +
+ To be able to see content, please use a modern version of Chrome or Firefox! +
+
+
+ + diff --git a/challenge #6/egorbwork/simple-stupid-solution/CloudCircle.js b/challenge #6/egorbwork/simple-stupid-solution/CloudCircle.js new file mode 100644 index 0000000..7c192c9 --- /dev/null +++ b/challenge #6/egorbwork/simple-stupid-solution/CloudCircle.js @@ -0,0 +1,40 @@ +class CloudCircle { + /** + * @param {Point} centerPoint + * @param {number} radius + */ + constructor(centerPoint, radius) { + this.centerPoint = centerPoint; + this.radius = radius; + this.tangents = []; + this.tangentCircles = []; + } + + /** + * @param {CloudCircle} circle + * @returns {boolean} + */ + isTangentCircle(circle) { + let distanceBetweenCircles = this.centerPoint.getDistanceFor(circle.centerPoint); + return distanceBetweenCircles < (this.radius + circle.radius) && !this.isCircleInside(circle); + } + + /** + * @param {Point} point + * @returns {boolean} + */ + isPointInsideOfCircle(point) { + let distanceTillPoint = point.getDistanceFor(this.centerPoint); + return distanceTillPoint <= this.radius; + } + + /** + * @param {CloudCircle} circle + * @returns {boolean} + */ + isCircleInside(circle) { + return this.isPointInsideOfCircle(circle.centerPoint) + && this.radius > circle.radius + && (circle.centerPoint.getDistanceFor(this.centerPoint) + circle.radius) < this.radius; + } +} diff --git a/challenge #6/egorbwork/simple-stupid-solution/CloudDrawingEngine.js b/challenge #6/egorbwork/simple-stupid-solution/CloudDrawingEngine.js new file mode 100644 index 0000000..5e695d1 --- /dev/null +++ b/challenge #6/egorbwork/simple-stupid-solution/CloudDrawingEngine.js @@ -0,0 +1,65 @@ +class CloudDrawingEngine { + /** + * @param {CanvasRenderingContext2D} drawingContext + * @param {number} lineWidth + */ + constructor(drawingContext, lineWidth) { + this.drawingContext = drawingContext; + this.drawingContext.lineWidth = lineWidth; + this.drawingContext.fillStyle = 'white'; + this.drawingContext.strokeStyle = 'black'; + } + + /** + * @param {Array} cloud + */ + drawCloud(cloud) { + this.drawCloudScheme(cloud); + this.removeOverlappingParts(cloud); + } + + /** + * Clear canvas and Draw scheme with all circles + * @param {Array} cloud + */ + drawCloudScheme(cloud) { + this.drawingContext.clearRect(0, 0, 5000, 5000); + this.drawScheme(cloud); + } + + /** + * For each circle in cloud draw + * @param {Array} cloud + */ + drawScheme(cloud) { + for (let circle of cloud) { + this.drawingContext.beginPath(); + this.drawingContext.arc( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY, + circle.radius, + 0, + 2 * Math.PI + ); + this.drawingContext.stroke(); + } + } + + /** + * @param {Array} cloud + */ + removeOverlappingParts(cloud) { + for (let circle of cloud) { + this.drawingContext.beginPath(); + this.drawingContext.arc( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY, + // For more clear image it is better to have it a bit bigger radius + circle.radius, + 0, + 2 * Math.PI + ); + this.drawingContext.fill(); + } + } +} diff --git a/challenge #6/egorbwork/simple-stupid-solution/CloudGeneratorBuilder.js b/challenge #6/egorbwork/simple-stupid-solution/CloudGeneratorBuilder.js new file mode 100644 index 0000000..809e2d4 --- /dev/null +++ b/challenge #6/egorbwork/simple-stupid-solution/CloudGeneratorBuilder.js @@ -0,0 +1,236 @@ +class CloudGeneratorBuilder { + /** + * @param {number} radiusMinimum + * @param {number} radiusMaximum + * @param {number} lineWidthConstraint + */ + constructor(radiusMinimum, radiusMaximum, lineWidthConstraint) { + this.radiusMinimum = radiusMinimum; + this.radiusMaximum = radiusMaximum; + this.coordinatesRandomGenerator = this.getCoordinatesRandomGenerator(); + this.numberOfCircles = 2; + this.lineWidthConstraint = lineWidthConstraint; + } + + /** + * @param {number} numberOfCircles + */ + setNumberOfCircles(numberOfCircles) { + this.numberOfCircles = numberOfCircles; + } + + /** + * @returns {number} + */ + getRandomRadius() { + return this.getRandomNumber(this.radiusMinimum, this.radiusMaximum / this.numberOfCircles); + } + + /** + * @param {number} minimum + * @param {number} maximum + * @returns {number} + */ + getRandomNumber(minimum, maximum) { + return Math.floor(Math.random() * (maximum - minimum)) + minimum; + } + + /** + * @param {number} minimum + * @param {number} maximum + */ + * getCoordinatesRandomGenerator() { + let minimum = 200; + let maximum = 900; + while (true) { + ({minimum, maximum} = yield this.getRandomNumber(minimum, maximum) || {minimum, maximum}); + } + } + + /** + * @returns {iterator} + */ + * getCloudGenerator() { + do { + let cloud = []; + for (let counter = 0; counter < this.numberOfCircles; counter++) { + let circle; + do { + circle = this.generateCircleForCloud(cloud); + } while (!this.isValidCircleForCloud(circle, cloud)); + cloud.push(circle) + } + yield cloud; + } while(true); + } + + /** + * @param {Array} cloud + * @returns {CloudCircle} + */ + generateCircleForCloud(cloud) { + let radius = this.getRandomRadius(); + let coordinateX = this.coordinatesRandomGenerator.next( + { + minimum: cloud.reduce( + (minimum, circle) => { + return minimum < (circle.centerPoint.coordinateX - circle.radius) + ? minimum + : circle.centerPoint.coordinateX - circle.radius; + } + , 400 + ) - 2 * radius + this.lineWidthConstraint * 2, + maximum: cloud.reduce( + (maximum, circle) => { + return maximum > (circle.centerPoint.coordinateX + circle.radius) + ? maximum + : circle.centerPoint.coordinateX + circle.radius; + } + , 800 + ) + 2 * radius - this.lineWidthConstraint * 2 + } + ).value; + let coordinateY = this.coordinatesRandomGenerator.next( + { + minimum: cloud.reduce( + (minimum, circle) => { + return minimum < (circle.centerPoint.coordinateY - circle.radius) + ? minimum + : circle.centerPoint.coordinateY - circle.radius; + } + , 400 + ) - 2 * radius + this.lineWidthConstraint * 2, + maximum: cloud.reduce( + (maximum, circle) => { + return maximum > (circle.centerPoint.coordinateY + circle.radius) + ? maximum + : circle.centerPoint.coordinateY + circle.radius; + } + , 800 + ) + 2 * radius - this.lineWidthConstraint * 2 + } + ).value; + let centerPoint = new Point(coordinateX, coordinateY); + + return new CloudCircle(centerPoint, radius); + } + + /** + * @param {CloudCircle} circle + * @param {Array} cloud + * @returns {boolean} + */ + isValidCircleForCloud(circle, cloud) { + // For empty cloud circle tangent status is true + let tangentStatus = cloud.length === 0; + for (let currentCircle of cloud) { + if (currentCircle.centerPoint.isSamePoint(circle.centerPoint)) { + return false; + } + if (currentCircle.isCircleInside(circle)) { + return false; + } + if (currentCircle.isTangentCircle(circle)) { + tangentStatus = true; + } + } + let insideCloudStatus = this.isCirclePointsInsideOfExistingCloud(circle, cloud); + let cloudCirclesInsideStatus = this.isCirclesPointsInsideOfExtendedCloud(circle, cloud); + return tangentStatus && !(insideCloudStatus || cloudCirclesInsideStatus); + } + + /** + * @param {CloudCircle} circle + * @param {Array} cloud + * @returns {boolean} + */ + isCirclesPointsInsideOfExtendedCloud(circle, cloud) { + let extendedCloud = cloud.slice(0); + extendedCloud.push(circle); + for (let currentCircle of extendedCloud) { + let remainingCloud = extendedCloud.filter(remainingCircle => remainingCircle !== currentCircle); + if (this.isCirclePointsInsideOfExistingCloud(currentCircle, remainingCloud)) { + return true; + } + } + return false; + } + + /** + * @param {CloudCircle} circle + * @param {Array} cloud + * @returns {boolean} + */ + isCirclePointsInsideOfExistingCloud(circle, cloud) { + // Can be used only radius but than some of the cases will look like overlapping from line Width + // Line width is doubled to be sure that it looks fine with simplified version + let deltaRadius = circle.radius- this.lineWidthConstraint / 2 - 1; + let northPoint = new Point( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY - deltaRadius + ); + let southPoint = new Point( + circle.centerPoint.coordinateX, + circle.centerPoint.coordinateY + deltaRadius + ); + let eastPoint = new Point( + circle.centerPoint.coordinateX + deltaRadius, + circle.centerPoint.coordinateY + ); + let westPoint = new Point( + circle.centerPoint.coordinateX - deltaRadius, + circle.centerPoint.coordinateY + ); + // Not accurate but better than nothing + // Using for point coordinates calculation angle of 45 degrees + let sin45 = Math.sin(this.convertDegreesToRadians(45)); + let cos45 = Math.cos(this.convertDegreesToRadians(45)); + + let northEastPoint = new Point( + circle.centerPoint.coordinateX + deltaRadius * cos45, + circle.centerPoint.coordinateY - deltaRadius * sin45 + ); + let northWestPoint = new Point( + circle.centerPoint.coordinateX - deltaRadius * cos45, + circle.centerPoint.coordinateY - deltaRadius * sin45 + ); + let southEastPoint = new Point( + circle.centerPoint.coordinateX + deltaRadius * cos45, + circle.centerPoint.coordinateY + deltaRadius * sin45 + ); + let southWestPoint = new Point( + circle.centerPoint.coordinateX - deltaRadius * cos45, + circle.centerPoint.coordinateY + deltaRadius * sin45 + ); + + return this.isPointInsideOfExistingCloud(northPoint, cloud) + && this.isPointInsideOfExistingCloud(southPoint, cloud) + && this.isPointInsideOfExistingCloud(eastPoint, cloud) + && this.isPointInsideOfExistingCloud(westPoint, cloud) + && this.isPointInsideOfExistingCloud(northEastPoint, cloud) + && this.isPointInsideOfExistingCloud(northWestPoint, cloud) + && this.isPointInsideOfExistingCloud(southEastPoint, cloud) + && this.isPointInsideOfExistingCloud(southWestPoint, cloud); + } + + /** + * @param {Point} point + * @param {Array} cloud + * @returns {boolean} + */ + isPointInsideOfExistingCloud(point, cloud) { + let isPointInsideOfCloud = false; + for (let currentCircle of cloud) { + isPointInsideOfCloud = isPointInsideOfCloud || currentCircle.isPointInsideOfCircle(point); + } + return isPointInsideOfCloud; + } + + /** + * @param {number} degrees + * @returns {number} + */ + convertDegreesToRadians(degrees) { + return (Math.PI / 180) * degrees; + } +} diff --git a/challenge #6/egorbwork/simple-stupid-solution/Point.js b/challenge #6/egorbwork/simple-stupid-solution/Point.js new file mode 100644 index 0000000..50a4729 --- /dev/null +++ b/challenge #6/egorbwork/simple-stupid-solution/Point.js @@ -0,0 +1,28 @@ +class Point { + /** + * @param {number} coordinateX + * @param {number} coordinateY + */ + constructor(coordinateX, coordinateY) { + this.coordinateX = coordinateX; + this.coordinateY = coordinateY; + } + + /** + * @param {Point} point + * @returns {number} + */ + getDistanceFor(point) { + return Math.sqrt( + (point.coordinateX - this.coordinateX) ** 2 + (point.coordinateY - this.coordinateY) ** 2 + ); + } + + /** + * @param {Point} point + * @returns {boolean} + */ + isSamePoint(point) { + return this.coordinateX === point.coordinateX && this.coordinateY === point.coordinateY; + } +} diff --git a/challenge #6/egorbwork/simple-stupid-solution/application.js b/challenge #6/egorbwork/simple-stupid-solution/application.js new file mode 100644 index 0000000..8584a32 --- /dev/null +++ b/challenge #6/egorbwork/simple-stupid-solution/application.js @@ -0,0 +1,53 @@ +/** + * Simple solution, has problem with overlapping + */ + +let lineWidth = 5; +let cloudGeneratorBuilder = new CloudGeneratorBuilder(10, 800, lineWidth); +let cloudGenerator = cloudGeneratorBuilder.getCloudGenerator(); +let scheme = false; +let drawingEngine, cloud; + +document.addEventListener("DOMContentLoaded", function() { + cloudGeneratorBuilder.setNumberOfCircles(8); + drawingEngine = new CloudDrawingEngine(document.getElementById('cloud').getContext('2d'), lineWidth); + cloud = cloudGenerator.next().value; + drawingEngine.drawCloud(cloud); + + addSchemeButtonHandler(); + addRedrawButtonHandler(); +}); + +function addSchemeButtonHandler() { + let schemeButton = document.getElementById('scheme'); + + schemeButton.addEventListener('click', () => { + let alternativeText = schemeButton.getAttribute('data-text'); + schemeButton.setAttribute('data-text', schemeButton.textContent); + schemeButton.textContent = alternativeText; + scheme = !scheme; + if (scheme) { + drawingEngine.drawScheme(cloud); + } else { + drawingEngine.drawCloud(cloud); + } + }); +} + +function addRedrawButtonHandler() { + let redrawButton = document.getElementById('redraw'); + let inputNumber = document.getElementById('numberOfCircles'); + + redrawButton.addEventListener('click', () => { + let numberOfCircles = inputNumber.value; + cloudGeneratorBuilder.setNumberOfCircles(numberOfCircles); + cloud = cloudGenerator.next().value; + if (scheme) { + drawingEngine.drawCloudScheme(cloud); + } else { + drawingEngine.drawCloud(cloud); + } + }); +} + + diff --git a/challenge #6/egorbwork/simple-stupid-solution/cloud.html b/challenge #6/egorbwork/simple-stupid-solution/cloud.html new file mode 100644 index 0000000..0ae321e --- /dev/null +++ b/challenge #6/egorbwork/simple-stupid-solution/cloud.html @@ -0,0 +1,27 @@ + + + + + Cloud Generator + + + + + + + +
+ + + + +
+
+ +
+ To be able to see content, please use a modern version of Chrome or Firefox! +
+
+
+ +