diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/public/404.svg b/public/404.svg new file mode 100644 index 0000000..d4bd90e --- /dev/null +++ b/public/404.svg @@ -0,0 +1,9 @@ + + + Media/Text Copia + + + 404 + + + \ No newline at end of file diff --git a/public/bimi.svg b/public/bimi.svg new file mode 100644 index 0000000..af7dc2c --- /dev/null +++ b/public/bimi.svg @@ -0,0 +1,20 @@ + + + bimi-svg-tiny-12-ps + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/layout.jsx b/src/app/layout.jsx index 5030cfc..460c292 100644 --- a/src/app/layout.jsx +++ b/src/app/layout.jsx @@ -26,11 +26,11 @@ import Head from "@semantyk/frontend/ui/components/atoms/Head"; import Body from "@semantyk/frontend/ui/components/molecules/Body"; import { getLang } from "@semantyk/frontend/logic/services/getLang"; import Content from "@semantyk/frontend/ui/components/molecules/Content"; -import Model from "@semantyk/frontend/ui/models/atoms/Model"; +import Model from "@semantyk/frontend/ui/components/molecules/Model/Model"; //* Main -export async function generateMetadata() {return await getMetadata();} +export async function generateMetadata() { return await getMetadata(); } export default function RootLayout({ children }) { // Logic @@ -39,13 +39,13 @@ export default function RootLayout({ children }) { return ( // TODO: Add logic for dynamic language - - - - - {children} - - + + + + + {children} + + ); } \ No newline at end of file diff --git a/src/frontend/ui/components/atoms/Head.jsx b/src/frontend/ui/components/atoms/Head.jsx index 2bfad07..0c880a8 100644 --- a/src/frontend/ui/components/atoms/Head.jsx +++ b/src/frontend/ui/components/atoms/Head.jsx @@ -22,7 +22,5 @@ import Analytics from "@semantyk/frontend/logic/analytics/Analytics"; //* Main export default function Head() { // Return - return (<> - - ); + return ; } \ No newline at end of file diff --git a/src/frontend/ui/models/atoms/Canvas/index.css b/src/frontend/ui/components/molecules/Canvas/index.css similarity index 100% rename from src/frontend/ui/models/atoms/Canvas/index.css rename to src/frontend/ui/components/molecules/Canvas/index.css diff --git a/src/frontend/ui/models/atoms/Canvas/index.jsx b/src/frontend/ui/components/molecules/Canvas/index.jsx similarity index 92% rename from src/frontend/ui/models/atoms/Canvas/index.jsx rename to src/frontend/ui/components/molecules/Canvas/index.jsx index ff58031..a1bd7b3 100644 --- a/src/frontend/ui/models/atoms/Canvas/index.jsx +++ b/src/frontend/ui/components/molecules/Canvas/index.jsx @@ -19,7 +19,7 @@ //* Imports import React from "react"; //* Local Imports -import CanvasLayout from "@semantyk/frontend/ui/models/atoms/Canvas/layout"; +import CanvasLayout from "@semantyk/frontend/ui/components/molecules/Canvas/layout"; //* Main export default function Canvas({ children }) { diff --git a/src/frontend/ui/models/atoms/Canvas/layout.jsx b/src/frontend/ui/components/molecules/Canvas/layout.jsx similarity index 93% rename from src/frontend/ui/models/atoms/Canvas/layout.jsx rename to src/frontend/ui/components/molecules/Canvas/layout.jsx index b0f8f69..b9cea6d 100644 --- a/src/frontend/ui/models/atoms/Canvas/layout.jsx +++ b/src/frontend/ui/components/molecules/Canvas/layout.jsx @@ -18,7 +18,7 @@ import React from "react"; import { Canvas } from "@react-three/fiber"; //* Local Imports -import "@semantyk/frontend/ui/models/atoms/Canvas/index.css"; +import "@semantyk/frontend/ui/components/molecules/Canvas/index.css"; //* Main export default function CanvasLayout(props) { diff --git a/src/frontend/ui/components/molecules/Model/Model.jsx b/src/frontend/ui/components/molecules/Model/Model.jsx new file mode 100644 index 0000000..aacf6d3 --- /dev/null +++ b/src/frontend/ui/components/molecules/Model/Model.jsx @@ -0,0 +1,49 @@ +/** + * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– + * # `index.jsx` + * @organization: Semantyk + * @project: Client + * + * @file: This file contains the logic for a generic Three.js model component. + * + * @created: Jul 17, 2024 + * @modified: Mar 12, 2025 + * + * @author: Semantyk Team + * @maintainer: Daniel Bakas + * + * @copyright: Semantyk © 2025. All rights reserved. + * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– + */ + +"use client"; + +//* Imports +import React from "react"; +import Canvas from "@semantyk/frontend/ui/components/molecules/Canvas"; +import GraphModel from "@semantyk/frontend/ui/models/Graph"; +import { usePathname, useRouter } from "next/navigation"; +import Particles from "@semantyk/frontend/ui/models/Particles/Particles"; + +//* Main +export default function Model() { + // Hooks + const pathname = usePathname(); + // Logic + const model = () => { + switch (pathname) { + case "/": + return ; + case "/knowledge": + return ; + default: + return ; + } + }; + // Return + return ( + + {model()} + + ); +} \ No newline at end of file diff --git a/src/frontend/ui/components/molecules/Model/logic/manager.js b/src/frontend/ui/components/molecules/Model/logic/manager.js new file mode 100644 index 0000000..07dcdb0 --- /dev/null +++ b/src/frontend/ui/components/molecules/Model/logic/manager.js @@ -0,0 +1,27 @@ +/** + * Base manager class for models using the Strategy pattern + */ +export class ModelManager { + constructor() { + if (this.constructor === ModelManager) { + throw new Error('Abstract class ModelManager cannot be instantiated directly'); + } + } + + static call(object, member, ...args) { + if (!object) return; + else object[member](...args); + } + + static execute(collection, item, member, ...args) { + const object = collection[item]; + if (!object) return; + else this.call(object, member, ...args); + } + + static executeAll(collection, member, ...args) { + Object.values(collection).forEach(object => { + this.call(object, member, ...args); + }); + } +} \ No newline at end of file diff --git a/src/frontend/ui/components/molecules/Model/logic/strategy.js b/src/frontend/ui/components/molecules/Model/logic/strategy.js new file mode 100644 index 0000000..4c8e189 --- /dev/null +++ b/src/frontend/ui/components/molecules/Model/logic/strategy.js @@ -0,0 +1,40 @@ +/** + * Base class for model strategies following the Strategy Pattern + */ +export class ModelStrategy { + /** + * Add the event listener + * @param {Object} args - Event arguments + * @throws {Error} Must be implemented by child classes + */ + static add(args) { + throw new Error('Strategy must implement add method'); + } + + /** + * Handle the event + * @param {Object} args - Event arguments + * @throws {Error} Must be implemented by child classes + */ + static handle(args) { + throw new Error('Strategy must implement handle method'); + } + + /** + * Execute the strategy + * @param {Object} args - Strategy arguments + * @throws {Error} Must be implemented by child classes + */ + static execute(args) { + throw new Error('Strategy must implement execute method'); + } + + /** + * Remove the event listener + * @param {Object} args - Event arguments + * @throws {Error} Must be implemented by child classes + */ + static remove(args) { + throw new Error('Strategy must implement remove method'); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/atoms/Model/index.jsx b/src/frontend/ui/models/Graph/index.jsx similarity index 58% rename from src/frontend/ui/models/atoms/Model/index.jsx rename to src/frontend/ui/models/Graph/index.jsx index 230a9f9..e2a95ba 100644 --- a/src/frontend/ui/models/atoms/Model/index.jsx +++ b/src/frontend/ui/models/Graph/index.jsx @@ -1,13 +1,13 @@ /** * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - * # `index.jsx` + * # `index.jsx` | `GraphModel` * @organization: Semantyk * @project: Client * - * @file: This file contains the logic for a generic Three.js model component. + * @file: This file contains the logic for the Graph model. * - * @created: Jul 17, 2024 - * @modified: Mar 7, 2025 + * @created: Mar 13, 2025 + * @modified: Mar 13, 2025 * * @author: Semantyk Team * @maintainer: Daniel Bakas @@ -16,19 +16,6 @@ * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ -"use client"; - -//* Imports -import React from "react"; -import Canvas from "@semantyk/frontend/ui/models/atoms/Canvas"; -import ParticlesModel from "@semantyk/frontend/ui/models/molecule/Particles"; - //* Main -export default function Model() { - // Return - return ( - - - - ); +export default function GraphModel() { }; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/Particles.jsx b/src/frontend/ui/models/Particles/Particles.jsx new file mode 100644 index 0000000..ee68122 --- /dev/null +++ b/src/frontend/ui/models/Particles/Particles.jsx @@ -0,0 +1,34 @@ +/** + * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– + * # `ParticlesScene.jsx` + * @organization: Semantyk + * @project: Client + * + * @file: This file contains the logic for the ParticlesScene component. + * + * @created: Mar 13, 2025 + * @modified: Mar 13, 2025 + * + * @author: Semantyk Team + * @maintainer: Daniel Bakas + * + * @copyright: Semantyk © 2025. All rights reserved. + * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– + */ + +//* Imports +import { useArgs } from "@semantyk/frontend/ui/models/Particles/hooks/useArgs"; +import { Controls } from "./components/molecules"; +import { System } from "./components/organisms"; + + +//* Main +export default function Particles({ path }) { + // Hooks + const args = useArgs({ path }); + // Return + return (<> + + + ); +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/Particles.logic.js b/src/frontend/ui/models/Particles/Particles.logic.js new file mode 100644 index 0000000..23edd40 --- /dev/null +++ b/src/frontend/ui/models/Particles/Particles.logic.js @@ -0,0 +1,50 @@ +import { ModelManager } from '@semantyk/frontend/ui/components/molecules/Model/logic/manager'; +import { Camera, Circle, Mouse, Plane, Raycaster, RayLine } from './components/atoms'; +import ParticlesLogic from './components/molecules/Particles/Particles.logic'; +/** + * Manager class for particle strategies using the Strategy pattern + */ +export class ParticlesManager extends ModelManager { + static logic = { + camera: Camera.logic, + circle: Circle.logic, + mouse: Mouse.logic, + plane: Plane.logic, + particles: ParticlesLogic, // TODO: Improve this fix + raycaster: Raycaster.logic, + rayLine: RayLine.logic + } + + static handlers = { + mouseMove: Mouse.logic + }; + + static listeners = { + mouse: Mouse.logic + }; + + static handle(item, ...args) { + const collection = this.handlers; + return super.execute(collection, item, 'handle', ...args); + } + + static setup(item, ...args) { + const collection = this.logic; + return super.execute(collection, item, 'setup', ...args); + } + + static update(item, ...args) { + const collection = this.logic; + return super.execute(collection, item, 'update', ...args); + } + + static addAll(...args) { + const collection = this.listeners; + return super.executeAll(collection, "add", ...args); + } + + static removeAll(...args) { + const collection = this.listeners; + return super.executeAll(collection, "remove", ...args); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Box/Box.jsx b/src/frontend/ui/models/Particles/components/atoms/Box/Box.jsx new file mode 100644 index 0000000..b8210d0 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Box/Box.jsx @@ -0,0 +1,22 @@ +/** + * Box.jsx + * Atom component for the box in the Particles model + */ + +//* Main +export default function Box({ config, data, refs }) { + // Props + const { general: { showControls } } = config; + // Return + return ( + + + + + ); +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Box/index.js b/src/frontend/ui/models/Particles/components/atoms/Box/index.js new file mode 100644 index 0000000..6f296f0 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Box/index.js @@ -0,0 +1,2 @@ +export { default } from './Box'; +export { default as Box } from './Box'; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Camera/Camera.jsx b/src/frontend/ui/models/Particles/components/atoms/Camera/Camera.jsx new file mode 100644 index 0000000..4dd7677 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Camera/Camera.jsx @@ -0,0 +1,28 @@ +/** + * Camera.jsx + * Atom component for the perspective camera in the Particles model + */ + +import { PerspectiveCamera } from "@react-three/drei"; +import { CameraHelper } from "three"; +import { useHelper } from "@react-three/drei"; +import CameraLogic from "./Camera.logic"; +function Camera({ config, refs }) { + // Props + const { + general: { showControls } + } = config; + // Logic + useHelper(showControls && refs.camera, CameraHelper); + + return ( + + ); +} + +Camera.logic = CameraLogic; + +export default Camera; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Camera/Camera.logic.js b/src/frontend/ui/models/Particles/components/atoms/Camera/Camera.logic.js new file mode 100644 index 0000000..e92dc39 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Camera/Camera.logic.js @@ -0,0 +1,16 @@ +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; + +export default class CameraLogic extends ModelStrategy { + static setup({ config, data: { unit }, refs: { camera } }) { + const { camera: { margin } } = config; + + const aspectRatio = window.innerWidth / window.innerHeight; + let x = (1 + margin) / ((aspectRatio >= 1) ? 2 : (2 * aspectRatio)); + const fx = 2 * Math.atan(x) * (180 / Math.PI); + + camera.current.fov = fx; + camera.current.aspect = aspectRatio; + camera.current.position.z = unit / 2; + camera.current.updateProjectionMatrix(); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Camera/index.js b/src/frontend/ui/models/Particles/components/atoms/Camera/index.js new file mode 100644 index 0000000..7247c9a --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Camera/index.js @@ -0,0 +1,2 @@ +export { default } from './Camera'; +export { default as Camera } from './Camera'; diff --git a/src/frontend/ui/models/Particles/components/atoms/Circle/Circle.jsx b/src/frontend/ui/models/Particles/components/atoms/Circle/Circle.jsx new file mode 100644 index 0000000..fd05603 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Circle/Circle.jsx @@ -0,0 +1,35 @@ +/** + * Circle.jsx + * Atom component for the circle in the Particles model + */ + +//* Imports +import CircleLogic from "./Circle.logic"; +//* Main +function Circle({ config, data, refs }) { + // Props + const { + general: { showControls }, + animations: { chaos: { radius } } + } = config; + // Return + return ( + + + + + ); +} + +Circle.logic = CircleLogic; + +export default Circle; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Circle/Circle.logic.js b/src/frontend/ui/models/Particles/components/atoms/Circle/Circle.logic.js new file mode 100644 index 0000000..86a6ac9 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Circle/Circle.logic.js @@ -0,0 +1,8 @@ +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; + +export default class CircleLogic extends ModelStrategy { + static update({ objects: { raycaster }, refs: { circle, plane }, target }) { + raycaster.ray.intersectPlane(plane.current, target); + circle.current.position.copy(target); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Circle/index.js b/src/frontend/ui/models/Particles/components/atoms/Circle/index.js new file mode 100644 index 0000000..fefad1e --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Circle/index.js @@ -0,0 +1,2 @@ +export { default } from './Circle'; +export { default as Circle } from './Circle'; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Mouse/Mouse.js b/src/frontend/ui/models/Particles/components/atoms/Mouse/Mouse.js new file mode 100644 index 0000000..5a5e3d6 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Mouse/Mouse.js @@ -0,0 +1,5 @@ +import MouseLogic from './Mouse.logic'; + +const Mouse = { logic: MouseLogic }; + +export default Mouse; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Mouse/Mouse.logic.js b/src/frontend/ui/models/Particles/components/atoms/Mouse/Mouse.logic.js new file mode 100644 index 0000000..6279b57 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Mouse/Mouse.logic.js @@ -0,0 +1,45 @@ +import { onMouseMove } from "@semantyk/frontend/logic/services/callbacks"; +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; +import { Vector3 } from "three"; +import { ParticlesManager } from "../../../Particles.logic"; + +export default class MouseLogic extends ModelStrategy { + static add({ handleMouseMove }) { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("touchmove", handleMouseMove); + } + + static remove({ handleMouseMove }) { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("touchmove", handleMouseMove); + } + + static handle({ event, ...args }) { + const { mouse, moveMouseTimeout } = args.refs; + clearTimeout(moveMouseTimeout.current); + mouse.current.isMoving = true; + moveMouseTimeout.current = setTimeout(() => mouse.current.isMoving = false, 1); + + let clientX, clientY; + if (event.type === "mousemove") { + clientX = event.clientX; + clientY = event.clientY; + } else if (event.type === "touchmove") { + clientX = event.touches[0].clientX; + clientY = event.touches[0].clientY; + } + + const target = new Vector3() + const events = { mousemove: { clientX, clientY } } + ParticlesManager.update("circle", { events, target, ...args }); + ParticlesManager.update("rayLine", { events, target, ...args }); + ParticlesManager.update("mouse", { events, ...args }); + ParticlesManager.update("raycaster", { events, ...args }); + } + + static update({ refs, events }) { + const { x, y } = onMouseMove(events.mousemove); + refs.mouse.current.x = x * 2 - 1; + refs.mouse.current.y = -y * 2 + 1; + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Mouse/index.js b/src/frontend/ui/models/Particles/components/atoms/Mouse/index.js new file mode 100644 index 0000000..a2301c1 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Mouse/index.js @@ -0,0 +1,2 @@ +export { default } from './Mouse'; +export { default as Mouse } from './Mouse'; diff --git a/src/frontend/ui/models/Particles/components/atoms/Plane/Plane.jsx b/src/frontend/ui/models/Particles/components/atoms/Plane/Plane.jsx new file mode 100644 index 0000000..505c9c3 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Plane/Plane.jsx @@ -0,0 +1,30 @@ +//* Imports +import PlaneLogic from "./Plane.logic"; + +//* Main +function Plane({ config, data, refs }) { + // Props + const { + general: { showControls } + } = config; + // Return + return ( + + + + + ); +} + +Plane.logic = PlaneLogic; + +export default Plane; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Plane/Plane.logic.js b/src/frontend/ui/models/Particles/components/atoms/Plane/Plane.logic.js new file mode 100644 index 0000000..b98e6b8 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Plane/Plane.logic.js @@ -0,0 +1,9 @@ +import { Plane, Vector3 } from "three"; +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; + +export default class PlaneLogic extends ModelStrategy { + static setup({ data: { unit }, refs: { plane } }) { + const normal = new Vector3(0, 0, 1); + plane.current = new Plane(normal, unit / 2); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Plane/index.js b/src/frontend/ui/models/Particles/components/atoms/Plane/index.js new file mode 100644 index 0000000..c83936e --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Plane/index.js @@ -0,0 +1,2 @@ +export { default } from './Plane'; +export { default as Plane } from './Plane'; diff --git a/src/frontend/ui/models/Particles/components/atoms/RayLine/RayLine.jsx b/src/frontend/ui/models/Particles/components/atoms/RayLine/RayLine.jsx new file mode 100644 index 0000000..b219533 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/RayLine/RayLine.jsx @@ -0,0 +1,26 @@ +/** + * RayLine.jsx + * Atom component for the ray line in the Particles model + */ + +//* Imports +import RayLineLogic from './RayLine.logic'; + +//* Main +function RayLine({ config, refs }) { + // Props + const { + general: { showControls } + } = config; + // Return + return ( + + + + + ); +} + +RayLine.logic = RayLineLogic; + +export default RayLine; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/RayLine/RayLine.logic.js b/src/frontend/ui/models/Particles/components/atoms/RayLine/RayLine.logic.js new file mode 100644 index 0000000..53dfb44 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/RayLine/RayLine.logic.js @@ -0,0 +1,12 @@ +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; +import { BufferGeometry } from "three"; + +export default class RayLineLogic extends ModelStrategy { + static update({ objects, refs, target }) { + const { origin } = objects.raycaster.ray; + const points = [origin, target]; + const geometry = new BufferGeometry().setFromPoints(points); + refs.rayLine.current.geometry.dispose(); + refs.rayLine.current.geometry = geometry; + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/RayLine/index.js b/src/frontend/ui/models/Particles/components/atoms/RayLine/index.js new file mode 100644 index 0000000..d13cae3 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/RayLine/index.js @@ -0,0 +1,2 @@ +export { default } from './RayLine' +export { default as RayLine } from './RayLine'; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Raycaster/Raycaster.js b/src/frontend/ui/models/Particles/components/atoms/Raycaster/Raycaster.js new file mode 100644 index 0000000..0307992 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Raycaster/Raycaster.js @@ -0,0 +1,12 @@ +/** + * Raycaster.jsx + * Atom component for the raycaster in the Particles model + */ + +import RaycasterLogic from "./Raycaster.logic"; + +const Raycaster = { + logic: RaycasterLogic +}; + +export default Raycaster; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Raycaster/Raycaster.logic.js b/src/frontend/ui/models/Particles/components/atoms/Raycaster/Raycaster.logic.js new file mode 100644 index 0000000..49d8765 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Raycaster/Raycaster.logic.js @@ -0,0 +1,17 @@ +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; +import { Vector2 } from "three"; + +export default class RaycasterLogic extends ModelStrategy { + static setup({ config, data: { unit }, objects: { raycaster } }) { + const { animations: { chaos: { radius } } } = config; + raycaster.params.Points.threshold = radius * unit; + } + + static update({ objects, refs }) { + const { raycaster } = objects; + const camera = refs.camera.current; + const mouse = refs.mouse.current; + const coords = new Vector2(mouse.x, mouse.y); + raycaster.setFromCamera(coords, camera); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/Raycaster/index.js b/src/frontend/ui/models/Particles/components/atoms/Raycaster/index.js new file mode 100644 index 0000000..e803f87 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/Raycaster/index.js @@ -0,0 +1,2 @@ +export { default as Raycaster } from './Raycaster'; +export { default as RaycasterLogic } from './Raycaster.logic'; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/atoms/index.js b/src/frontend/ui/models/Particles/components/atoms/index.js new file mode 100644 index 0000000..052689e --- /dev/null +++ b/src/frontend/ui/models/Particles/components/atoms/index.js @@ -0,0 +1,8 @@ +export * from './Box'; +export * from './Camera'; +export * from './Circle'; +export * from './Mouse'; +export * from './Plane'; +export * from '../molecules/Particles'; +export * from './Raycaster'; +export * from './RayLine'; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Controls/Controls.jsx b/src/frontend/ui/models/Particles/components/molecules/Controls/Controls.jsx new file mode 100644 index 0000000..bff466a --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Controls/Controls.jsx @@ -0,0 +1,22 @@ +//* Imports +import { OrbitControls } from "@react-three/drei"; +import Box from "../../atoms/Box"; +import Circle from "../../atoms/Circle/Circle"; +import Plane from "../../atoms/Plane/Plane"; +import RayLine from "../../atoms/RayLine/RayLine"; +import Camera from "../../atoms/Camera/Camera"; + +//* Main +export default function Controls(args) { + // Props + const { general: { showControls } } = args.config; + // Return + return (<> + + + + + + {showControls && } + ); +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Controls/index.js b/src/frontend/ui/models/Particles/components/molecules/Controls/index.js new file mode 100644 index 0000000..71b5b47 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Controls/index.js @@ -0,0 +1,2 @@ +export { default } from './Controls'; +export { default as Controls } from './Controls'; diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/Particles.jsx b/src/frontend/ui/models/Particles/components/molecules/Particles/Particles.jsx new file mode 100644 index 0000000..b2406fa --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/Particles.jsx @@ -0,0 +1,19 @@ +/** + * Particles.jsx + * Atom component for the particles in the Particles model + */ + +//* Main +function Particles({ config, refs }) { + return ( + + + + + ); +} + +export default Particles; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/Particles.logic.js b/src/frontend/ui/models/Particles/components/molecules/Particles/Particles.logic.js new file mode 100644 index 0000000..fca5c5d --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/Particles.logic.js @@ -0,0 +1,76 @@ +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; +import { getImageData } from "../../../utils/image"; +import { Float32BufferAttribute } from "three"; +import { ParticlesManager } from "../../../Particles.logic"; +import { ColorEffect, PositionEffect } from "./effects"; + +export default class ParticlesLogic extends ModelStrategy { + static setup({ config, data: { color, unit }, objects: { image }, refs }) { + const { particle } = config; + const particles = refs.particles.current; + const { data } = getImageData({ data: { unit }, objects: { image } }); + + particles.data = { + count: 0, + chaotic: [], + colors: [], + positions: { ideal: [], initial: [], offsets: [] }, + }; + + const dimensions = { + x: unit, + y: (image.height / image.width) * unit, + z: unit + }; + + for (let y = 0; y < dimensions.y; y += particle.density) { + for (let x = 0; x < dimensions.x; x += particle.density) { + const alpha = data[(x + y * dimensions.x) * 4 + 3]; + if (alpha > 128) { + particles.data.chaotic.push(0); + particles.data.colors.push(color.r, color.g, color.b); + particles.data.positions.ideal.push( + x - dimensions.x / 2, + -y + dimensions.y / 2, + -dimensions.z / 2); + particles.data.positions.initial.push( + (Math.random() - 0.5) * unit * 2, + (Math.random() - 0.5) * unit * 2, + (Math.random() - 0.5) * unit * 2 + ); + particles.data.positions.offsets.push( + Math.random() * Math.PI * 2, + Math.random() * Math.PI * 2, + Math.random() * Math.PI * 2, + ); + particles.data.count++; + } + } + } + + const colorsArray = particles.data.colors; + const colorsValue = new Float32BufferAttribute(colorsArray, 3); + particles.geometry.setAttribute("color", colorsValue); + + const positionsArray = particles.data.positions.ideal; + const positionsValue = new Float32BufferAttribute(positionsArray, 3); + particles.geometry.setAttribute("position", positionsValue); + + const ratio = window.innerWidth / window.innerHeight; + const size = Math.min(Math.max(particle.size * ratio, 0), particle.size); + particles.material.size = size; + } + + static update(args) { + const intersects = args.objects.raycaster.intersectObject(args.refs.particles.current); + const idxs = new Set(intersects.map(({ index }) => index)); + + for (let i = 0; i < args.refs.particles.current.data.count; i++) { + // ColorEffect.execute({ i, ...args }); + PositionEffect.execute({ i, idxs, ...args }); + } + + // args.refs.particles.current.geometry.attributes.color.needsUpdate = true; + args.refs.particles.current.geometry.attributes.position.needsUpdate = true; + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/effects/chaos.js b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/chaos.js new file mode 100644 index 0000000..82d941c --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/chaos.js @@ -0,0 +1,25 @@ +import { ModelStrategy } from '@semantyk/frontend/ui/components/molecules/Model/logic/strategy'; + +export class ChaosEffect extends ModelStrategy { + static execute({ config, data, i, idxs, final, objects: { clock }, refs: { mouse, particles } }) { + const { unit } = data; + const { animations: { chaos, order, interpolation } } = config; + const elapsedTime = clock.current.getElapsedTime(); + + if (elapsedTime < interpolation.duration) return; + + let magnitude; + let currentChaos = particles.current.data.chaotic[i]; + + if (idxs.has(i) && mouse.current.isMoving) { + currentChaos += chaos.magnitude; + magnitude = Math.min(currentChaos, 1); + } else { + currentChaos -= order.magnitude / unit; + magnitude = Math.max(currentChaos, 0); + } + + particles.current.data.chaotic[i] = magnitude; + final.multiplyScalar(magnitude); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/effects/color.js b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/color.js new file mode 100644 index 0000000..a81d467 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/color.js @@ -0,0 +1,12 @@ +import { Color } from 'three'; +import { ModelStrategy } from '@semantyk/frontend/ui/components/molecules/Model/logic/strategy'; + +export class ColorEffect extends ModelStrategy { + static execute({ data: { color }, i, refs: { particles } }) { + const chaoticValue = particles.current.data.chaotic[i]; + const final = color.clone(); + const target = new Color(1, 0, 0); + final.lerp(target, chaoticValue); + particles.current.geometry.attributes.color.set(final.toArray(), i * 3); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/effects/entropy.js b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/entropy.js new file mode 100644 index 0000000..b63e6b8 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/entropy.js @@ -0,0 +1,20 @@ +import { Vector3 } from 'three'; +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; + +export class EntropyEffect extends ModelStrategy { + static execute({ config, i, idxs, final, ...args }) { + const { animations: { expansion, interpolation } } = config; + const elapsedTime = args.objects.clock.current.getElapsedTime(); + + if (elapsedTime < interpolation.duration) return; + const { ideal } = args.refs.particles.current.data.positions; + const positions = args.refs.particles.current.geometry.attributes.position.array; + + const source = new Vector3().fromArray(positions, i * 3); + const target = new Vector3().fromArray(ideal, i * 3); + const effect = new Vector3().subVectors(source, target); + effect.multiplyScalar(expansion.magnitude); + + final.add(effect); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/effects/flotation.js b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/flotation.js new file mode 100644 index 0000000..e5a8307 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/flotation.js @@ -0,0 +1,20 @@ +import { Vector3 } from 'three'; +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; + +export class FlotationEffect extends ModelStrategy { + static execute({ config, i, final, objects: { clock }, refs: { particles } }) { + const { offsets } = particles.current.data.positions; + const { animations: { flotation } } = config; + const elapsedTime = clock.current.getElapsedTime(); + + const vector = new Vector3().fromArray(offsets, i * 3); + vector.addScalar(elapsedTime * flotation.speed); + const effect = new Vector3( + Math.sin(vector.x), + Math.sin(vector.y) + ); + effect.multiplyScalar(flotation.magnitude); + + final.add(effect); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/effects/index.js b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/index.js new file mode 100644 index 0000000..3ad4338 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/index.js @@ -0,0 +1,4 @@ +export { ChaosEffect } from './chaos'; +export { EntropyEffect } from './entropy'; +export { PositionEffect } from './position'; +export { ColorEffect } from './color'; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/effects/interpolation.js b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/interpolation.js new file mode 100644 index 0000000..675baa9 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/interpolation.js @@ -0,0 +1,21 @@ +import { Vector3 } from 'three'; +import { ease } from "@semantyk/frontend/ui/models/Particles/utils/ease"; +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; + +export class InterpolationEffect extends ModelStrategy { + static execute({ config, i, final, objects: { clock }, refs: { particles } }) { + const { ideal, initial } = particles.current.data.positions; + const { animations: { interpolation: { duration } } } = config; + + const source = new Vector3().fromArray(initial, i * 3); + const target = new Vector3().fromArray(ideal, i * 3); + + const elapsedTime = clock.current.getElapsedTime(); + const easedTime = ease(elapsedTime, duration); + source.multiplyScalar(1 - easedTime); + target.multiplyScalar(easedTime); + + final.add(source); + final.add(target); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/effects/position.js b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/position.js new file mode 100644 index 0000000..273ab23 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/effects/position.js @@ -0,0 +1,23 @@ +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; +import { InterpolationEffect } from './interpolation'; +import { FlotationEffect } from './flotation'; +import { EntropyEffect } from './entropy'; +import { Vector3 } from 'three'; +import { ChaosEffect } from "./chaos"; + +export class PositionEffect extends ModelStrategy { + static execute({ object, i, idxs, ...args }) { + const { particles } = args.refs; + const final = new Vector3() + + //* Effects + //! Order Matters + EntropyEffect.execute({ i, idxs, final, ...args }); + ChaosEffect.execute({ i, idxs, final, ...args }); + InterpolationEffect.execute({ i, final, ...args }); + FlotationEffect.execute({ i, final, ...args }); + + const positions = particles.current.geometry.attributes.position.array; + positions.set(final.toArray(), i * 3); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/Particles/index.js b/src/frontend/ui/models/Particles/components/molecules/Particles/index.js new file mode 100644 index 0000000..0a7f788 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/Particles/index.js @@ -0,0 +1,3 @@ +export * from './effects'; +export { default } from './Particles'; +export { default as Particles } from './Particles'; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/molecules/index.js b/src/frontend/ui/models/Particles/components/molecules/index.js new file mode 100644 index 0000000..4d69a19 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/molecules/index.js @@ -0,0 +1 @@ +export * from './Controls'; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/organisms/System/System.jsx b/src/frontend/ui/models/Particles/components/organisms/System/System.jsx new file mode 100644 index 0000000..35916cb --- /dev/null +++ b/src/frontend/ui/models/Particles/components/organisms/System/System.jsx @@ -0,0 +1,35 @@ +/** + * ParticleSystem.jsx + * Molecule component that combines Particles with its logic + */ + +//* Imports +import { useEffect } from "react"; +import { useFrame } from "@react-three/fiber"; +import Particles from "../../molecules/Particles/Particles.jsx"; +import { ParticlesManager } from "../../../Particles.logic.js"; +import ParticlesSystemLogic from "./System.logic.js"; + +//* Main +function System(args) { + // Logic + useEffect(() => { + ParticlesSystemLogic.setup(args); + + const handleMouseMove = (event) => { + ParticlesManager.handle('mouseMove', { event, ...args }); + }; + + ParticlesManager.addAll({ handleMouseMove }); + return () => ParticlesManager.removeAll({ handleMouseMove }); + }, [args]); + + useFrame(({ clock }) => { + args.objects.clock.current = clock; + ParticlesManager.update("particles", args); + }); + + return ; +} + +export default System; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/organisms/System/System.logic.js b/src/frontend/ui/models/Particles/components/organisms/System/System.logic.js new file mode 100644 index 0000000..5b15dc8 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/organisms/System/System.logic.js @@ -0,0 +1,11 @@ +import { ParticlesManager } from "../../../Particles.logic"; +import { ModelStrategy } from "@semantyk/frontend/ui/components/molecules/Model/logic/strategy"; + +export default class ParticlesSystemLogic extends ModelStrategy { + static setup(args) { + ParticlesManager.setup('camera', args); + ParticlesManager.setup('particles', args); + ParticlesManager.setup('plane', args); + ParticlesManager.setup('raycaster', args); + } +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/organisms/System/index.js b/src/frontend/ui/models/Particles/components/organisms/System/index.js new file mode 100644 index 0000000..5c3ae48 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/organisms/System/index.js @@ -0,0 +1,2 @@ +export { default } from './System'; +export { default as System } from './System'; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/components/organisms/index.js b/src/frontend/ui/models/Particles/components/organisms/index.js new file mode 100644 index 0000000..4cc07e7 --- /dev/null +++ b/src/frontend/ui/models/Particles/components/organisms/index.js @@ -0,0 +1 @@ +export * from "./System"; \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/config/index.js b/src/frontend/ui/models/Particles/config/index.js new file mode 100644 index 0000000..6b4e4cd --- /dev/null +++ b/src/frontend/ui/models/Particles/config/index.js @@ -0,0 +1,57 @@ +/** + * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– + * # `config.js` + * @organization: Semantyk + * @project: Client + * + * @file: This file contains the configuration for the Particles model. + * + * @created: Mar 7, 2025 + * @modified: Mar 7, 2025 + * + * @author: Semantyk Team + * @maintainer: Daniel Bakas + * + * @copyright: Semantyk © 2025. All rights reserved. + * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– + */ + +//* Main +export const config = { + // General + general: { + showControls: false, + scale: 1, + size: 150, + }, + // Camera + camera: { + margin: 1 / 3, + makeDefault: true + }, + // Animations + animations: { + chaos: { + magnitude: 0.25, + radius: 0.10 + }, + order: { + magnitude: 0.25 + }, + expansion: { + magnitude: 1, + }, + flotation: { + magnitude: 1, + speed: 1 + }, + interpolation: { + duration: 5 + } + }, + // Particles + particle: { + density: 1, + size: 0.75 + } +}; \ No newline at end of file diff --git a/src/frontend/ui/models/molecule/Particles/hooks.jsx b/src/frontend/ui/models/Particles/hooks/useArgs.jsx similarity index 83% rename from src/frontend/ui/models/molecule/Particles/hooks.jsx rename to src/frontend/ui/models/Particles/hooks/useArgs.jsx index 4d2a4a4..f52fc11 100644 --- a/src/frontend/ui/models/molecule/Particles/hooks.jsx +++ b/src/frontend/ui/models/Particles/hooks/useArgs.jsx @@ -19,13 +19,13 @@ import { useRef } from "react"; import { useLoader } from "@react-three/fiber"; import { Color, Raycaster, TextureLoader } from "three"; //* Local Imports -import { props } from "@semantyk/frontend/ui/models/molecule/Particles/logic"; +import { config } from "@semantyk/frontend/ui/models/Particles/config"; import useColorScheme from "@semantyk/frontend/hooks/useColorScheme"; //* Main -export function useArgs() { +export function useArgs({ path }) { // Props - const { general: { scale, size }, image: { path } } = props; + const { general: { scale, size } } = config; // Hooks const { colorScheme } = useColorScheme(); const { image } = useLoader(TextureLoader, path); @@ -34,7 +34,7 @@ export function useArgs() { const colorV3 = new Color(color, color, color); // Return return { - /// Data + // Data data: { color: colorV3, unit: scale * size @@ -45,12 +45,15 @@ export function useArgs() { image, raycaster: new Raycaster() }, + // Props + config, // Refs refs: { box: useRef(), camera: useRef(), circle: useRef(), - mouse: useRef({ x: 0, y: 0 }), + mouse: useRef({ current: { x: 0, y: 0, isMoving: false } }), + moveMouseTimeout: useRef(), particles: useRef(), plane: useRef(), rayLine: useRef(), diff --git a/src/frontend/ui/models/Particles/utils/ease.js b/src/frontend/ui/models/Particles/utils/ease.js new file mode 100644 index 0000000..64a4da0 --- /dev/null +++ b/src/frontend/ui/models/Particles/utils/ease.js @@ -0,0 +1,4 @@ +export function ease(time, duration) { + const t = Math.min(time / duration, 1); + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} \ No newline at end of file diff --git a/src/frontend/ui/models/Particles/utils/image.js b/src/frontend/ui/models/Particles/utils/image.js new file mode 100644 index 0000000..7b6567f --- /dev/null +++ b/src/frontend/ui/models/Particles/utils/image.js @@ -0,0 +1,17 @@ +/** + * Image utilities for Particles + */ + +export function getImageData(args) { + // Args + const { data: { unit }, objects: { image } } = args; + // Logic + let { width, height } = image; + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + canvas.width = unit; + canvas.height = (height / width) * unit; + context.drawImage(image, 0, 0, canvas.width, canvas.height); + // Return + return context.getImageData(0, 0, canvas.width, canvas.height); +} \ No newline at end of file diff --git a/src/frontend/ui/models/molecule/Particles/effects.js b/src/frontend/ui/models/molecule/Particles/effects.js deleted file mode 100644 index 3be6ec7..0000000 --- a/src/frontend/ui/models/molecule/Particles/effects.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - * # `effects.js` - * @organization: Semantyk - * @project: Client - * - * @created: Sep 17, 2024 - * @modified: Mar 7, 2025 - * - * @author: Semantyk Team - * @maintainer: Daniel Bakas - * - * @copyright: Semantyk © 2025. All rights reserved. - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - */ - -// - expansion -import { - ease, - props, -} from "@semantyk/frontend/ui/models/molecule/Particles/logic"; -import { Color, Vector3 } from "three"; - -//* Main -//* ---------------------------------------------------------------------------- -// Effect Builder -export function addEffect(type, args) { - // Logic - // - declare options - const options = { - // - THREE.point - color: addPointColorEffect, - position: addPointPositionEffect, - // - custom - expansion: addExpansionEffect, - flotation: addFlotationEffect, - interpolation: addInterpolationEffect, - }; - // - select option - let option = options[type]; - // Update - if (option) - option(args); -} - -//* ---------------------------------------------------------------------------- -// THREE.point Effects -// - color -export function addPointColorEffect({ particles, i, final, colors }) { - // Logic - const chaoticValue = particles.data.chaotic[i]; - const target = new Color(1, 0, 0); - // Transform - final.lerp(target, chaoticValue); - // Update - colors.set(final.toArray(), i * 3); -} - -// - position -export function addPointPositionEffect(args) { - // Props - const { animations: { interpolation } } = props; - // Effects - addEffect("interpolation", args); - addEffect("flotation", args); - if (args.time >= interpolation.duration) - addEffect("expansion", args); -} - -//* ---------------------------------------------------------------------------- -// Custom Effects -// - expansion -export function addExpansionEffect({ object, i, final }) { - // Props - const { expansion } = props.animations; - const chaosValue = object.data.chaotic[i]; - const { ideal } = object.data.positions; - const positions = object.geometry.attributes.position.array; - // Logic - const source = new Vector3().fromArray(positions, i * 3); - const target = new Vector3().fromArray(ideal, i * 3); - const effect = new Vector3().subVectors(source, target); - effect.multiplyScalar(chaosValue); - effect.multiplyScalar(expansion.magnitude); - // Add Effect - final.add(effect); -} - -// - flotation -export function addFlotationEffect({ object, i, final, time }) { - // Props - const { offsets } = object.data.positions; - const { animations: { flotation } } = props; - // Logic - const vector = new Vector3().fromArray(offsets, i * 3); - vector.addScalar(time * flotation.speed); - const effect = new Vector3( - Math.sin(vector.x), - Math.sin(vector.y) - ); - effect.multiplyScalar(flotation.magnitude); - // Prepare for Update - final.add(effect); -} - -// - interpolation -export function addInterpolationEffect(args) { - // Args - const { time, object, i, final } = args; - // Props - const { ideal, initial } = object.data.positions; - const { interpolation: { duration } } = props.animations; - // Logic - const source = new Vector3().fromArray(initial, i * 3); - const target = new Vector3().fromArray(ideal, i * 3); - // - ease timeease - const easedTime = ease(time, duration); - // - interpolate - source.multiplyScalar(1 - easedTime); - target.multiplyScalar(easedTime); - // Add Effect - final.add(source); - final.add(target); -} \ No newline at end of file diff --git a/src/frontend/ui/models/molecule/Particles/index.jsx b/src/frontend/ui/models/molecule/Particles/index.jsx deleted file mode 100644 index d3fc0f6..0000000 --- a/src/frontend/ui/models/molecule/Particles/index.jsx +++ /dev/null @@ -1,160 +0,0 @@ -/** - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - * # `index.jsx` | `ParticlesModel` - * @organization: Semantyk - * @project: Client - * - * @file: This file contains the logic for the Particles model. - * - * @created: Sep 12, 2024 - * @modified: Mar 7, 2025 - * - * @author: Semantyk Team - * @maintainer: Daniel Bakas - * - * @copyright: Semantyk © 2025. All rights reserved. - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - */ - -//* Imports -import { useEffect, useRef } from "react"; -import { CameraHelper } from "three"; -import { OrbitControls, PerspectiveCamera, useHelper } from "@react-three/drei"; -import { useFrame } from "@react-three/fiber"; -//* Local Imports -import { - addEventListeners, - props, - removeEventListeners, - setupObjects, - updateObjects, - updateOnMouseMove, -} from "@semantyk/frontend/ui/models/molecule/Particles/logic"; -import { useArgs } from "@semantyk/frontend/ui/models/molecule/Particles/hooks"; -import { - setupCamera -} from "@semantyk/frontend/ui/models/molecule/Particles/setups"; - -//* Main -export default function ParticlesModel() { - // Props - const { - general: { showHelpers }, - animations: { chaos: { radius } } - } = props; - // Hooks - // - useArgs - const args = useArgs(); - const { data, objects, refs } = args; - // Logic - const moveMouseTimeoutRef = useRef(null); - // Hooks - // - useEffect - useEffect(() => { - // Setup Objects - setupObjects({ data, objects, refs }); - // Listeners - // - mousemove/touchmove - const handleMouseMove = (event) => { - const { mouse } = refs; - clearTimeout(moveMouseTimeoutRef.current); - mouse.current.isMoving = true; - moveMouseTimeoutRef.current = setTimeout(() => mouse.current.isMoving = false, 1); - let clientX, clientY; - if (event.type === "mousemove") { - clientX = event.clientX; - clientY = event.clientY; - } else if (event.type === "touchmove") { - clientX = event.touches[0].clientX; - clientY = event.touches[0].clientY; - } - updateOnMouseMove({ - events: { mousemove: { clientX, clientY } }, - data, - objects, - refs - }); - }; - // - resize - const handleResize = () => { - const { particles } = refs; - setupCamera(args); - // Resize Particles - const { particle } = props; - const ratio = window.innerWidth / window.innerHeight; - const size = Math.min(Math.max(particle.size * ratio, 0), particle.size); - particles.current.material.size = size; - }; - // - add - addEventListeners({ handleMouseMove, handleResize }); - // - remove - return () => removeEventListeners({ handleMouseMove, handleResize }); - }, [args, data, objects, refs]); - // - useFrame - useFrame(({ clock }) => { - objects.clock.current = clock; - updateObjects(args); - }); - // - useHelpers - useHelper(showHelpers && refs.camera, CameraHelper); - // Return - return ( - <> - {/* Camera */} - - {/* Orbit Controls */} - {showHelpers && } - {/* Box */} - - - - - {/* Circle */} - - - - - {/* Particles */} - - - - - {/* Plane */} - - - - - {/* RayLine */} - - - - - - ); -} \ No newline at end of file diff --git a/src/frontend/ui/models/molecule/Particles/logic.js b/src/frontend/ui/models/molecule/Particles/logic.js deleted file mode 100644 index d37c3de..0000000 --- a/src/frontend/ui/models/molecule/Particles/logic.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - * # `logic.js` - * @organization: Semantyk - * @project: Client - * - * @created: Jul 17, 2024 - * @modified: Mar 7, 2025 - * - * @author: Semantyk Team - * @maintainer: Daniel Bakas - * - * @copyright: Semantyk © 2025. All rights reserved. - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - */ - -//* Imports -import { Vector3 } from "three"; -//* Local Imports -import { - updateObject -} from "@semantyk/frontend/ui/models/molecule/Particles/updates"; -import { - setupObject -} from "@semantyk/frontend/ui/models/molecule/Particles/setups"; - -//* Main -// props -export const props = { - // General - general: { - showHelpers: false, - scale: 1, - size: 150, - }, - // Camera - camera: { - margin: 1 / 3, - makeDefault: true - }, - // Animations - animations: { - chaos: { - magnitude: 0.25, - radius: 0.10 - }, - order: { - magnitude: 0.25 - }, - expansion: { - magnitude: 1, - }, - flotation: { - magnitude: 1, - speed: 1 - }, - interpolation: { - duration: 5 - } - }, - // Image - image: { - path: "/favicon.png" - }, - // Particles - particle: { - density: 1, - size: 0.75 - } -}; - -export function getImageData(args) { - // Args - const { data: { unit }, objects: { image } } = args; - // Logic - let { width, height } = image; - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - canvas.width = unit; - canvas.height = (height / width) * unit; - context.drawImage(image, 0, 0, canvas.width, canvas.height); - // Return - return context.getImageData(0, 0, canvas.width, canvas.height); -} - -export function ease(time, duration) { - const t = Math.min(time / duration, 1); - return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; -} - -export function addEventListeners(args) { - // Args - const { handleMouseMove, handleResize } = args; - // Listeners - window.addEventListener("mousemove", handleMouseMove); - window.addEventListener("touchmove", handleMouseMove); - window.addEventListener("resize", handleResize); -} - -export function removeEventListeners(args) { - // Args - const { handleMouseMove, handleResize } = args; - // Listeners - window.removeEventListener("mousemove", handleMouseMove); - window.removeEventListener("touchmove", handleMouseMove); - window.removeEventListener("resize", handleResize); -} - -export function setupObjects(args) { - // Setup - // - camera - setupObject("camera", args); - // - plane - setupObject("plane", args); - // - object - setupObject("particles", args); - // - raycaster - setupObject("raycaster", args); -} - -export function updateObjects(args) { - updateObject("particles", args); -} - -export function updateOnMouseMove(args) { - // Logic - const target = new Vector3(); - updateObject("camera", args); - updateObject("raycaster", args); - updateObject("mouse", args); - updateObject("circle", { target, ...args }); - updateObject("line", { target, ...args }); -} \ No newline at end of file diff --git a/src/frontend/ui/models/molecule/Particles/setups.js b/src/frontend/ui/models/molecule/Particles/setups.js deleted file mode 100644 index 162595a..0000000 --- a/src/frontend/ui/models/molecule/Particles/setups.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - * # `setups.js` - * @organization: Semantyk - * @project: Client - * - * @created: Sep 17, 2024 - * @modified: Mar 7, 2025 - * - * @author: Semantyk Team - * @maintainer: Daniel Bakas - * - * @copyright: Semantyk © 2025. All rights reserved. - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - */ - -//* Imports -import { - getImageData, - props -} from "@semantyk/frontend/ui/models/molecule/Particles/logic"; -// - plane -import { Float32BufferAttribute, Plane, Vector3 } from "three"; - -//* Main -export function setupObject(type, args) { - switch (type) { - case "camera": - setupCamera(args); - case "particles": - setupParticles(args); - case "plane": - setupPlane(args); - case "raycaster": - setupRaycaster(args); - default: - return; - } -} - -export function setupCamera(args) { - // Args - const { data: { unit }, refs: { camera } } = args; - let { camera: { margin } } = props; - // Logic - // - camera - const aspectRatio = window.innerWidth / window.innerHeight; - let x = (1 + margin) / ((aspectRatio >= 1) ? 2 : (2 * aspectRatio)); - const fx = 2 * Math.atan(x) * (180 / Math.PI); - camera.current.fov = fx; - camera.current.aspect = aspectRatio; - camera.current.position.z = unit / 2; - camera.current.updateProjectionMatrix(); -} - -// - object -export function setupParticles(args) { - // Args - const { data: { color, unit }, objects: { image }, refs } = args; - const { particle } = props; - const particles = refs.particles.current; - const { data } = getImageData(args); - particles.data = { - label: "particles", - count: 0, - chaotic: [], - color, - colors: [], - positions: { ideal: [], initial: [], offsets: [] }, - }; - - const dimensions = { - x: unit, - y: (image.height / image.width) * unit, - z: unit - }; - for (let y = 0; y < dimensions.y; y += particle.density) { - for (let x = 0; x < dimensions.x; x += particle.density) { - const alpha = data[(x + y * dimensions.x) * 4 + 3]; - if (alpha > 128) { - particles.data.chaotic.push(0); - particles.data.colors.push(color.r, color.g, color.b); - particles.data.positions.ideal.push( - x - dimensions.x / 2, - -y + dimensions.y / 2, - -dimensions.z / 2); - particles.data.positions.initial.push( - (Math.random() - 0.5) * unit * 2, - (Math.random() - 0.5) * unit * 2, - (Math.random() - 0.5) * unit * 2 - ); - particles.data.positions.offsets.push( - Math.random() * Math.PI * 2, - Math.random() * Math.PI * 2, - Math.random() * Math.PI * 2, - ); - particles.data.count++; - } - } - } - // - color - const colorsArray = particles.data.colors; - const colorsValue = new Float32BufferAttribute(colorsArray, 3); - particles.geometry.setAttribute("color", colorsValue); - // - position - const positionsArray = particles.data.positions.ideal; - const positionsValue = new Float32BufferAttribute(positionsArray, 3); - particles.geometry.setAttribute("position", positionsValue); - // - size - const ratio = window.innerWidth / window.innerHeight; - const size = Math.min(Math.max(particle.size * ratio, 0), particle.size); - particles.material.size = size; -} - -// - plane -export function setupPlane(args) { - // Args - const { data: { unit }, refs: { plane } } = args; - const normal = new Vector3(0, 0, 1); - plane.current = new Plane(normal, unit / 2); -} - -// - raycaster -export function setupRaycaster(args) { - // Args - const { data: { unit }, objects: { raycaster } } = args; - // Props - const { animations: { chaos: { radius } } } = props; - // Logic - raycaster.params.Points.threshold = radius * unit; -} \ No newline at end of file diff --git a/src/frontend/ui/models/molecule/Particles/updates.js b/src/frontend/ui/models/molecule/Particles/updates.js deleted file mode 100644 index 3a99803..0000000 --- a/src/frontend/ui/models/molecule/Particles/updates.js +++ /dev/null @@ -1,182 +0,0 @@ -/** - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - * # `updates.js` - * @organization: Semantyk - * @project: Client - * - * @created: Sep 17, 2024 - * @modified: Mar 7, 2025 - * - * @author: Semantyk Team - * @maintainer: Daniel Bakas - * - * @copyright: Semantyk © 2025. All rights reserved. - * ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– - */ - -//* Imports -import { BufferGeometry, Color, Vector2, Vector3 } from "three"; -import { - addEffect -} from "@semantyk/frontend/ui/models/molecule/Particles/effects"; -import { onMouseMove } from "@semantyk/frontend/logic/services/callbacks"; -//* ---------------------------------------------------------------------------- -// Particle Updates -// - particle.chaos -import { props } from "@semantyk/frontend/ui/models/molecule/Particles/logic"; - -//* Main -//* ---------------------------------------------------------------------------- -// Update Factories -// - particles -export function updateObject(type, args) { - // Logic - // - declare options - const options = { - circle: updateCircle, - line: updateLine, - mouse: updateMouse, - particles: updateParticles, - raycaster: updateRaycaster, - }; - // - select option - let option = options[type]; - // Update - if (option) - option(args); -} - -// - attribute -export function updateAttribute(type, args) { - // Logic - // - declare options - const options = { - chaos: updateChaos, - color: updateColor, - position: updatePosition - }; - // - select option - let option = options[type]; - // Update - if (option) - option(args); -} - -//* ---------------------------------------------------------------------------- -// Object Updates -// - circle -export function updateCircle({ objects, refs, target }) { - // Logic - objects.raycaster.ray.intersectPlane(refs.plane.current, target); - refs.circle.current.position.copy(target); -} - -// - particles -export const updateLine = ({ objects, refs, target }) => { - // Logic - const { origin } = objects.raycaster.ray; - const points = [origin, target]; - const geometry = new BufferGeometry().setFromPoints(points); - refs.rayLine.current.geometry.dispose(); - // Update - refs.rayLine.current.geometry = geometry; -}; - -// - mouse -export const updateMouse = ({ refs, events }) => { - const { x, y } = onMouseMove(events.mousemove); - refs.mouse.current.x = x * 2 - 1; - refs.mouse.current.y = -y * 2 + 1; -}; - -// - particles -export const updateParticles = ({ - objects, - refs: { mouse, particles }, - ...args - }) => { - // Props - let { clock } = objects; - const { interpolation } = props.animations; - // Logic - const object = particles.current; - const time = clock.current.getElapsedTime(); - const intersects = objects.raycaster.intersectObject(object); - const idxs = new Set(intersects.map(({ index }) => index)); - const colors = object.geometry.attributes.color.array; - const positions = object.geometry.attributes.position.array; - // Update - // - each particle - for (let i = 0; i < object.data.count; i++) { - if (time >= interpolation.duration) { - updateAttribute("chaos", { - i, - idxs, - mouse, - particles: object, - ...args - }); - // updateAttribute("color", { i, colors, particles: object, ...args }); - } - updateAttribute("position", { - i, - idxs, - object, - positions, - particles, - time, - ...args - }); - } - // - all particles - object.geometry.attributes.color.needsUpdate = true; - object.geometry.attributes.position.needsUpdate = true; -}; - -// - raycaster -export function updateRaycaster({ objects, refs }) { - // Args - const { raycaster } = objects; - const camera = refs.camera.current; - const mouse = refs.mouse.current; - const coords = new Vector2(mouse.x, mouse.y); - raycaster.setFromCamera(coords, camera); -} - -const updateChaos = ({ data, i, idxs, mouse, particles }) => { - // Props - const { unit } = data; - const { animations: { chaos, order } } = props; - // Logic - let magnitude; - let currentChaos = particles.data.chaotic[i]; - if (idxs.has(i) && mouse.current.isMoving) { - currentChaos += chaos.magnitude; - magnitude = Math.min(currentChaos, 1); - } else { - // magnitude over time - currentChaos -= order.magnitude / unit; - magnitude = Math.max(currentChaos, 0); - } - particles.data.chaotic[i] = magnitude; -}; - -// - particle.color -const updateColor = ({ i, colors, particles, ...args }) => { - // Logic - let final = new Color(); - // Effects - addEffect("color", { colors, particles, i, final, ...args }); - // Update - colors.set(final.toArray(), i * 3); -}; - -// - particle.position -function updatePosition({ object, positions, i, ...args }) { - // Logic - let final = new Vector3(); - // Effects - addEffect("position", { positions, object, i, final, ...args }); - // Update - positions.set(final.toArray(), i * 3); -} \ No newline at end of file