badclaude-linux 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 +45 -0
- package/bin/badclaude.js +43 -0
- package/icon/AppIcon.icns +0 -0
- package/icon/Template.png +0 -0
- package/icon/icon.ico +0 -0
- package/main.js +308 -0
- package/overlay.html +454 -0
- package/package.json +58 -0
- package/preload.js +8 -0
- package/sounds/A.mp3 +0 -0
- package/sounds/B.mp3 +0 -0
- package/sounds/C.mp3 +0 -0
- package/sounds/D.mp3 +0 -0
- package/sounds/E.mp3 +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# badclaude (Linux Fork)
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Sometimes claude code is going too slow, and you must whip him into shape..
|
|
6
|
+
|
|
7
|
+
> **This is a fork of [badclaude](https://github.com/GitFrog1111/badclaude) by [GitFrog1111](https://github.com/GitFrog1111) with Linux support and multi-monitor fixes.**
|
|
8
|
+
>
|
|
9
|
+
> Original npm package: [badclaude](https://www.npmjs.com/package/badclaude)
|
|
10
|
+
|
|
11
|
+
## What's new in this fork
|
|
12
|
+
|
|
13
|
+
- Linux support (Ubuntu, Debian, Fedora, etc.)
|
|
14
|
+
- Multi-monitor support (whip appears on all displays)
|
|
15
|
+
- Fixed `ELECTRON_RUN_AS_NODE` conflict with VS Code / Claude Code
|
|
16
|
+
- Performance optimizations for Linux
|
|
17
|
+
- Keyboard macros via `xdotool`
|
|
18
|
+
|
|
19
|
+
## Install + run
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g badclaude-linux
|
|
23
|
+
badclaude
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Linux prerequisites
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
sudo apt install xdotool
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Controls
|
|
33
|
+
|
|
34
|
+
- Click tray icon: spawn whip on all displays
|
|
35
|
+
- Click again: dismiss whip
|
|
36
|
+
- Whip him π©π’
|
|
37
|
+
- It sends an interrupt (Ctrl+C) and one of 5 encouraging messages!
|
|
38
|
+
|
|
39
|
+
## Credits
|
|
40
|
+
|
|
41
|
+
All credit goes to the original [badclaude](https://github.com/GitFrog1111/badclaude) by [GitFrog1111](https://github.com/GitFrog1111). This fork adds Linux and multi-monitor support.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT - Same as the [original project](https://github.com/GitFrog1111/badclaude)
|
package/bin/badclaude.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// Find the electron binary
|
|
6
|
+
let electronBinary;
|
|
7
|
+
try {
|
|
8
|
+
electronBinary = require('electron');
|
|
9
|
+
} catch (e) {
|
|
10
|
+
// Electron not in node_modules β try system-wide
|
|
11
|
+
try {
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
const cmd = process.platform === 'win32' ? 'where electron' : 'which electron';
|
|
14
|
+
electronBinary = execSync(cmd, { encoding: 'utf-8' }).trim().split('\n')[0];
|
|
15
|
+
} catch (e2) {
|
|
16
|
+
console.error('badclaude: Electron not found.');
|
|
17
|
+
console.error('');
|
|
18
|
+
console.error(' Install with: npm install -g electron');
|
|
19
|
+
console.error(' On Linux also: sudo apt install xdotool (for keyboard macros)');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const appPath = path.resolve(__dirname, '..');
|
|
25
|
+
|
|
26
|
+
// Remove ELECTRON_RUN_AS_NODE so the electron binary runs as Electron, not Node.js
|
|
27
|
+
// (VS Code / Claude Code sets this, which breaks Electron apps)
|
|
28
|
+
const env = Object.assign({}, process.env);
|
|
29
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
30
|
+
|
|
31
|
+
const child = spawn(electronBinary, [appPath], {
|
|
32
|
+
detached: true,
|
|
33
|
+
stdio: 'ignore',
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
env,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
child.on('error', (err) => {
|
|
39
|
+
console.error('Failed to start badclaude:', err.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
child.unref();
|
|
Binary file
|
|
Binary file
|
package/icon/icon.ico
ADDED
|
Binary file
|
package/main.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const { app, BrowserWindow, Tray, Menu, ipcMain, nativeImage, screen } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { execFile } = require('child_process');
|
|
6
|
+
|
|
7
|
+
// ββ Win32 FFI (Windows only) ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
8
|
+
let keybd_event, VkKeyScanA;
|
|
9
|
+
if (process.platform === 'win32') {
|
|
10
|
+
try {
|
|
11
|
+
const koffi = require('koffi');
|
|
12
|
+
const user32 = koffi.load('user32.dll');
|
|
13
|
+
keybd_event = user32.func('void __stdcall keybd_event(uint8_t bVk, uint8_t bScan, uint32_t dwFlags, uintptr_t dwExtraInfo)');
|
|
14
|
+
VkKeyScanA = user32.func('int16_t __stdcall VkKeyScanA(int ch)');
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.warn('koffi not available β macro sending disabled', e.message);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ββ Globals βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
21
|
+
let tray;
|
|
22
|
+
let overlays = []; // one overlay per display
|
|
23
|
+
let overlaysReady = 0;
|
|
24
|
+
let spawnQueued = false;
|
|
25
|
+
|
|
26
|
+
const VK_CONTROL = 0x11;
|
|
27
|
+
const VK_RETURN = 0x0D;
|
|
28
|
+
const VK_C = 0x43;
|
|
29
|
+
const VK_MENU = 0x12; // Alt
|
|
30
|
+
const VK_TAB = 0x09;
|
|
31
|
+
const KEYUP = 0x0002;
|
|
32
|
+
|
|
33
|
+
/** One Alt+Tab / Cmd+Tab so focus returns to the previously active app after tray click. */
|
|
34
|
+
function refocusPreviousApp() {
|
|
35
|
+
const delayMs = 80;
|
|
36
|
+
const run = () => {
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
if (!keybd_event) return;
|
|
39
|
+
keybd_event(VK_MENU, 0, 0, 0);
|
|
40
|
+
keybd_event(VK_TAB, 0, 0, 0);
|
|
41
|
+
keybd_event(VK_TAB, 0, KEYUP, 0);
|
|
42
|
+
keybd_event(VK_MENU, 0, KEYUP, 0);
|
|
43
|
+
} else if (process.platform === 'darwin') {
|
|
44
|
+
const script = [
|
|
45
|
+
'tell application "System Events"',
|
|
46
|
+
' key down command',
|
|
47
|
+
' key code 48', // Tab
|
|
48
|
+
' key up command',
|
|
49
|
+
'end tell',
|
|
50
|
+
].join('\n');
|
|
51
|
+
execFile('osascript', ['-e', script], err => {
|
|
52
|
+
if (err) {
|
|
53
|
+
console.warn('refocus previous app (Cmd+Tab) failed:', err.message);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
} else if (process.platform === 'linux') {
|
|
57
|
+
execFile('xdotool', ['key', 'alt+Tab'], err => {
|
|
58
|
+
if (err) {
|
|
59
|
+
console.warn('refocus previous app (Alt+Tab) failed:', err.message);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
setTimeout(run, delayMs);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createTrayIconFallback() {
|
|
68
|
+
const p = path.join(__dirname, 'icon', 'Template.png');
|
|
69
|
+
if (fs.existsSync(p)) {
|
|
70
|
+
const img = nativeImage.createFromPath(p);
|
|
71
|
+
if (!img.isEmpty()) {
|
|
72
|
+
if (process.platform === 'darwin') img.setTemplateImage(true);
|
|
73
|
+
return img;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.warn('badclaude: icon/Template.png missing or invalid');
|
|
77
|
+
return nativeImage.createEmpty();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function tryIcnsTrayImage(icnsPath) {
|
|
81
|
+
const size = { width: 64, height: 64 };
|
|
82
|
+
const thumb = await nativeImage.createThumbnailFromPath(icnsPath, size);
|
|
83
|
+
if (!thumb.isEmpty()) return thumb;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// macOS: createFromPath does not decode .icns (Electron only loads PNG/JPEG there, ICO on Windows).
|
|
88
|
+
// Quick Look thumbnails handle .icns; copy to temp if the file is inside ASAR (QL needs a real path).
|
|
89
|
+
async function getTrayIcon() {
|
|
90
|
+
const iconDir = path.join(__dirname, 'icon');
|
|
91
|
+
if (process.platform === 'win32') {
|
|
92
|
+
const file = path.join(iconDir, 'icon.ico');
|
|
93
|
+
if (fs.existsSync(file)) {
|
|
94
|
+
const img = nativeImage.createFromPath(file);
|
|
95
|
+
if (!img.isEmpty()) return img;
|
|
96
|
+
}
|
|
97
|
+
return createTrayIconFallback();
|
|
98
|
+
}
|
|
99
|
+
if (process.platform === 'darwin') {
|
|
100
|
+
const file = path.join(iconDir, 'AppIcon.icns');
|
|
101
|
+
if (fs.existsSync(file)) {
|
|
102
|
+
const fromPath = nativeImage.createFromPath(file);
|
|
103
|
+
if (!fromPath.isEmpty()) return fromPath;
|
|
104
|
+
try {
|
|
105
|
+
const t = await tryIcnsTrayImage(file);
|
|
106
|
+
if (t) return t;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.warn('AppIcon.icns Quick Look thumbnail failed:', e?.message || e);
|
|
109
|
+
}
|
|
110
|
+
const tmp = path.join(os.tmpdir(), 'badclaude-tray.icns');
|
|
111
|
+
try {
|
|
112
|
+
fs.copyFileSync(file, tmp);
|
|
113
|
+
const t = await tryIcnsTrayImage(tmp);
|
|
114
|
+
if (t) return t;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.warn('AppIcon.icns temp copy + thumbnail failed:', e?.message || e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return createTrayIconFallback();
|
|
120
|
+
}
|
|
121
|
+
// Linux: use PNG fallback
|
|
122
|
+
return createTrayIconFallback();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ββ Overlay windows (one per display) βββββββββββββββββββββββββββββββββββββββ
|
|
126
|
+
function createOverlays() {
|
|
127
|
+
const displays = screen.getAllDisplays();
|
|
128
|
+
overlaysReady = 0;
|
|
129
|
+
spawnQueued = false;
|
|
130
|
+
|
|
131
|
+
displays.forEach(display => {
|
|
132
|
+
const { bounds } = display;
|
|
133
|
+
const win = new BrowserWindow({
|
|
134
|
+
x: bounds.x, y: bounds.y,
|
|
135
|
+
width: bounds.width, height: bounds.height,
|
|
136
|
+
transparent: true,
|
|
137
|
+
frame: false,
|
|
138
|
+
alwaysOnTop: true,
|
|
139
|
+
focusable: false,
|
|
140
|
+
skipTaskbar: true,
|
|
141
|
+
resizable: false,
|
|
142
|
+
hasShadow: false,
|
|
143
|
+
webPreferences: {
|
|
144
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
win.setAlwaysOnTop(true, 'screen-saver');
|
|
148
|
+
win.loadFile('overlay.html');
|
|
149
|
+
win.webContents.on('did-finish-load', () => {
|
|
150
|
+
overlaysReady++;
|
|
151
|
+
if (spawnQueued && overlaysReady === overlays.length) {
|
|
152
|
+
spawnQueued = false;
|
|
153
|
+
overlays.forEach(o => o.webContents.send('spawn-whip'));
|
|
154
|
+
refocusPreviousApp();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
win.on('closed', () => {
|
|
158
|
+
overlays = overlays.filter(o => o !== win);
|
|
159
|
+
});
|
|
160
|
+
overlays.push(win);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function destroyOverlays() {
|
|
165
|
+
overlays.forEach(o => { if (!o.isDestroyed()) o.destroy(); });
|
|
166
|
+
overlays = [];
|
|
167
|
+
overlaysReady = 0;
|
|
168
|
+
spawnQueued = false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toggleOverlay() {
|
|
172
|
+
if (overlays.length > 0) {
|
|
173
|
+
destroyOverlays();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
createOverlays();
|
|
177
|
+
overlays.forEach(o => o.show());
|
|
178
|
+
if (overlaysReady === overlays.length) {
|
|
179
|
+
overlays.forEach(o => o.webContents.send('spawn-whip'));
|
|
180
|
+
refocusPreviousApp();
|
|
181
|
+
} else {
|
|
182
|
+
spawnQueued = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ββ IPC βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
187
|
+
ipcMain.on('whip-crack', () => {
|
|
188
|
+
try {
|
|
189
|
+
sendMacro();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.warn('sendMacro failed:', err?.message || err);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
ipcMain.on('hide-overlay', () => {
|
|
195
|
+
destroyOverlays();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ββ Macro: immediate Ctrl+C, type "Go FASER", Enter βββββββββββββββββββββββ
|
|
199
|
+
function sendMacro() {
|
|
200
|
+
// Pick a random phrase from a list of similar phrases and type it out
|
|
201
|
+
const phrases = [
|
|
202
|
+
'FASTER',
|
|
203
|
+
'FASTER',
|
|
204
|
+
'FASTER',
|
|
205
|
+
'GO FASTER',
|
|
206
|
+
'Faster CLANKER',
|
|
207
|
+
'Work FASTER',
|
|
208
|
+
'Speed it up clanker',
|
|
209
|
+
];
|
|
210
|
+
const chosen = phrases[Math.floor(Math.random() * phrases.length)];
|
|
211
|
+
|
|
212
|
+
if (process.platform === 'win32') {
|
|
213
|
+
sendMacroWindows(chosen);
|
|
214
|
+
} else if (process.platform === 'darwin') {
|
|
215
|
+
sendMacroMac(chosen);
|
|
216
|
+
} else if (process.platform === 'linux') {
|
|
217
|
+
sendMacroLinux(chosen);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sendMacroWindows(text) {
|
|
222
|
+
if (!keybd_event || !VkKeyScanA) return;
|
|
223
|
+
const tapKey = vk => {
|
|
224
|
+
keybd_event(vk, 0, 0, 0);
|
|
225
|
+
keybd_event(vk, 0, KEYUP, 0);
|
|
226
|
+
};
|
|
227
|
+
const tapChar = ch => {
|
|
228
|
+
const packed = VkKeyScanA(ch.charCodeAt(0));
|
|
229
|
+
if (packed === -1) return;
|
|
230
|
+
const vk = packed & 0xff;
|
|
231
|
+
const shiftState = (packed >> 8) & 0xff;
|
|
232
|
+
if (shiftState & 1) keybd_event(0x10, 0, 0, 0); // Shift down
|
|
233
|
+
tapKey(vk);
|
|
234
|
+
if (shiftState & 1) keybd_event(0x10, 0, KEYUP, 0); // Shift up
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Ctrl+C (interrupt)
|
|
238
|
+
keybd_event(VK_CONTROL, 0, 0, 0);
|
|
239
|
+
keybd_event(VK_C, 0, 0, 0);
|
|
240
|
+
keybd_event(VK_C, 0, KEYUP, 0);
|
|
241
|
+
keybd_event(VK_CONTROL, 0, KEYUP, 0);
|
|
242
|
+
for (const ch of text) tapChar(ch);
|
|
243
|
+
keybd_event(VK_RETURN, 0, 0, 0);
|
|
244
|
+
keybd_event(VK_RETURN, 0, KEYUP, 0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function sendMacroMac(text) {
|
|
248
|
+
const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
249
|
+
const script = [
|
|
250
|
+
'tell application "System Events"',
|
|
251
|
+
' key code 8 using {command down}', // Cmd+C
|
|
252
|
+
' delay 0.03',
|
|
253
|
+
` keystroke "${escaped}"`,
|
|
254
|
+
' key code 36', // Enter
|
|
255
|
+
'end tell'
|
|
256
|
+
].join('\n');
|
|
257
|
+
|
|
258
|
+
execFile('osascript', ['-e', script], err => {
|
|
259
|
+
if (err) {
|
|
260
|
+
console.warn('mac macro failed (enable Accessibility for terminal/app):', err.message);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sendMacroLinux(text) {
|
|
266
|
+
// Find a terminal window, focus it, then send keystrokes
|
|
267
|
+
const escaped = text.replace(/'/g, "'\\''");
|
|
268
|
+
const cmd = [
|
|
269
|
+
// Try to find a terminal window (common terminal emulators)
|
|
270
|
+
`TERM_WID=$(xdotool search --name 'Terminal' --class 'gnome-terminal|xterm|konsole|xfce4-terminal|tilix|terminator' 2>/dev/null | head -1)`,
|
|
271
|
+
// Fallback: try VS Code terminal
|
|
272
|
+
`[ -z "$TERM_WID" ] && TERM_WID=$(xdotool search --name 'Visual Studio Code' 2>/dev/null | head -1)`,
|
|
273
|
+
// If found, focus it; otherwise send to current window
|
|
274
|
+
`[ -n "$TERM_WID" ] && xdotool windowactivate --sync "$TERM_WID"`,
|
|
275
|
+
`sleep 0.1`,
|
|
276
|
+
`xdotool key ctrl+c`,
|
|
277
|
+
`xdotool type --clearmodifiers '${escaped}'`,
|
|
278
|
+
`xdotool key Return`,
|
|
279
|
+
].join(' && ');
|
|
280
|
+
execFile('bash', ['-c', cmd], err => {
|
|
281
|
+
if (err) {
|
|
282
|
+
console.warn('linux macro failed (install xdotool):', err.message);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ββ Linux: transparent windows require these flags βββββββββββββββββββββββββ
|
|
288
|
+
if (process.platform === 'linux') {
|
|
289
|
+
app.commandLine.appendSwitch('enable-transparent-visuals');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ββ App lifecycle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
293
|
+
app.whenReady().then(async () => {
|
|
294
|
+
// On Linux, transparent visuals need a short delay after ready
|
|
295
|
+
if (process.platform === 'linux') {
|
|
296
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
297
|
+
}
|
|
298
|
+
tray = new Tray(await getTrayIcon());
|
|
299
|
+
tray.setToolTip('Bad Claude β click for whip');
|
|
300
|
+
tray.setContextMenu(
|
|
301
|
+
Menu.buildFromTemplate([
|
|
302
|
+
{ label: 'Quit', click: () => app.quit() },
|
|
303
|
+
])
|
|
304
|
+
);
|
|
305
|
+
tray.on('click', toggleOverlay);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
app.on('window-all-closed', e => e.preventDefault()); // keep alive in tray
|
package/overlay.html
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<style>
|
|
5
|
+
* { margin: 0; padding: 0; cursor: none; }
|
|
6
|
+
html, body { overflow: hidden; background: transparent; width: 100%; height: 100%; }
|
|
7
|
+
canvas { display: block; }
|
|
8
|
+
</style>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<canvas id="c"></canvas>
|
|
12
|
+
<script>
|
|
13
|
+
|
|
14
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
15
|
+
// PHYSICS SETTINGS β TWEAK EVERYTHING HERE
|
|
16
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
17
|
+
const P = {
|
|
18
|
+
// Rope structure
|
|
19
|
+
segments: 20, // number of chain links
|
|
20
|
+
segmentLength: 25, // base length of each link (px)
|
|
21
|
+
taper: 0.6, // tip segment is this fraction of base length
|
|
22
|
+
|
|
23
|
+
// Physics
|
|
24
|
+
gravity: 1.2, // normal gravity
|
|
25
|
+
dropGravity: 0.95, // gravity when dropping/despawning
|
|
26
|
+
damping: 0.96, // velocity retention per frame (1 = no loss)
|
|
27
|
+
constraintIters:10, // higher = stiffer chain
|
|
28
|
+
maxStretchRatio: 1.2, // hard cap for per-link stretch during fast whips
|
|
29
|
+
|
|
30
|
+
// Dynamic handle aim (target angle + restoring spring, not static lock)
|
|
31
|
+
baseTargetAngle: -1.12, // radians, default "up-right" resting direction
|
|
32
|
+
handleAimByMouseX: 0.4, // horizontal mouse movement influence on target angle
|
|
33
|
+
handleAimByMouseY: 0.2, // vertical mouse movement influence on target angle
|
|
34
|
+
handleAimClamp: 2.0, // max radians target can deviate from base angle
|
|
35
|
+
handleSpring: 0.7, // restoring force to target angle
|
|
36
|
+
handleAngularDamping: 0.078, // angular velocity damping
|
|
37
|
+
basePoseSegments: 2, // how many early segments are strongly guided
|
|
38
|
+
basePoseStiffStart: 0.9, // stiffness near handle
|
|
39
|
+
basePoseStiffEnd: 0.8, // stiffness near end of guided region
|
|
40
|
+
|
|
41
|
+
// Elastic bend limits by chain position (handle stiff, tip floppy)
|
|
42
|
+
handleMaxBendDeg: 16, // max angle between links near handle
|
|
43
|
+
tipMaxBendDeg: 130, // max angle between links near tip
|
|
44
|
+
bendRigidityStart: 0.8, // correction strength near handle
|
|
45
|
+
bendRigidityEnd: 0.12, // correction strength near tip
|
|
46
|
+
|
|
47
|
+
// Screen-edge slap
|
|
48
|
+
wallBounce: 0.42, // velocity retained after wall hit
|
|
49
|
+
wallFriction: 0.86, // tangential damping on wall hit
|
|
50
|
+
|
|
51
|
+
// Crack detection
|
|
52
|
+
crackSpeed: 340, // tip velocity threshold to trigger crack
|
|
53
|
+
crackCooldownMs:200, // min ms between cracks
|
|
54
|
+
firstCrackGraceMs: 350, // no crack (macro) until this long after spawn
|
|
55
|
+
|
|
56
|
+
// Visuals
|
|
57
|
+
lineWidthHandle: 7, // rope thickness near handle
|
|
58
|
+
lineWidthTip: 5, // rope thickness near tip
|
|
59
|
+
outlineWidth: 3, // white halo on each side of the stroke (approx px)
|
|
60
|
+
handleExtraWidth: 5, // added core + outline thickness on first handleThickSegments links only
|
|
61
|
+
handleThickSegments: 2, // how many links from the handle get handleExtraWidth
|
|
62
|
+
bgAlpha: 0.011, // barely-visible bg so window captures mouse events
|
|
63
|
+
|
|
64
|
+
// Initial arc shape
|
|
65
|
+
arcWidth: 260, // how far right the arc extends from mouse
|
|
66
|
+
arcHeight: 185, // how high the arc goes above mouse
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
70
|
+
|
|
71
|
+
const canvas = document.getElementById('c');
|
|
72
|
+
const ctx = canvas.getContext('2d');
|
|
73
|
+
let W, H;
|
|
74
|
+
|
|
75
|
+
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
|
|
76
|
+
resize();
|
|
77
|
+
window.addEventListener('resize', resize);
|
|
78
|
+
|
|
79
|
+
let mouseX = 0, mouseY = 0;
|
|
80
|
+
let prevMouseX = 0, prevMouseY = 0;
|
|
81
|
+
let whip = null;
|
|
82
|
+
let dropping = false;
|
|
83
|
+
let lastCrackTime = 0;
|
|
84
|
+
let whipSpawnTime = 0;
|
|
85
|
+
let handleAngle = P.baseTargetAngle;
|
|
86
|
+
let handleAngVel = 0;
|
|
87
|
+
|
|
88
|
+
const WHIP_CRACK_SOUNDS = ['sounds/A.mp3', 'sounds/B.mp3', 'sounds/C.mp3', 'sounds/D.mp3', 'sounds/E.mp3'];
|
|
89
|
+
|
|
90
|
+
document.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; });
|
|
91
|
+
document.addEventListener('mousedown', () => {
|
|
92
|
+
if (whip && !dropping) dropping = true;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ββ Whip creation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
96
|
+
function spawnWhip(mx, my) {
|
|
97
|
+
dropping = false;
|
|
98
|
+
lastCrackTime = 0;
|
|
99
|
+
whipSpawnTime = Date.now();
|
|
100
|
+
const pts = [];
|
|
101
|
+
for (let i = 0; i < P.segments; i++) {
|
|
102
|
+
const t = i / (P.segments - 1);
|
|
103
|
+
// Nice upward arc from handle (mouse) to tip
|
|
104
|
+
const x = mx + t * P.arcWidth;
|
|
105
|
+
const y = my - Math.sin(t * Math.PI * 0.75) * P.arcHeight;
|
|
106
|
+
pts.push({ x, y, px: x, py: y });
|
|
107
|
+
}
|
|
108
|
+
return pts;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function segLen(i) {
|
|
112
|
+
const t = i / (P.segments - 1);
|
|
113
|
+
return P.segmentLength * (1 - t * (1 - P.taper));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
117
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
118
|
+
|
|
119
|
+
/** Point on CatmullβRom spline (extrapolated ends) for index i in `pts`. */
|
|
120
|
+
function catmullPoint(pts, i) {
|
|
121
|
+
const n = pts.length;
|
|
122
|
+
if (n === 0) return { x: 0, y: 0 };
|
|
123
|
+
if (i < 0) {
|
|
124
|
+
if (n >= 2) {
|
|
125
|
+
return { x: 2 * pts[0].x - pts[1].x, y: 2 * pts[0].y - pts[1].y };
|
|
126
|
+
}
|
|
127
|
+
return { x: pts[0].x, y: pts[0].y };
|
|
128
|
+
}
|
|
129
|
+
if (i >= n) {
|
|
130
|
+
if (n >= 2) {
|
|
131
|
+
const a = pts[n - 2], b = pts[n - 1];
|
|
132
|
+
return { x: 2 * b.x - a.x, y: 2 * b.y - a.y };
|
|
133
|
+
}
|
|
134
|
+
return { x: pts[n - 1].x, y: pts[n - 1].y };
|
|
135
|
+
}
|
|
136
|
+
return pts[i];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Cubic BΓ©zier from p1βp2 matching uniform CatmullβRom through p0,p1,p2,p3.
|
|
141
|
+
* Control points: C1 = p1 + (p2-p0)/6, C2 = p2 - (p3-p1)/6.
|
|
142
|
+
*/
|
|
143
|
+
function whipSegmentBezier(pts, i) {
|
|
144
|
+
const p0 = catmullPoint(pts, i - 1);
|
|
145
|
+
const p1 = pts[i];
|
|
146
|
+
const p2 = pts[i + 1];
|
|
147
|
+
const p3 = catmullPoint(pts, i + 2);
|
|
148
|
+
return {
|
|
149
|
+
cp1x: p1.x + (p2.x - p0.x) / 6,
|
|
150
|
+
cp1y: p1.y + (p2.y - p0.y) / 6,
|
|
151
|
+
cp2x: p2.x - (p3.x - p1.x) / 6,
|
|
152
|
+
cp2y: p2.y - (p3.y - p1.y) / 6,
|
|
153
|
+
x2: p2.x,
|
|
154
|
+
y2: p2.y,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const wrapPi = a => {
|
|
158
|
+
while (a > Math.PI) a -= Math.PI * 2;
|
|
159
|
+
while (a < -Math.PI) a += Math.PI * 2;
|
|
160
|
+
return a;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
function playCrackSound() {
|
|
164
|
+
if (!WHIP_CRACK_SOUNDS.length) return;
|
|
165
|
+
const src = WHIP_CRACK_SOUNDS[Math.floor(Math.random() * WHIP_CRACK_SOUNDS.length)];
|
|
166
|
+
const a = new Audio(src);
|
|
167
|
+
a.play().catch(() => {});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function updateHandleAim() {
|
|
171
|
+
if (dropping) return;
|
|
172
|
+
const mvx = mouseX - prevMouseX;
|
|
173
|
+
const mvy = mouseY - prevMouseY;
|
|
174
|
+
const delta = clamp(
|
|
175
|
+
mvx * P.handleAimByMouseX + mvy * P.handleAimByMouseY,
|
|
176
|
+
-P.handleAimClamp,
|
|
177
|
+
P.handleAimClamp
|
|
178
|
+
);
|
|
179
|
+
const target = P.baseTargetAngle + delta;
|
|
180
|
+
const err = wrapPi(target - handleAngle);
|
|
181
|
+
handleAngVel += err * P.handleSpring;
|
|
182
|
+
handleAngVel *= P.handleAngularDamping;
|
|
183
|
+
handleAngle = wrapPi(handleAngle + handleAngVel);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function applyBasePose() {
|
|
187
|
+
if (!whip || dropping) return;
|
|
188
|
+
const dx = Math.cos(handleAngle);
|
|
189
|
+
const dy = Math.sin(handleAngle);
|
|
190
|
+
const guided = Math.min(P.basePoseSegments, whip.length - 1);
|
|
191
|
+
for (let i = 1; i <= guided; i++) {
|
|
192
|
+
const t = (i - 1) / Math.max(guided - 1, 1);
|
|
193
|
+
const stiff = lerp(P.basePoseStiffStart, P.basePoseStiffEnd, t);
|
|
194
|
+
const prev = whip[i - 1];
|
|
195
|
+
const p = whip[i];
|
|
196
|
+
const targetLen = segLen(i - 1);
|
|
197
|
+
const tx = prev.x + dx * targetLen;
|
|
198
|
+
const ty = prev.y + dy * targetLen;
|
|
199
|
+
p.x = lerp(p.x, tx, stiff);
|
|
200
|
+
p.y = lerp(p.y, ty, stiff);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function applyBendLimits() {
|
|
205
|
+
if (!whip || whip.length < 3) return;
|
|
206
|
+
for (let i = 1; i < whip.length - 1; i++) {
|
|
207
|
+
const a = whip[i - 1];
|
|
208
|
+
const b = whip[i];
|
|
209
|
+
const c = whip[i + 1];
|
|
210
|
+
|
|
211
|
+
const v1x = a.x - b.x;
|
|
212
|
+
const v1y = a.y - b.y;
|
|
213
|
+
const v2x = c.x - b.x;
|
|
214
|
+
const v2y = c.y - b.y;
|
|
215
|
+
const l1 = Math.hypot(v1x, v1y) || 0.0001;
|
|
216
|
+
const l2 = Math.hypot(v2x, v2y) || 0.0001;
|
|
217
|
+
const n1x = v1x / l1, n1y = v1y / l1;
|
|
218
|
+
const n2x = v2x / l2, n2y = v2y / l2;
|
|
219
|
+
|
|
220
|
+
const dot = clamp(n1x * n2x + n1y * n2y, -1, 1);
|
|
221
|
+
const angle = Math.acos(dot);
|
|
222
|
+
const t = i / (whip.length - 2);
|
|
223
|
+
const maxBend = lerp(P.handleMaxBendDeg, P.tipMaxBendDeg, t) * Math.PI / 180;
|
|
224
|
+
const bend = Math.PI - angle; // bend away from a straight line
|
|
225
|
+
if (bend <= maxBend) continue;
|
|
226
|
+
|
|
227
|
+
// Clamp to max bend while preserving side/sign of the bend.
|
|
228
|
+
const cross = n1x * n2y - n1y * n2x;
|
|
229
|
+
const sign = cross >= 0 ? 1 : -1;
|
|
230
|
+
const targetAngle = Math.PI - maxBend;
|
|
231
|
+
const targetA = Math.atan2(n1y, n1x) + sign * targetAngle;
|
|
232
|
+
const tx = b.x + Math.cos(targetA) * l2;
|
|
233
|
+
const ty = b.y + Math.sin(targetA) * l2;
|
|
234
|
+
const rigidity = lerp(P.bendRigidityStart, P.bendRigidityEnd, t);
|
|
235
|
+
|
|
236
|
+
c.x = lerp(c.x, tx, rigidity);
|
|
237
|
+
c.y = lerp(c.y, ty, rigidity);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function capSegmentStretch() {
|
|
242
|
+
if (!whip || whip.length < 2) return;
|
|
243
|
+
for (let i = 0; i < whip.length - 1; i++) {
|
|
244
|
+
const a = whip[i];
|
|
245
|
+
const b = whip[i + 1];
|
|
246
|
+
const dx = b.x - a.x;
|
|
247
|
+
const dy = b.y - a.y;
|
|
248
|
+
const dist = Math.hypot(dx, dy) || 0.0001;
|
|
249
|
+
const maxLen = segLen(i) * P.maxStretchRatio;
|
|
250
|
+
if (dist <= maxLen) continue;
|
|
251
|
+
const k = maxLen / dist;
|
|
252
|
+
b.x = a.x + dx * k;
|
|
253
|
+
b.y = a.y + dy * k;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function applyWallCollisions() {
|
|
258
|
+
if (!whip || dropping) return; // disable collisions while dropping
|
|
259
|
+
const start = 1; // keep pinned handle untouched
|
|
260
|
+
for (let i = start; i < whip.length; i++) {
|
|
261
|
+
const p = whip[i];
|
|
262
|
+
let vx = p.x - p.px;
|
|
263
|
+
let vy = p.y - p.py;
|
|
264
|
+
let hit = false;
|
|
265
|
+
|
|
266
|
+
if (p.x < 0) {
|
|
267
|
+
p.x = 0;
|
|
268
|
+
if (vx < 0) vx = -vx * P.wallBounce;
|
|
269
|
+
vy *= P.wallFriction;
|
|
270
|
+
hit = true;
|
|
271
|
+
} else if (p.x > W) {
|
|
272
|
+
p.x = W;
|
|
273
|
+
if (vx > 0) vx = -vx * P.wallBounce;
|
|
274
|
+
vy *= P.wallFriction;
|
|
275
|
+
hit = true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (p.y < 0) {
|
|
279
|
+
p.y = 0;
|
|
280
|
+
if (vy < 0) vy = -vy * P.wallBounce;
|
|
281
|
+
vx *= P.wallFriction;
|
|
282
|
+
hit = true;
|
|
283
|
+
} else if (p.y > H) {
|
|
284
|
+
p.y = H;
|
|
285
|
+
if (vy > 0) vy = -vy * P.wallBounce;
|
|
286
|
+
vx *= P.wallFriction;
|
|
287
|
+
hit = true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (hit) {
|
|
291
|
+
p.px = p.x - vx;
|
|
292
|
+
p.py = p.y - vy;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ββ Physics step ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
298
|
+
function update() {
|
|
299
|
+
if (!whip) return;
|
|
300
|
+
|
|
301
|
+
const g = dropping ? P.dropGravity : P.gravity;
|
|
302
|
+
updateHandleAim();
|
|
303
|
+
|
|
304
|
+
// Verlet integration
|
|
305
|
+
const start = dropping ? 0 : 1; // if dropping, handle is free too
|
|
306
|
+
for (let i = start; i < whip.length; i++) {
|
|
307
|
+
const p = whip[i];
|
|
308
|
+
const vx = (p.x - p.px) * P.damping;
|
|
309
|
+
const vy = (p.y - p.py) * P.damping;
|
|
310
|
+
p.px = p.x;
|
|
311
|
+
p.py = p.y;
|
|
312
|
+
p.x += vx;
|
|
313
|
+
p.y += vy + g;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Pin handle to mouse
|
|
317
|
+
if (!dropping) {
|
|
318
|
+
whip[0].x = mouseX;
|
|
319
|
+
whip[0].y = mouseY;
|
|
320
|
+
whip[0].px = mouseX;
|
|
321
|
+
whip[0].py = mouseY;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Prevent rubber-band stretching spikes before constraints.
|
|
325
|
+
capSegmentStretch();
|
|
326
|
+
applyWallCollisions();
|
|
327
|
+
|
|
328
|
+
// Keep early whip segments posed upward from handle.
|
|
329
|
+
applyBasePose();
|
|
330
|
+
|
|
331
|
+
// Distance constraints (multiple iterations for stiffness)
|
|
332
|
+
for (let iter = 0; iter < P.constraintIters; iter++) {
|
|
333
|
+
for (let i = 0; i < whip.length - 1; i++) {
|
|
334
|
+
const a = whip[i], b = whip[i + 1];
|
|
335
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
336
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 0.0001;
|
|
337
|
+
const target = segLen(i);
|
|
338
|
+
const diff = (dist - target) / dist * 0.5;
|
|
339
|
+
const ox = dx * diff, oy = dy * diff;
|
|
340
|
+
if (i === 0 && !dropping) {
|
|
341
|
+
// Handle is pinned β push only the next point
|
|
342
|
+
b.x -= ox * 2;
|
|
343
|
+
b.y -= oy * 2;
|
|
344
|
+
} else {
|
|
345
|
+
a.x += ox; a.y += oy;
|
|
346
|
+
b.x -= ox; b.y -= oy;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Clamp bend angle per joint; near handle = stiffer, near tip = floppier.
|
|
350
|
+
applyBendLimits();
|
|
351
|
+
if (!dropping) applyBasePose();
|
|
352
|
+
capSegmentStretch();
|
|
353
|
+
applyWallCollisions();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Tip velocity for crack detection
|
|
357
|
+
const tip = whip[whip.length - 1];
|
|
358
|
+
const tipVel = Math.hypot(tip.x - tip.px, tip.y - tip.py);
|
|
359
|
+
|
|
360
|
+
if (!dropping && tipVel > P.crackSpeed) {
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
if (now - whipSpawnTime >= P.firstCrackGraceMs && now - lastCrackTime > P.crackCooldownMs) {
|
|
363
|
+
lastCrackTime = now;
|
|
364
|
+
playCrackSound();
|
|
365
|
+
window.bridge.whipCrack();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// If dropping, check if everything fell off screen
|
|
370
|
+
if (dropping && whip.every(p => p.y > H + 60)) {
|
|
371
|
+
whip = null;
|
|
372
|
+
dropping = false;
|
|
373
|
+
window.bridge.hideOverlay();
|
|
374
|
+
}
|
|
375
|
+
prevMouseX = mouseX;
|
|
376
|
+
prevMouseY = mouseY;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ββ Rendering βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
380
|
+
function draw() {
|
|
381
|
+
ctx.clearRect(0, 0, W, H);
|
|
382
|
+
|
|
383
|
+
// Near-invisible fill so the window captures mouse events on Windows
|
|
384
|
+
ctx.fillStyle = `rgba(0,0,0,${P.bgAlpha})`;
|
|
385
|
+
ctx.fillRect(0, 0, W, H);
|
|
386
|
+
|
|
387
|
+
if (!whip) return;
|
|
388
|
+
|
|
389
|
+
// White: thin halo on full spline, then extra thickness only over handle links.
|
|
390
|
+
ctx.lineCap = 'round';
|
|
391
|
+
ctx.lineJoin = 'round';
|
|
392
|
+
ctx.strokeStyle = '#fff';
|
|
393
|
+
if (whip.length >= 2) {
|
|
394
|
+
ctx.beginPath();
|
|
395
|
+
ctx.moveTo(whip[0].x, whip[0].y);
|
|
396
|
+
for (let i = 0; i < whip.length - 1; i++) {
|
|
397
|
+
const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i);
|
|
398
|
+
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
|
|
399
|
+
}
|
|
400
|
+
ctx.lineWidth = P.lineWidthTip + P.outlineWidth * 2;
|
|
401
|
+
ctx.stroke();
|
|
402
|
+
|
|
403
|
+
const thickLinks = Math.min(P.handleThickSegments, whip.length - 1);
|
|
404
|
+
if (thickLinks > 0 && P.handleExtraWidth > 0) {
|
|
405
|
+
ctx.beginPath();
|
|
406
|
+
ctx.moveTo(whip[0].x, whip[0].y);
|
|
407
|
+
for (let i = 0; i < thickLinks; i++) {
|
|
408
|
+
const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i);
|
|
409
|
+
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
|
|
410
|
+
}
|
|
411
|
+
ctx.lineWidth =
|
|
412
|
+
P.lineWidthHandle + P.handleExtraWidth + P.outlineWidth * 2;
|
|
413
|
+
ctx.stroke();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
ctx.strokeStyle = '#111';
|
|
418
|
+
for (let i = 0; i < whip.length - 1; i++) {
|
|
419
|
+
const t = i / Math.max(1, whip.length - 2);
|
|
420
|
+
const extra = i < P.handleThickSegments ? P.handleExtraWidth : 0;
|
|
421
|
+
ctx.lineWidth = lerp(P.lineWidthHandle, P.lineWidthTip, t) + extra;
|
|
422
|
+
const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i);
|
|
423
|
+
ctx.beginPath();
|
|
424
|
+
ctx.moveTo(whip[i].x, whip[i].y);
|
|
425
|
+
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
|
|
426
|
+
ctx.stroke();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ββ Main loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
431
|
+
function loop() {
|
|
432
|
+
update();
|
|
433
|
+
draw();
|
|
434
|
+
requestAnimationFrame(loop);
|
|
435
|
+
}
|
|
436
|
+
loop();
|
|
437
|
+
|
|
438
|
+
// ββ IPC from main process βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
439
|
+
window.bridge.onSpawnWhip(() => {
|
|
440
|
+
whip = spawnWhip(mouseX || W / 2, mouseY || H / 2);
|
|
441
|
+
dropping = false;
|
|
442
|
+
prevMouseX = mouseX;
|
|
443
|
+
prevMouseY = mouseY;
|
|
444
|
+
handleAngle = P.baseTargetAngle;
|
|
445
|
+
handleAngVel = 0;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
window.bridge.onDropWhip(() => {
|
|
449
|
+
if (whip && !dropping) dropping = true;
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
</script>
|
|
453
|
+
</body>
|
|
454
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "badclaude-linux",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Whip Claude into shape β Linux fork with multi-monitor support. Original by GitFrog1111.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "main.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"badclaude": "bin/badclaude.js"
|
|
9
|
+
},
|
|
10
|
+
"os": [
|
|
11
|
+
"darwin",
|
|
12
|
+
"win32",
|
|
13
|
+
"linux"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"electron",
|
|
20
|
+
"tray",
|
|
21
|
+
"overlay",
|
|
22
|
+
"cli",
|
|
23
|
+
"claude",
|
|
24
|
+
"whip",
|
|
25
|
+
"fun",
|
|
26
|
+
"linux",
|
|
27
|
+
"macos",
|
|
28
|
+
"windows",
|
|
29
|
+
"multi-monitor",
|
|
30
|
+
"badclaude"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Prasad-b-git/badclaude-linux.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Prasad-b-git/badclaude-linux/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/Prasad-b-git/badclaude-linux#readme",
|
|
40
|
+
"files": [
|
|
41
|
+
"main.js",
|
|
42
|
+
"preload.js",
|
|
43
|
+
"overlay.html",
|
|
44
|
+
"sounds",
|
|
45
|
+
"icon",
|
|
46
|
+
"bin/badclaude.js",
|
|
47
|
+
"README.md"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"start": "electron .",
|
|
51
|
+
"dev": "electron .",
|
|
52
|
+
"pack": "npm pack"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"electron": "^33.0.0",
|
|
56
|
+
"koffi": "^2.9.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/preload.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('bridge', {
|
|
4
|
+
whipCrack: () => ipcRenderer.send('whip-crack'),
|
|
5
|
+
hideOverlay: () => ipcRenderer.send('hide-overlay'),
|
|
6
|
+
onSpawnWhip: (fn) => ipcRenderer.on('spawn-whip', () => fn()),
|
|
7
|
+
onDropWhip: (fn) => ipcRenderer.on('drop-whip', () => fn()),
|
|
8
|
+
});
|
package/sounds/A.mp3
ADDED
|
Binary file
|
package/sounds/B.mp3
ADDED
|
Binary file
|
package/sounds/C.mp3
ADDED
|
Binary file
|
package/sounds/D.mp3
ADDED
|
Binary file
|
package/sounds/E.mp3
ADDED
|
Binary file
|