mirror of
https://github.com/GitFrog1111/badclaude.git
synced 2026-04-21 19:46:24 +02:00
3deaabfaa6
Made-with: Cursor
455 lines
15 KiB
HTML
455 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<style>
|
||
* { margin: 0; padding: 0; cursor: none; }
|
||
html, body { overflow: hidden; background: transparent; width: 100%; height: 100%; }
|
||
canvas { display: block; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="c"></canvas>
|
||
<script>
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// PHYSICS SETTINGS — TWEAK EVERYTHING HERE
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
const P = {
|
||
// Rope structure
|
||
segments: 28, // number of chain links
|
||
segmentLength: 25, // base length of each link (px)
|
||
taper: 0.6, // tip segment is this fraction of base length
|
||
|
||
// Physics
|
||
gravity: 1.2, // normal gravity
|
||
dropGravity: 0.95, // gravity when dropping/despawning
|
||
damping: 0.96, // velocity retention per frame (1 = no loss)
|
||
constraintIters:20, // higher = stiffer chain
|
||
maxStretchRatio: 1.2, // hard cap for per-link stretch during fast whips
|
||
|
||
// Dynamic handle aim (target angle + restoring spring, not static lock)
|
||
baseTargetAngle: -1.12, // radians, default "up-right" resting direction
|
||
handleAimByMouseX: 0.4, // horizontal mouse movement influence on target angle
|
||
handleAimByMouseY: 0.2, // vertical mouse movement influence on target angle
|
||
handleAimClamp: 2.0, // max radians target can deviate from base angle
|
||
handleSpring: 0.7, // restoring force to target angle
|
||
handleAngularDamping: 0.078, // angular velocity damping
|
||
basePoseSegments: 2, // how many early segments are strongly guided
|
||
basePoseStiffStart: 0.9, // stiffness near handle
|
||
basePoseStiffEnd: 0.8, // stiffness near end of guided region
|
||
|
||
// Elastic bend limits by chain position (handle stiff, tip floppy)
|
||
handleMaxBendDeg: 16, // max angle between links near handle
|
||
tipMaxBendDeg: 130, // max angle between links near tip
|
||
bendRigidityStart: 0.8, // correction strength near handle
|
||
bendRigidityEnd: 0.12, // correction strength near tip
|
||
|
||
// Screen-edge slap
|
||
wallBounce: 0.42, // velocity retained after wall hit
|
||
wallFriction: 0.86, // tangential damping on wall hit
|
||
|
||
// Crack detection
|
||
crackSpeed: 340, // tip velocity threshold to trigger crack
|
||
crackCooldownMs:200, // min ms between cracks
|
||
firstCrackGraceMs: 350, // no crack (macro) until this long after spawn
|
||
|
||
// Visuals
|
||
lineWidthHandle: 7, // rope thickness near handle
|
||
lineWidthTip: 5, // rope thickness near tip
|
||
outlineWidth: 3, // white halo on each side of the stroke (approx px)
|
||
handleExtraWidth: 5, // added core + outline thickness on first handleThickSegments links only
|
||
handleThickSegments: 2, // how many links from the handle get handleExtraWidth
|
||
bgAlpha: 0.011, // barely-visible bg so window captures mouse events
|
||
|
||
// Initial arc shape
|
||
arcWidth: 260, // how far right the arc extends from mouse
|
||
arcHeight: 185, // how high the arc goes above mouse
|
||
};
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
|
||
const canvas = document.getElementById('c');
|
||
const ctx = canvas.getContext('2d');
|
||
let W, H;
|
||
|
||
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
|
||
resize();
|
||
window.addEventListener('resize', resize);
|
||
|
||
let mouseX = 0, mouseY = 0;
|
||
let prevMouseX = 0, prevMouseY = 0;
|
||
let whip = null;
|
||
let dropping = false;
|
||
let lastCrackTime = 0;
|
||
let whipSpawnTime = 0;
|
||
let handleAngle = P.baseTargetAngle;
|
||
let handleAngVel = 0;
|
||
|
||
const WHIP_CRACK_SOUNDS = ['sounds/A.mp3', 'sounds/B.mp3', 'sounds/C.mp3', 'sounds/D.mp3', 'sounds/E.mp3'];
|
||
|
||
document.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; });
|
||
document.addEventListener('mousedown', () => {
|
||
if (whip && !dropping) dropping = true;
|
||
});
|
||
|
||
// ── Whip creation ───────────────────────────────────────────────────────────
|
||
function spawnWhip(mx, my) {
|
||
dropping = false;
|
||
lastCrackTime = 0;
|
||
whipSpawnTime = Date.now();
|
||
const pts = [];
|
||
for (let i = 0; i < P.segments; i++) {
|
||
const t = i / (P.segments - 1);
|
||
// Nice upward arc from handle (mouse) to tip
|
||
const x = mx + t * P.arcWidth;
|
||
const y = my - Math.sin(t * Math.PI * 0.75) * P.arcHeight;
|
||
pts.push({ x, y, px: x, py: y });
|
||
}
|
||
return pts;
|
||
}
|
||
|
||
function segLen(i) {
|
||
const t = i / (P.segments - 1);
|
||
return P.segmentLength * (1 - t * (1 - P.taper));
|
||
}
|
||
|
||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||
const lerp = (a, b, t) => a + (b - a) * t;
|
||
|
||
/** Point on Catmull–Rom spline (extrapolated ends) for index i in `pts`. */
|
||
function catmullPoint(pts, i) {
|
||
const n = pts.length;
|
||
if (n === 0) return { x: 0, y: 0 };
|
||
if (i < 0) {
|
||
if (n >= 2) {
|
||
return { x: 2 * pts[0].x - pts[1].x, y: 2 * pts[0].y - pts[1].y };
|
||
}
|
||
return { x: pts[0].x, y: pts[0].y };
|
||
}
|
||
if (i >= n) {
|
||
if (n >= 2) {
|
||
const a = pts[n - 2], b = pts[n - 1];
|
||
return { x: 2 * b.x - a.x, y: 2 * b.y - a.y };
|
||
}
|
||
return { x: pts[n - 1].x, y: pts[n - 1].y };
|
||
}
|
||
return pts[i];
|
||
}
|
||
|
||
/**
|
||
* Cubic Bézier from p1→p2 matching uniform Catmull–Rom through p0,p1,p2,p3.
|
||
* Control points: C1 = p1 + (p2-p0)/6, C2 = p2 - (p3-p1)/6.
|
||
*/
|
||
function whipSegmentBezier(pts, i) {
|
||
const p0 = catmullPoint(pts, i - 1);
|
||
const p1 = pts[i];
|
||
const p2 = pts[i + 1];
|
||
const p3 = catmullPoint(pts, i + 2);
|
||
return {
|
||
cp1x: p1.x + (p2.x - p0.x) / 6,
|
||
cp1y: p1.y + (p2.y - p0.y) / 6,
|
||
cp2x: p2.x - (p3.x - p1.x) / 6,
|
||
cp2y: p2.y - (p3.y - p1.y) / 6,
|
||
x2: p2.x,
|
||
y2: p2.y,
|
||
};
|
||
}
|
||
const wrapPi = a => {
|
||
while (a > Math.PI) a -= Math.PI * 2;
|
||
while (a < -Math.PI) a += Math.PI * 2;
|
||
return a;
|
||
};
|
||
|
||
function playCrackSound() {
|
||
if (!WHIP_CRACK_SOUNDS.length) return;
|
||
const src = WHIP_CRACK_SOUNDS[Math.floor(Math.random() * WHIP_CRACK_SOUNDS.length)];
|
||
const a = new Audio(src);
|
||
a.play().catch(() => {});
|
||
}
|
||
|
||
function updateHandleAim() {
|
||
if (dropping) return;
|
||
const mvx = mouseX - prevMouseX;
|
||
const mvy = mouseY - prevMouseY;
|
||
const delta = clamp(
|
||
mvx * P.handleAimByMouseX + mvy * P.handleAimByMouseY,
|
||
-P.handleAimClamp,
|
||
P.handleAimClamp
|
||
);
|
||
const target = P.baseTargetAngle + delta;
|
||
const err = wrapPi(target - handleAngle);
|
||
handleAngVel += err * P.handleSpring;
|
||
handleAngVel *= P.handleAngularDamping;
|
||
handleAngle = wrapPi(handleAngle + handleAngVel);
|
||
}
|
||
|
||
function applyBasePose() {
|
||
if (!whip || dropping) return;
|
||
const dx = Math.cos(handleAngle);
|
||
const dy = Math.sin(handleAngle);
|
||
const guided = Math.min(P.basePoseSegments, whip.length - 1);
|
||
for (let i = 1; i <= guided; i++) {
|
||
const t = (i - 1) / Math.max(guided - 1, 1);
|
||
const stiff = lerp(P.basePoseStiffStart, P.basePoseStiffEnd, t);
|
||
const prev = whip[i - 1];
|
||
const p = whip[i];
|
||
const targetLen = segLen(i - 1);
|
||
const tx = prev.x + dx * targetLen;
|
||
const ty = prev.y + dy * targetLen;
|
||
p.x = lerp(p.x, tx, stiff);
|
||
p.y = lerp(p.y, ty, stiff);
|
||
}
|
||
}
|
||
|
||
function applyBendLimits() {
|
||
if (!whip || whip.length < 3) return;
|
||
for (let i = 1; i < whip.length - 1; i++) {
|
||
const a = whip[i - 1];
|
||
const b = whip[i];
|
||
const c = whip[i + 1];
|
||
|
||
const v1x = a.x - b.x;
|
||
const v1y = a.y - b.y;
|
||
const v2x = c.x - b.x;
|
||
const v2y = c.y - b.y;
|
||
const l1 = Math.hypot(v1x, v1y) || 0.0001;
|
||
const l2 = Math.hypot(v2x, v2y) || 0.0001;
|
||
const n1x = v1x / l1, n1y = v1y / l1;
|
||
const n2x = v2x / l2, n2y = v2y / l2;
|
||
|
||
const dot = clamp(n1x * n2x + n1y * n2y, -1, 1);
|
||
const angle = Math.acos(dot);
|
||
const t = i / (whip.length - 2);
|
||
const maxBend = lerp(P.handleMaxBendDeg, P.tipMaxBendDeg, t) * Math.PI / 180;
|
||
const bend = Math.PI - angle; // bend away from a straight line
|
||
if (bend <= maxBend) continue;
|
||
|
||
// Clamp to max bend while preserving side/sign of the bend.
|
||
const cross = n1x * n2y - n1y * n2x;
|
||
const sign = cross >= 0 ? 1 : -1;
|
||
const targetAngle = Math.PI - maxBend;
|
||
const targetA = Math.atan2(n1y, n1x) + sign * targetAngle;
|
||
const tx = b.x + Math.cos(targetA) * l2;
|
||
const ty = b.y + Math.sin(targetA) * l2;
|
||
const rigidity = lerp(P.bendRigidityStart, P.bendRigidityEnd, t);
|
||
|
||
c.x = lerp(c.x, tx, rigidity);
|
||
c.y = lerp(c.y, ty, rigidity);
|
||
}
|
||
}
|
||
|
||
function capSegmentStretch() {
|
||
if (!whip || whip.length < 2) return;
|
||
for (let i = 0; i < whip.length - 1; i++) {
|
||
const a = whip[i];
|
||
const b = whip[i + 1];
|
||
const dx = b.x - a.x;
|
||
const dy = b.y - a.y;
|
||
const dist = Math.hypot(dx, dy) || 0.0001;
|
||
const maxLen = segLen(i) * P.maxStretchRatio;
|
||
if (dist <= maxLen) continue;
|
||
const k = maxLen / dist;
|
||
b.x = a.x + dx * k;
|
||
b.y = a.y + dy * k;
|
||
}
|
||
}
|
||
|
||
function applyWallCollisions() {
|
||
if (!whip || dropping) return; // disable collisions while dropping
|
||
const start = 1; // keep pinned handle untouched
|
||
for (let i = start; i < whip.length; i++) {
|
||
const p = whip[i];
|
||
let vx = p.x - p.px;
|
||
let vy = p.y - p.py;
|
||
let hit = false;
|
||
|
||
if (p.x < 0) {
|
||
p.x = 0;
|
||
if (vx < 0) vx = -vx * P.wallBounce;
|
||
vy *= P.wallFriction;
|
||
hit = true;
|
||
} else if (p.x > W) {
|
||
p.x = W;
|
||
if (vx > 0) vx = -vx * P.wallBounce;
|
||
vy *= P.wallFriction;
|
||
hit = true;
|
||
}
|
||
|
||
if (p.y < 0) {
|
||
p.y = 0;
|
||
if (vy < 0) vy = -vy * P.wallBounce;
|
||
vx *= P.wallFriction;
|
||
hit = true;
|
||
} else if (p.y > H) {
|
||
p.y = H;
|
||
if (vy > 0) vy = -vy * P.wallBounce;
|
||
vx *= P.wallFriction;
|
||
hit = true;
|
||
}
|
||
|
||
if (hit) {
|
||
p.px = p.x - vx;
|
||
p.py = p.y - vy;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Physics step ────────────────────────────────────────────────────────────
|
||
function update() {
|
||
if (!whip) return;
|
||
|
||
const g = dropping ? P.dropGravity : P.gravity;
|
||
updateHandleAim();
|
||
|
||
// Verlet integration
|
||
const start = dropping ? 0 : 1; // if dropping, handle is free too
|
||
for (let i = start; i < whip.length; i++) {
|
||
const p = whip[i];
|
||
const vx = (p.x - p.px) * P.damping;
|
||
const vy = (p.y - p.py) * P.damping;
|
||
p.px = p.x;
|
||
p.py = p.y;
|
||
p.x += vx;
|
||
p.y += vy + g;
|
||
}
|
||
|
||
// Pin handle to mouse
|
||
if (!dropping) {
|
||
whip[0].x = mouseX;
|
||
whip[0].y = mouseY;
|
||
whip[0].px = mouseX;
|
||
whip[0].py = mouseY;
|
||
}
|
||
|
||
// Prevent rubber-band stretching spikes before constraints.
|
||
capSegmentStretch();
|
||
applyWallCollisions();
|
||
|
||
// Keep early whip segments posed upward from handle.
|
||
applyBasePose();
|
||
|
||
// Distance constraints (multiple iterations for stiffness)
|
||
for (let iter = 0; iter < P.constraintIters; iter++) {
|
||
for (let i = 0; i < whip.length - 1; i++) {
|
||
const a = whip[i], b = whip[i + 1];
|
||
const dx = b.x - a.x, dy = b.y - a.y;
|
||
const dist = Math.sqrt(dx * dx + dy * dy) || 0.0001;
|
||
const target = segLen(i);
|
||
const diff = (dist - target) / dist * 0.5;
|
||
const ox = dx * diff, oy = dy * diff;
|
||
if (i === 0 && !dropping) {
|
||
// Handle is pinned – push only the next point
|
||
b.x -= ox * 2;
|
||
b.y -= oy * 2;
|
||
} else {
|
||
a.x += ox; a.y += oy;
|
||
b.x -= ox; b.y -= oy;
|
||
}
|
||
}
|
||
// Clamp bend angle per joint; near handle = stiffer, near tip = floppier.
|
||
applyBendLimits();
|
||
if (!dropping) applyBasePose();
|
||
capSegmentStretch();
|
||
applyWallCollisions();
|
||
}
|
||
|
||
// Tip velocity for crack detection
|
||
const tip = whip[whip.length - 1];
|
||
const tipVel = Math.hypot(tip.x - tip.px, tip.y - tip.py);
|
||
|
||
if (!dropping && tipVel > P.crackSpeed) {
|
||
const now = Date.now();
|
||
if (now - whipSpawnTime >= P.firstCrackGraceMs && now - lastCrackTime > P.crackCooldownMs) {
|
||
lastCrackTime = now;
|
||
playCrackSound();
|
||
window.bridge.whipCrack();
|
||
}
|
||
}
|
||
|
||
// If dropping, check if everything fell off screen
|
||
if (dropping && whip.every(p => p.y > H + 60)) {
|
||
whip = null;
|
||
dropping = false;
|
||
window.bridge.hideOverlay();
|
||
}
|
||
prevMouseX = mouseX;
|
||
prevMouseY = mouseY;
|
||
}
|
||
|
||
// ── Rendering ───────────────────────────────────────────────────────────────
|
||
function draw() {
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
// Near-invisible fill so the window captures mouse events on Windows
|
||
ctx.fillStyle = `rgba(0,0,0,${P.bgAlpha})`;
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
if (!whip) return;
|
||
|
||
// White: thin halo on full spline, then extra thickness only over handle links.
|
||
ctx.lineCap = 'round';
|
||
ctx.lineJoin = 'round';
|
||
ctx.strokeStyle = '#fff';
|
||
if (whip.length >= 2) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(whip[0].x, whip[0].y);
|
||
for (let i = 0; i < whip.length - 1; i++) {
|
||
const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i);
|
||
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
|
||
}
|
||
ctx.lineWidth = P.lineWidthTip + P.outlineWidth * 2;
|
||
ctx.stroke();
|
||
|
||
const thickLinks = Math.min(P.handleThickSegments, whip.length - 1);
|
||
if (thickLinks > 0 && P.handleExtraWidth > 0) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(whip[0].x, whip[0].y);
|
||
for (let i = 0; i < thickLinks; i++) {
|
||
const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i);
|
||
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
|
||
}
|
||
ctx.lineWidth =
|
||
P.lineWidthHandle + P.handleExtraWidth + P.outlineWidth * 2;
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
ctx.strokeStyle = '#111';
|
||
for (let i = 0; i < whip.length - 1; i++) {
|
||
const t = i / Math.max(1, whip.length - 2);
|
||
const extra = i < P.handleThickSegments ? P.handleExtraWidth : 0;
|
||
ctx.lineWidth = lerp(P.lineWidthHandle, P.lineWidthTip, t) + extra;
|
||
const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i);
|
||
ctx.beginPath();
|
||
ctx.moveTo(whip[i].x, whip[i].y);
|
||
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
// ── Main loop ───────────────────────────────────────────────────────────────
|
||
function loop() {
|
||
update();
|
||
draw();
|
||
requestAnimationFrame(loop);
|
||
}
|
||
loop();
|
||
|
||
// ── IPC from main process ───────────────────────────────────────────────────
|
||
window.bridge.onSpawnWhip(() => {
|
||
whip = spawnWhip(mouseX || W / 2, mouseY || H / 2);
|
||
dropping = false;
|
||
prevMouseX = mouseX;
|
||
prevMouseY = mouseY;
|
||
handleAngle = P.baseTargetAngle;
|
||
handleAngVel = 0;
|
||
});
|
||
|
||
window.bridge.onDropWhip(() => {
|
||
if (whip && !dropping) dropping = true;
|
||
});
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|