diff --git a/client/src/features/visualizer-threejs/CameraControls.tsx b/client/src/features/visualizer-threejs/CameraControls.tsx index c6f422394..a8353cac9 100644 --- a/client/src/features/visualizer-threejs/CameraControls.tsx +++ b/client/src/features/visualizer-threejs/CameraControls.tsx @@ -1,45 +1,33 @@ import { CameraControls as DreiCameraControls } from "@react-three/drei"; -import React, { useEffect } from "react"; +import { getCameraAngles } from "./utils"; +import React, { useEffect, useState } from "react"; import { useThree } from "@react-three/fiber"; import { CanvasElement } from "./enums"; -import { useConfigStore } from "./store"; +import { useTangleStore, useConfigStore } from "./store"; import { VISUALIZER_PADDINGS } from "./constants"; -import { getCameraAngles } from "./utils"; const CAMERA_ANGLES = getCameraAngles(); const CameraControls = () => { const controls = React.useRef(null); + const [shouldLockZoom, setShouldLockZoom] = useState(false); const scene = useThree((state) => state.scene); + const zoom = useTangleStore((state) => state.zoom); const mesh = scene.getObjectByName(CanvasElement.TangleWrapperMesh) as THREE.Mesh | undefined; const canvasDimensions = useConfigStore((state) => state.dimensions); - /** - * Locks the camera zoom to the current zoom value. - */ - function lockCameraZoom(controls: DreiCameraControls) { - const zoom = controls.camera.zoom; - controls.maxZoom = zoom; - controls.minZoom = zoom; - } - - /** - * Unlocks the camera zoom for free movement. - */ - function unlockCameraZoom(controls: DreiCameraControls) { - controls.maxZoom = Infinity; - controls.minZoom = 0.01; - } - /** * Fits the camera to the TangleMesh. */ function fitCameraToTangle(controls: DreiCameraControls | null, mesh?: THREE.Mesh) { if (controls && mesh) { - unlockCameraZoom(controls); + const previousZoom = controls.camera.zoom; + controls.minZoom = 0.01; + controls.maxZoom = Infinity; controls.fitToBox(mesh, false, { ...VISUALIZER_PADDINGS }); - lockCameraZoom(controls); + controls.minZoom = previousZoom; + controls.maxZoom = previousZoom; } } @@ -53,7 +41,9 @@ const CameraControls = () => { const camera = controls.current?.camera; const renderVerticalScene = canvasDimensions.width < canvasDimensions.height; const cameraUp: [number, number, number] = renderVerticalScene ? [1, 0, 0] : [0, 1, 0]; + setShouldLockZoom(false); camera.up.set(...cameraUp); + setShouldLockZoom(true); } }, [canvasDimensions, controls, mesh]); @@ -70,6 +60,16 @@ const CameraControls = () => { }; }, [controls, mesh]); + /** + * Locks the camera zoom to the current zoom value. + */ + useEffect(() => { + if (controls.current) { + controls.current.maxZoom = shouldLockZoom ? zoom : Infinity; + controls.current.minZoom = shouldLockZoom ? zoom : 0.01; + } + }, [controls.current, shouldLockZoom, zoom]); + return ; }; diff --git a/client/src/features/visualizer-threejs/Emitter.tsx b/client/src/features/visualizer-threejs/Emitter.tsx index fba26e0c7..b19a07765 100644 --- a/client/src/features/visualizer-threejs/Emitter.tsx +++ b/client/src/features/visualizer-threejs/Emitter.tsx @@ -4,7 +4,14 @@ import React, { RefObject, Dispatch, SetStateAction, useEffect, useRef, useLayou import * as THREE from "three"; import { useConfigStore, useTangleStore } from "./store"; import { useRenderTangle } from "./useRenderTangle"; -import { getTangleDistances, getEmitterPositions, generateRandomPeriods, generateRandomAmplitudes } from "./utils"; +import { + getTangleDistances, + getEmitterPositions, + generateRandomPeriods, + generateRandomAmplitudes, + generateRandomTiltings, + getCurrentTiltValue, +} from "./utils"; import { CanvasElement } from "./enums"; import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer"; import { EMITTER_DEPTH, EMITTER_HEIGHT, EMITTER_WIDTH } from "./constants"; @@ -36,14 +43,19 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte const randomSinusoidAmplitudes = useConfigStore((state) => state.randomSinusoidAmplitudes); const setRandomSinusoidAmplitudes = useConfigStore((state) => state.setRandomSinusoidAmplitudes); + const randomTilts = useConfigStore((state) => state.randomTilts); + const setRandomTilts = useConfigStore((state) => state.setRandomTilts); + const tangleWrapperRef = useRef(null); useLayoutEffect(() => { const { periods, sum: periodsSum } = generateRandomPeriods(); const amplitudes = generateRandomAmplitudes(); + const tiltings = generateRandomTiltings(); setSinusoidRandomPeriods(periods); setSinusoidPeriodsSum(periodsSum); setRandomSinusoidAmplitudes(amplitudes); + setRandomTilts(tiltings); }, []); useEffect(() => { @@ -68,6 +80,7 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte */ useFrame(() => { const currentAnimationTime = getVisualizerTimeDiff(); + const currentTilt = getCurrentTiltValue(currentAnimationTime, randomTilts); const { x, y } = getEmitterPositions({ currentAnimationTime, periods: sinusoidRandomPeriods, @@ -79,6 +92,7 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte if (emitterRef.current) { emitterRef.current.position.x = x; emitterRef.current.position.y = y; + emitterRef.current.rotation.z = THREE.MathUtils.degToRad(currentTilt); } if (tangleWrapperRef.current) { diff --git a/client/src/features/visualizer-threejs/VisualizerInstance.tsx b/client/src/features/visualizer-threejs/VisualizerInstance.tsx index 26f62570f..ff59a791d 100644 --- a/client/src/features/visualizer-threejs/VisualizerInstance.tsx +++ b/client/src/features/visualizer-threejs/VisualizerInstance.tsx @@ -24,13 +24,13 @@ import { Wrapper } from "./wrapper/Wrapper"; import { CanvasElement } from "./enums"; import { useGetThemeMode } from "~/helpers/hooks/useGetThemeMode"; import { TSelectFeedItemNova } from "~/app/types/visualizer.types"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { BasicBlockBody, Utils, type IBlockMetadata, type BlockState, type SlotIndex, type BlockId } from "@iota/sdk-wasm-nova/web"; +import { BasicBlockBody, Utils, type IBlockMetadata, type BlockState, type SlotIndex } from "@iota/sdk-wasm-nova/web"; import { IFeedBlockData } from "~/models/api/nova/feed/IFeedBlockData"; import CameraControls from "./CameraControls"; -import "./Visualizer.scss"; import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer"; import { getBlockInitPosition, getBlockTargetPosition } from "./blockPositions"; +import { getCurrentTiltValue } from "./utils"; +import "./Visualizer.scss"; const features = { statsEnabled: false, @@ -76,6 +76,7 @@ const VisualizerInstance: React.FC> = const sinusoidPeriodsSum = useConfigStore((s) => s.sinusoidPeriodsSum); const sinusoidRandomPeriods = useConfigStore((s) => s.sinusoidRandomPeriods); const sinusoidRandomAmplitudes = useConfigStore((s) => s.randomSinusoidAmplitudes); + const randomTilts = useConfigStore((state) => state.randomTilts); const selectedFeedItem: TSelectFeedItemNova = clickedInstanceId ? blockMetadata.get(clickedInstanceId) ?? null : null; const resetConfigState = useTangleStore((s) => s.resetConfigState); @@ -208,7 +209,8 @@ const VisualizerInstance: React.FC> = periodsSum: sinusoidPeriodsSum, sinusoidAmplitudes: sinusoidRandomAmplitudes, }); - const targetPosition = getBlockTargetPosition(initPosition, bps); + const blockTiltFactor = getCurrentTiltValue(currentAnimationTime, randomTilts); + const targetPosition = getBlockTargetPosition(initPosition, bps, blockTiltFactor); bpsCounter.addBlock(); @@ -227,11 +229,10 @@ const VisualizerInstance: React.FC> = if (blockWeakParents.length > 0) { addToEdgeQueue(blockData.blockId, blockWeakParents); } - addBlock({ id: blockData.blockId, color: PENDING_BLOCK_COLOR, - blockAddedTimestamp: getCurrentAnimationTime(), + blockAddedTimestamp: currentAnimationTime, targetPosition, initPosition, }); diff --git a/client/src/features/visualizer-threejs/blockPositions.ts b/client/src/features/visualizer-threejs/blockPositions.ts index a01f521a6..7f398e038 100644 --- a/client/src/features/visualizer-threejs/blockPositions.ts +++ b/client/src/features/visualizer-threejs/blockPositions.ts @@ -1,29 +1,22 @@ -import { EMITTER_WIDTH, EMITTER_X_POSITION_MULTIPLIER } from "./constants"; -import { ISinusoidalPositionParams } from "./interfaces"; -import { getEmitterPositions, getGenerateDynamicYZPosition, getTangleDistances, randomIntFromInterval } from "./utils"; +import { ISinusoidalPositionParams, IThreeDimensionalPosition } from "./interfaces"; +import { getEmitterPositions, getTangleDistances, getBlockPositionGenerator } from "./utils"; -const generateYZPositions = getGenerateDynamicYZPosition(); +const generateBlockTargetPosition = getBlockPositionGenerator(); -interface IPos { - x: number; - y: number; - z: number; +export function getBlockTargetPosition( + initPosition: IThreeDimensionalPosition, + bps: number, + tiltDegress: number, +): IThreeDimensionalPosition { + return generateBlockTargetPosition(bps, initPosition, tiltDegress); } -export function getBlockTargetPosition(initPosition: IPos, bps: number): IPos { - const { y, z } = generateYZPositions(bps, initPosition); - const emitterMinX = initPosition.x - EMITTER_WIDTH / 2; - const emitterMaxX = initPosition.x + EMITTER_WIDTH / 2; - - const minX = emitterMinX - (emitterMaxX - emitterMinX) * EMITTER_X_POSITION_MULTIPLIER; - const maxX = emitterMaxX + (emitterMaxX - emitterMinX) * EMITTER_X_POSITION_MULTIPLIER; - - const x = randomIntFromInterval(minX, maxX); - - return { x, y, z }; -} - -export function getBlockInitPosition({ currentAnimationTime, periods, periodsSum, sinusoidAmplitudes }: ISinusoidalPositionParams): IPos { +export function getBlockInitPosition({ + currentAnimationTime, + periods, + periodsSum, + sinusoidAmplitudes, +}: ISinusoidalPositionParams): IThreeDimensionalPosition { const { xTangleDistance } = getTangleDistances(); const { x: xEmitterPos, y, z } = getEmitterPositions({ currentAnimationTime, periods, periodsSum, sinusoidAmplitudes }); const x = xEmitterPos + xTangleDistance / 2; diff --git a/client/src/features/visualizer-threejs/constants.ts b/client/src/features/visualizer-threejs/constants.ts index cc588c7f8..6e78c4962 100644 --- a/client/src/features/visualizer-threejs/constants.ts +++ b/client/src/features/visualizer-threejs/constants.ts @@ -19,7 +19,6 @@ export const ZOOM_DEFAULT = 2; export const TIME_DIFF_COUNTER = 250; export const SECOND = 1000; export const DATA_SENDER_TIME_INTERVAL = 500; -export const ANIMATION_TIME_SECONDS = 3; // colors export const PENDING_BLOCK_COLOR = new Color("#A6C3FC"); @@ -35,7 +34,7 @@ export const BLOCK_STATE_TO_COLOR = new Map([ ]); // emitter -export const EMITTER_SPEED_MULTIPLIER = 80; +export const EMITTER_SPEED_MULTIPLIER = 150; export const EMITTER_PADDING_RIGHT = 150; export const VISUALIZER_SAFE_ZONE = 150; @@ -72,10 +71,10 @@ export const EMITTER_HEIGHT = 250; export const EMITTER_DEPTH = 250; // conic emitter -export const MIN_TANGLE_RADIUS = 100; -export const MAX_TANGLE_RADIUS = 300; +export const MIN_TANGLE_RADIUS = 200; +export const MAX_TANGLE_RADIUS = 600; -export const MIN_BLOCKS_PER_SECOND = 100; +export const MIN_BLOCKS_PER_SECOND = 150; export const MAX_BLOCKS_PER_SECOND = 250; export const MIN_BLOCK_NEAR_RADIUS = 20; @@ -83,7 +82,8 @@ export const MIN_BLOCK_NEAR_RADIUS = 20; export const MAX_POINT_RETRIES = 10; export const MAX_PREV_POINTS = 20; -export const EMITTER_X_POSITION_MULTIPLIER = 3; +export const SPRAY_DISTANCE = 400; +export const SPRAY_ANIMATION_DURATION = SPRAY_DISTANCE / EMITTER_SPEED_MULTIPLIER; /* Values for randomizing the tangle */ export const NUMBER_OF_RANDOM_PERIODS = 100; @@ -93,3 +93,8 @@ export const MAX_SINUSOID_PERIOD = 8; export const NUMBER_OF_RANDOM_AMPLITUDES = 100; export const MIN_SINUSOID_AMPLITUDE = 100; export const MAX_SINUSOID_AMPLITUDE = 200; + +export const NUMBER_OF_RANDOM_TILTINGS = 100; +export const TILT_DURATION_SECONDS = 4; +export const MAX_TILT_FACTOR_DEGREES = 16; +export const MIN_TILT_FACTOR_DEGREES = 1; diff --git a/client/src/features/visualizer-threejs/interfaces.ts b/client/src/features/visualizer-threejs/interfaces.ts index 81bae2dd5..732fce9b1 100644 --- a/client/src/features/visualizer-threejs/interfaces.ts +++ b/client/src/features/visualizer-threejs/interfaces.ts @@ -5,12 +5,21 @@ export interface ICameraAngles { maxAzimuthAngle: number; } +export interface ITwoDimensionalPosition { + x: number; + y: number; +} + export interface IThreeDimensionalPosition { x: number; y: number; z: number; } +export interface IThreeDimensionalPositionWithTilt extends IThreeDimensionalPosition { + tiltFactor: number; +} + export interface ITimeBasedPositionParams { currentAnimationTime: number; } diff --git a/client/src/features/visualizer-threejs/store/config.ts b/client/src/features/visualizer-threejs/store/config.ts index d505f5177..8c5d9ce9d 100644 --- a/client/src/features/visualizer-threejs/store/config.ts +++ b/client/src/features/visualizer-threejs/store/config.ts @@ -23,6 +23,9 @@ interface ConfigState { randomSinusoidAmplitudes: number[]; setRandomSinusoidAmplitudes: (randomizedAmplitudes: number[]) => void; + + randomTilts: number[]; + setRandomTilts: (randomTilts: number[]) => void; } export const useConfigStore = create((set) => ({ @@ -110,4 +113,15 @@ export const useConfigStore = create((set) => ({ randomSinusoidAmplitudes: randomizedAmplitudes, })); }, + + /** + * Randomized tilts for the tangle. + */ + randomTilts: [], + setRandomTilts: (randomTilts) => { + set((state) => ({ + ...state, + randomTilts, + })); + }, })); diff --git a/client/src/features/visualizer-threejs/store/tangle.ts b/client/src/features/visualizer-threejs/store/tangle.ts index 4fb7e376d..5044b9914 100644 --- a/client/src/features/visualizer-threejs/store/tangle.ts +++ b/client/src/features/visualizer-threejs/store/tangle.ts @@ -1,7 +1,7 @@ import { Color } from "three"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; -import { ZOOM_DEFAULT, ANIMATION_TIME_SECONDS } from "../constants"; +import { ZOOM_DEFAULT, EMITTER_SPEED_MULTIPLIER, SPRAY_DISTANCE } from "../constants"; import { IFeedBlockData } from "~models/api/nova/feed/IFeedBlockData"; import { IThreeDimensionalPosition } from "../interfaces"; import { BlockId, SlotIndex } from "@iota/sdk-wasm-nova/web"; @@ -104,7 +104,8 @@ export const useTangleStore = create()( }); for (const [key, value] of state.blockIdToAnimationPosition) { - if (value.elapsedTime > ANIMATION_TIME_SECONDS) { + const animationTime = SPRAY_DISTANCE / EMITTER_SPEED_MULTIPLIER; + if (value.elapsedTime > animationTime) { state.blockIdToAnimationPosition.delete(key); } } diff --git a/client/src/features/visualizer-threejs/useRenderTangle.tsx b/client/src/features/visualizer-threejs/useRenderTangle.tsx index 14f8c3b27..4da8ff476 100644 --- a/client/src/features/visualizer-threejs/useRenderTangle.tsx +++ b/client/src/features/visualizer-threejs/useRenderTangle.tsx @@ -1,7 +1,7 @@ -import { useFrame, useThree } from "@react-three/fiber"; -import { useEffect, useRef } from "react"; +import { useThree } from "@react-three/fiber"; +import { useEffect, useRef, useState } from "react"; import * as THREE from "three"; -import { ANIMATION_TIME_SECONDS, MAX_BLOCK_INSTANCES, NODE_SIZE_DEFAULT } from "./constants"; +import { SPRAY_ANIMATION_DURATION, MAX_BLOCK_INSTANCES, NODE_SIZE_DEFAULT } from "./constants"; import { useMouseMove } from "./hooks/useMouseMove"; import { IBlockState, IBlockAnimationPosition, useConfigStore, useTangleStore } from "./store"; import { useRenderEdges } from "./useRenderEdges"; @@ -15,7 +15,8 @@ const INITIAL_SPHERE_SCALE = 0.7; export const useRenderTangle = () => { const tangleMeshRef = useRef(new THREE.InstancedMesh(SPHERE_GEOMETRY, SPHERE_MATERIAL, MAX_BLOCK_INSTANCES)); - const objectIndexRef = useRef(0); + const [updateAnimationPositionQueue, setUpdateAnimationPositionQueue] = useState>(new Map()); + const objectIndexRef = useRef(1); const { scene } = useThree(); const isPlaying = useConfigStore((s) => s.isPlaying); @@ -28,6 +29,8 @@ export const useRenderTangle = () => { const blockIdToIndex = useTangleStore((s) => s.blockIdToIndex); const updateBlockIdToIndex = useTangleStore((s) => s.updateBlockIdToIndex); const blockIdToAnimationPosition = useTangleStore((s) => s.blockIdToAnimationPosition); + const updateBlockIdToAnimationPosition = useTangleStore((s) => s.updateBlockIdToAnimationPosition); + const getVisualizerTimeDiff = useVisualizerTimer(); const assignBlockToMesh = (block: IBlockState) => { @@ -42,10 +45,10 @@ export const useRenderTangle = () => { // Reuses old indexes when MAX_INSTANCES is reached // This also makes it so that old nodes are removed - if (objectIndexRef.current < MAX_BLOCK_INSTANCES - 1) { + if (objectIndexRef.current < MAX_BLOCK_INSTANCES) { objectIndexRef.current += 1; } else { - objectIndexRef.current = 0; + objectIndexRef.current = 1; } return block.id; @@ -146,33 +149,49 @@ export const useRenderTangle = () => { /** * Spray animation */ - useFrame(() => { - const isPlaying = useConfigStore.getState().isPlaying; - - if (!isPlaying) { - return; - } - - const blockIdToAnimationPosition = useTangleStore.getState().blockIdToAnimationPosition; - const updateBlockIdToAnimationPosition = useTangleStore.getState().updateBlockIdToAnimationPosition; - + useEffect(() => { const updatedAnimationPositions: Map = new Map(); + const updateAnimationPositionQueue: Map = new Map(); + const SPRAY_FRAMES_PER_SECOND = 24; + + const interval = setInterval(() => { + blockIdToAnimationPosition.forEach((properties, blockId) => { + const { initPosition, targetPosition, blockAddedTimestamp } = properties; + const currentAnimationTime = getVisualizerTimeDiff(); + const elapsedTime = currentAnimationTime - blockAddedTimestamp; + const animationAlpha = Math.min(elapsedTime / SPRAY_ANIMATION_DURATION, 1); + const targetPositionVector = new THREE.Vector3(); + + targetPositionVector.lerpVectors(positionToVector(initPosition), positionToVector(targetPosition), animationAlpha); + updatedAnimationPositions.set(blockId, { initPosition, elapsedTime, targetPosition, blockAddedTimestamp }); + + const index = blockIdToIndex.get(blockId); + if (index) { + if (isPlaying) { + updateInstancedMeshPosition(tangleMeshRef.current, index, targetPositionVector); + } else { + updateAnimationPositionQueue.set(index, targetPositionVector); + } + } + }); + }, 1000 / SPRAY_FRAMES_PER_SECOND); - blockIdToAnimationPosition.forEach(({ initPosition, targetPosition, blockAddedTimestamp }, blockId) => { - const currentAnimationTime = getVisualizerTimeDiff(); - const elapsedTime = currentAnimationTime - blockAddedTimestamp; - const positionBasedOnTime = Math.min(elapsedTime / ANIMATION_TIME_SECONDS, 1); - const targetPositionVector = new THREE.Vector3(); - - targetPositionVector.lerpVectors(positionToVector(initPosition), positionToVector(targetPosition), positionBasedOnTime); - updatedAnimationPositions.set(blockId, { initPosition, elapsedTime, targetPosition, blockAddedTimestamp }); + updateBlockIdToAnimationPosition(updatedAnimationPositions); + setUpdateAnimationPositionQueue(updateAnimationPositionQueue); - const index = blockIdToIndex.get(blockId); - if (index) { - updateInstancedMeshPosition(tangleMeshRef.current, index, targetPositionVector); - } - }); + return () => clearInterval(interval); + }, [blockIdToAnimationPosition, isPlaying]); - updateBlockIdToAnimationPosition(updatedAnimationPositions); - }); + /** + * Update animation positions after unpausing + */ + useEffect(() => { + if (isPlaying) { + updateAnimationPositionQueue.forEach((position, index) => { + updateInstancedMeshPosition(tangleMeshRef.current, index, position); + }); + updateAnimationPositionQueue.clear(); + setUpdateAnimationPositionQueue(updateAnimationPositionQueue); + } + }, [isPlaying, updateAnimationPositionQueue]); }; diff --git a/client/src/features/visualizer-threejs/utils.ts b/client/src/features/visualizer-threejs/utils.ts index bba6c9ec2..a77a28edd 100644 --- a/client/src/features/visualizer-threejs/utils.ts +++ b/client/src/features/visualizer-threejs/utils.ts @@ -1,13 +1,9 @@ -import { Vector3 } from "three"; +import { Vector3, MathUtils } from "three"; import { - BLOCK_STEP_PX, MIN_BLOCKS_PER_SECOND, MAX_BLOCKS_PER_SECOND, MIN_TANGLE_RADIUS, MAX_TANGLE_RADIUS, - MIN_BLOCK_NEAR_RADIUS, - MAX_PREV_POINTS, - MAX_POINT_RETRIES, MAX_BLOCK_INSTANCES, EMITTER_SPEED_MULTIPLIER, CAMERA_X_AXIS_MOVEMENT, @@ -20,8 +16,16 @@ import { NUMBER_OF_RANDOM_AMPLITUDES, MIN_SINUSOID_AMPLITUDE, MAX_SINUSOID_AMPLITUDE, + NUMBER_OF_RANDOM_TILTINGS, + TILT_DURATION_SECONDS, + SPRAY_DISTANCE, + MAX_PREV_POINTS, + MAX_POINT_RETRIES, + MIN_BLOCK_NEAR_RADIUS, + MIN_TILT_FACTOR_DEGREES, + MAX_TILT_FACTOR_DEGREES, } from "./constants"; -import type { ICameraAngles, ISinusoidalPositionParams, IThreeDimensionalPosition } from "./interfaces"; +import type { ICameraAngles, ISinusoidalPositionParams, IThreeDimensionalPosition, ITwoDimensionalPosition } from "./interfaces"; /** * Generates a random number within a specified range. @@ -69,16 +73,6 @@ interface IBlockTanglePosition { z: number; } -/** - * Calculates the distance between two points. - * @returns the distance between two points. - */ -function distanceBetweenPoints(point1: IBlockTanglePosition, point2: IBlockTanglePosition): number { - const { z: z1, y: y1 } = point1; - const { z: z2, y: y2 } = point2; - return Math.sqrt((y2 - y1) ** 2 + (z2 - z1) ** 2); -} - /** * Calculates the radius of the circle based on the blocks per second. * @returns the radius of the circle. @@ -94,28 +88,8 @@ function getLinearRadius(bps: number): number { return radius; } -/** - * Generates a random point on a circle. - * @returns the random point on a circle. - */ -function getDynamicRandomYZPoints( - bps: number, - initialPosition: IThreeDimensionalPosition = { - x: 0, - y: 0, - z: 0, - }, -): IBlockTanglePosition { - const theta = Math.random() * (2 * Math.PI); - - const maxRadius = getLinearRadius(bps); - const randomFactor = Math.random(); - const radius = randomFactor * maxRadius; - - const y = radius * Math.cos(theta) + initialPosition.y; - const z = radius * Math.sin(theta) + initialPosition.z; - - return { y, z }; +function distanceBetweenPoints(point1: IBlockTanglePosition, point2: IBlockTanglePosition): number { + return Math.sqrt(Math.pow(point1.y - point2.y, 2) + Math.pow(point1.z - point2.z, 2)); } /** @@ -130,6 +104,20 @@ function pointPassesAllChecks(point: IBlockTanglePosition, prevPoints: IBlockTan return prevPoints.some((prevPoint) => distanceBetweenPoints(point, prevPoint) > MIN_BLOCK_NEAR_RADIUS); } +export function getBlockPositionGenerator(): ( + bps: number, + initialPosition: IThreeDimensionalPosition, + tiltDegress: number, +) => IThreeDimensionalPosition { + const prevPoints: IBlockTanglePosition[] = []; + + return (bps: number, initialPosition: IThreeDimensionalPosition, tiltDegress: number) => { + const point = generateAValidRandomPoint(bps, initialPosition, prevPoints, tiltDegress); + prevPoints.push({ y: point.y, z: point.z }); + return point; + }; +} + /** * Retries to generate a point until it passes all the checks. * @returns the point that passes all the checks. @@ -138,18 +126,20 @@ function generateAValidRandomPoint( bps: number, initialPosition: IThreeDimensionalPosition, prevPoints: IBlockTanglePosition[], -): IBlockTanglePosition { - let trialPoint: IBlockTanglePosition; + tiltDegress: number, +): IThreeDimensionalPosition { + let trialPoint: IThreeDimensionalPosition; let passAllChecks = false; let retries = 0; do { - trialPoint = getDynamicRandomYZPoints(bps, initialPosition); + trialPoint = generateRandomXYZPoints(bps, initialPosition, tiltDegress); passAllChecks = pointPassesAllChecks(trialPoint, prevPoints); retries++; } while (!passAllChecks && retries < MAX_POINT_RETRIES); prevPoints.push(trialPoint); + if (prevPoints.length > MAX_PREV_POINTS) { prevPoints.shift(); } @@ -158,23 +148,30 @@ function generateAValidRandomPoint( } /** - * Gets a function to generate a random point on a circle. - * @returns the function to generate the random point on a circle. + * Generates a random point on a circle. + * @returns the random point on a circle. */ -export function getGenerateDynamicYZPosition(): typeof getDynamicRandomYZPoints { - const prevPoints: IBlockTanglePosition[] = []; - - return (bps: number, initialPosition: IThreeDimensionalPosition = { x: 0, y: 0, z: 0 }): IBlockTanglePosition => { - const validPoint = generateAValidRandomPoint(bps, initialPosition, prevPoints); +export function generateRandomXYZPoints( + bps: number, + initialPosition: IThreeDimensionalPosition, + tiltDegrees: number, +): IThreeDimensionalPosition { + const tiltRad = MathUtils.degToRad(-tiltDegrees); + const opposite = SPRAY_DISTANCE * Math.sin(tiltRad); + const adjacent = SPRAY_DISTANCE * Math.cos(tiltRad); + const circumferenceCenter: ITwoDimensionalPosition = { + x: initialPosition.x - adjacent, + y: initialPosition.y + opposite, + }; - const randomYNumber = randomNumberFromInterval(0, BLOCK_STEP_PX / 20); - const randomZNumber = randomNumberFromInterval(0, BLOCK_STEP_PX / 20); + const _radius = getLinearRadius(bps); + const randomFactor = Math.random(); + const radius = _radius * randomFactor; - validPoint.y += randomYNumber; - validPoint.z += randomZNumber; + const y = circumferenceCenter.y + radius * Math.cos(radius); + const z = initialPosition.z + radius * Math.sin(radius); - return validPoint; - }; + return { x: circumferenceCenter.x, y, z }; } /** @@ -343,5 +340,47 @@ export function generateRandomAmplitudes(): number[] { currentAmplitude = getNextAmplitudeWithVariation(currentAmplitude); amplitudes.push(currentAmplitude); } + return amplitudes; } + +export function generateRandomTiltings(): number[] { + let previousValue: number; + + const tilts: number[] = Array.from({ length: NUMBER_OF_RANDOM_TILTINGS }, () => { + let randomTilt = randomIntFromInterval(MIN_TILT_FACTOR_DEGREES, MAX_TILT_FACTOR_DEGREES); + + if ((previousValue < 0 && randomTilt < 0) || (previousValue > 0 && randomTilt > 0)) { + randomTilt *= -1; + } + + previousValue = randomTilt; + + return randomTilt; + }); + return tilts; +} + +export function getCurrentTiltValue(animationTime: number, tilts: number[]): number { + const tiltAnimationDuration = TILT_DURATION_SECONDS * 2; // Multiplied by 2 so it goes back to the initial position + const totalIntervalDuration = tilts.length * tiltAnimationDuration; // The total duration of the random tilts + + const currentTiltAnimationSeconds = animationTime % tiltAnimationDuration; + const currentAnimationSecondsInInterval = animationTime % totalIntervalDuration; + + const currentTiltIndex = Math.floor(currentAnimationSecondsInInterval / tiltAnimationDuration); + const tilt = tilts[currentTiltIndex]; + + // Calculate the proportion of the current animation time within the half-duration + const proportionOfHalfDuration = currentTiltAnimationSeconds / (tiltAnimationDuration / 2); + let currentTilt; + + if (currentTiltAnimationSeconds <= tiltAnimationDuration / 2) { + currentTilt = tilt * proportionOfHalfDuration; + } else { + // We subtract from 2 to reverse the effect after the peak + currentTilt = tilt * (2 - proportionOfHalfDuration); + } + + return currentTilt; +}