react-robot-vacuum 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -0
- package/dist/RobotVacuum.module.css +151 -0
- package/dist/index.css +129 -0
- package/dist/index.d.mts +45 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +297 -0
- package/dist/index.mjs +276 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# 🤖 React Robot Vacuum
|
|
2
|
+
|
|
3
|
+
An animated React component featuring an autonomous robot vacuum that cleans your page by collecting dirt particles. Built with TypeScript, React 19, and CSS Modules.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## ✨ Features
|
|
8
|
+
|
|
9
|
+
- 🎯 **Autonomous Navigation** - Robot intelligently navigates to dirt particles
|
|
10
|
+
- 🔄 **Smooth Animations** - Realistic acceleration, rotation, and movement
|
|
11
|
+
- ⏸️ **Window Focus Detection** - Automatically pauses when tab is inactive
|
|
12
|
+
- 🎮 **Imperative Control** - Start, stop, and reset via ref
|
|
13
|
+
- 📢 **Lifecycle Callbacks** - Track cleaning progress with events
|
|
14
|
+
- ⚙️ **Fully Customizable** - Props for size, position, speed, and more
|
|
15
|
+
- 💪 **TypeScript** - Full type safety with exported interfaces
|
|
16
|
+
- 🎨 **CSS Modules** - Scoped styling, no conflicts
|
|
17
|
+
- ⚡ **React 17/18/19** - Compatible with all modern React versions
|
|
18
|
+
|
|
19
|
+
## 📦 Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install react-robot-vacuum
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yarn add react-robot-vacuum
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add react-robot-vacuum
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 🚀 Quick Start
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { RobotVacuum } from "react-robot-vacuum";
|
|
37
|
+
|
|
38
|
+
function App() {
|
|
39
|
+
return <RobotVacuum />;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's it! The robot will automatically start cleaning your page.
|
|
44
|
+
|
|
45
|
+
## 📖 API Reference
|
|
46
|
+
|
|
47
|
+
### Props
|
|
48
|
+
|
|
49
|
+
| Prop | Type | Default | Description |
|
|
50
|
+
|------|------|---------|-------------|
|
|
51
|
+
| `numberOfDirtBits` | `number` | `5` | Number of dirt particles to spawn |
|
|
52
|
+
| `autoStart` | `boolean` | `true` | Whether cleaning starts automatically |
|
|
53
|
+
| `minSpeed` | `number` | `0.5` | Minimum movement speed in seconds |
|
|
54
|
+
| `speedFactor` | `number` | `100` | Factor for calculating speed based on distance |
|
|
55
|
+
| `rotationDuration` | `number` | `0.6` | Duration of rotation animation in seconds |
|
|
56
|
+
| `onCleaningStart` | `() => void` | `undefined` | Callback when cleaning starts |
|
|
57
|
+
| `onCleaningComplete` | `() => void` | `undefined` | Callback when robot returns to dock |
|
|
58
|
+
| `onDirtCollected` | `(collected: number, total: number) => void` | `undefined` | Callback fired each time dirt is collected |
|
|
59
|
+
|
|
60
|
+
### Ref Methods
|
|
61
|
+
|
|
62
|
+
Use a ref to control the robot imperatively:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { useRef } from "react";
|
|
66
|
+
import { RobotVacuum, RobotVacuumRef } from "react-robot-vacuum";
|
|
67
|
+
|
|
68
|
+
function App() {
|
|
69
|
+
const robotRef = useRef<RobotVacuumRef>(null);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<RobotVacuum ref={robotRef} autoStart={false} />
|
|
74
|
+
<button onClick={() => robotRef.current?.startCleaning()}>
|
|
75
|
+
Start Cleaning
|
|
76
|
+
</button>
|
|
77
|
+
<button onClick={() => robotRef.current?.reset()}>
|
|
78
|
+
Reset
|
|
79
|
+
</button>
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Methods
|
|
86
|
+
|
|
87
|
+
- **`startCleaning()`** - Manually start the cleaning process
|
|
88
|
+
- **`reset()`** - Reset robot to dock and generate new dirt
|
|
89
|
+
|
|
90
|
+
### Types
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
export interface RobotVacuumRef {
|
|
94
|
+
startCleaning: () => void;
|
|
95
|
+
reset: () => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface RobotVacuumProps {
|
|
99
|
+
readonly numberOfDirtBits?: number;
|
|
100
|
+
readonly minSpeed?: number;
|
|
101
|
+
readonly speedFactor?: number;
|
|
102
|
+
readonly rotationDuration?: number;
|
|
103
|
+
readonly autoStart?: boolean;
|
|
104
|
+
readonly onCleaningStart?: () => void;
|
|
105
|
+
readonly onCleaningComplete?: () => void;
|
|
106
|
+
readonly onDirtCollected?: (collected: number, total: number) => void;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 🎨 Examples
|
|
111
|
+
|
|
112
|
+
### Manual Control with Callbacks
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { useRef, useState } from "react";
|
|
116
|
+
import { RobotVacuum, RobotVacuumRef } from "react-robot-vacuum";
|
|
117
|
+
|
|
118
|
+
function App() {
|
|
119
|
+
const robotRef = useRef<RobotVacuumRef>(null);
|
|
120
|
+
const [progress, setProgress] = useState("0/0");
|
|
121
|
+
const [status, setStatus] = useState("Ready");
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
<RobotVacuum
|
|
126
|
+
ref={robotRef}
|
|
127
|
+
autoStart={false}
|
|
128
|
+
numberOfDirtBits={10}
|
|
129
|
+
onCleaningStart={() => setStatus("Cleaning...")}
|
|
130
|
+
onDirtCollected={(collected, total) => {
|
|
131
|
+
setProgress(`${collected}/${total}`);
|
|
132
|
+
}}
|
|
133
|
+
onCleaningComplete={() => setStatus("Complete!")}
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
<div style={{ position: "fixed", top: 20, right: 20 }}>
|
|
137
|
+
<p>Status: {status}</p>
|
|
138
|
+
<p>Progress: {progress}</p>
|
|
139
|
+
<button onClick={() => robotRef.current?.startCleaning()}>
|
|
140
|
+
Start
|
|
141
|
+
</button>
|
|
142
|
+
<button onClick={() => robotRef.current?.reset()}>
|
|
143
|
+
Reset
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Slow and Methodical Cleaning
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { RobotVacuum } from "react-robot-vacuum";
|
|
155
|
+
|
|
156
|
+
function App() {
|
|
157
|
+
return (
|
|
158
|
+
<RobotVacuum
|
|
159
|
+
minSpeed={1.5}
|
|
160
|
+
speedFactor={50}
|
|
161
|
+
rotationDuration={1.2}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 🎮 Live Demo
|
|
168
|
+
|
|
169
|
+
Try it on [CodeSandbox](https://codesandbox.io/s/react-robot-vacuum-demo)
|
|
170
|
+
|
|
171
|
+
## 🛠️ Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Install dependencies
|
|
175
|
+
npm install
|
|
176
|
+
|
|
177
|
+
# Run dev server
|
|
178
|
+
npm run vite:dev
|
|
179
|
+
|
|
180
|
+
# Build for production
|
|
181
|
+
npm run build
|
|
182
|
+
|
|
183
|
+
# Lint
|
|
184
|
+
npm run lint
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## 📄 License
|
|
188
|
+
|
|
189
|
+
MIT © [Your Name]
|
|
190
|
+
|
|
191
|
+
## 🤝 Contributing
|
|
192
|
+
|
|
193
|
+
Contributions, issues, and feature requests are welcome!
|
|
194
|
+
|
|
195
|
+
## ⭐ Show your support
|
|
196
|
+
|
|
197
|
+
Give a ⭐️ if this project helped you!
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
Made with ❤️ and TypeScript
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
position: absolute;
|
|
3
|
+
width: 100%;
|
|
4
|
+
height: 100%;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
top: 0;
|
|
7
|
+
left: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.robot {
|
|
11
|
+
visibility: hidden; /* Hide the robot by default */
|
|
12
|
+
position: absolute;
|
|
13
|
+
z-index: 100;
|
|
14
|
+
width: 25px;
|
|
15
|
+
height: 25px;
|
|
16
|
+
background-color: #333;
|
|
17
|
+
border-radius: 50%;
|
|
18
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Robot center button */
|
|
22
|
+
.robot::before {
|
|
23
|
+
content: "";
|
|
24
|
+
position: absolute;
|
|
25
|
+
top: 50%;
|
|
26
|
+
left: 50%;
|
|
27
|
+
transform: translate(-50%, -50%);
|
|
28
|
+
width: 7px;
|
|
29
|
+
height: 7px;
|
|
30
|
+
background-color: #666;
|
|
31
|
+
border-radius: 50%;
|
|
32
|
+
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* State styles for the robot */
|
|
36
|
+
.idle::before {
|
|
37
|
+
background-color: #c0c0c0;
|
|
38
|
+
box-shadow: 0 0 10px rgba(255, 255, 255, 0.8); /* Glowing effect */
|
|
39
|
+
animation: glow 1s infinite alternate;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.cleaning::before {
|
|
43
|
+
background-color: #00ff00;
|
|
44
|
+
animation: flash 0.5s infinite alternate;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.returning::before {
|
|
48
|
+
background-color: orange;
|
|
49
|
+
animation: flash 0.5s infinite alternate;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Keyframe for glowing effect */
|
|
53
|
+
@keyframes glow {
|
|
54
|
+
0% {
|
|
55
|
+
box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
|
|
56
|
+
}
|
|
57
|
+
100% {
|
|
58
|
+
box-shadow: 0 0 20px rgba(255, 255, 255, 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Keyframe for flashing effect */
|
|
63
|
+
@keyframes flash {
|
|
64
|
+
0% {
|
|
65
|
+
opacity: 1;
|
|
66
|
+
}
|
|
67
|
+
100% {
|
|
68
|
+
opacity: 0.5;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Robot wheels */
|
|
73
|
+
.wheel {
|
|
74
|
+
position: absolute;
|
|
75
|
+
width: 2px;
|
|
76
|
+
height: 7px;
|
|
77
|
+
background-color: #444;
|
|
78
|
+
border-radius: 1px;
|
|
79
|
+
top: calc(50% - 4px);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.wheel.left {
|
|
83
|
+
left: -1px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.wheel.right {
|
|
87
|
+
right: -1px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Robot sensors */
|
|
91
|
+
.sensor {
|
|
92
|
+
position: absolute;
|
|
93
|
+
width: 4px;
|
|
94
|
+
height: 4px;
|
|
95
|
+
background-color: #444;
|
|
96
|
+
border-radius: 50%;
|
|
97
|
+
top: 3px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.sensor.left {
|
|
101
|
+
left: 3px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.sensor.right {
|
|
105
|
+
right: 3px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Robot front panel */
|
|
109
|
+
.front-panel {
|
|
110
|
+
position: absolute;
|
|
111
|
+
top: 15px;
|
|
112
|
+
left: 50%;
|
|
113
|
+
transform: translateX(-50%);
|
|
114
|
+
width: 13px;
|
|
115
|
+
height: 7px;
|
|
116
|
+
background-color: #666;
|
|
117
|
+
border-radius: 7px 7px 0 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.dirt {
|
|
121
|
+
position: absolute;
|
|
122
|
+
z-index: 100;
|
|
123
|
+
width: 5px;
|
|
124
|
+
height: 5px;
|
|
125
|
+
background-color: #fff; /* Light gray color */
|
|
126
|
+
border: 1px solid black;
|
|
127
|
+
}
|
|
128
|
+
.dock {
|
|
129
|
+
position: absolute;
|
|
130
|
+
top: 5px;
|
|
131
|
+
left: 25px;
|
|
132
|
+
transform: translateX(-50%);
|
|
133
|
+
width: 20px;
|
|
134
|
+
height: 20px;
|
|
135
|
+
background-color: #333; /* Dark color for the dock */
|
|
136
|
+
border-radius: 10px; /* Rounded corners */
|
|
137
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Subtle shadow */
|
|
138
|
+
justify-content: center;
|
|
139
|
+
align-items: center;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.dock::before {
|
|
143
|
+
content: "";
|
|
144
|
+
position: absolute;
|
|
145
|
+
left: -5px;
|
|
146
|
+
top: -5px; /* Adjust as needed */
|
|
147
|
+
width: 30px; /* Adjust as needed */
|
|
148
|
+
height: 10px; /* Adjust as needed */
|
|
149
|
+
background-color: #333; /* Same color as the dock */
|
|
150
|
+
border-radius: 5px 5px 0 0; /* Rounded top corners */
|
|
151
|
+
}
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* src/RobotVacuum/RobotVacuum.module.css */
|
|
2
|
+
.container {
|
|
3
|
+
position: absolute;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
top: 0;
|
|
8
|
+
left: 0;
|
|
9
|
+
}
|
|
10
|
+
.robot {
|
|
11
|
+
visibility: hidden;
|
|
12
|
+
position: absolute;
|
|
13
|
+
z-index: 100;
|
|
14
|
+
width: 25px;
|
|
15
|
+
height: 25px;
|
|
16
|
+
background-color: #333;
|
|
17
|
+
border-radius: 50%;
|
|
18
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
19
|
+
}
|
|
20
|
+
.robot::before {
|
|
21
|
+
content: "";
|
|
22
|
+
position: absolute;
|
|
23
|
+
top: 50%;
|
|
24
|
+
left: 50%;
|
|
25
|
+
transform: translate(-50%, -50%);
|
|
26
|
+
width: 7px;
|
|
27
|
+
height: 7px;
|
|
28
|
+
background-color: #666;
|
|
29
|
+
border-radius: 50%;
|
|
30
|
+
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
31
|
+
}
|
|
32
|
+
.idle::before {
|
|
33
|
+
background-color: #c0c0c0;
|
|
34
|
+
box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
|
|
35
|
+
animation: glow 1s infinite alternate;
|
|
36
|
+
}
|
|
37
|
+
.cleaning::before {
|
|
38
|
+
background-color: #00ff00;
|
|
39
|
+
animation: flash 0.5s infinite alternate;
|
|
40
|
+
}
|
|
41
|
+
.returning::before {
|
|
42
|
+
background-color: orange;
|
|
43
|
+
animation: flash 0.5s infinite alternate;
|
|
44
|
+
}
|
|
45
|
+
@keyframes glow {
|
|
46
|
+
0% {
|
|
47
|
+
box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
|
|
48
|
+
}
|
|
49
|
+
100% {
|
|
50
|
+
box-shadow: 0 0 20px rgba(255, 255, 255, 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
@keyframes flash {
|
|
54
|
+
0% {
|
|
55
|
+
opacity: 1;
|
|
56
|
+
}
|
|
57
|
+
100% {
|
|
58
|
+
opacity: 0.5;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
.wheel {
|
|
62
|
+
position: absolute;
|
|
63
|
+
width: 2px;
|
|
64
|
+
height: 7px;
|
|
65
|
+
background-color: #444;
|
|
66
|
+
border-radius: 1px;
|
|
67
|
+
top: calc(50% - 4px);
|
|
68
|
+
}
|
|
69
|
+
.wheel.left {
|
|
70
|
+
left: -1px;
|
|
71
|
+
}
|
|
72
|
+
.wheel.right {
|
|
73
|
+
right: -1px;
|
|
74
|
+
}
|
|
75
|
+
.sensor {
|
|
76
|
+
position: absolute;
|
|
77
|
+
width: 4px;
|
|
78
|
+
height: 4px;
|
|
79
|
+
background-color: #444;
|
|
80
|
+
border-radius: 50%;
|
|
81
|
+
top: 3px;
|
|
82
|
+
}
|
|
83
|
+
.sensor.left {
|
|
84
|
+
left: 3px;
|
|
85
|
+
}
|
|
86
|
+
.sensor.right {
|
|
87
|
+
right: 3px;
|
|
88
|
+
}
|
|
89
|
+
.front-panel {
|
|
90
|
+
position: absolute;
|
|
91
|
+
top: 15px;
|
|
92
|
+
left: 50%;
|
|
93
|
+
transform: translateX(-50%);
|
|
94
|
+
width: 13px;
|
|
95
|
+
height: 7px;
|
|
96
|
+
background-color: #666;
|
|
97
|
+
border-radius: 7px 7px 0 0;
|
|
98
|
+
}
|
|
99
|
+
.dirt {
|
|
100
|
+
position: absolute;
|
|
101
|
+
z-index: 100;
|
|
102
|
+
width: 5px;
|
|
103
|
+
height: 5px;
|
|
104
|
+
background-color: #fff;
|
|
105
|
+
border: 1px solid black;
|
|
106
|
+
}
|
|
107
|
+
.dock {
|
|
108
|
+
position: absolute;
|
|
109
|
+
top: 5px;
|
|
110
|
+
left: 25px;
|
|
111
|
+
transform: translateX(-50%);
|
|
112
|
+
width: 20px;
|
|
113
|
+
height: 20px;
|
|
114
|
+
background-color: #333;
|
|
115
|
+
border-radius: 10px;
|
|
116
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
117
|
+
justify-content: center;
|
|
118
|
+
align-items: center;
|
|
119
|
+
}
|
|
120
|
+
.dock::before {
|
|
121
|
+
content: "";
|
|
122
|
+
position: absolute;
|
|
123
|
+
left: -5px;
|
|
124
|
+
top: -5px;
|
|
125
|
+
width: 30px;
|
|
126
|
+
height: 10px;
|
|
127
|
+
background-color: #333;
|
|
128
|
+
border-radius: 5px 5px 0 0;
|
|
129
|
+
}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Methods exposed via ref for imperative control
|
|
5
|
+
*/
|
|
6
|
+
interface RobotVacuumRef {
|
|
7
|
+
startCleaning: () => void;
|
|
8
|
+
reset: () => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Props for RobotVacuum component
|
|
12
|
+
*/
|
|
13
|
+
interface RobotVacuumProps {
|
|
14
|
+
readonly numberOfDirtBits?: number;
|
|
15
|
+
readonly minSpeed?: number;
|
|
16
|
+
readonly speedFactor?: number;
|
|
17
|
+
readonly rotationDuration?: number;
|
|
18
|
+
readonly autoStart?: boolean;
|
|
19
|
+
readonly onCleaningStart?: () => void;
|
|
20
|
+
readonly onCleaningComplete?: () => void;
|
|
21
|
+
readonly onDirtCollected?: (collected: number, total: number) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Robotv2 - Advanced robot vacuum component for React 19
|
|
25
|
+
*
|
|
26
|
+
* Features:
|
|
27
|
+
* - Full TypeScript support with immutable types
|
|
28
|
+
* - Creates an overlay on the page with random dirt particles
|
|
29
|
+
* - Robot navigates to each dirt particle and collects it
|
|
30
|
+
* - Smooth acceleration and rotation animations
|
|
31
|
+
* - Automatic return to dock when finished
|
|
32
|
+
* - Imperative control via ref (startCleaning, reset)
|
|
33
|
+
*
|
|
34
|
+
* @param numberOfDirtBits - Number of dirt particles to spawn (default: 5)
|
|
35
|
+
* @param minSpeed - Minimum movement speed in seconds (default: 0.5)
|
|
36
|
+
* @param speedFactor - Factor for calculating speed based on distance (default: 100)
|
|
37
|
+
* @param rotationDuration - Duration of rotation animation in seconds (default: 0.6)
|
|
38
|
+
* @param autoStart - Whether to automatically start cleaning on mount (default: true)
|
|
39
|
+
* @param onCleaningStart - Callback fired when cleaning starts
|
|
40
|
+
* @param onCleaningComplete - Callback fired when robot returns to dock
|
|
41
|
+
* @param onDirtCollected - Callback fired when dirt is collected
|
|
42
|
+
*/
|
|
43
|
+
declare const Robotv2: react.ForwardRefExoticComponent<RobotVacuumProps & react.RefAttributes<RobotVacuumRef>>;
|
|
44
|
+
|
|
45
|
+
export { Robotv2 as RobotVacuum, type RobotVacuumProps, type RobotVacuumRef };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Methods exposed via ref for imperative control
|
|
5
|
+
*/
|
|
6
|
+
interface RobotVacuumRef {
|
|
7
|
+
startCleaning: () => void;
|
|
8
|
+
reset: () => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Props for RobotVacuum component
|
|
12
|
+
*/
|
|
13
|
+
interface RobotVacuumProps {
|
|
14
|
+
readonly numberOfDirtBits?: number;
|
|
15
|
+
readonly minSpeed?: number;
|
|
16
|
+
readonly speedFactor?: number;
|
|
17
|
+
readonly rotationDuration?: number;
|
|
18
|
+
readonly autoStart?: boolean;
|
|
19
|
+
readonly onCleaningStart?: () => void;
|
|
20
|
+
readonly onCleaningComplete?: () => void;
|
|
21
|
+
readonly onDirtCollected?: (collected: number, total: number) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Robotv2 - Advanced robot vacuum component for React 19
|
|
25
|
+
*
|
|
26
|
+
* Features:
|
|
27
|
+
* - Full TypeScript support with immutable types
|
|
28
|
+
* - Creates an overlay on the page with random dirt particles
|
|
29
|
+
* - Robot navigates to each dirt particle and collects it
|
|
30
|
+
* - Smooth acceleration and rotation animations
|
|
31
|
+
* - Automatic return to dock when finished
|
|
32
|
+
* - Imperative control via ref (startCleaning, reset)
|
|
33
|
+
*
|
|
34
|
+
* @param numberOfDirtBits - Number of dirt particles to spawn (default: 5)
|
|
35
|
+
* @param minSpeed - Minimum movement speed in seconds (default: 0.5)
|
|
36
|
+
* @param speedFactor - Factor for calculating speed based on distance (default: 100)
|
|
37
|
+
* @param rotationDuration - Duration of rotation animation in seconds (default: 0.6)
|
|
38
|
+
* @param autoStart - Whether to automatically start cleaning on mount (default: true)
|
|
39
|
+
* @param onCleaningStart - Callback fired when cleaning starts
|
|
40
|
+
* @param onCleaningComplete - Callback fired when robot returns to dock
|
|
41
|
+
* @param onDirtCollected - Callback fired when dirt is collected
|
|
42
|
+
*/
|
|
43
|
+
declare const Robotv2: react.ForwardRefExoticComponent<RobotVacuumProps & react.RefAttributes<RobotVacuumRef>>;
|
|
44
|
+
|
|
45
|
+
export { Robotv2 as RobotVacuum, type RobotVacuumProps, type RobotVacuumRef };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.tsx
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
RobotVacuum: () => RobotVacuum_default2
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/RobotVacuum/RobotVacuum.tsx
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
|
|
30
|
+
// src/RobotVacuum/RobotVacuum.module.css
|
|
31
|
+
var RobotVacuum_default = {};
|
|
32
|
+
|
|
33
|
+
// src/RobotVacuum/RobotVacuum.tsx
|
|
34
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
35
|
+
var Robotv2 = (0, import_react.forwardRef)(
|
|
36
|
+
({
|
|
37
|
+
numberOfDirtBits = 5,
|
|
38
|
+
minSpeed = 0.5,
|
|
39
|
+
speedFactor = 100,
|
|
40
|
+
rotationDuration = 0.6,
|
|
41
|
+
autoStart = true,
|
|
42
|
+
onCleaningStart,
|
|
43
|
+
onCleaningComplete,
|
|
44
|
+
onDirtCollected
|
|
45
|
+
}, ref) => {
|
|
46
|
+
const [dirtPositions, setDirtPositions] = (0, import_react.useState)(
|
|
47
|
+
[]
|
|
48
|
+
);
|
|
49
|
+
const [robotState, setRobotState] = (0, import_react.useState)("idle");
|
|
50
|
+
const [isDocumentVisible, setIsDocumentVisible] = (0, import_react.useState)(true);
|
|
51
|
+
const robotRef = (0, import_react.useRef)(null);
|
|
52
|
+
const backgroundRef = (0, import_react.useRef)(null);
|
|
53
|
+
const cleaningAbortedRef = (0, import_react.useRef)(false);
|
|
54
|
+
const ROBOT_SIZE = 25;
|
|
55
|
+
const ROBOT_HALF_SIZE = ROBOT_SIZE / 2;
|
|
56
|
+
const DOCKED_POSITION = {
|
|
57
|
+
x: 12,
|
|
58
|
+
y: 5
|
|
59
|
+
};
|
|
60
|
+
const initializeRobotPosition = async () => {
|
|
61
|
+
const robotElement = robotRef.current;
|
|
62
|
+
if (!robotElement) return;
|
|
63
|
+
robotElement.style.visibility = "visible";
|
|
64
|
+
robotElement.style.left = `${DOCKED_POSITION.x}px`;
|
|
65
|
+
robotElement.style.top = `${DOCKED_POSITION.y}px`;
|
|
66
|
+
robotElement.style.transform = "rotate(0deg)";
|
|
67
|
+
};
|
|
68
|
+
const getRandomPosition = (width, height) => ({
|
|
69
|
+
x: Math.floor(Math.random() * width),
|
|
70
|
+
y: Math.floor(Math.random() * height),
|
|
71
|
+
collected: false
|
|
72
|
+
});
|
|
73
|
+
const isPositionEmpty = (x, y) => {
|
|
74
|
+
const backgroundElement = backgroundRef.current;
|
|
75
|
+
if (!backgroundElement) return false;
|
|
76
|
+
const elementAtPosition = document.elementFromPoint(x, y);
|
|
77
|
+
return elementAtPosition === backgroundElement || elementAtPosition !== null && backgroundElement.contains(elementAtPosition);
|
|
78
|
+
};
|
|
79
|
+
const createRandomDirt = () => {
|
|
80
|
+
const backgroundElement = backgroundRef.current;
|
|
81
|
+
if (!backgroundElement) return;
|
|
82
|
+
const { width, height } = backgroundElement.getBoundingClientRect();
|
|
83
|
+
const newDirt = [];
|
|
84
|
+
let attempts = 0;
|
|
85
|
+
const maxAttempts = 1e3;
|
|
86
|
+
while (newDirt.length < numberOfDirtBits && attempts < maxAttempts) {
|
|
87
|
+
const position = getRandomPosition(width, height);
|
|
88
|
+
if (isPositionEmpty(position.x, position.y)) {
|
|
89
|
+
newDirt.push(position);
|
|
90
|
+
}
|
|
91
|
+
attempts++;
|
|
92
|
+
}
|
|
93
|
+
newDirt.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
94
|
+
setDirtPositions(Object.freeze(newDirt));
|
|
95
|
+
};
|
|
96
|
+
const getRobotPosition = () => {
|
|
97
|
+
const robotElement = robotRef.current;
|
|
98
|
+
if (!robotElement) return null;
|
|
99
|
+
const rect = robotElement.getBoundingClientRect();
|
|
100
|
+
const transform = robotElement.style.transform;
|
|
101
|
+
const rotationMatch = transform.match(/rotate\(([^)]+)deg\)/);
|
|
102
|
+
const rotation = rotationMatch ? parseFloat(rotationMatch[1]) : 0;
|
|
103
|
+
return {
|
|
104
|
+
x: rect.left + rect.width / 2,
|
|
105
|
+
y: rect.top + rect.height / 2,
|
|
106
|
+
rotation
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
const calculateShortestRotation = (targetAngle) => {
|
|
110
|
+
const currentPos = getRobotPosition();
|
|
111
|
+
if (!currentPos) return targetAngle;
|
|
112
|
+
let current = currentPos.rotation % 360;
|
|
113
|
+
let target = targetAngle % 360;
|
|
114
|
+
if (current < 0) current += 360;
|
|
115
|
+
if (target < 0) target += 360;
|
|
116
|
+
let diff = target - current;
|
|
117
|
+
if (diff > 180) {
|
|
118
|
+
diff -= 360;
|
|
119
|
+
} else if (diff < -180) {
|
|
120
|
+
diff += 360;
|
|
121
|
+
}
|
|
122
|
+
return current + diff;
|
|
123
|
+
};
|
|
124
|
+
const rotateRobot = async (targetAngle) => {
|
|
125
|
+
const robotElement = robotRef.current;
|
|
126
|
+
if (!robotElement) return;
|
|
127
|
+
const shortestAngle = calculateShortestRotation(targetAngle);
|
|
128
|
+
robotElement.style.transition = `transform ${rotationDuration}s ease-in-out`;
|
|
129
|
+
robotElement.style.transform = `rotate(${shortestAngle}deg)`;
|
|
130
|
+
await new Promise(
|
|
131
|
+
(resolve) => setTimeout(resolve, rotationDuration * 1e3)
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
const calculateMovementTime = (distance) => {
|
|
135
|
+
return Math.max(Math.sqrt(distance / speedFactor), minSpeed);
|
|
136
|
+
};
|
|
137
|
+
const moveRobotToPosition = async (targetX, targetY) => {
|
|
138
|
+
const robotElement = robotRef.current;
|
|
139
|
+
if (!robotElement) return;
|
|
140
|
+
const currentPos = getRobotPosition();
|
|
141
|
+
if (!currentPos) return;
|
|
142
|
+
if (Math.abs(currentPos.x - targetX) > 1) {
|
|
143
|
+
const deltaX = targetX - currentPos.x;
|
|
144
|
+
const angle = deltaX > 0 ? 90 : -90;
|
|
145
|
+
await rotateRobot(angle);
|
|
146
|
+
const distanceX = Math.abs(deltaX);
|
|
147
|
+
const movementTime = calculateMovementTime(distanceX);
|
|
148
|
+
robotElement.style.transition = `left ${movementTime}s ease-in-out`;
|
|
149
|
+
robotElement.style.left = `${targetX - ROBOT_HALF_SIZE}px`;
|
|
150
|
+
await new Promise(
|
|
151
|
+
(resolve) => setTimeout(resolve, movementTime * 1e3)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (Math.abs(currentPos.y - targetY) > 1) {
|
|
155
|
+
const deltaY = targetY - currentPos.y;
|
|
156
|
+
const angle = deltaY > 0 ? 180 : 0;
|
|
157
|
+
await rotateRobot(angle);
|
|
158
|
+
const distanceY = Math.abs(deltaY);
|
|
159
|
+
const movementTime = calculateMovementTime(distanceY);
|
|
160
|
+
robotElement.style.transition = `top ${movementTime}s ease-in-out`;
|
|
161
|
+
robotElement.style.top = `${targetY - ROBOT_HALF_SIZE}px`;
|
|
162
|
+
await new Promise(
|
|
163
|
+
(resolve) => setTimeout(resolve, movementTime * 1e3)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const cleanAndReturn = async () => {
|
|
168
|
+
if (dirtPositions.length === 0) return;
|
|
169
|
+
onCleaningStart?.();
|
|
170
|
+
cleaningAbortedRef.current = false;
|
|
171
|
+
const uncollectedDirt = [...dirtPositions];
|
|
172
|
+
let collectedCount = 0;
|
|
173
|
+
for (let i = 0; i < uncollectedDirt.length; i++) {
|
|
174
|
+
if (cleaningAbortedRef.current) {
|
|
175
|
+
setRobotState("idle");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const dirt = uncollectedDirt[i];
|
|
179
|
+
if (dirt.collected) continue;
|
|
180
|
+
setRobotState("cleaning");
|
|
181
|
+
const currentPos2 = getRobotPosition();
|
|
182
|
+
if (currentPos2) {
|
|
183
|
+
await moveRobotToPosition(dirt.x, dirt.y);
|
|
184
|
+
if (cleaningAbortedRef.current) {
|
|
185
|
+
setRobotState("idle");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
setDirtPositions(
|
|
189
|
+
(prev) => Object.freeze(
|
|
190
|
+
prev.map((d, idx) => idx === i ? { ...d, collected: true } : d)
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
collectedCount++;
|
|
194
|
+
onDirtCollected?.(collectedCount, uncollectedDirt.length);
|
|
195
|
+
}
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
197
|
+
}
|
|
198
|
+
if (cleaningAbortedRef.current) {
|
|
199
|
+
setRobotState("idle");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
setRobotState("returning");
|
|
203
|
+
const currentPos = getRobotPosition();
|
|
204
|
+
if (currentPos) {
|
|
205
|
+
const dockCenterX = DOCKED_POSITION.x + ROBOT_HALF_SIZE;
|
|
206
|
+
const dockCenterY = DOCKED_POSITION.y + ROBOT_HALF_SIZE;
|
|
207
|
+
await moveRobotToPosition(dockCenterX, dockCenterY);
|
|
208
|
+
}
|
|
209
|
+
setRobotState("idle");
|
|
210
|
+
onCleaningComplete?.();
|
|
211
|
+
};
|
|
212
|
+
(0, import_react.useImperativeHandle)(ref, () => ({
|
|
213
|
+
startCleaning: () => {
|
|
214
|
+
if (robotState === "idle" && isDocumentVisible) {
|
|
215
|
+
cleanAndReturn();
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
reset: () => {
|
|
219
|
+
cleaningAbortedRef.current = true;
|
|
220
|
+
setRobotState("idle");
|
|
221
|
+
initializeRobotPosition();
|
|
222
|
+
createRandomDirt();
|
|
223
|
+
}
|
|
224
|
+
}));
|
|
225
|
+
(0, import_react.useEffect)(() => {
|
|
226
|
+
const handleVisibilityChange = () => {
|
|
227
|
+
const isVisible = !document.hidden;
|
|
228
|
+
setIsDocumentVisible(isVisible);
|
|
229
|
+
if (!isVisible) {
|
|
230
|
+
cleaningAbortedRef.current = true;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
234
|
+
return () => {
|
|
235
|
+
document.removeEventListener(
|
|
236
|
+
"visibilitychange",
|
|
237
|
+
handleVisibilityChange
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
}, []);
|
|
241
|
+
(0, import_react.useEffect)(() => {
|
|
242
|
+
initializeRobotPosition();
|
|
243
|
+
createRandomDirt();
|
|
244
|
+
}, []);
|
|
245
|
+
(0, import_react.useEffect)(() => {
|
|
246
|
+
if (autoStart && dirtPositions.length > 0 && robotState === "idle" && isDocumentVisible) {
|
|
247
|
+
cleanAndReturn();
|
|
248
|
+
}
|
|
249
|
+
}, [dirtPositions, robotState, isDocumentVisible, autoStart]);
|
|
250
|
+
(0, import_react.useEffect)(() => {
|
|
251
|
+
const robotElement = robotRef.current;
|
|
252
|
+
if (!robotElement) return;
|
|
253
|
+
robotElement.classList.remove(
|
|
254
|
+
RobotVacuum_default.idle,
|
|
255
|
+
RobotVacuum_default.cleaning,
|
|
256
|
+
RobotVacuum_default.returning
|
|
257
|
+
);
|
|
258
|
+
robotElement.classList.add(RobotVacuum_default[robotState]);
|
|
259
|
+
}, [robotState]);
|
|
260
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: backgroundRef, className: RobotVacuum_default.container, children: [
|
|
261
|
+
dirtPositions.map(
|
|
262
|
+
(dirt, index) => !dirt.collected ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
263
|
+
"div",
|
|
264
|
+
{
|
|
265
|
+
className: RobotVacuum_default.dirt,
|
|
266
|
+
style: { left: `${dirt.x}px`, top: `${dirt.y}px` },
|
|
267
|
+
role: "presentation",
|
|
268
|
+
"aria-label": "dirt particle"
|
|
269
|
+
},
|
|
270
|
+
`dirt-${index}`
|
|
271
|
+
) : null
|
|
272
|
+
),
|
|
273
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: RobotVacuum_default.dock, role: "presentation", "aria-label": "dock" }),
|
|
274
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
275
|
+
"div",
|
|
276
|
+
{
|
|
277
|
+
ref: robotRef,
|
|
278
|
+
className: RobotVacuum_default.robot,
|
|
279
|
+
role: "presentation",
|
|
280
|
+
"aria-label": "robot vacuum",
|
|
281
|
+
children: [
|
|
282
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `${RobotVacuum_default.wheel} ${RobotVacuum_default.left}` }),
|
|
283
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `${RobotVacuum_default.wheel} ${RobotVacuum_default.right}` }),
|
|
284
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `${RobotVacuum_default.sensor} ${RobotVacuum_default.left}` }),
|
|
285
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `${RobotVacuum_default.sensor} ${RobotVacuum_default.right}` })
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
] });
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
Robotv2.displayName = "Robotv2";
|
|
293
|
+
var RobotVacuum_default2 = Robotv2;
|
|
294
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
295
|
+
0 && (module.exports = {
|
|
296
|
+
RobotVacuum
|
|
297
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// src/RobotVacuum/RobotVacuum.tsx
|
|
2
|
+
import {
|
|
3
|
+
forwardRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
7
|
+
useState
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
// src/RobotVacuum/RobotVacuum.module.css
|
|
11
|
+
var RobotVacuum_default = {};
|
|
12
|
+
|
|
13
|
+
// src/RobotVacuum/RobotVacuum.tsx
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
var Robotv2 = forwardRef(
|
|
16
|
+
({
|
|
17
|
+
numberOfDirtBits = 5,
|
|
18
|
+
minSpeed = 0.5,
|
|
19
|
+
speedFactor = 100,
|
|
20
|
+
rotationDuration = 0.6,
|
|
21
|
+
autoStart = true,
|
|
22
|
+
onCleaningStart,
|
|
23
|
+
onCleaningComplete,
|
|
24
|
+
onDirtCollected
|
|
25
|
+
}, ref) => {
|
|
26
|
+
const [dirtPositions, setDirtPositions] = useState(
|
|
27
|
+
[]
|
|
28
|
+
);
|
|
29
|
+
const [robotState, setRobotState] = useState("idle");
|
|
30
|
+
const [isDocumentVisible, setIsDocumentVisible] = useState(true);
|
|
31
|
+
const robotRef = useRef(null);
|
|
32
|
+
const backgroundRef = useRef(null);
|
|
33
|
+
const cleaningAbortedRef = useRef(false);
|
|
34
|
+
const ROBOT_SIZE = 25;
|
|
35
|
+
const ROBOT_HALF_SIZE = ROBOT_SIZE / 2;
|
|
36
|
+
const DOCKED_POSITION = {
|
|
37
|
+
x: 12,
|
|
38
|
+
y: 5
|
|
39
|
+
};
|
|
40
|
+
const initializeRobotPosition = async () => {
|
|
41
|
+
const robotElement = robotRef.current;
|
|
42
|
+
if (!robotElement) return;
|
|
43
|
+
robotElement.style.visibility = "visible";
|
|
44
|
+
robotElement.style.left = `${DOCKED_POSITION.x}px`;
|
|
45
|
+
robotElement.style.top = `${DOCKED_POSITION.y}px`;
|
|
46
|
+
robotElement.style.transform = "rotate(0deg)";
|
|
47
|
+
};
|
|
48
|
+
const getRandomPosition = (width, height) => ({
|
|
49
|
+
x: Math.floor(Math.random() * width),
|
|
50
|
+
y: Math.floor(Math.random() * height),
|
|
51
|
+
collected: false
|
|
52
|
+
});
|
|
53
|
+
const isPositionEmpty = (x, y) => {
|
|
54
|
+
const backgroundElement = backgroundRef.current;
|
|
55
|
+
if (!backgroundElement) return false;
|
|
56
|
+
const elementAtPosition = document.elementFromPoint(x, y);
|
|
57
|
+
return elementAtPosition === backgroundElement || elementAtPosition !== null && backgroundElement.contains(elementAtPosition);
|
|
58
|
+
};
|
|
59
|
+
const createRandomDirt = () => {
|
|
60
|
+
const backgroundElement = backgroundRef.current;
|
|
61
|
+
if (!backgroundElement) return;
|
|
62
|
+
const { width, height } = backgroundElement.getBoundingClientRect();
|
|
63
|
+
const newDirt = [];
|
|
64
|
+
let attempts = 0;
|
|
65
|
+
const maxAttempts = 1e3;
|
|
66
|
+
while (newDirt.length < numberOfDirtBits && attempts < maxAttempts) {
|
|
67
|
+
const position = getRandomPosition(width, height);
|
|
68
|
+
if (isPositionEmpty(position.x, position.y)) {
|
|
69
|
+
newDirt.push(position);
|
|
70
|
+
}
|
|
71
|
+
attempts++;
|
|
72
|
+
}
|
|
73
|
+
newDirt.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
74
|
+
setDirtPositions(Object.freeze(newDirt));
|
|
75
|
+
};
|
|
76
|
+
const getRobotPosition = () => {
|
|
77
|
+
const robotElement = robotRef.current;
|
|
78
|
+
if (!robotElement) return null;
|
|
79
|
+
const rect = robotElement.getBoundingClientRect();
|
|
80
|
+
const transform = robotElement.style.transform;
|
|
81
|
+
const rotationMatch = transform.match(/rotate\(([^)]+)deg\)/);
|
|
82
|
+
const rotation = rotationMatch ? parseFloat(rotationMatch[1]) : 0;
|
|
83
|
+
return {
|
|
84
|
+
x: rect.left + rect.width / 2,
|
|
85
|
+
y: rect.top + rect.height / 2,
|
|
86
|
+
rotation
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const calculateShortestRotation = (targetAngle) => {
|
|
90
|
+
const currentPos = getRobotPosition();
|
|
91
|
+
if (!currentPos) return targetAngle;
|
|
92
|
+
let current = currentPos.rotation % 360;
|
|
93
|
+
let target = targetAngle % 360;
|
|
94
|
+
if (current < 0) current += 360;
|
|
95
|
+
if (target < 0) target += 360;
|
|
96
|
+
let diff = target - current;
|
|
97
|
+
if (diff > 180) {
|
|
98
|
+
diff -= 360;
|
|
99
|
+
} else if (diff < -180) {
|
|
100
|
+
diff += 360;
|
|
101
|
+
}
|
|
102
|
+
return current + diff;
|
|
103
|
+
};
|
|
104
|
+
const rotateRobot = async (targetAngle) => {
|
|
105
|
+
const robotElement = robotRef.current;
|
|
106
|
+
if (!robotElement) return;
|
|
107
|
+
const shortestAngle = calculateShortestRotation(targetAngle);
|
|
108
|
+
robotElement.style.transition = `transform ${rotationDuration}s ease-in-out`;
|
|
109
|
+
robotElement.style.transform = `rotate(${shortestAngle}deg)`;
|
|
110
|
+
await new Promise(
|
|
111
|
+
(resolve) => setTimeout(resolve, rotationDuration * 1e3)
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
const calculateMovementTime = (distance) => {
|
|
115
|
+
return Math.max(Math.sqrt(distance / speedFactor), minSpeed);
|
|
116
|
+
};
|
|
117
|
+
const moveRobotToPosition = async (targetX, targetY) => {
|
|
118
|
+
const robotElement = robotRef.current;
|
|
119
|
+
if (!robotElement) return;
|
|
120
|
+
const currentPos = getRobotPosition();
|
|
121
|
+
if (!currentPos) return;
|
|
122
|
+
if (Math.abs(currentPos.x - targetX) > 1) {
|
|
123
|
+
const deltaX = targetX - currentPos.x;
|
|
124
|
+
const angle = deltaX > 0 ? 90 : -90;
|
|
125
|
+
await rotateRobot(angle);
|
|
126
|
+
const distanceX = Math.abs(deltaX);
|
|
127
|
+
const movementTime = calculateMovementTime(distanceX);
|
|
128
|
+
robotElement.style.transition = `left ${movementTime}s ease-in-out`;
|
|
129
|
+
robotElement.style.left = `${targetX - ROBOT_HALF_SIZE}px`;
|
|
130
|
+
await new Promise(
|
|
131
|
+
(resolve) => setTimeout(resolve, movementTime * 1e3)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (Math.abs(currentPos.y - targetY) > 1) {
|
|
135
|
+
const deltaY = targetY - currentPos.y;
|
|
136
|
+
const angle = deltaY > 0 ? 180 : 0;
|
|
137
|
+
await rotateRobot(angle);
|
|
138
|
+
const distanceY = Math.abs(deltaY);
|
|
139
|
+
const movementTime = calculateMovementTime(distanceY);
|
|
140
|
+
robotElement.style.transition = `top ${movementTime}s ease-in-out`;
|
|
141
|
+
robotElement.style.top = `${targetY - ROBOT_HALF_SIZE}px`;
|
|
142
|
+
await new Promise(
|
|
143
|
+
(resolve) => setTimeout(resolve, movementTime * 1e3)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const cleanAndReturn = async () => {
|
|
148
|
+
if (dirtPositions.length === 0) return;
|
|
149
|
+
onCleaningStart?.();
|
|
150
|
+
cleaningAbortedRef.current = false;
|
|
151
|
+
const uncollectedDirt = [...dirtPositions];
|
|
152
|
+
let collectedCount = 0;
|
|
153
|
+
for (let i = 0; i < uncollectedDirt.length; i++) {
|
|
154
|
+
if (cleaningAbortedRef.current) {
|
|
155
|
+
setRobotState("idle");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const dirt = uncollectedDirt[i];
|
|
159
|
+
if (dirt.collected) continue;
|
|
160
|
+
setRobotState("cleaning");
|
|
161
|
+
const currentPos2 = getRobotPosition();
|
|
162
|
+
if (currentPos2) {
|
|
163
|
+
await moveRobotToPosition(dirt.x, dirt.y);
|
|
164
|
+
if (cleaningAbortedRef.current) {
|
|
165
|
+
setRobotState("idle");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setDirtPositions(
|
|
169
|
+
(prev) => Object.freeze(
|
|
170
|
+
prev.map((d, idx) => idx === i ? { ...d, collected: true } : d)
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
collectedCount++;
|
|
174
|
+
onDirtCollected?.(collectedCount, uncollectedDirt.length);
|
|
175
|
+
}
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
177
|
+
}
|
|
178
|
+
if (cleaningAbortedRef.current) {
|
|
179
|
+
setRobotState("idle");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
setRobotState("returning");
|
|
183
|
+
const currentPos = getRobotPosition();
|
|
184
|
+
if (currentPos) {
|
|
185
|
+
const dockCenterX = DOCKED_POSITION.x + ROBOT_HALF_SIZE;
|
|
186
|
+
const dockCenterY = DOCKED_POSITION.y + ROBOT_HALF_SIZE;
|
|
187
|
+
await moveRobotToPosition(dockCenterX, dockCenterY);
|
|
188
|
+
}
|
|
189
|
+
setRobotState("idle");
|
|
190
|
+
onCleaningComplete?.();
|
|
191
|
+
};
|
|
192
|
+
useImperativeHandle(ref, () => ({
|
|
193
|
+
startCleaning: () => {
|
|
194
|
+
if (robotState === "idle" && isDocumentVisible) {
|
|
195
|
+
cleanAndReturn();
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
reset: () => {
|
|
199
|
+
cleaningAbortedRef.current = true;
|
|
200
|
+
setRobotState("idle");
|
|
201
|
+
initializeRobotPosition();
|
|
202
|
+
createRandomDirt();
|
|
203
|
+
}
|
|
204
|
+
}));
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const handleVisibilityChange = () => {
|
|
207
|
+
const isVisible = !document.hidden;
|
|
208
|
+
setIsDocumentVisible(isVisible);
|
|
209
|
+
if (!isVisible) {
|
|
210
|
+
cleaningAbortedRef.current = true;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
214
|
+
return () => {
|
|
215
|
+
document.removeEventListener(
|
|
216
|
+
"visibilitychange",
|
|
217
|
+
handleVisibilityChange
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
}, []);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
initializeRobotPosition();
|
|
223
|
+
createRandomDirt();
|
|
224
|
+
}, []);
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (autoStart && dirtPositions.length > 0 && robotState === "idle" && isDocumentVisible) {
|
|
227
|
+
cleanAndReturn();
|
|
228
|
+
}
|
|
229
|
+
}, [dirtPositions, robotState, isDocumentVisible, autoStart]);
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const robotElement = robotRef.current;
|
|
232
|
+
if (!robotElement) return;
|
|
233
|
+
robotElement.classList.remove(
|
|
234
|
+
RobotVacuum_default.idle,
|
|
235
|
+
RobotVacuum_default.cleaning,
|
|
236
|
+
RobotVacuum_default.returning
|
|
237
|
+
);
|
|
238
|
+
robotElement.classList.add(RobotVacuum_default[robotState]);
|
|
239
|
+
}, [robotState]);
|
|
240
|
+
return /* @__PURE__ */ jsxs("div", { ref: backgroundRef, className: RobotVacuum_default.container, children: [
|
|
241
|
+
dirtPositions.map(
|
|
242
|
+
(dirt, index) => !dirt.collected ? /* @__PURE__ */ jsx(
|
|
243
|
+
"div",
|
|
244
|
+
{
|
|
245
|
+
className: RobotVacuum_default.dirt,
|
|
246
|
+
style: { left: `${dirt.x}px`, top: `${dirt.y}px` },
|
|
247
|
+
role: "presentation",
|
|
248
|
+
"aria-label": "dirt particle"
|
|
249
|
+
},
|
|
250
|
+
`dirt-${index}`
|
|
251
|
+
) : null
|
|
252
|
+
),
|
|
253
|
+
/* @__PURE__ */ jsx("div", { className: RobotVacuum_default.dock, role: "presentation", "aria-label": "dock" }),
|
|
254
|
+
/* @__PURE__ */ jsxs(
|
|
255
|
+
"div",
|
|
256
|
+
{
|
|
257
|
+
ref: robotRef,
|
|
258
|
+
className: RobotVacuum_default.robot,
|
|
259
|
+
role: "presentation",
|
|
260
|
+
"aria-label": "robot vacuum",
|
|
261
|
+
children: [
|
|
262
|
+
/* @__PURE__ */ jsx("div", { className: `${RobotVacuum_default.wheel} ${RobotVacuum_default.left}` }),
|
|
263
|
+
/* @__PURE__ */ jsx("div", { className: `${RobotVacuum_default.wheel} ${RobotVacuum_default.right}` }),
|
|
264
|
+
/* @__PURE__ */ jsx("div", { className: `${RobotVacuum_default.sensor} ${RobotVacuum_default.left}` }),
|
|
265
|
+
/* @__PURE__ */ jsx("div", { className: `${RobotVacuum_default.sensor} ${RobotVacuum_default.right}` })
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
] });
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
Robotv2.displayName = "Robotv2";
|
|
273
|
+
var RobotVacuum_default2 = Robotv2;
|
|
274
|
+
export {
|
|
275
|
+
RobotVacuum_default2 as RobotVacuum
|
|
276
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-robot-vacuum",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An animated React component featuring a robot vacuum that autonomously cleans your page by collecting dirt particles",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./styles.css": "./dist/RobotVacuum.module.css"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.tsx --format cjs,esm --dts --external react --external react-dom && cp src/RobotVacuum/RobotVacuum.module.css dist/",
|
|
22
|
+
"dev": "tsup src/index.tsx --format cjs,esm --dts --watch",
|
|
23
|
+
"vite:dev": "vite",
|
|
24
|
+
"vite:build": "vite build",
|
|
25
|
+
"lint": "eslint src/**/*.{ts,tsx}",
|
|
26
|
+
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"react",
|
|
32
|
+
"component",
|
|
33
|
+
"robot",
|
|
34
|
+
"vacuum",
|
|
35
|
+
"animation",
|
|
36
|
+
"interactive",
|
|
37
|
+
"typescript",
|
|
38
|
+
"react19",
|
|
39
|
+
"css-modules"
|
|
40
|
+
],
|
|
41
|
+
"author": "ZhanmuTW",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/zhanmu-tw/react-robot-vacuum.git"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/zhanmu-tw/react-robot-vacuum/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/zhanmu-tw/react-robot-vacuum#readme",
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
53
|
+
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@eslint/js": "^9.39.2",
|
|
57
|
+
"@types/eslint__js": "^8.42.3",
|
|
58
|
+
"@types/react": "^18.2.0",
|
|
59
|
+
"@types/react-dom": "^18.2.0",
|
|
60
|
+
"eslint": "^9.39.2",
|
|
61
|
+
"eslint-plugin-react": "^7.37.5",
|
|
62
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
63
|
+
"react": "^18.2.0",
|
|
64
|
+
"react-dom": "^18.2.0",
|
|
65
|
+
"tsup": "^8.0.0",
|
|
66
|
+
"typescript": "^5.3.0",
|
|
67
|
+
"typescript-eslint": "^8.51.0",
|
|
68
|
+
"vite": "^7.3.0"
|
|
69
|
+
}
|
|
70
|
+
}
|