diff --git a/package.json b/package.json index 9bf1ae9..9fb1506 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "start": "ts-node ./src/index.ts", "try": "ts-node ./src/try.ts", "example-ellipse": "ts-node ./src/example-ellipse.ts", + "example-magic-circle": "ts-node ./src/example-magic-circle.ts", + "example-circle-in-circle": "ts-node ./src/example-circle-in-circle.ts", "example-bezier": "ts-node ./src/example-bezier.ts", "example-rectangle": "ts-node ./src/example-rectangle.ts", "example-dark-vs-light": "ts-node ./src/example-dark-vs-light.ts", diff --git a/src/controller/circle.controller.ts b/src/controller/circle.controller.ts index 015b0cf..900aa10 100644 --- a/src/controller/circle.controller.ts +++ b/src/controller/circle.controller.ts @@ -8,6 +8,11 @@ export interface CircleConfig extends ElementConfig { parameters: { center: Coordinate r: number + angle?: { + startDegree?: number + endDegree?: number + } + counterClockwise?: boolean } } @@ -20,11 +25,22 @@ export class CircleController { circleConfig.parameters.center.x, circleConfig.parameters.center.y, circleConfig.parameters.r, - 0, - 2 * Math.PI, + circleConfig.parameters.angle && circleConfig.parameters.angle.startDegree + ? CircleController.toRadians(circleConfig.parameters.angle.startDegree) + : 0, + circleConfig.parameters.angle && circleConfig.parameters.angle.endDegree + ? CircleController.toRadians(circleConfig.parameters.angle.endDegree) + : 2 * Math.PI, + circleConfig.parameters.counterClockwise !== undefined + ? circleConfig.parameters.counterClockwise + : false, ) ElementController.postProcessing(config, circleConfig) return config } + + private static toRadians(degree: number): number { + return (degree * Math.PI) / 180 + } } diff --git a/src/controller/utils/circle.utils.ts b/src/controller/utils/circle.utils.ts new file mode 100644 index 0000000..acedf4d --- /dev/null +++ b/src/controller/utils/circle.utils.ts @@ -0,0 +1,24 @@ +/* eslint-disable id-length */ +import { Coordinate } from '../../types/Coordinate.type' + +export class CircleUtils { + // calculates a point on the outline of a circle based on + // center, radius and degree + public static pointOnCircleOutlineByAngle( + center: Coordinate, + radius: number, + degree: number, + ): Coordinate { + const coordinate: Coordinate = { + x: center.x + radius * Math.sin(degree), + y: center.y + radius * Math.cos(degree), + } + return coordinate + } + + // calculates angle in degrees to get a length on cicrle outline + public static angleForLengthOnPerimeterByRadius(radius: number): number { + const perimeter = 2 * Math.PI * radius + return (5 / perimeter) * 360 + } +} diff --git a/src/controller/utils/vector.utils.ts b/src/controller/utils/vector.utils.ts new file mode 100644 index 0000000..dd84472 --- /dev/null +++ b/src/controller/utils/vector.utils.ts @@ -0,0 +1,20 @@ +/* eslint-disable id-length */ +import { Coordinate } from '../../types/Coordinate.type' +import { DistanceController } from './distance.controller' + +export class VectorUtils { + public static lineWithLengthBetweenTwoPoints( + start: Coordinate, + end: Coordinate, + length: number, + ): { start: Coordinate; end: Coordinate } { + const distanceBetweenCoordinates = DistanceController.distance(start, end) + const factor = length / distanceBetweenCoordinates + + const calculatedEnd: Coordinate = { + x: start.x + factor * (end.x - Math.abs(start.x)), + y: start.y + factor * (end.y - Math.abs(start.y)), + } + return { start, end: calculatedEnd } + } +} diff --git a/src/example-circle-in-circle.ts b/src/example-circle-in-circle.ts new file mode 100644 index 0000000..6ca9abc --- /dev/null +++ b/src/example-circle-in-circle.ts @@ -0,0 +1,117 @@ +/* eslint-disable id-length */ +import * as path from 'path' +import { CAMUNDA, DAFTPUNK, SUNSET } from './constants/Colors.constants' +import { BackgroundController } from './controller/background.controller' +import { CanvasController } from './controller/canvas.controller' +import { CircleConfig, CircleController } from './controller/circle.controller' +import { OutputController } from './controller/output.controller' +import { CircleUtils } from './controller/utils/circle.utils' +import { ColorController } from './controller/utils/color.controller' +import { MathController } from './controller/utils/math.controller' +import { VectorUtils } from './controller/utils/vector.utils' +import { Element } from './enums/Element.enum' +import { Config } from './types/Config.type' +import { Coordinate } from './types/Coordinate.type' + +async function run() { + const colors = ColorController.randomAlpha( + ColorController.allHexToRgb([...CAMUNDA, ...SUNSET, ...DAFTPUNK]), + ) + + let config: Config = { + width: 1000, + height: 1000, + colors, + } + config = CanvasController.init(config) + config = BackgroundController.fill(config, '#0c0c0c') + + // params + + const radiusBigCircle = 200 + const centerX = config.height / 2 + const centerY = config.width / 2 + const numberOfInnerCircles = 100 + + // big circle + + const bigCircleConfig: CircleConfig = { + color: { + stroke: 'white', + }, + element: Element.CIRCLE, + closePath: false, + lineWidth: 2, + parameters: { + center: { + x: centerX, + y: centerY, + }, + r: radiusBigCircle, + }, + } + + config = CircleController.circle(config, bigCircleConfig) + + // inner circles + for (let i = 0; i < numberOfInnerCircles; i++) { + const smallCircleConfig = generateSmallCircleConfig( + centerX, + centerY, + radiusBigCircle, + colors, + ) + config = CircleController.circle(config, smallCircleConfig) + } + + await OutputController.save( + config, + path.resolve(__dirname, '..', 'output', 'circle-in-circle.png'), + ) +} + +function generateSmallCircleConfig( + centerX: number, + centerY: number, + radiusBigCircle: number, + colors?: string[], +): CircleConfig { + const randomRadiusSmallCircle = MathController.random( + 10, + radiusBigCircle * 0.8, + ) + const center: Coordinate = { + x: centerX, + y: centerY, + } + const randomDegree = MathController.random(0, 360) + const bigCircleCoordinate = CircleUtils.pointOnCircleOutlineByAngle( + center, + radiusBigCircle, + randomDegree, + ) + const lineCoordinates = VectorUtils.lineWithLengthBetweenTwoPoints( + bigCircleCoordinate, + center, + randomRadiusSmallCircle, + ) + const smallCircleCenter: Coordinate = lineCoordinates.end + + const smallCircleConfig: CircleConfig = { + color: { + stroke: colors + ? colors[MathController.random(0, colors.length - 1)] + : 'white', + }, + element: Element.CIRCLE, + closePath: false, + lineWidth: 2, + parameters: { + center: smallCircleCenter, + r: randomRadiusSmallCircle, + }, + } + return smallCircleConfig +} + +run() diff --git a/src/example-magic-circle.ts b/src/example-magic-circle.ts new file mode 100644 index 0000000..0b450f4 --- /dev/null +++ b/src/example-magic-circle.ts @@ -0,0 +1,122 @@ +/* eslint-disable id-length */ +import * as path from 'path' +import { CAMUNDA, DAFTPUNK, SUNSET } from './constants/Colors.constants' +import { BackgroundController } from './controller/background.controller' +import { CanvasController } from './controller/canvas.controller' +import { CircleController } from './controller/circle.controller' +import { OutputController } from './controller/output.controller' +import { CircleUtils } from './controller/utils/circle.utils' +import { ColorController } from './controller/utils/color.controller' +import { MathController } from './controller/utils/math.controller' +import { Element } from './enums/Element.enum' +import { Config } from './types/Config.type' +import { Coordinate } from './types/Coordinate.type' + +async function run() { + const colors = ColorController.randomAlpha( + ColorController.allHexToRgb([...CAMUNDA, ...SUNSET, ...DAFTPUNK]), + ) + + let config: Config = { + width: 1000, + height: 1000, + colors, + } + config = CanvasController.init(config) + config = BackgroundController.fill(config, '#0c0c0c') + + // params + + const likelihoodToShow = 100 + + const lineWidth = 3 + const radiusDifference = lineWidth + 3 + let circleSectionDifference = 5 + + let maxAngleDifference = 30 + + const center: Coordinate = { + x: config.height / 2, + y: config.width / 2, + } + + // overall index + let index = 0 + + // loop over radius + for ( + let radius = 10; + radius < config.width + 0.2 * config.width; + radius += radiusDifference + ) { + // update base parameters depending on index + maxAngleDifference = maxAngleDifferenceByRadius(radius) + // circleSectionDifference = circleSectionDifferenceByRadius(radius) + circleSectionDifference = CircleUtils.angleForLengthOnPerimeterByRadius( + radius, + ) + + // loop over circles per radius + let currentAngleStart = MathController.random(0, 360) + let currentAngle = MathController.random(0, maxAngleDifference) + let currentAngleEnd = currentAngleStart + currentAngle + let angleSum = currentAngle + while (angleSum < 360) { + const shouldDraw = + likelihoodToShow === 100 || + MathController.random(0, 100) <= likelihoodToShow + + if (shouldDraw) { + config = CircleController.circle(config, { + color: { + stroke: + config.colors[MathController.random(0, config.colors.length - 1)], + }, + element: Element.CIRCLE, + closePath: false, + lineWidth, + parameters: { + center, + r: radius, + angle: { + startDegree: currentAngleStart, + endDegree: currentAngleEnd, + }, + }, + }) + } + + // new angle calculation + const differenceToFull = 360 - angleSum + if (differenceToFull < maxAngleDifference + circleSectionDifference) { + currentAngle = differenceToFull - circleSectionDifference + } else { + currentAngle = MathController.random(0, maxAngleDifference) + } + currentAngleStart = currentAngleEnd + circleSectionDifference + currentAngleEnd = currentAngleStart + currentAngle + angleSum += circleSectionDifference + currentAngle + } + index++ + } + + await OutputController.save( + config, + path.resolve(__dirname, '..', 'output', 'magic-circle.png'), + ) +} + +function maxAngleDifferenceByRadius(radius: number): number { + if (radius < 100) { + return 90 + } else if (radius < 300) { + return 60 + } else if (radius < 600) { + return 45 + } else if (radius < 900) { + return 30 + } + return 20 +} + +run()