mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-11 09:03:33 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
633e092ecf | ||
|
|
d6282e6499 | ||
|
|
75d599d2fa | ||
|
|
a3e0f89a32 | ||
|
|
3b5bd45d8c | ||
|
|
daead9273c | ||
|
|
429cc190b4 | ||
|
|
ffd4010561 | ||
|
|
9ebce15dd2 | ||
|
|
180d7b3226 |
@@ -6,40 +6,219 @@ interface State {
|
||||
enabled: boolean
|
||||
target: { x: number; y: number }
|
||||
position: { x: number; y: number }
|
||||
stepSizeX: number
|
||||
stepSizeY: number
|
||||
startPosition: { x: number; y: number }
|
||||
startTime: number
|
||||
duration: number
|
||||
clicking: boolean
|
||||
jitterSeed: number
|
||||
overshootTarget: { x: number; y: number } | null
|
||||
isOvershootCorrection: boolean
|
||||
}
|
||||
|
||||
class Demo extends React.Component<{ classes: any }, State> {
|
||||
private timer: any
|
||||
private clickTimer: any
|
||||
private frameInterval = 20
|
||||
private readonly correctionDelayMs = 50 // Delay before overshoot correction starts
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { enabled: false, target: { x: 0, y: 0 }, position: { x: 0, y: 0 }, stepSizeX: 1, stepSizeY: 1 }
|
||||
this.state = {
|
||||
enabled: false,
|
||||
target: { x: 0, y: 0 },
|
||||
position: { x: 0, y: 0 },
|
||||
startPosition: { x: 0, y: 0 },
|
||||
startTime: 0,
|
||||
duration: 0,
|
||||
clicking: false,
|
||||
jitterSeed: Math.random(),
|
||||
overshootTarget: null,
|
||||
isOvershootCorrection: false,
|
||||
}
|
||||
}
|
||||
|
||||
private moveCloser(steps: number = 0) {
|
||||
const steSizeX = Math.min(this.state.stepSizeX, Math.abs(this.state.position.x - this.state.target.x))
|
||||
const steSizeY = Math.min(this.state.stepSizeY, Math.abs(this.state.position.y - this.state.target.y))
|
||||
const dirX = this.state.position.x > this.state.target.x ? -1 : 1
|
||||
const dirY = this.state.position.y > this.state.target.y ? -1 : 1
|
||||
/**
|
||||
* Generates a seeded random value using a simple LCG algorithm
|
||||
* Better quality than sin-based PRNG for consistent jitter patterns
|
||||
*/
|
||||
private seededRandom(seed: number): number {
|
||||
// Linear Congruential Generator with common constants
|
||||
const a = 1664525
|
||||
const c = 1013904223
|
||||
const m = Math.pow(2, 32)
|
||||
const x = (a * seed + c) % m
|
||||
return x / m
|
||||
}
|
||||
|
||||
if (steSizeX <= 0.1 && steSizeY <= 0.1) {
|
||||
this.timer && clearTimeout(this.timer)
|
||||
return
|
||||
/**
|
||||
* Adds human-like micro-movements to the cursor position
|
||||
* Small random offsets that don't affect the overall path significantly
|
||||
*/
|
||||
private addJitter(x: number, y: number, progress: number): { x: number; y: number } {
|
||||
// Jitter is strongest in the middle of the movement (more natural)
|
||||
const jitterStrength = Math.sin(progress * Math.PI) * 2 // 0 at start/end, max at middle
|
||||
|
||||
// Use progress to vary the seed for different jitter at each frame
|
||||
const seed1 = this.state.jitterSeed * 1000 + progress * 100
|
||||
const seed2 = this.state.jitterSeed * 2000 + progress * 150
|
||||
|
||||
const jitterX = (this.seededRandom(seed1) - 0.5) * jitterStrength
|
||||
const jitterY = (this.seededRandom(seed2) - 0.5) * jitterStrength
|
||||
|
||||
return {
|
||||
x: x + jitterX,
|
||||
y: y + jitterY,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cubic bezier easing function for natural-looking mouse movement
|
||||
* Uses control points (0.25, 0.75) for a smooth ease-in-out curve
|
||||
*/
|
||||
private cubicBezier(t: number, p0: number, p1: number, p2: number, p3: number): number {
|
||||
const u = 1 - t
|
||||
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3
|
||||
}
|
||||
|
||||
/**
|
||||
* Easing function that creates a smooth acceleration and deceleration
|
||||
* This makes the mouse movement appear more human-like
|
||||
*/
|
||||
private easeInOutCubic(t: number): number {
|
||||
// Cubic bezier approximation for ease-in-out
|
||||
return this.cubicBezier(t, 0, 0.25, 0.75, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an overshoot target for more realistic human-like movement
|
||||
* Returns null if no overshoot, or a point slightly past the target
|
||||
*/
|
||||
private calculateOvershootTarget(
|
||||
startX: number,
|
||||
startY: number,
|
||||
targetX: number,
|
||||
targetY: number
|
||||
): { x: number; y: number } | null {
|
||||
const dx = targetX - startX
|
||||
const dy = targetY - startY
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
// Don't overshoot for very short distances (less than 10px)
|
||||
if (distance < 10) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 30% chance of overshoot for natural variation
|
||||
if (this.seededRandom(this.state.jitterSeed * 3) > 0.3) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Overshoot by 2-8% of the distance
|
||||
const overshootPercent = 0.02 + this.seededRandom(this.state.jitterSeed * 4) * 0.06
|
||||
const overshootDistance = distance * overshootPercent
|
||||
|
||||
// Overshoot in the direction of movement
|
||||
const dirX = dx / distance
|
||||
const dirY = dy / distance
|
||||
|
||||
return {
|
||||
x: targetX + dirX * overshootDistance,
|
||||
y: targetY + dirY * overshootDistance,
|
||||
}
|
||||
}
|
||||
|
||||
private moveCloser() {
|
||||
const elapsed = Date.now() - this.state.startTime
|
||||
const progress = Math.min(elapsed / (this.state.duration || 1), 1)
|
||||
|
||||
// Apply easing function for smooth, human-like movement timing
|
||||
const easedProgress = this.easeInOutCubic(progress)
|
||||
|
||||
// Determine actual target (could be overshoot target or final target)
|
||||
const actualTarget = this.state.overshootTarget || this.state.target
|
||||
|
||||
// Calculate bezier curve control points for a natural arc trajectory
|
||||
// Instead of moving in a straight line, the cursor follows a curved path
|
||||
const startX = this.state.startPosition.x
|
||||
const startY = this.state.startPosition.y
|
||||
const endX = actualTarget.x
|
||||
const endY = actualTarget.y
|
||||
|
||||
// Create control points for a quadratic bezier curve
|
||||
// The control point is offset perpendicular to the direct line, creating an arc
|
||||
const dx = endX - startX
|
||||
const dy = endY - startY
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
// Arc height is proportional to distance (80% of distance, capped at 200px max)
|
||||
// Quadrupled from original 20% to make the curve highly visible
|
||||
// For overshoot correction, use smaller arc (more direct)
|
||||
const arcPercent = this.state.isOvershootCorrection ? 0.3 : 0.8
|
||||
const arcCap = this.state.isOvershootCorrection ? 50 : 200
|
||||
const arcHeight = Math.min(distance * arcPercent, arcCap)
|
||||
|
||||
// Calculate perpendicular offset for the control point
|
||||
const perpX = -dy / (distance || 1)
|
||||
const perpY = dx / (distance || 1)
|
||||
|
||||
// Add slight randomness to control point to avoid perfectly predictable curves
|
||||
const controlPointJitter = this.seededRandom(this.state.jitterSeed * 5) * 20 - 10
|
||||
|
||||
// Control point is at the midpoint, offset perpendicular to create an arc
|
||||
const controlX = (startX + endX) / 2 + perpX * (arcHeight + controlPointJitter)
|
||||
const controlY = (startY + endY) / 2 + perpY * (arcHeight + controlPointJitter)
|
||||
|
||||
// Calculate position on quadratic bezier curve: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂
|
||||
const t = easedProgress
|
||||
const u = 1 - t
|
||||
let newX = u * u * startX + 2 * u * t * controlX + t * t * endX
|
||||
let newY = u * u * startY + 2 * u * t * controlY + t * t * endY
|
||||
|
||||
// Add jitter for human-like micro-movements
|
||||
const jittered = this.addJitter(newX, newY, progress)
|
||||
newX = jittered.x
|
||||
newY = jittered.y
|
||||
|
||||
this.setState({
|
||||
position: {
|
||||
x: this.state.position.x + dirX * steSizeX,
|
||||
y: this.state.position.y + dirY * steSizeY,
|
||||
x: newX,
|
||||
y: newY,
|
||||
},
|
||||
})
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.moveCloser(steps + 1)
|
||||
}, this.frameInterval)
|
||||
// Continue animation if not complete
|
||||
if (progress < 1) {
|
||||
// Add random delay variation (jitter in timing)
|
||||
// Frame interval varies between 15-25ms for irregular movement
|
||||
// Use modulo to keep seed in reasonable range
|
||||
const jitterDelay = this.seededRandom((this.state.jitterSeed * 6 + elapsed) % 100000) * 10 - 5
|
||||
const nextInterval = Math.max(10, this.frameInterval + jitterDelay)
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.moveCloser()
|
||||
}, nextInterval)
|
||||
} else {
|
||||
this.timer && clearTimeout(this.timer)
|
||||
|
||||
// If we just reached overshoot target, start correction to actual target
|
||||
if (this.state.overshootTarget && !this.state.isOvershootCorrection) {
|
||||
const correctionDuration = this.state.duration * 0.15 // Correction takes 15% of original time
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
startPosition: { x: this.state.position.x, y: this.state.position.y },
|
||||
target: this.state.target, // Keep original target
|
||||
overshootTarget: null, // Clear overshoot
|
||||
isOvershootCorrection: true,
|
||||
startTime: Date.now(),
|
||||
duration: correctionDuration,
|
||||
})
|
||||
this.moveCloser()
|
||||
}, this.correctionDelayMs)
|
||||
} else {
|
||||
// Reset overshoot state for next movement
|
||||
this.setState({ isOvershootCorrection: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
@@ -47,11 +226,40 @@ class Demo extends React.Component<{ classes: any }, State> {
|
||||
this.setState({ enabled: true })
|
||||
}
|
||||
;(window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
|
||||
const stepSizeX = Math.abs(this.state.position.x - x) / (animationTime / this.frameInterval)
|
||||
const stepSizeY = Math.abs(this.state.position.y - y) / (animationTime / this.frameInterval)
|
||||
this.setState({ stepSizeX, stepSizeY, enabled: true, target: { x, y } })
|
||||
const newJitterSeed = Math.random()
|
||||
const currentPos = this.state.position
|
||||
|
||||
// Calculate if this movement should have overshoot
|
||||
const overshoot = this.calculateOvershootTarget(currentPos.x, currentPos.y, x, y)
|
||||
|
||||
// If overshoot exists, adjust duration to account for correction time
|
||||
// Main movement gets 85% of time, correction gets 15%
|
||||
const mainDuration = overshoot ? animationTime * 0.85 : animationTime
|
||||
|
||||
this.setState({
|
||||
enabled: true,
|
||||
target: { x, y }, // Store the actual target
|
||||
overshootTarget: overshoot, // May be null or an overshoot position
|
||||
isOvershootCorrection: false,
|
||||
startPosition: { x: currentPos.x, y: currentPos.y },
|
||||
startTime: Date.now(),
|
||||
duration: mainDuration,
|
||||
jitterSeed: newJitterSeed,
|
||||
})
|
||||
this.moveCloser()
|
||||
}
|
||||
;(window as any).demo.clickMouse = () => {
|
||||
this.setState({ clicking: true })
|
||||
this.clickTimer && clearTimeout(this.clickTimer)
|
||||
this.clickTimer = setTimeout(() => {
|
||||
this.setState({ clicking: false })
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.timer && clearTimeout(this.timer)
|
||||
this.clickTimer && clearTimeout(this.clickTimer)
|
||||
}
|
||||
|
||||
public render() {
|
||||
@@ -64,11 +272,26 @@ class Demo extends React.Component<{ classes: any }, State> {
|
||||
top: this.state.position.y + 2,
|
||||
}
|
||||
|
||||
return <img src={cursor} style={cursorStyle} className={this.props.classes.cursor} />
|
||||
return (
|
||||
<>
|
||||
<img src={cursor} style={cursorStyle} className={this.props.classes.cursor} />
|
||||
{this.state.clicking && <div style={cursorStyle} className={this.props.classes.clickRipple} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const style = (theme: Theme) => ({
|
||||
'@keyframes clickPulse': {
|
||||
from: {
|
||||
transform: 'scale(1)',
|
||||
opacity: 1,
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(2.5)',
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
cursor: {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
@@ -77,6 +300,16 @@ const style = (theme: Theme) => ({
|
||||
filter: theme.palette.type === 'light' ? undefined : 'invert(100%)',
|
||||
pointerEvents: 'none' as 'none',
|
||||
},
|
||||
clickRipple: {
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
position: 'fixed' as 'fixed',
|
||||
zIndex: 1000000 - 1, // Just below cursor
|
||||
borderRadius: '50%',
|
||||
border: '4px solid #4CAF50',
|
||||
animation: '$clickPulse 300ms ease-out',
|
||||
pointerEvents: 'none' as 'none',
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(style)(Demo)
|
||||
|
||||
@@ -92,6 +92,8 @@ export async function clickOn(
|
||||
|
||||
await moveToCenterOfElement(element)
|
||||
await element.hover()
|
||||
// Trigger click visualization
|
||||
await runJavascript('window.demo.clickMouse && window.demo.clickMouse();', element.page())
|
||||
await element.click({ delay, button, force, clickCount: clicks })
|
||||
await sleep(50)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user