diff --git a/README.md b/README.md
index 88248211..d453def9 100644
--- a/README.md
+++ b/README.md
@@ -78,9 +78,12 @@ API documentation is available [online](https://robotwebtools.github.io/rclnodej
Create rich, interactive desktop applications using Electron and web technologies like Three.js. Build 3D visualizations, monitoring dashboards, and control interfaces that run on Windows, macOS, and Linux.
-Try the `electron_demo/turtle_tf2` demo for real-time coordinate frame visualization with dynamic transforms and keyboard-controlled turtle movement. More examples in [electron_demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo).
+| Demo | Description | Screenshot |
+| :-----------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------: |
+| **🐢 [turtle_tf2](./electron_demo/turtle_tf2)** | Real-time coordinate frame visualization with turtle control. Features TF2 transforms, keyboard control, and dynamic frame updates. |  |
+| **🦾 [manipulator](./electron_demo/manipulator)** | Interactive two-joint robotic arm simulation. Features 3D joint visualization, manual/automatic control, and visual movement markers. |  |
-
+Explore more examples in [electron_demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo).
## Using rclnodejs with TypeScript
diff --git a/electron_demo/manipulator/README.md b/electron_demo/manipulator/README.md
new file mode 100644
index 00000000..63f4b860
--- /dev/null
+++ b/electron_demo/manipulator/README.md
@@ -0,0 +1,300 @@
+# Two-Joint Manipulator Demo
+
+An interactive Electron application demonstrating a two-joint robotic manipulator visualization using rclnodejs and Three.js.
+
+
+
+## 🚀 Features
+
+- **Real-time 3D Visualization**: Interactive two-joint robotic arm with Three.js
+- **ROS2 Integration**: Publishes and subscribes to `sensor_msgs/msg/JointState` topics
+- **Interactive Control**: Manual joint control with sliders
+- **Automatic Animation**: Smooth sinusoidal motion patterns
+- **Live Feedback**: Real-time joint position display and ROS2 message frequency
+- **Visual Movement Markers**: Color-coded rings, arrows, and labels to identify joint movements
+- **Modern UI**: Clean, responsive interface with 3D orbit controls
+
+## 📋 Prerequisites
+
+- **Node.js** (>= 16.13.0) - JavaScript runtime
+- **ROS 2** (Humble, Jazzy, or newer) - Robot Operating System 2
+- **rclnodejs compatible environment** - Linux recommended (tested on Ubuntu/WSL)
+
+## 🛠️ Installation
+
+1. **Navigate to the demo directory**:
+
+ ```bash
+ cd rclnodejs/electron_demo/manipulator
+ ```
+
+2. **Install dependencies**:
+
+ ```bash
+ npm install
+ ```
+
+3. **Rebuild native modules for Electron**:
+ ```bash
+ npm run rebuild
+ ```
+
+## 📜 Available Scripts
+
+- **`npm start`** - Run demo (requires manual ROS2 environment setup)
+- **`npm run rebuild`** - Rebuild native modules after dependency changes
+
+## 🚀 Quick Start
+
+### Option 1: Simple Demo (Works Everywhere)
+
+```bash
+npm start
+```
+
+- ✅ **No ROS2 environment required**
+- ✅ **Works without external setup**
+- ✅ **Pure visualization and manual control**
+- ⚠️ No ROS2 topic publishing (local mode only)
+
+### Option 2: Manual ROS2 Setup (Recommended for ROS2 Integration)
+
+1. **Source your ROS2 environment**:
+
+ ```bash
+ source /opt/ros/humble/setup.bash # or your ROS2 installation path
+ ```
+
+2. **Run the demo**:
+ ```bash
+ npm start
+ ```
+
+- ✅ **Publishes to `/joint_states` topic**
+- ✅ **Full ROS2 ecosystem integration**
+- ✅ **Real-time ROS2 message monitoring**
+
+## 🎮 Usage
+
+### Interactive Controls
+
+- **Joint Sliders**: Use the sliders in the control panel to manually adjust joint angles
+
+ - **Joint 1 (Base)**: Rotates the entire arm around the vertical axis (±180°)
+ - **Joint 2 (Elbow)**: Bends the upper arm segment (±135°)
+
+- **Animation**: Click "Start Animation" for automatic smooth motion
+- **Reset**: Click "Reset Position" to return to zero configuration
+
+### 3D Visualization Controls
+
+- **Orbit**: Click and drag to rotate the camera around the manipulator
+- **Zoom**: Use mouse wheel to zoom in/out
+- **View**: The manipulator is shown with color-coded components:
+ - **Gray Base**: Fixed mounting base
+ - **Red Joint 1**: Base rotation joint
+ - **Blue Link 1**: Upper arm segment
+ - **Green Joint 2**: Elbow rotation joint
+ - **Yellow Link 2**: Forearm segment
+ - **Purple End Effector**: Tool attachment point
+
+### Visual Movement Markers
+
+The demo includes visual indicators to help identify joint movements:
+
+- **🔴 Red Markers**: Joint 1 (Base rotation)
+
+ - Red ring around the base joint
+ - Red arrow showing rotation direction
+ - "Joint1" text label
+
+- **🟢 Green Markers**: Joint 2 (Elbow)
+
+ - Green ring around the elbow joint
+ - Green arrow showing rotation direction
+ - "Joint2" text label
+
+- **⚪ White Reference Ring**: Fixed base reference point
+
+These markers make it easy to visually confirm which joints are moving during manual control or animation.
+
+## 🔧 ROS2 Topics
+
+### Published Topics
+
+- **`/joint_states`** (`sensor_msgs/msg/JointState`)
+ - Joint names: `['joint1', 'joint2']`
+ - Positions: Current joint angles in radians
+ - Published at 10 Hz
+ - Velocity and effort fields included (set to zero)
+
+### Subscribed Topics
+
+- **`/joint_states`** (`sensor_msgs/msg/JointState`)
+ - Receives external joint commands
+ - Updates visualization in real-time
+ - Displays message frequency and count
+
+### Monitoring Topics
+
+To verify the demo is working correctly, you can monitor the published topics:
+
+```bash
+# In a separate terminal, source ROS2 environment
+source /opt/ros/humble/setup.bash # or your ROS2 installation
+
+# List all available topics
+ros2 topic list
+
+# Monitor joint state messages
+ros2 topic echo /joint_states
+
+# Check publishing frequency
+ros2 topic hz /joint_states
+
+# View topic info
+ros2 topic info /joint_states
+```
+
+## 🏗️ Architecture
+
+### Main Process (`main.js`)
+
+- **ROS2 Node Management**: Creates and manages the manipulator node
+- **Joint State Publishing**: Publishes current joint positions at 10 Hz
+- **Animation Control**: Handles smooth motion generation
+- **IPC Communication**: Bridges ROS2 data with renderer process
+
+### Renderer Process (`renderer.js`)
+
+- **3D Visualization**: Three.js scene with interactive manipulator model
+- **Visual Markers**: Color-coded rings, arrows, and labels for joint identification
+- **User Interface**: Control panels and status displays
+- **Real-time Updates**: Smooth joint motion and camera controls
+- **Message Handling**: Processes ROS2 data and user interactions
+
+### Component Hierarchy
+
+```
+Scene
+├── Lighting (Ambient + Directional + Point)
+├── Ground Plane
+├── Coordinate Axes
+├── Base (Fixed)
+├── Joint Markers (Red/Green Rings, Arrows, Labels)
+└── Joint1 Group (Rotates around Y-axis)
+ ├── Joint1 Sphere
+ ├── Link1 Cylinder
+ └── Joint2 Group (Rotates around Z-axis)
+ ├── Joint2 Sphere
+ ├── Link2 Cylinder
+ └── End Effector Cube
+```
+
+## 🎯 Technical Details
+
+### Joint Configuration
+
+- **Joint 1 (Base)**: Revolute joint, Y-axis rotation, ±180° range
+- **Joint 2 (Elbow)**: Revolute joint, Z-axis rotation, ±135° range
+- **Forward Kinematics**: Hierarchical transformation chains
+- **Coordinate System**: Right-handed, Z-up convention
+
+### Animation Patterns
+
+```javascript
+// Sinusoidal motion with different frequencies (from main.js)
+joint1_angle = (sin(time * 1.0) * π) / 3; // ±60° at 1.0x speed
+joint2_angle = (sin(time * 1.5) * π) / 4; // ±45° at 1.5x speed
+```
+
+### Performance
+
+- **Rendering**: 60 FPS with requestAnimationFrame
+- **ROS2 Publishing**: 10 Hz joint state updates
+- **Animation**: 20 Hz smooth motion updates
+- **UI Updates**: Real-time slider and display synchronization
+
+## 🛠️ Development
+
+### File Structure
+
+```
+manipulator/
+├── package.json # Dependencies and scripts
+├── main.js # Electron main process + ROS2 integration
+├── renderer.js # Three.js visualization + UI controls
+├── index.html # Application UI layout and styling
+├── start-demo.sh # Convenient ROS2 startup script
+├── README.md # This documentation
+├── VERSION_UPGRADE.md # rclnodejs upgrade details
+└── COMPLETION_SUMMARY.md # Implementation summary
+```
+
+### Key Dependencies
+
+- **electron**: `^31.7.7` - Desktop application framework
+- **rclnodejs**: `^1.5.1` - ROS2 JavaScript client library (latest compatible)
+- **@electron/rebuild**: `^3.7.2` - Native module rebuilding tool
+- **three.js**: `r128` - 3D graphics library (loaded via CDN)
+
+### Debugging
+
+- Press `F12` to open Developer Tools
+- Console logs show ROS2 initialization and message flow
+- Check ROS2 topics with: `ros2 topic list` and `ros2 topic echo /joint_states`
+
+## 🔍 Troubleshooting
+
+### Common Issues
+
+1. **"librcl.so not found"**
+
+ ```bash
+ # Source ROS2 environment manually
+ source /opt/ros/humble/setup.bash # or your ROS2 installation path
+ npm start
+ ```
+
+2. **Build errors with latest Electron**
+
+ - This demo uses Electron 31.7.7 for rclnodejs compatibility
+ - Electron 38+ requires C++20, which rclnodejs doesn't support yet
+ - The current versions are tested and stable
+
+3. **No ROS2 messages received**
+
+ - Check if ROS2 daemon is running: `ros2 daemon start`
+ - Verify topic exists: `ros2 topic list`
+ - Check message flow: `ros2 topic echo /joint_states`
+
+4. **Blank 3D scene**
+ - Check browser console for Three.js errors
+ - Ensure internet connection for Three.js CDN
+ - Press F12 to open Developer Tools for debugging
+
+### Performance Tips
+
+- Reduce animation frequency for slower systems
+- Disable shadows in Three.js for better performance
+- Adjust rendering quality in renderer settings
+
+## 📚 Learning Resources
+
+- **ROS2 Concepts**: [ROS2 Documentation](https://docs.ros.org/en/humble/)
+- **Three.js Guide**: [Three.js Documentation](https://threejs.org/docs/)
+- **Electron Tutorials**: [Electron Documentation](https://electronjs.org/docs)
+- **rclnodejs API**: [rclnodejs Repository](https://github.com/RobotWebTools/rclnodejs)
+
+## 🤝 Contributing
+
+This demo is part of the rclnodejs project. Contributions welcome!
+
+1. Fork the repository
+2. Create your feature branch
+3. Add tests for new functionality
+4. Submit a pull request
+
+## 📄 License
+
+Licensed under the Apache License, Version 2.0. See the main rclnodejs repository for details.
diff --git a/electron_demo/manipulator/index.html b/electron_demo/manipulator/index.html
new file mode 100644
index 00000000..8de52b26
--- /dev/null
+++ b/electron_demo/manipulator/index.html
@@ -0,0 +1,170 @@
+
+
+
+
+ Two-Joint Manipulator Demo
+
+
+
+
+
+
+
+
Manipulator Control
+
+
+
+ 🔴 Red markers - Rotates entire arm around vertical axis
+
+
+
+
+
+ 🟢 Green markers - Bends upper arm segment
+
+
+
+
+
+
+
+
+
+
+
ROS2 Status
+
Disconnected
+
+
Topic: /joint_states
+
Frequency: 0.0 Hz
+
Messages: 0
+
+
+
+
+
Two-Joint Manipulator Demo
+
This demo shows a simple 2-DOF robotic arm using:
+
+ - Electron - Desktop application framework
+ - rclnodejs - ROS2 JavaScript client
+ - Three.js - 3D visualization
+ - JointState - ROS2 sensor_msgs
+
+
Use the sliders to control joint angles or start automatic animation.
+
+
🎯 What to Look For:
+
+ - 🔴 Red Ring - Joint 1 (Base) - Should rotate entire arm
+ - 🟢 Green Ring - Joint 2 (Elbow) - Should bend upper arm
+ - ⚪ White Ring - Fixed base reference
+ - 🔺 Arrows - Show rotation directions
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/electron_demo/manipulator/main.js b/electron_demo/manipulator/main.js
new file mode 100644
index 00000000..f3e29946
--- /dev/null
+++ b/electron_demo/manipulator/main.js
@@ -0,0 +1,218 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const { app, BrowserWindow, ipcMain } = require('electron');
+const rclnodejs = require('rclnodejs');
+
+let mainWindow;
+let manipulatorNode;
+let jointStatePublisher;
+let jointStateSubscriber;
+let isAnimating = false;
+let animationInterval;
+
+// Current joint positions (in radians)
+let currentJointPositions = {
+ joint1: 0.0, // Base rotation
+ joint2: 0.0, // Elbow rotation
+};
+
+// Animation parameters
+let animationTime = 0;
+const animationSpeed = 0.02;
+
+function createWindow() {
+ mainWindow = new BrowserWindow({
+ width: 1200,
+ height: 800,
+ webPreferences: {
+ nodeIntegration: true,
+ contextIsolation: false,
+ },
+ });
+
+ mainWindow.loadFile('index.html');
+
+ // Open DevTools in development
+ if (process.env.NODE_ENV === 'development') {
+ mainWindow.webContents.openDevTools();
+ }
+}
+
+app.whenReady().then(async () => {
+ createWindow();
+
+ // Wait for window to be ready before initializing ROS2
+ mainWindow.webContents.once('did-finish-load', async () => {
+ try {
+ await initializeROS2();
+ console.log('ROS2 initialized successfully');
+ } catch (error) {
+ console.error('Failed to initialize ROS2:', error);
+ // Send error status to renderer
+ mainWindow.webContents.send('ros2-status', {
+ connected: false,
+ error: error.message,
+ });
+ }
+ });
+
+ app.on('activate', function () {
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
+ });
+});
+
+app.on('window-all-closed', function () {
+ if (process.platform !== 'darwin') {
+ // Clean up ROS2 resources
+ if (animationInterval) {
+ clearInterval(animationInterval);
+ }
+ app.quit();
+ }
+});
+
+async function initializeROS2() {
+ console.log('Initializing ROS2...');
+
+ // Initialize rclnodejs
+ await rclnodejs.init();
+
+ // Create the manipulator node
+ manipulatorNode = rclnodejs.createNode('manipulator_demo');
+
+ // Create publisher for joint states
+ jointStatePublisher = manipulatorNode.createPublisher(
+ 'sensor_msgs/msg/JointState',
+ '/joint_states'
+ );
+
+ // Create subscriber to receive joint state commands (for external control)
+ jointStateSubscriber = manipulatorNode.createSubscription(
+ 'sensor_msgs/msg/JointState',
+ '/joint_states',
+ (msg) => {
+ // Forward joint state data to renderer
+ mainWindow.webContents.send('joint-state-received', {
+ names: msg.name,
+ positions: msg.position,
+ velocities: msg.velocity || [],
+ efforts: msg.effort || [],
+ });
+ }
+ );
+
+ // Start spinning the node
+ rclnodejs.spin(manipulatorNode);
+
+ // Send success status to renderer
+ mainWindow.webContents.send('ros2-status', {
+ connected: true,
+ message: 'Connected to ROS2',
+ });
+
+ // Start publishing joint states
+ startJointStatePublishing();
+}
+
+function startJointStatePublishing() {
+ // Publish joint states at 10 Hz
+ setInterval(() => {
+ publishJointStates();
+ }, 100); // 100ms = 10 Hz
+}
+
+function publishJointStates() {
+ if (!jointStatePublisher) return;
+
+ const now = Date.now() / 1000; // Convert to seconds
+
+ const jointStateMsg = {
+ header: {
+ stamp: {
+ sec: Math.floor(now),
+ nanosec: Math.floor((now - Math.floor(now)) * 1e9),
+ },
+ frame_id: 'base_link',
+ },
+ name: ['joint1', 'joint2'],
+ position: [currentJointPositions.joint1, currentJointPositions.joint2],
+ velocity: [0.0, 0.0], // For simplicity, set velocities to zero
+ effort: [0.0, 0.0], // For simplicity, set efforts to zero
+ };
+
+ jointStatePublisher.publish(jointStateMsg);
+}
+
+// IPC handlers for communication with renderer process
+ipcMain.on('set-joint-positions', (event, positions) => {
+ currentJointPositions.joint1 = positions.joint1;
+ currentJointPositions.joint2 = positions.joint2;
+});
+
+ipcMain.on('start-animation', (event) => {
+ if (!isAnimating) {
+ isAnimating = true;
+ animationTime = 0;
+
+ animationInterval = setInterval(() => {
+ animationTime += animationSpeed;
+
+ // Create smooth sinusoidal motion for both joints
+ currentJointPositions.joint1 = (Math.sin(animationTime) * Math.PI) / 3; // ±60 degrees
+ currentJointPositions.joint2 =
+ (Math.sin(animationTime * 1.5) * Math.PI) / 4; // ±45 degrees
+
+ // Send updated positions to renderer
+ event.sender.send('joint-positions-updated', {
+ joint1: currentJointPositions.joint1,
+ joint2: currentJointPositions.joint2,
+ });
+ }, 50); // 50ms = 20 Hz animation
+
+ event.sender.send('animation-status', { running: true });
+ }
+});
+
+ipcMain.on('stop-animation', (event) => {
+ if (isAnimating) {
+ isAnimating = false;
+ if (animationInterval) {
+ clearInterval(animationInterval);
+ animationInterval = null;
+ }
+ event.sender.send('animation-status', { running: false });
+ }
+});
+
+ipcMain.on('reset-position', (event) => {
+ // Stop animation if running
+ if (isAnimating) {
+ isAnimating = false;
+ if (animationInterval) {
+ clearInterval(animationInterval);
+ animationInterval = null;
+ }
+ }
+
+ // Reset to zero position
+ currentJointPositions.joint1 = 0.0;
+ currentJointPositions.joint2 = 0.0;
+
+ // Send updated positions to renderer
+ event.sender.send('joint-positions-updated', {
+ joint1: 0.0,
+ joint2: 0.0,
+ });
+
+ event.sender.send('animation-status', { running: false });
+});
diff --git a/electron_demo/manipulator/manipulator-demo.gif b/electron_demo/manipulator/manipulator-demo.gif
new file mode 100644
index 00000000..ca72eddf
Binary files /dev/null and b/electron_demo/manipulator/manipulator-demo.gif differ
diff --git a/electron_demo/manipulator/manipulator-demo.png b/electron_demo/manipulator/manipulator-demo.png
new file mode 100644
index 00000000..d2dc09b7
Binary files /dev/null and b/electron_demo/manipulator/manipulator-demo.png differ
diff --git a/electron_demo/manipulator/package.json b/electron_demo/manipulator/package.json
new file mode 100644
index 00000000..f46413da
--- /dev/null
+++ b/electron_demo/manipulator/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "rclnodejs-manipulator-demo",
+ "version": "1.0.0",
+ "description": "Electron application demonstrating a two-joint manipulator visualization using rclnodejs and Three.js",
+ "main": "main.js",
+ "scripts": {
+ "start": "electron .",
+ "rebuild": "electron-rebuild"
+ },
+ "keywords": [
+ "Electron",
+ "rclnodejs",
+ "manipulator",
+ "robotics",
+ "joints",
+ "Three.js",
+ "3D",
+ "visualization",
+ "demo"
+ ],
+ "license": "Apache-2.0",
+ "dependencies": {
+ "rclnodejs": "^1.5.1"
+ },
+ "devDependencies": {
+ "@electron/rebuild": "^3.7.2",
+ "electron": "^31.7.7"
+ }
+}
diff --git a/electron_demo/manipulator/renderer.js b/electron_demo/manipulator/renderer.js
new file mode 100644
index 00000000..5f2da319
--- /dev/null
+++ b/electron_demo/manipulator/renderer.js
@@ -0,0 +1,486 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const { ipcRenderer } = require('electron');
+
+// Three.js scene components
+let scene, camera, renderer, controls;
+let manipulator = {};
+let isAnimating = false;
+let messageCount = 0;
+let lastMessageTime = 0;
+let frequencyUpdateInterval;
+
+// Joint angles (in radians)
+let jointAngles = {
+ joint1: 0.0,
+ joint2: 0.0,
+};
+
+// Initialize the 3D scene
+function initScene() {
+ const container = document.getElementById('canvas-container');
+
+ // Scene setup
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x222222);
+
+ // Camera setup
+ camera = new THREE.PerspectiveCamera(
+ 75,
+ window.innerWidth / window.innerHeight,
+ 0.1,
+ 1000
+ );
+ camera.position.set(5, 5, 5);
+ camera.lookAt(0, 0, 0);
+
+ // Renderer setup
+ renderer = new THREE.WebGLRenderer({ antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.shadowMap.enabled = true;
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+ container.appendChild(renderer.domElement);
+
+ // Lighting
+ setupLighting();
+
+ // Create the manipulator
+ createManipulator();
+
+ // Create ground plane
+ createGround();
+
+ // Setup camera controls (basic orbit controls)
+ setupCameraControls();
+
+ // Start render loop
+ animate();
+}
+
+function setupLighting() {
+ // Ambient light
+ const ambientLight = new THREE.AmbientLight(0x404040, 0.3);
+ scene.add(ambientLight);
+
+ // Directional light (main light)
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
+ directionalLight.position.set(10, 10, 5);
+ directionalLight.castShadow = true;
+ directionalLight.shadow.mapSize.width = 2048;
+ directionalLight.shadow.mapSize.height = 2048;
+ scene.add(directionalLight);
+
+ // Point light for additional illumination
+ const pointLight = new THREE.PointLight(0xffffff, 0.5, 100);
+ pointLight.position.set(-5, 5, 5);
+ scene.add(pointLight);
+}
+
+function createManipulator() {
+ // Base (fixed)
+ const baseGeometry = new THREE.CylinderGeometry(0.8, 1.0, 0.5, 16);
+ const baseMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
+ manipulator.base = new THREE.Mesh(baseGeometry, baseMaterial);
+ manipulator.base.position.y = 0.25;
+ manipulator.base.castShadow = true;
+ manipulator.base.receiveShadow = true;
+ scene.add(manipulator.base);
+
+ // Joint 1 (rotating around Y-axis) - this will be a child of base
+ const joint1Geometry = new THREE.SphereGeometry(0.3, 16, 16);
+ const joint1Material = new THREE.MeshLambertMaterial({ color: 0xff4444 });
+ manipulator.joint1 = new THREE.Mesh(joint1Geometry, joint1Material);
+ manipulator.joint1.position.y = 0.5;
+ manipulator.joint1.castShadow = true;
+ scene.add(manipulator.joint1);
+
+ // Link 1 (between joint1 and joint2)
+ const link1Geometry = new THREE.CylinderGeometry(0.15, 0.15, 2.5, 16);
+ const link1Material = new THREE.MeshLambertMaterial({ color: 0x4444ff });
+ manipulator.link1 = new THREE.Mesh(link1Geometry, link1Material);
+ manipulator.link1.position.set(0, 1.25, 0);
+ manipulator.link1.castShadow = true;
+
+ // Create a group for joint1 and its children (link1, joint2, link2, endEffector)
+ manipulator.joint1Group = new THREE.Group();
+ manipulator.joint1Group.position.y = 0.5; // Position at top of base
+ manipulator.joint1Group.add(manipulator.joint1);
+ manipulator.joint1Group.add(manipulator.link1);
+ scene.add(manipulator.joint1Group);
+
+ // Joint 2 (rotating around Z-axis)
+ const joint2Geometry = new THREE.SphereGeometry(0.25, 16, 16);
+ const joint2Material = new THREE.MeshLambertMaterial({ color: 0x44ff44 });
+ manipulator.joint2 = new THREE.Mesh(joint2Geometry, joint2Material);
+ manipulator.joint2.position.set(0, 2.5, 0);
+ manipulator.joint2.castShadow = true;
+
+ // Link 2 (end effector arm)
+ const link2Geometry = new THREE.CylinderGeometry(0.1, 0.1, 2.0, 16);
+ const link2Material = new THREE.MeshLambertMaterial({ color: 0xffff44 });
+ manipulator.link2 = new THREE.Mesh(link2Geometry, link2Material);
+ manipulator.link2.position.set(0, 1.0, 0);
+ manipulator.link2.castShadow = true;
+
+ // End effector
+ const endEffectorGeometry = new THREE.BoxGeometry(0.3, 0.3, 0.3);
+ const endEffectorMaterial = new THREE.MeshLambertMaterial({
+ color: 0xff44ff,
+ });
+ manipulator.endEffector = new THREE.Mesh(
+ endEffectorGeometry,
+ endEffectorMaterial
+ );
+ manipulator.endEffector.position.set(0, 2.0, 0);
+ manipulator.endEffector.castShadow = true;
+
+ // Create a group for joint2 and its children (link2, endEffector)
+ manipulator.joint2Group = new THREE.Group();
+ manipulator.joint2Group.position.set(0, 2.5, 0); // Position at top of link1
+ manipulator.joint2Group.add(manipulator.joint2);
+ manipulator.joint2Group.add(manipulator.link2);
+ manipulator.joint2Group.add(manipulator.endEffector);
+
+ // Add joint2 group to joint1 group so it moves with joint1
+ manipulator.joint1Group.add(manipulator.joint2Group);
+
+ // Create coordinate axes for reference
+ createCoordinateAxes();
+
+ // Add visual markers for joints
+ createJointMarkers();
+}
+
+function createCoordinateAxes() {
+ const axesHelper = new THREE.AxesHelper(1);
+ scene.add(axesHelper);
+}
+
+function createJointMarkers() {
+ // Create 3D text labels and visual indicators for joints
+
+ // Base marker - Fixed reference point
+ const baseMarkerGeometry = new THREE.RingGeometry(1.2, 1.4, 16);
+ const baseMarkerMaterial = new THREE.MeshBasicMaterial({
+ color: 0xffffff,
+ side: THREE.DoubleSide,
+ transparent: true,
+ opacity: 0.8,
+ });
+ const baseMarker = new THREE.Mesh(baseMarkerGeometry, baseMarkerMaterial);
+ baseMarker.rotation.x = -Math.PI / 2; // Rotate to lie flat on ground
+ baseMarker.position.y = 0.01; // Just above ground
+ scene.add(baseMarker);
+
+ // Joint 1 (Base) indicator - Rotates with the arm
+ const joint1IndicatorGeometry = new THREE.TorusGeometry(0.5, 0.05, 8, 16);
+ const joint1IndicatorMaterial = new THREE.MeshBasicMaterial({
+ color: 0xff0000,
+ transparent: true,
+ opacity: 0.7,
+ });
+ manipulator.joint1Indicator = new THREE.Mesh(
+ joint1IndicatorGeometry,
+ joint1IndicatorMaterial
+ );
+ manipulator.joint1Indicator.position.y = 0.5;
+ manipulator.joint1Indicator.rotation.x = Math.PI / 2; // Rotate to be horizontal
+ manipulator.joint1Group.add(manipulator.joint1Indicator);
+
+ // Joint 2 (Elbow) indicator - Rotates with the elbow
+ const joint2IndicatorGeometry = new THREE.TorusGeometry(0.4, 0.04, 8, 16);
+ const joint2IndicatorMaterial = new THREE.MeshBasicMaterial({
+ color: 0x00ff00,
+ transparent: true,
+ opacity: 0.7,
+ });
+ manipulator.joint2Indicator = new THREE.Mesh(
+ joint2IndicatorGeometry,
+ joint2IndicatorMaterial
+ );
+ manipulator.joint2Indicator.position.set(0, 2.5, 0);
+ // This will rotate around Z-axis with joint2Group
+ manipulator.joint2Group.add(manipulator.joint2Indicator);
+
+ // Create arrow indicators to show rotation directions
+ createRotationArrows();
+
+ // Create text labels (using simple 3D geometry since we can't easily use text)
+ createJointLabels();
+}
+
+function createRotationArrows() {
+ // Joint 1 rotation arrow (around Y-axis)
+ const arrowGeometry1 = new THREE.ConeGeometry(0.1, 0.3, 8);
+ const arrowMaterial1 = new THREE.MeshBasicMaterial({ color: 0xff0000 });
+
+ // Create multiple arrow cones to show circular motion
+ for (let i = 0; i < 4; i++) {
+ const arrow = new THREE.Mesh(arrowGeometry1, arrowMaterial1);
+ const angle = (i / 4) * Math.PI * 2;
+ arrow.position.set(Math.cos(angle) * 0.8, 0.7, Math.sin(angle) * 0.8);
+ arrow.rotation.y = angle + Math.PI / 2; // Point in rotation direction
+ arrow.rotation.z = Math.PI / 2; // Point horizontally
+ manipulator.joint1Group.add(arrow);
+ }
+
+ // Joint 2 rotation arrow (around Z-axis)
+ const arrowGeometry2 = new THREE.ConeGeometry(0.08, 0.25, 8);
+ const arrowMaterial2 = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
+
+ for (let i = 0; i < 4; i++) {
+ const arrow = new THREE.Mesh(arrowGeometry2, arrowMaterial2);
+ const angle = (i / 4) * Math.PI * 2;
+ arrow.position.set(Math.cos(angle) * 0.6, 2.5, Math.sin(angle) * 0.6);
+ arrow.rotation.y = angle + Math.PI / 2; // Point in rotation direction
+ arrow.rotation.z = Math.PI / 2; // Point horizontally
+ manipulator.joint2Group.add(arrow);
+ }
+}
+
+function createJointLabels() {
+ // Create simple geometric shapes to represent labels
+
+ // "BASE" label using boxes
+ const labelMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
+
+ // BASE label at ground level
+ const baseLabel = new THREE.Group();
+ // Create letter-like shapes for "BASE" - simplified geometric representation
+ const baseLabelGeometry = new THREE.BoxGeometry(0.8, 0.1, 0.1);
+ const baseLabelMesh = new THREE.Mesh(baseLabelGeometry, labelMaterial);
+ baseLabelMesh.position.set(2, 0.2, 0);
+ scene.add(baseLabelMesh);
+
+ // "JOINT 1" label near the base joint
+ const joint1LabelGeometry = new THREE.BoxGeometry(0.6, 0.08, 0.08);
+ const joint1LabelMesh = new THREE.Mesh(
+ joint1LabelGeometry,
+ new THREE.MeshBasicMaterial({ color: 0xff0000 })
+ );
+ joint1LabelMesh.position.set(1.5, 0.8, 0);
+ manipulator.joint1Group.add(joint1LabelMesh);
+
+ // "JOINT 2" label near the elbow joint
+ const joint2LabelGeometry = new THREE.BoxGeometry(0.6, 0.08, 0.08);
+ const joint2LabelMesh = new THREE.Mesh(
+ joint2LabelGeometry,
+ new THREE.MeshBasicMaterial({ color: 0x00ff00 })
+ );
+ joint2LabelMesh.position.set(1.2, 2.5, 0);
+ manipulator.joint2Group.add(joint2LabelMesh);
+}
+
+function createGround() {
+ const groundGeometry = new THREE.PlaneGeometry(20, 20);
+ const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x555555 });
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
+ ground.rotation.x = -Math.PI / 2;
+ ground.receiveShadow = true;
+ scene.add(ground);
+}
+
+function setupCameraControls() {
+ // Simple orbit controls using mouse
+ let isDragging = false;
+ let previousMousePosition = { x: 0, y: 0 };
+
+ renderer.domElement.addEventListener('mousedown', (event) => {
+ isDragging = true;
+ previousMousePosition = { x: event.clientX, y: event.clientY };
+ });
+
+ renderer.domElement.addEventListener('mousemove', (event) => {
+ if (isDragging) {
+ const deltaMove = {
+ x: event.clientX - previousMousePosition.x,
+ y: event.clientY - previousMousePosition.y,
+ };
+
+ // Rotate camera around the scene
+ const spherical = new THREE.Spherical();
+ spherical.setFromVector3(camera.position);
+ spherical.theta -= deltaMove.x * 0.01;
+ spherical.phi += deltaMove.y * 0.01;
+ spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
+
+ camera.position.setFromSpherical(spherical);
+ camera.lookAt(0, 1, 0);
+
+ previousMousePosition = { x: event.clientX, y: event.clientY };
+ }
+ });
+
+ renderer.domElement.addEventListener('mouseup', () => {
+ isDragging = false;
+ });
+
+ // Zoom with mouse wheel
+ renderer.domElement.addEventListener('wheel', (event) => {
+ const scale = event.deltaY > 0 ? 1.1 : 0.9;
+ camera.position.multiplyScalar(scale);
+ event.preventDefault();
+ });
+}
+
+function updateManipulator() {
+ if (manipulator.joint1Group && manipulator.joint2Group) {
+ // Update joint1 (base rotation around Y-axis)
+ manipulator.joint1Group.rotation.y = jointAngles.joint1;
+
+ // Update joint2 (elbow rotation around Z-axis)
+ manipulator.joint2Group.rotation.z = jointAngles.joint2;
+ }
+}
+
+function animate() {
+ requestAnimationFrame(animate);
+ updateManipulator();
+ renderer.render(scene, camera);
+}
+
+// Handle window resize
+window.addEventListener('resize', () => {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+});
+
+// UI event handlers
+document.addEventListener('DOMContentLoaded', () => {
+ initScene();
+ setupUIEventHandlers();
+ startFrequencyMeasurement();
+});
+
+function setupUIEventHandlers() {
+ const joint1Slider = document.getElementById('joint1-slider');
+ const joint2Slider = document.getElementById('joint2-slider');
+ const joint1Value = document.getElementById('joint1-value');
+ const joint2Value = document.getElementById('joint2-value');
+ const animateBtn = document.getElementById('animate-btn');
+ const resetBtn = document.getElementById('reset-btn');
+
+ // Joint sliders
+ joint1Slider.addEventListener('input', (e) => {
+ const degrees = parseFloat(e.target.value);
+ const radians = (degrees * Math.PI) / 180;
+ jointAngles.joint1 = radians;
+ joint1Value.textContent = degrees.toFixed(1) + '°';
+
+ // Send to main process
+ ipcRenderer.send('set-joint-positions', {
+ joint1: radians,
+ joint2: jointAngles.joint2,
+ });
+ });
+
+ joint2Slider.addEventListener('input', (e) => {
+ const degrees = parseFloat(e.target.value);
+ const radians = (degrees * Math.PI) / 180;
+ jointAngles.joint2 = radians;
+ joint2Value.textContent = degrees.toFixed(1) + '°';
+
+ // Send to main process
+ ipcRenderer.send('set-joint-positions', {
+ joint1: jointAngles.joint1,
+ joint2: radians,
+ });
+ });
+
+ // Animation button
+ animateBtn.addEventListener('click', () => {
+ if (!isAnimating) {
+ ipcRenderer.send('start-animation');
+ } else {
+ ipcRenderer.send('stop-animation');
+ }
+ });
+
+ // Reset button
+ resetBtn.addEventListener('click', () => {
+ ipcRenderer.send('reset-position');
+ });
+}
+
+function startFrequencyMeasurement() {
+ // Update frequency display every second
+ frequencyUpdateInterval = setInterval(() => {
+ const now = Date.now();
+ const timeDiff = (now - lastMessageTime) / 1000;
+ const frequency = timeDiff > 0 ? messageCount / timeDiff : 0;
+
+ document.getElementById('frequency').textContent =
+ frequency.toFixed(1) + ' Hz';
+ messageCount = 0;
+ lastMessageTime = now;
+ }, 1000);
+}
+
+// IPC event handlers
+ipcRenderer.on('ros2-status', (event, status) => {
+ const statusElement = document.getElementById('connection-status');
+ if (status.connected) {
+ statusElement.textContent = 'Connected';
+ statusElement.className = 'status-connected';
+ } else {
+ statusElement.textContent = 'Disconnected';
+ statusElement.className = 'status-disconnected';
+ if (status.error) {
+ console.error('ROS2 Error:', status.error);
+ }
+ }
+});
+
+ipcRenderer.on('joint-state-received', (event, data) => {
+ messageCount++;
+ document.getElementById('msg-count').textContent = messageCount.toString();
+
+ // Update joint angles from received data
+ if (data.names && data.positions) {
+ for (let i = 0; i < data.names.length; i++) {
+ if (data.names[i] === 'joint1') {
+ jointAngles.joint1 = data.positions[i];
+ } else if (data.names[i] === 'joint2') {
+ jointAngles.joint2 = data.positions[i];
+ }
+ }
+
+ // Update UI sliders
+ updateUIFromJointAngles();
+ }
+});
+
+ipcRenderer.on('joint-positions-updated', (event, positions) => {
+ jointAngles.joint1 = positions.joint1;
+ jointAngles.joint2 = positions.joint2;
+ updateUIFromJointAngles();
+});
+
+ipcRenderer.on('animation-status', (event, status) => {
+ isAnimating = status.running;
+ const animateBtn = document.getElementById('animate-btn');
+ animateBtn.textContent = isAnimating ? 'Stop Animation' : 'Start Animation';
+});
+
+function updateUIFromJointAngles() {
+ const joint1Degrees = (jointAngles.joint1 * 180) / Math.PI;
+ const joint2Degrees = (jointAngles.joint2 * 180) / Math.PI;
+
+ document.getElementById('joint1-slider').value = joint1Degrees;
+ document.getElementById('joint2-slider').value = joint2Degrees;
+ document.getElementById('joint1-value').textContent =
+ joint1Degrees.toFixed(1) + '°';
+ document.getElementById('joint2-value').textContent =
+ joint2Degrees.toFixed(1) + '°';
+}
diff --git a/electron_demo/turtle_tf2/turtle-tf2-demo.png b/electron_demo/turtle_tf2/turtle-tf2-demo.png
index e5565c51..a485afb9 100644
Binary files a/electron_demo/turtle_tf2/turtle-tf2-demo.png and b/electron_demo/turtle_tf2/turtle-tf2-demo.png differ
diff --git a/scripts/npmjs-readme.md b/scripts/npmjs-readme.md
index 03646987..43bc3512 100644
--- a/scripts/npmjs-readme.md
+++ b/scripts/npmjs-readme.md
@@ -73,9 +73,12 @@ See [TypeScript demos](https://github.com/RobotWebTools/rclnodejs/tree/develop/t
Create rich, interactive desktop applications using Electron and web technologies like Three.js. Build 3D visualizations, monitoring dashboards, and control interfaces that run on Windows, macOS, and Linux.
-Try the `electron_demo/turtle_tf2` demo for real-time coordinate frame visualization with dynamic transforms and keyboard-controlled turtle movement. More examples in [electron_demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo).
+| Demo | Description | Screenshot |
+| :-----------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------: |
+| **🐢 [turtle_tf2](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo/turtle_tf2)** | Real-time coordinate frame visualization with turtle control. Features TF2 transforms, keyboard control, and dynamic frame updates. |  |
+| **🦾 [manipulator](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo/manipulator)** | Interactive two-joint robotic arm simulation. Features 3D joint visualization, manual/automatic control, and visual movement markers. |  |
-
+Explore more examples in [electron_demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo).
## License