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