Jump to content

[Tutorial] Advanced Mouse Paint and Mouse Spline Implementations


Recommended Posts

Hello everybody! We're diving into the world of mouse paints and mouse splines. We'll cover what the code is doing, the data structures it's using (hello, circular buffer!), why aiming for 50fps is crucial, and some Big O notation(O(1) to be precise). Let's get painting.

What the Code is Doing

Our tutorial involves creating and managing various mouse paints, which help us visualize mouse movements and clicks. We have different styles of mouse paints, such as MouseCursorPaint, OriginalMouseCursorPaint, and PlusSignMouseCursorPaint. These classes define how the mouse movements are drawn on the screen.

Here's a brief overview of the core components:

  1. MouseCursorPaintConfig: Configures the size, colors, and gradients used in the mouse paints.
  2. PolymorphicMousePaint: An interface for mouse paint implementations with an additional ripple effect control.
  3. MouseCursorPaint, OriginalMouseCursorPaint, PlusSignMouseCursorPaint: Implementations of PolymorphicMousePaint that define different styles of mouse paints.
  4. MouseRipple and MouseRipplePaintConfig: Handle the ripple effects when the mouse is clicked.
  5. MousePaintThread: The thread that manages the painting of mouse trails and handles the ripple effects.

Data Structures: Circular Buffer (Infinite Memory!)

A significant part of our implementation uses a circular buffer. Imagine a circular buffer as a conveyor belt with a fixed number of items allowed on it (15 in our case). You add mouse positions in order, and when you run out of space, you go back to the first point and overwrite it. This way, we always keep the latest 15 mouse positions without running out of "memory."

Circular Buffer Benefits:

  • Efficiently Manages a Fixed Number of Elements: The buffer maintains a set number of elements (like our 15 mouse positions), ensuring we use a consistent amount of memory.

  • Old Data is Automatically Discarded: As new data comes in, old data is overwritten, keeping the buffer fresh with the latest entries without any manual cleanup.

  • Constant Time Complexity, O(1): Adding, updating, and removing elements in a circular buffer is extremely efficient, as each operation takes a constant amount of time regardless of the buffer size.

  • Extremely Efficient: Circular buffers are known for their efficiency in managing fixed size datasets, making them perfect for real time applications like our mouse trail system.

Why 50 FPS?

In our mouse paint thread, we're aiming for 50 frames per second (fps). Why? Because smooth visuals matter. At 50fps, the mouse paint animations look fluid and responsive. Here's the math:

  • Tick Rate Calculation: tickRate = 1 second / fps
  • For 50 fps: tickRate = 1000ms / 50fps = 20ms

This means our thread updates every 20 milliseconds, ensuring we maintain that buttery smooth animation.

Why Delta Time (dt)?

When implementing fade effects, it’s crucial to ensure that the animation progresses smoothly and consistently across different frame rates. This is where delta time (dt) comes into play.

What is Delta Time (dt)?

Delta time (dt) represents the time elapsed between the current frame and the previous frame. By using dt, we can adjust our animations based on the actual time that has passed, rather than relying on a fixed time step.

Benefits of Using Delta Time:

  1. Frame Rate Independence:

    • Without dt: If the frame rate drops (e.g., from 50 fps to 30 fps), animations would appear slower because the fixed time step remains the same.
    • With dt: The animation speed remains consistent because it adjusts according to the time elapsed between frames.
  2. Smooth Animations:

    • Using dt ensures that fade effects (or any time based animations) proceed at the same pace regardless of frame rate fluctuations, leading to smoother transitions and a more polished user experience.

The Big O Notation: O(1)

In our circular buffer, adding, updating and removing elements have a time complexity of O(1). This means these operations take a constant amount of time, regardless of the buffer size. This efficiency is key for maintaining high performance in real time applications like our mouse point system.

Differences in Mouse Paints

  1. MouseCursorPaint:

    • Rotates an arc around the mouse position with a gradient and dynamic stroke width.
    • Can activate a ripple effect for click feedback.

      Screenshot2024-07-21221437.png.aebccae844b3ad664975ea1199620477.png
  2. OriginalMouseCursorPaint:

    • Draws simple cross lines (TRiBot classic).
    • No fancy effects, just the basics.

      Screenshot2024-07-21221238.png.ab06c3620f22030479e5f4b3e748aba1.png
  3. PlusSignMouseCursorPaint:

    • Draws a plus sign at the mouse position.
    • Focuses on clear, precise indicators.

      Screenshot2024-07-21220929.png.22f689601cf0e715d7b04669b3781866.png

The Spline Trail Magic

Now, let's talk about drawing those smooth, sleek, and sexy trails using quadratic bézier curves. This is where the real fun begins. We use the MousePaintThread class to handle everything from mouse clicks to drawing the trails.

Here’s a quick tour of the important bits:

  • Circular Buffer: Our spline trail is stored using a circular buffer (arrays xPoints, yPointscolors, head, tail)

  • Quadratic Bézier Curves: When drawing the trail, we use quadratic bézier curves. This math magic helps us create smooth sexy curves between points. Here’s how it works:

    • Points & Path: We keep track of mouse positions in arrays (xPoints, yPoints). These points form the backbone of our trail.
    • GeneralPath: This is where the magic happens. We use a GeneralPath object to draw smooth curves. For each segment of the trail, we move from one point to the next, creating a quadratic bézier curve in between.
    • Curving It Up: We call quadTo() on the GeneralPath to draw curves. It takes three points: the starting point, a control point that determines the curve’s direction, and the ending point. The result? Buttery smooth trails that look like you’re painting with the damn wind!
       
class MousePaintThread(
    private var mouseSplinePaintConfig: MouseSplinePaintConfig = MouseSplinePaintConfig(),
    private var mouseCursorPaintConfig: MouseCursorPaintConfig = MouseCursorPaintConfig(),
    private var mouseRipplePaintConfig: MouseRipplePaintConfig = MouseRipplePaintConfig(),
    private var mouseCursorPaint: PolymorphicMousePaint = MouseCursorPaint(mouseCursorPaintConfig)
) : Thread("Mouse Paint Thread"),
    Consumer<Graphics2D>,
    MouseClickListener {
    private val maxPoints = 15
    private val xPoints = IntArray(maxPoints)
    private val yPoints = IntArray(maxPoints)
    private val colors = IntArray(maxPoints) { mouseSplinePaintConfig.trailColor.rgb }
    private val trailPath = GeneralPath()

    private var pointCount = 0
    private var head = 0
    private var tail = 0
    private var lastMouseX = -1
    private var lastMouseY = -1
    private var lastUpdateTime = currentTimeMillis()
    private val ripples = synchronizedList(mutableListOf<MouseRipple>())
    private var clickCount = 0

    fun configure(
        mouseCursorPaintConfig: MouseCursorPaintConfig,
        mouseSplinePaintConfig: MouseSplinePaintConfig,
        mouseRipplePaintConfig: MouseRipplePaintConfig,
        polymorphicMousePaint: PolymorphicMousePaint
    ) {
        this.mouseCursorPaintConfig = mouseCursorPaintConfig
        this.mouseSplinePaintConfig = mouseSplinePaintConfig
        this.mouseRipplePaintConfig = mouseRipplePaintConfig
        this.mouseCursorPaint = polymorphicMousePaint
        setMousePaint(this.mouseCursorPaint)
    }

    override fun mouseClicked(
        point: Point,
        mouseButton: Int,
        isBot: Boolean
    ) {
        val color = when (clickCount) {
            0 -> mouseRipplePaintConfig.rippleColorOne
            1 -> mouseRipplePaintConfig.rippleColorTwo
            else -> mouseRipplePaintConfig.rippleColorThree
        }

        addRipple(MouseRipple(center = point, color = color))

        // Reset after 3 ripples
        clickCount = (clickCount + 1) % 3
    }

    override fun run() {
        addMouseClickListener(this)
        addPaint(this)
        setMousePaint(mouseCursorPaint)
        setMouseSplinePaint { _, _ -> }

        while (true) {
            // We want 50 fps (max limit)
            wait(20)

            val currentTime = currentTimeMillis()
            val deltaTime = currentTime - lastUpdateTime

            val mousePos = getPos()
            addPoint(mousePos.x, mousePos.y)
            fadeEffect(deltaTime)

            synchronized(ripples) {
                val iterator = ripples.iterator()
                while (iterator.hasNext()) {
                    mouseCursorPaint.isRippleActive = true
                    val ripple = iterator.next()

                    if (ripple.expanding) {
                        ripple.radius += 4
                        ripple.alpha -= 8
                        if (ripple.radius >= 50) {
                            // When the radius reaches 50, start contracting
                            ripple.expanding = false
                        }
                    } else {
                        ripple.radius -= 4
                        ripple.alpha += 8
                        if (ripple.radius <= 5 || ripple.alpha >= 255) {
                            // When the radius is too small, or alpha is back to max, remove
                            iterator.remove()
                            mouseCursorPaint.isRippleActive = false
                        }
                    }
                }
            }

            lastUpdateTime = currentTime
        }
    }

    override fun accept(g2: Graphics2D) = draw(g2)

    private fun addRipple(ripple: MouseRipple) = synchronized(ripples) {
        ripples.add(ripple)
    }

    private fun addPoint(x: Int, y: Int) {
        // Mouse hasn't moved, so don't add a new point
        if (x == lastMouseX && y == lastMouseY) return

        // Update last mouse position
        lastMouseX = x
        lastMouseY = y

        xPoints[head] = x
        yPoints[head] = y
        colors[head] = mouseSplinePaintConfig.trailColor.rgb

        head = (head + 1) % maxPoints
        if (pointCount < maxPoints) {
            pointCount++
        } else {
            tail = (tail + 1) % maxPoints
        }
    }

    private fun fadeEffect(deltaTime: Long) {
        // Ensure a minimum fade amount
        val fadeAmount = (deltaTime * 10 / 50).coerceAtLeast(1)

        for (i in 0 until pointCount) {
            val index = (tail + i) % maxPoints
            val currentAlpha = colors[index] ushr 24
            val newAlpha = (currentAlpha - fadeAmount).coerceIn(0, 255)

            colors[index] = (((newAlpha shl 24) or (colors[index] and 0x00FFFFFF).toLong()).toInt())
        }
    }

    private fun draw(g: Graphics2D) {
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)

        if (pointCount >= 3) {
            // IMPORTANT: Reset the path before iteration
            trailPath.reset()
            for (i in 0 until pointCount - 2) {
                val index = (tail + i) % maxPoints
                val nextIndex = (index + 1) % maxPoints
                trailPath.moveTo(xPoints[index].toFloat(), yPoints[index].toFloat())

                val midX = (xPoints[index] + xPoints[nextIndex]) / 2f
                val midY = (yPoints[index] + yPoints[nextIndex]) / 2f
                trailPath.quadTo(midX, midY, xPoints[nextIndex].toFloat(), yPoints[nextIndex].toFloat())

                val color = Color(colors[index], true)
                val nextColor = Color(colors[nextIndex], true)
                val gradient = GradientPaint(xPoints[index].toFloat(), yPoints[index].toFloat(), color, midX, midY, nextColor)

                g.paint = gradient
                g.draw(trailPath)
                trailPath.reset()
            }
        }

        synchronized(ripples) {
            for (ripple in ripples) {
                g.color = Color(ripple.color.red, ripple.color.green, ripple.color.blue, ripple.alpha)
                g.drawOval(
                    ripple.center.x - ripple.radius.toInt(),
                    ripple.center.y - ripple.radius.toInt(),
                    (ripple.radius * 2).toInt(),
                    (ripple.radius * 2).toInt()
                )
            }
        }
    }
}

Wrapping Up

Congratulations! You've successfully transformed simple mouse movements into visually captivating trails using circular buffers, quadratic bézier curves, and smooth animations.

Remember, this is just the beginning. Feel free to experiment with different colors, sizes, and effects to tailor the visuals to your unique style. Happy coding, and may your mouse trails always be smooth and sexy!

Github: https://github.com/its-jackson/tribot-mouse-paints

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...