diff --git a/package-lock.json b/package-lock.json index 1389a32..60dd848 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5609,6 +5609,11 @@ "invert-kv": "^1.0.0" } }, + "line-intersect": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/line-intersect/-/line-intersect-2.2.1.tgz", + "integrity": "sha512-uOPErCqtEnHYsnesl56XmKm9nWF27kOqZCuibJEkyAJ23FHsKmOHo8FPT6SRAp2h3wzvyXJNjbPsq9FF5x29vw==" + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", diff --git a/package.json b/package.json index caedf60..1b2115b 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "dependencies": { "@eix-js/utils": "0.0.6", "deepmerge": "^4.0.0", + "line-intersect": "^2.2.1", "mainloop.js": "^1.0.4", "react": "^16.8.6", "react-dom": "^16.8.6", diff --git a/src/modules/core/classes/Screen.ts b/src/modules/core/classes/Screen.ts index 13e68f6..ac0f2cb 100644 --- a/src/modules/core/classes/Screen.ts +++ b/src/modules/core/classes/Screen.ts @@ -1,6 +1,7 @@ import { Singleton } from '@eix-js/utils' import { Observable, fromEvent, BehaviorSubject } from 'rxjs' import { map } from 'rxjs/operators' +import { multiply } from '../../vector2/helpers/basic' @Singleton export class Screen { @@ -25,4 +26,8 @@ export class Screen { public get y() { return this.height.value } + + public get center() { + return multiply([this.x, this.y], 0.5) + } } diff --git a/src/modules/simulation/classes/Gate.ts b/src/modules/simulation/classes/Gate.ts index 0637c98..8d930fc 100644 --- a/src/modules/simulation/classes/Gate.ts +++ b/src/modules/simulation/classes/Gate.ts @@ -1,10 +1,9 @@ -import { Transform, vector2 } from './Transform' +import { Transform } from './Transform' export class Gate { public static lastId = 0 public transform = new Transform() public id = Gate.lastId++ - public shadow: vector2 = [0, 0] public constructor(public color = 'blue') {} } diff --git a/src/modules/simulation/classes/SimulationRenderer.ts b/src/modules/simulation/classes/SimulationRenderer.ts index c35faa3..c45ff76 100644 --- a/src/modules/simulation/classes/SimulationRenderer.ts +++ b/src/modules/simulation/classes/SimulationRenderer.ts @@ -5,26 +5,17 @@ import { MouseEventInfo } from '../../core/components/FluidCanvas' import { pointInSquare } from '../helpers/pointInSquare' import { vector2 } from './Transform' import merge from 'deepmerge' -import { smoothStep } from '../../vector2/helpers/smoothStep' import { renderGate } from '../helpers/renderGate' import { renderGateShadow } from '../helpers/renderGateShadow' -import { Gate } from './Gate' import { MouseManager } from './MouseManager' import { Screen } from '../../core/classes/Screen' -import { - add, - invert, - ofLength, - length, - multiply -} from '../../vector2/helpers/basic' export interface SimulationRendererOptions { shadows: { enabled: boolean color: string - offset: number - speed: number + lightHeight: number + gateHeight: number } dnd: { rotation: number @@ -35,8 +26,8 @@ export const defaultSimulationRendererOptions: SimulationRendererOptions = { shadows: { enabled: true, color: 'rgba(0,0,0,0.3)', - offset: 15, - speed: 1 + gateHeight: 10, + lightHeight: 50 }, dnd: { rotation: Math.PI / 12 // 7.5 degrees @@ -133,13 +124,22 @@ export class SimulationRenderer { public render(ctx: CanvasRenderingContext2D) { this.clear(ctx) + const center = this.screen.center + // render gates for (const gate of this.simulation.gates) { + renderGate(ctx, gate) if (this.options.shadows.enabled) { - renderGateShadow(ctx, this.options.shadows.color, gate) + renderGateShadow( + ctx, + this.options.shadows.color, + gate, + this.options.shadows.gateHeight, + [center[0], center[1], this.options.shadows.lightHeight] + ) } - renderGate(ctx, gate) + // renderGate(ctx, gate) } } @@ -152,32 +152,7 @@ export class SimulationRenderer { return this.simulation.gates.get(id) } - public getOptimalShadow(gate: Gate) { - const center = multiply([this.screen.x, this.screen.y] as vector2, 0.5) - - const difference = add(center, invert(gate.transform.position)) - - return add( - add(difference, center), - ofLength(difference, this.options.shadows.offset) - ) - } - - public getShadowPosition(gate: Gate) { - return gate.transform.position.map( - (value, index) => value - this.getOptimalShadow(gate)[index] - ) as vector2 - } - public update(delta: number) { - for (const gate of this.simulation.gates) { - gate.shadow = smoothStep( - this.options.shadows.speed, - gate.shadow, - this.getShadowPosition(gate) - ) - } - const selected = this.getSelected() if (selected && this.movedSelection) { @@ -187,6 +162,10 @@ export class SimulationRenderer { } else { this.mouseManager.update() } + + // for (const gate of this.simulation.gates) { + // gate.transform.rotation += 0.01 + // } } public getSelected() { diff --git a/src/modules/simulation/classes/Transform.ts b/src/modules/simulation/classes/Transform.ts index ffb78f0..1f1c604 100644 --- a/src/modules/simulation/classes/Transform.ts +++ b/src/modules/simulation/classes/Transform.ts @@ -1,7 +1,20 @@ import { BehaviorSubject } from 'rxjs' +import { allCombinations } from '../helpers/allCombinations' +import { rotateAroundVector } from '../../vector2/helpers/rotate' export type vector2 = [number, number] +export type vector3 = [number, number, number] export type vector4 = [number, number, number, number] +export type vector8 = [ + number, + number, + number, + number, + number, + number, + number, + number +] export class Transform { public constructor( @@ -14,6 +27,40 @@ export class Transform { return [...this.position, ...this.scale] as vector4 } + public getPoints() { + const combinations = Array.from(allCombinations([0, 1], [0, 1])) + + // those are not in the right order + const points = combinations.map(combination => [ + this.x + this.height * combination[0], + this.y + this.width * combination[1] + ]) + + const pointsInTheRightOrder = [ + points[0], + points[1], + points[3], + points[2] + ] as vector2[] + + const result = pointsInTheRightOrder.map(point => + rotateAroundVector(point, this.center, this.rotation) + ) as vector2[] + + return result + } + + public getEdges() { + const points = this.getPoints() + const edges = [] + + for (let index = 0; index < points.length; index++) { + edges.push([points[index], points[(index + 1) % points.length]]) + } + + return edges as [vector2, vector2][] + } + /** Short forms for random stuff */ get x() { @@ -40,6 +87,10 @@ export class Transform { return this.y + this.height } + get center() { + return [this.x + this.width / 2, this.y + this.height / 2] as vector2 + } + set x(value: number) { this.position = [value, this.y] } diff --git a/src/modules/simulation/helpers/allCombinations.ts b/src/modules/simulation/helpers/allCombinations.ts new file mode 100644 index 0000000..eabdc4b --- /dev/null +++ b/src/modules/simulation/helpers/allCombinations.ts @@ -0,0 +1,8 @@ +export function* allCombinations(first: T[], second: T[]): Iterable<[T, T]> { + for (const item of first) { + // TODO: change name + for (const element of second) { + yield [item, element] + } + } +} diff --git a/src/modules/simulation/helpers/drawPolygon.ts b/src/modules/simulation/helpers/drawPolygon.ts new file mode 100644 index 0000000..af0d7e4 --- /dev/null +++ b/src/modules/simulation/helpers/drawPolygon.ts @@ -0,0 +1,23 @@ +import { vector2 } from '../classes/Transform' + +export const drawPolygon = ( + ctx: CanvasRenderingContext2D, + points: vector2[], + fill = true, + stroke = false +) => { + ctx.beginPath() + + for (const point of points) { + ctx.lineTo(...point) + } + + ctx.closePath() + + if (fill) { + ctx.fill() + } + if (stroke) { + ctx.stroke() + } +} diff --git a/src/modules/simulation/helpers/drawRotatedSquare.ts b/src/modules/simulation/helpers/drawRotatedSquare.ts index 00f77ad..54fd12b 100644 --- a/src/modules/simulation/helpers/drawRotatedSquare.ts +++ b/src/modules/simulation/helpers/drawRotatedSquare.ts @@ -2,30 +2,16 @@ import { Transform } from '../classes/Transform' export const drawRotatedSquare = ( ctx: CanvasRenderingContext2D, - { position, scale, rotation }: Transform, - rotationMode = 0 + { position, scale, rotation }: Transform ) => { ctx.save() ctx.translate(...position) - - if (rotationMode === 0) { - ctx.translate(scale[0] / 2, scale[1] / 2) - } else if (rotationMode === 1) { - ctx.translate(scale[0], scale[1]) - } else if (rotationMode === 1) { - ctx.translate(0, scale[1]) - } + ctx.translate(scale[0] / 2, scale[1] / 2) ctx.rotate(rotation) - if (rotationMode === 0) { - ctx.fillRect(scale[0] / -2, scale[1] / -2, ...scale) - } else if (rotationMode === 1) { - ctx.fillRect(-scale[0], -scale[1], ...scale) - } else if (rotationMode === -1) { - ctx.fillRect(0, 0, ...scale) - } + ctx.fillRect(scale[0] / -2, scale[1] / -2, ...scale) ctx.restore() } diff --git a/src/modules/simulation/helpers/projectPoint.ts b/src/modules/simulation/helpers/projectPoint.ts new file mode 100644 index 0000000..8008429 --- /dev/null +++ b/src/modules/simulation/helpers/projectPoint.ts @@ -0,0 +1,8 @@ +import { vector3, vector2 } from '../classes/Transform' + +export const projectPointOnPlane = (point: vector3, light: vector3) => + point.slice(0, 2).map((position, index) => { + const delta = light[index] - position + + return light[index] - (delta + (point[2] * delta) / light[2]) + }) as vector2 diff --git a/src/modules/simulation/helpers/renderGate.ts b/src/modules/simulation/helpers/renderGate.ts index 63509ae..e431053 100644 --- a/src/modules/simulation/helpers/renderGate.ts +++ b/src/modules/simulation/helpers/renderGate.ts @@ -1,13 +1,7 @@ import { Gate } from '../classes/Gate' import { drawRotatedSquare } from './drawRotatedSquare' -import { MouseManager } from '../classes/MouseManager' export const renderGate = (ctx: CanvasRenderingContext2D, gate: Gate) => { - let mode = 0 - - if (gate.transform.rotation > 0) mode = 1 - else if (gate.transform.rotation < 0) mode = -1 - ctx.fillStyle = gate.color - drawRotatedSquare(ctx, gate.transform, mode) + drawRotatedSquare(ctx, gate.transform) } diff --git a/src/modules/simulation/helpers/renderGateShadow.ts b/src/modules/simulation/helpers/renderGateShadow.ts index a6567ca..7a46844 100644 --- a/src/modules/simulation/helpers/renderGateShadow.ts +++ b/src/modules/simulation/helpers/renderGateShadow.ts @@ -1,19 +1,110 @@ import { Gate } from '../classes/Gate' -import { vector2, Transform } from '../classes/Transform' -import { clamp } from './clamp' -import { drawRotatedSquare } from './drawRotatedSquare' +import { projectPointOnPlane } from './projectPoint' +import { drawPolygon } from './drawPolygon' +import { vector3, vector2, vector4, vector8 } from '../classes/Transform' +import { checkIntersection } from 'line-intersect' +import { reverseArray } from './reverseArray' +import { length, add, invert } from '../../vector2/helpers/basic' + +export const pointRecivesLight = ( + points: vector2[], //this needs to have an even length + light: vector3, + index: number, + ctx: CanvasRenderingContext2D +) => { + const point = points[index] + const oposittePoint = points[(index + points.length / 2) % points.length] + + const edgesToCheck = [ + [oposittePoint, points[(index + 1) % points.length]], + [oposittePoint, points[index === 0 ? points.length - 1 : index - 1]] + ].map(points => points.flat() as vector4) + + for (const edge of edgesToCheck) { + const intersectionCheckParameters = [ + [light[0], light[1]], + point, + edge + ].flat() as vector8 + + const result = checkIntersection(...intersectionCheckParameters).type + + if (result === 'intersecting') { + return false + } + } + + return true +} export const renderGateShadow = ( ctx: CanvasRenderingContext2D, color: string, - gate: Gate + gate: Gate, + gateHeight: number, + light: vector3 ) => { - const scale = gate.transform.scale - ctx.fillStyle = color - drawRotatedSquare( - ctx, - new Transform(gate.shadow, scale, gate.transform.rotation) + const points = gate.transform.getPoints() + const exposedPoints = points.filter((point, index) => + pointRecivesLight(points, light, index, ctx) ) + + let includedPoints = [...points] + + if (exposedPoints.length === 3) { + let min = Infinity + let current: null | vector2 = null + + for (const point of exposedPoints) { + const size = length( + add(point, invert(light.slice(0, 2) as vector2)) + ) + + if (size < min) { + min = size + current = point + } + } + + if (current) { + includedPoints.splice(includedPoints.indexOf(current), 1) + } + + if ( + includedPoints[0][1] < light[1] && + includedPoints[1][1] < light[1] && + !(includedPoints[2][1] > light[1]) + ) { + const temporary = includedPoints[0] + includedPoints[0] = includedPoints[1] + includedPoints[1] = temporary + } + } + + if (exposedPoints.length === 2) { + includedPoints = points.filter( + point => exposedPoints.indexOf(point) === -1 + ) + } + + const projections = includedPoints.map(point => + // ts doesnt let me do [...point, gateHeight] + projectPointOnPlane([point[0], point[1], gateHeight], light) + ) + + const polygon = [includedPoints, reverseArray(projections)].flat() + + drawPolygon(ctx, polygon) + + ctx.fillStyle = 'red' + for (const point of [...includedPoints, ...projections, light]) { + ctx.beginPath() + ctx.ellipse(point[0], point[1], 10, 10, 0, 0, Math.PI * 2) + ctx.fill() + } + + ctx.strokeStyle = 'yellow' + drawPolygon(ctx, points, false, true) } diff --git a/src/modules/simulation/helpers/reverseArray.ts b/src/modules/simulation/helpers/reverseArray.ts new file mode 100644 index 0000000..f7bd425 --- /dev/null +++ b/src/modules/simulation/helpers/reverseArray.ts @@ -0,0 +1,10 @@ +export const reverseArray = (array: T[]) => { + const arr: T[] = [] + + for (let index = array.length - 1; index >= 0; index--) { + const element = array[index] + arr.push(element) + } + + return arr +} diff --git a/src/modules/vector2/helpers/basic.ts b/src/modules/vector2/helpers/basic.ts index 0f8fc46..c3cf2e9 100644 --- a/src/modules/vector2/helpers/basic.ts +++ b/src/modules/vector2/helpers/basic.ts @@ -2,8 +2,15 @@ import { vector2 } from '../../simulation/classes/Transform' // Basic stuff for arrays -export const add = (first: vector2, second: vector2) => - first.map((value, index) => value + second[index]) as vector2 +// If i don't say vector2 as the type adnotation +// ts will throw some errors (because this is recursive) +export const add = (...vectors: vector2[]): vector2 => { + const first = vectors[0] + const others = vectors.slice(1) + const othersSum = others.length > 1 ? add(...others) : others[0] + + return first.map((value, index) => value + othersSum[index]) as vector2 +} export const invert = (vector: vector2) => vector.map(val => -val) as vector2 diff --git a/src/modules/vector2/helpers/rotate.ts b/src/modules/vector2/helpers/rotate.ts new file mode 100644 index 0000000..5d465fd --- /dev/null +++ b/src/modules/vector2/helpers/rotate.ts @@ -0,0 +1,22 @@ +import { vector2 } from '../../simulation/classes/Transform' +import { add, invert } from './basic' + +const { cos, sin } = Math + +export const rotate = (vector: vector2, angle: number): vector2 => { + const x = cos(angle) * vector[0] - sin(angle) * vector[1] + const y = sin(angle) * vector[0] + cos(angle) * vector[1] + + return [x, y] +} + +export const rotateAroundVector = ( + vector: vector2, + around: vector2, + angle: number +) => { + const translated = add(vector, invert(around)) + const rotated = rotate(translated, angle) + + return add(rotated, around) +} diff --git a/tsconfig.json b/tsconfig.json index 20fb5c4..56cbcf8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { + "baseUrl": ".", + "paths": { "*": ["types/*"] }, "moduleResolution": "node", "esModuleInterop": true, "jsx": "preserve", diff --git a/types/line-intersect.d.ts b/types/line-intersect.d.ts new file mode 100644 index 0000000..899d62d --- /dev/null +++ b/types/line-intersect.d.ts @@ -0,0 +1,13 @@ +export const checkIntersection: ( + x1: number, + y1: number, + x2: number, + y2: number, + x3: number, + y3: number, + x4: number, + y4: number +) => { + type: 'colinear' | 'parallel' | 'none' | 'intersecting' +} +export const colinearPointWithinSegment: any