fantomas 0.1.2__py3-none-any.whl
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.
- fantomas/__init__.py +11 -0
- fantomas/fantomas_no_driver.py +475 -0
- fantomas/identity.py +91 -0
- fantomas/no_driver/CursorIllustration.py +35 -0
- fantomas/no_driver/Geometry.py +38 -0
- fantomas/no_driver/IframeManager.py +10 -0
- fantomas/proxy.py +193 -0
- fantomas/random_sleeper.py +6 -0
- fantomas/raw_chrome.py +31 -0
- fantomas/raw_screen.py +47 -0
- fantomas/screen.py +41 -0
- fantomas/secondary_flow.py +27 -0
- fantomas/utils.py +21 -0
- fantomas/virtual_cursor_path.py +76 -0
- fantomas/xdotool_actions.py +339 -0
- fantomas-0.1.2.dist-info/METADATA +8 -0
- fantomas-0.1.2.dist-info/RECORD +19 -0
- fantomas-0.1.2.dist-info/WHEEL +5 -0
- fantomas-0.1.2.dist-info/top_level.txt +1 -0
fantomas/proxy.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import asyncio
|
|
3
|
+
import time
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from billiard import Process, Queue
|
|
7
|
+
from mitmproxy import http
|
|
8
|
+
from mitmproxy.tools.dump import DumpMaster
|
|
9
|
+
from mitmproxy.options import Options
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
import requests
|
|
12
|
+
from .utils import get_value_or_default, load_config
|
|
13
|
+
|
|
14
|
+
class Proxy:
|
|
15
|
+
def __init__(self,config_proxy=None):
|
|
16
|
+
self.config_proxy = config_proxy
|
|
17
|
+
self.upstream_enabled = 0
|
|
18
|
+
self.upstream_provider = ""
|
|
19
|
+
self.upstream_request_type = "broker"
|
|
20
|
+
self.proxy_process = None
|
|
21
|
+
self.count_data_queue = None
|
|
22
|
+
self.modifiers_array = []
|
|
23
|
+
self.retrievers_array = []
|
|
24
|
+
self.modifiers_request_array = []
|
|
25
|
+
|
|
26
|
+
if self.config_proxy:
|
|
27
|
+
self._parse_config_proxy()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def launch_proxy(self):
|
|
31
|
+
self._clean_previous_old_proxy()
|
|
32
|
+
time.sleep(0.3)
|
|
33
|
+
|
|
34
|
+
if self.upstream_enabled:
|
|
35
|
+
upstream_proxy_url = self.fetch_upstream_proxy()
|
|
36
|
+
else:
|
|
37
|
+
upstream_proxy_url = None
|
|
38
|
+
|
|
39
|
+
self.set_data_queue()
|
|
40
|
+
|
|
41
|
+
self.proxy_process = SubprocessedMitmProxy(
|
|
42
|
+
upstream_proxy_url=upstream_proxy_url,
|
|
43
|
+
count_data_queue=self.count_data_queue,
|
|
44
|
+
upstream_enabled=self.upstream_enabled,
|
|
45
|
+
modifiers_array = self.modifiers_array,
|
|
46
|
+
retrievers_array=self.retrievers_array,
|
|
47
|
+
modifiers_request_array=self.modifiers_request_array
|
|
48
|
+
)
|
|
49
|
+
self.proxy_process.start()
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def exit_local_proxy(self):
|
|
53
|
+
self.proxy_process.terminate()
|
|
54
|
+
|
|
55
|
+
def switch_upstream_proxy(self):
|
|
56
|
+
self.exit_local_proxy()
|
|
57
|
+
self.launch_proxy()
|
|
58
|
+
|
|
59
|
+
def set_data_queue(self):
|
|
60
|
+
self.count_data_queue = Queue()
|
|
61
|
+
return self.count_data_queue
|
|
62
|
+
|
|
63
|
+
def get_data_count(self):
|
|
64
|
+
for _ in range(self.count_data_queue.qsize()):
|
|
65
|
+
element = self.count_data_queue.get()
|
|
66
|
+
return element
|
|
67
|
+
|
|
68
|
+
def retrieve(self,queue_id):
|
|
69
|
+
for retriever in self.retrievers_array:
|
|
70
|
+
if retriever["queue_id"]==queue_id:
|
|
71
|
+
for _ in range(retriever["queue"].qsize()):
|
|
72
|
+
element = retriever["queue"].get()
|
|
73
|
+
return element
|
|
74
|
+
|
|
75
|
+
def _clean_previous_old_proxy(self):
|
|
76
|
+
result = subprocess.run(f"lsof -t -i:{8081}", shell=True, capture_output=True, text=True)
|
|
77
|
+
if result.stdout.strip():
|
|
78
|
+
pid = result.stdout.strip()
|
|
79
|
+
os.system(f"kill -9 {pid}")
|
|
80
|
+
else:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _parse_config_proxy(self):
|
|
85
|
+
self.config_proxy = load_config(self.config_proxy) if isinstance(self.config_proxy, str) else self.config_proxy
|
|
86
|
+
self.enabled = get_value_or_default(self.config_proxy.get("proxy_enabled"), self.upstream_enabled)
|
|
87
|
+
self.upstream_enabled = get_value_or_default(self.config_proxy.get("proxy_upstream_enabled"), self.upstream_enabled)
|
|
88
|
+
self.upstream_provider = get_value_or_default(self.config_proxy.get("proxy_upstream_provider"), self.upstream_provider)
|
|
89
|
+
self.upstream_request_type = get_value_or_default(self.config_proxy.get("proxy_upstream_request_type"), self.upstream_request_type)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def fetch_upstream_proxy(self):
|
|
93
|
+
return UpstreamProxyFetcher(self.upstream_provider,self.upstream_request_type).fetch_proxy()
|
|
94
|
+
|
|
95
|
+
def add_retriever(self,queue_id: str,retriever_function):
|
|
96
|
+
queue = Queue()
|
|
97
|
+
proxy_queue_dict = {"queue_id":queue_id,"queue":queue,"retriever_function":retriever_function}
|
|
98
|
+
self.retrievers_array.append(proxy_queue_dict)
|
|
99
|
+
|
|
100
|
+
def add_modifier(self,modifier_function):
|
|
101
|
+
self.modifiers_array.append(modifier_function)
|
|
102
|
+
|
|
103
|
+
def add_request_modifier(self,modifier_request_function):
|
|
104
|
+
self.modifiers_request_array.append(modifier_request_function)
|
|
105
|
+
|
|
106
|
+
class SubprocessedMitmProxy(Process):
|
|
107
|
+
def __init__(self, *, upstream_proxy_url: str, count_data_queue, upstream_enabled,modifiers_array,retrievers_array,modifiers_request_array):
|
|
108
|
+
super().__init__()
|
|
109
|
+
self.upstream_proxy_url = upstream_proxy_url
|
|
110
|
+
self.count_data_queue = count_data_queue
|
|
111
|
+
self.upstream_enabled = upstream_enabled
|
|
112
|
+
self.modifiers_array = modifiers_array
|
|
113
|
+
self.retrievers_array = retrievers_array
|
|
114
|
+
self.modifiers_request_array = modifiers_request_array
|
|
115
|
+
|
|
116
|
+
def run(self):
|
|
117
|
+
asyncio.run(self.asyncio_run())
|
|
118
|
+
|
|
119
|
+
async def asyncio_run(self):
|
|
120
|
+
upstream_mode = "upstream:http://"+self.upstream_proxy_url.split('@')[1]
|
|
121
|
+
if self.upstream_enabled:
|
|
122
|
+
opts = Options(listen_port=8081, mode=[upstream_mode], ssl_insecure=False)
|
|
123
|
+
else:
|
|
124
|
+
opts = Options(listen_port=8081, ssl_insecure=False)
|
|
125
|
+
master = DumpMaster(opts, with_termlog=False, with_dumper=False)
|
|
126
|
+
master.addons.add(ProxyAddOn(self.upstream_proxy_url, self.count_data_queue, self.modifiers_array, self.retrievers_array, self.modifiers_request_array))
|
|
127
|
+
await master.run()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ProxyAddOn:
|
|
131
|
+
def __init__(self, upstream_proxy_url, count_data_queue, modifiers_array,retrievers_array,modifiers_request_array):
|
|
132
|
+
self.total_data = 0
|
|
133
|
+
self.upstream_proxy_url = upstream_proxy_url
|
|
134
|
+
self.count_data_queue = count_data_queue
|
|
135
|
+
self.proxy_parsed_url = None
|
|
136
|
+
self.modifiers_array = modifiers_array
|
|
137
|
+
self.retrievers_array = retrievers_array
|
|
138
|
+
self.modifiers_request_array = modifiers_request_array
|
|
139
|
+
|
|
140
|
+
def http_connect_upstream(self, flow: http.HTTPFlow):
|
|
141
|
+
self.proxy_parsed_url = urlparse(self.upstream_proxy_url)
|
|
142
|
+
if self.proxy_parsed_url.username or self.proxy_parsed_url.password:
|
|
143
|
+
credentials = f"{self.proxy_parsed_url.username}:{self.proxy_parsed_url.password}"
|
|
144
|
+
encoded_credentials = base64.b64encode(credentials.encode()).decode()
|
|
145
|
+
flow.request.headers["proxy-authorization"] = f"Basic {encoded_credentials}"
|
|
146
|
+
|
|
147
|
+
def response(self, flow: http.HTTPFlow) -> None:
|
|
148
|
+
|
|
149
|
+
#Data Count
|
|
150
|
+
if flow.response:
|
|
151
|
+
content_length = flow.response.headers.get('Content-Length')
|
|
152
|
+
if content_length:
|
|
153
|
+
self.total_data += int(content_length)
|
|
154
|
+
self.count_data_queue.put(self.total_data)
|
|
155
|
+
|
|
156
|
+
#Executing retrievers
|
|
157
|
+
for j in self.retrievers_array:
|
|
158
|
+
j["retriever_function"](flow, j["queue"])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
#Executing modifiers
|
|
162
|
+
for i in self.modifiers_array:
|
|
163
|
+
i(flow)
|
|
164
|
+
|
|
165
|
+
def request(self, flow: http.HTTPFlow) -> None:
|
|
166
|
+
for i in self.modifiers_request_array:
|
|
167
|
+
i(flow)
|
|
168
|
+
|
|
169
|
+
class UpstreamProxyFetcher:
|
|
170
|
+
def __init__(self, provider, upstream_request_type):
|
|
171
|
+
self.ip = None
|
|
172
|
+
self.url = None
|
|
173
|
+
self.provider = provider
|
|
174
|
+
self.upstream_request_type = upstream_request_type
|
|
175
|
+
|
|
176
|
+
def fetch_proxy(self):
|
|
177
|
+
if self.upstream_request_type == "broker":
|
|
178
|
+
response = requests.get(self.provider)
|
|
179
|
+
if response.status_code == 200:
|
|
180
|
+
result = response.json()
|
|
181
|
+
self.ip = result.get('ip', 'No IP returned')
|
|
182
|
+
self.url = result.get('url', 'No URL returned')
|
|
183
|
+
else:
|
|
184
|
+
raise Exception(f"Request failed with status code {response.status_code}")
|
|
185
|
+
elif self.upstream_request_type == "direct":
|
|
186
|
+
self.url = self.provider
|
|
187
|
+
|
|
188
|
+
return self.url
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
fantomas/raw_chrome.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import threading
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
class RawChrome:
|
|
7
|
+
def __init__(self, display_number, browser_options, temp_path):
|
|
8
|
+
self.display_number = display_number
|
|
9
|
+
self.port = self.find_free_port()
|
|
10
|
+
self.temp_path = temp_path
|
|
11
|
+
self.browser_options = browser_options
|
|
12
|
+
|
|
13
|
+
def run_app_with_gpu(self):
|
|
14
|
+
env = os.environ.copy()
|
|
15
|
+
env["DISPLAY"] = self.display_number
|
|
16
|
+
|
|
17
|
+
command = ["vglrun","google-chrome","--user-data-dir="+str(self.temp_path),"--remote-debugging-port="+str(self.port)]
|
|
18
|
+
command.extend(self.browser_options)
|
|
19
|
+
|
|
20
|
+
subprocess.run(command, env=env)
|
|
21
|
+
|
|
22
|
+
def start_in_thread(self):
|
|
23
|
+
thread = threading.Thread(target=self.run_app_with_gpu, daemon=True)
|
|
24
|
+
thread.start()
|
|
25
|
+
return {"thread":thread, "port":self.port, "display":self.display_number}
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def find_free_port():
|
|
29
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
30
|
+
s.bind(('', 0)) # Let the OS choose a free port
|
|
31
|
+
return s.getsockname()[1] # Get the assigned port
|
fantomas/raw_screen.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import signal
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RawScreen:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.display_number = self.attribute_display_number()
|
|
11
|
+
self.screen_res = "1900x1060"
|
|
12
|
+
|
|
13
|
+
def attribute_display_number(self):
|
|
14
|
+
x_sockets = os.listdir("/tmp/.X11-unix/")
|
|
15
|
+
x_displays = [int(s[1:]) for s in x_sockets if s.startswith("X")]
|
|
16
|
+
print(x_displays)
|
|
17
|
+
while True:
|
|
18
|
+
x = random.randint(1,1000)
|
|
19
|
+
if x not in x_displays:
|
|
20
|
+
self.display_number = ":"+str(x)
|
|
21
|
+
return ":"+str(x)
|
|
22
|
+
|
|
23
|
+
def launch_xephyr(self):
|
|
24
|
+
|
|
25
|
+
print(f"Lancement de Xephyr sur DISPLAY {self.display_number}...")
|
|
26
|
+
self.xephyr_process = subprocess.Popen(["Xephyr", self.display_number, "-screen", self.screen_res], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
27
|
+
return self.display_number
|
|
28
|
+
|
|
29
|
+
def wait_for_display_ready(self, timeout=5):
|
|
30
|
+
print("Attente de l'initialisation de Xephyr...")
|
|
31
|
+
for _ in range(timeout * 10):
|
|
32
|
+
result = subprocess.run(["xdpyinfo", "-display", self.display_number],
|
|
33
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
34
|
+
if result.returncode == 0:
|
|
35
|
+
print("Xephyr est prêt.")
|
|
36
|
+
return True
|
|
37
|
+
time.sleep(0.1)
|
|
38
|
+
raise RuntimeError("Timeout: Xephyr ne s'est pas lancé correctement.")
|
|
39
|
+
|
|
40
|
+
def exit_xephyr(self):
|
|
41
|
+
print("Fermeture de Xephyr...")
|
|
42
|
+
self.xephyr_process.send_signal(signal.SIGTERM)
|
|
43
|
+
self.xephyr_process.wait()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
fantomas/screen.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import io
|
|
3
|
+
from pyvirtualdisplay import Display
|
|
4
|
+
from .utils import get_value_or_default, load_config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Screen:
|
|
8
|
+
def __init__(self, screen_params=None):
|
|
9
|
+
self.screen_params = screen_params
|
|
10
|
+
self.visible = 1
|
|
11
|
+
self.height = 1200
|
|
12
|
+
self.width = 800
|
|
13
|
+
self.display = None
|
|
14
|
+
if self.screen_params:
|
|
15
|
+
self.parse_screen_params()
|
|
16
|
+
|
|
17
|
+
def parse_screen_params(self):
|
|
18
|
+
self.screen_params = load_config(self.screen_params)
|
|
19
|
+
self.visible = get_value_or_default(self.screen_params.get("screen_visible"), self.visible)
|
|
20
|
+
self.height = get_value_or_default(self.screen_params.get("screen_height"), self.height)
|
|
21
|
+
self.width = get_value_or_default(self.screen_params.get("screen_width"), self.width)
|
|
22
|
+
|
|
23
|
+
def screenshot_screen(self):
|
|
24
|
+
screenshot_process = subprocess.run(
|
|
25
|
+
["import", "-window", "root", "png:-"],
|
|
26
|
+
stdout=subprocess.PIPE,
|
|
27
|
+
stderr=subprocess.DEVNULL)
|
|
28
|
+
if not screenshot_process.stdout:
|
|
29
|
+
raise ValueError("Empty Screenshot")
|
|
30
|
+
|
|
31
|
+
image_bytes = io.BytesIO(screenshot_process.stdout)
|
|
32
|
+
image_bytes.seek(0)
|
|
33
|
+
return image_bytes
|
|
34
|
+
|
|
35
|
+
def launch_screen(self):
|
|
36
|
+
self.display = Display(visible=self.visible, size=(self.width, self.height))
|
|
37
|
+
self.display.start()
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def stop_screen(self):
|
|
41
|
+
self.display.stop()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .fantomas_no_driver import FantomasNoDriver
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SecondaryFlow():
|
|
6
|
+
|
|
7
|
+
def __init__(self, sf_ctx,sf_function):
|
|
8
|
+
self.sf_ctx = sf_ctx
|
|
9
|
+
self.sf_no_driver_instance = sf_ctx.sf_no_driver_instance
|
|
10
|
+
self.sf_browser = sf_ctx.sf_browser
|
|
11
|
+
self.sf_function = sf_function
|
|
12
|
+
|
|
13
|
+
def launch_secondary_flow(self):
|
|
14
|
+
import multiprocessing
|
|
15
|
+
sf_queue = multiprocessing.Queue()
|
|
16
|
+
sf_process = multiprocessing.Process(target=self.launch_secondary_browser_sync, args=(sf_queue,self.sf_function))
|
|
17
|
+
sf_process.start()
|
|
18
|
+
sf_process.join()
|
|
19
|
+
queue_result = sf_queue.get()
|
|
20
|
+
return queue_result
|
|
21
|
+
|
|
22
|
+
def launch_secondary_browser_sync(self, sf_queue,sf_function):
|
|
23
|
+
self.sf_no_driver_instance.loop().run_until_complete(self.launch_secondary_browser_async(sf_queue,sf_function))
|
|
24
|
+
|
|
25
|
+
async def launch_secondary_browser_async(self,sf_queue,sf_function):
|
|
26
|
+
await sf_function(self.sf_browser, self.sf_ctx, sf_queue)
|
|
27
|
+
|
fantomas/utils.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_value_or_default(value, default):
|
|
5
|
+
"""Return value if not None, otherwise return default."""
|
|
6
|
+
return value if value is not None else default
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_config(config):
|
|
10
|
+
"""Load configuration from file path or return dict as-is.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
config: Either a file path (str) to a JSON file or a dict
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
dict: The configuration dictionary
|
|
17
|
+
"""
|
|
18
|
+
if isinstance(config, str):
|
|
19
|
+
with open(config, 'r', encoding='utf-8') as json_file:
|
|
20
|
+
return json.load(json_file)
|
|
21
|
+
return config
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
class VirtualCursorPath:
|
|
4
|
+
def __init__(self):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
def get_virtual_cursor_path(self,current_position,desired_position,viewport_width,viewport_height):
|
|
8
|
+
path = self._calculate_path(current_position[0],current_position[1],desired_position[0],desired_position[1],viewport_width,viewport_height)
|
|
9
|
+
path[0]=self._avoid_bounds(path[0], 0,viewport_width)
|
|
10
|
+
path[1]=self._avoid_bounds(path[1], 0,viewport_height)
|
|
11
|
+
return path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def _avoid_bounds(array_of_cursor_coordinates,minima_coordinate,maxima_coordinate):
|
|
16
|
+
array_avoiding_minima = [1 if cursor_coordinate == minima_coordinate else cursor_coordinate for cursor_coordinate in array_of_cursor_coordinates]
|
|
17
|
+
array_avoiding_maxima = [maxima_coordinate-1 if cursor_coordinate == maxima_coordinate else cursor_coordinate for cursor_coordinate in array_avoiding_minima]
|
|
18
|
+
return array_avoiding_maxima
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _calculate_path(start_x, start_y,dest_x, dest_y, x_max, y_max, G_0=5, W_0=10, M_0=15, D_0=30):
|
|
22
|
+
sqrt3 = np.sqrt(3)
|
|
23
|
+
sqrt5 = np.sqrt(5)
|
|
24
|
+
x_array = []
|
|
25
|
+
y_array = []
|
|
26
|
+
current_x,current_y = start_x,start_y
|
|
27
|
+
v_x = v_y = W_x = W_y = 0
|
|
28
|
+
while True:
|
|
29
|
+
dist = np.hypot(dest_x - start_x, dest_y - start_y)
|
|
30
|
+
if dist < 1:
|
|
31
|
+
break
|
|
32
|
+
|
|
33
|
+
# Force calculation
|
|
34
|
+
W_mag = min(W_0, dist)
|
|
35
|
+
if dist >= D_0:
|
|
36
|
+
W_x = W_x / sqrt3 + (2 * np.random.random() - 1) * W_mag / sqrt5
|
|
37
|
+
W_y = W_y / sqrt3 + (2 * np.random.random() - 1) * W_mag / sqrt5
|
|
38
|
+
else:
|
|
39
|
+
W_x /= sqrt3
|
|
40
|
+
W_y /= sqrt3
|
|
41
|
+
if M_0 < 3:
|
|
42
|
+
M_0 = np.random.random() * 3 + 3
|
|
43
|
+
else:
|
|
44
|
+
M_0 /= sqrt5
|
|
45
|
+
|
|
46
|
+
# Speed calculation
|
|
47
|
+
v_x += (W_x + G_0 * (dest_x - start_x) / dist)
|
|
48
|
+
v_y += (W_y + G_0 * (dest_y - start_y) / dist)
|
|
49
|
+
|
|
50
|
+
# Limit the maximum speed
|
|
51
|
+
v_mag = np.hypot(v_x, v_y)
|
|
52
|
+
if v_mag > M_0:
|
|
53
|
+
v_x = (v_x / v_mag) * M_0
|
|
54
|
+
v_y = (v_y / v_mag) * M_0
|
|
55
|
+
|
|
56
|
+
# Update positions
|
|
57
|
+
start_x += v_x
|
|
58
|
+
start_y += v_y
|
|
59
|
+
|
|
60
|
+
# Limit positions to [0, x_max] and [0, y_max]
|
|
61
|
+
start_x = max(0, min(start_x, x_max))
|
|
62
|
+
start_y = max(0, min(start_y, y_max))
|
|
63
|
+
|
|
64
|
+
move_x = int(np.round(start_x))
|
|
65
|
+
move_y = int(np.round(start_y))
|
|
66
|
+
|
|
67
|
+
if current_x != move_x or current_y != move_y:
|
|
68
|
+
current_x = move_x
|
|
69
|
+
current_y = move_y
|
|
70
|
+
x_array.append(current_x)
|
|
71
|
+
y_array.append(current_y)
|
|
72
|
+
return [x_array,y_array]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
path = VirtualCursorPath().get_virtual_cursor_path([0,0],[100,200],1000,1000)
|