Conversation
|
This looks very impressive 👀 |
|
Thank you - and it is the right question to ask, because on screen the two really do look The way I think about it: the indicator answers while, an effect answers when. The I did start from the other end, by trying to give the indicator more properties, and the What the two genuinely share is the drawing vocabulary - a polygon with an edge count, a There is one place where you can honestly do the same thing twice: an effect with If you want the overlap gone rather than documented, the honest answer is to make the And if, having read that, you still see this as overlap you do not want in the project, |
I think that could make sense. And if we did that maybe effects would just be called "indicator that can be animated (with key frames) if needed". I'm currently looking at the demo properties file you shared and then I'll also look at the code. I will just share my comments as I progress.
I'll stop here for now, I think my comment is long enough 😊 |
…e mouse A mode can define named effects - stacks of simple shape layers (dot, circle, square, triangle, line, cross) animated over one cycle by keyframes (size, rotation, opacity, position, color, show/hide) in an area centered on the mouse. start-effect.<name> and stop-effect.<name> combos start and stop them, so an effect can play on a click, loop exactly while a key is held, or flash on a release. EffectManager resolves each frame (testable without a UI); the Qt EffectRenderer only draws.
rotation-x / rotation-y tilt a layer with Qt's 3D-projected axis rotation (the sign picks the spin direction, so 0 -> -360 flips backward); layer<n>-speed runs one layer's timeline faster or slower than the effect's cycle (2 = twice per cycle); and an easing= token on a keyframe shapes the acceleration of the segment that ends at it. Keyframe sizes may now reach 0, since shrinking a layer to nothing is a legitimate way to end it.
…ebug logs The bare-QWidget effect window drew nothing on a real Windows 11 machine even though the start-effect combo fired (no error logged). The indicator and hint meshes render through a TransparentWindow with a child widget, so the effects now use the same proven pattern, positioned with moveAndResizeInPixels (which also fixes macOS point/pixel handling). DEBUG logs on the first frames and on hide record the window geometry, visibility and paint count, so a machine where it still fails can report where the pipeline stops.
On a real Windows 11 machine, the first effect frame froze the main loop hard enough that the console control handler had to kill the process. Creating the effect window lazily on that first frame dispatches native messages mid-loop, which can re-enter the low-level keyboard hook; the indicator avoids exactly this by creating its window at pre-warm, so the effect window now does the same (lazy creation remains as a fallback for configs loaded without a pre-warm). More step-level debug logs pin down the first frame's progress.
…uilt The native build printed 'vtable setup failed: EffectRenderer.EffectWidget::paintEvent' at startup: QtJambi binds overridden Qt virtuals through JNI, and a QWidget subclass missing from reflect-config.json / jni-config.json silently loses its paintEvent in the GraalVM native image - the effect window existed but never painted. Register EffectWidget the same way as GridWidget and the other widget subclasses.
Mode property mutation rebuilds the Mode record reflectively (ModePropertyMutator.createWithField), so the record's new effects component needs its accessor and the new canonical constructor registered in the native-image configs. Without them, any config using mutation branches (e.g. state-dependent indicator colors) crashed with MissingReflectionRegistrationError on the first virtual-key refresh.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…olygons, delay, repeat, color mixing Every animatable layer value now comes from the EffectProperty table (key, kind, default, range), which the parser, the keyframe resolver and the docs test all read: adding a property is one enum line plus its use in the renderer. New capabilities: rotation about a pivot (orbits), scale, animated thickness, corner-radius, polygon (indicator's edge-count convention), arc (indicator's fill-angle convention, pie when filled), per-layer delay, repeat counts and alternate direction, follow-mouse=false anchoring (EffectManager listens to mouse moves), and colors mixed in OkLab through the hint gradient code. The effect window is the union of the running effects' areas. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…h-offset dash sets the dash and gap lengths in logical pixels (solid by default) for outlines, lines, crosses and arcs; dash-offset moves the pattern along the outline, so animating it over one pattern period makes the dashes travel (a marquee). Both come from the EffectProperty table like every other value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A keyframe position may be a time (150ms) as well as a percent of the cycle, and the two can be mixed. Times are converted once the effect's duration is known (duration-millis may be written after the keyframes), which is also where the increasing order is now checked; a time beyond the duration is an error. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d and outline shape=text draws layer<n>-text in font-name / font-weight / font-italic, aligned on the layer's x by text-align, through the hint font machinery (same family fallback and antialiasing as hints and the indicator label). Its animatable values come from the table: font-size, color, opacity, a background-color box (rounded by corner-radius, grown by padding) and an outline-color drawn thickness wide. Text settings on a non-text layer, and a text layer without text, are configuration errors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
arc-sweep becomes arc-length (the arc's length in degrees, no jargon); a text outline's width is outline-thickness, pairing with outline-color like the indicator's; square becomes rect, since size=WxH already made it one (square is still accepted). dot, circle and triangle stay as aliases of polygon / rect shorthands and are documented as such, so the shape list reads as six shapes plus three shorthands rather than nine unrelated names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and the expected form Effect values were parsed with the raw Double/Integer/Boolean parsers, so a typo surfaced as "For input string: abc" and "yes" silently meant false. The effect section now parses through helpers that say which key, which value, and what was expected (the range, true or false, a whole number of milliseconds, the #RRGGBB form, the easing names); compound values (size, area, pivot, dash) describe their own form; and a bad keyframe token adds which keyframe of which layer it was in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t example Every EffectProperty carries a one-line meaning and an example, and the effect and layer settings have the same in the parser, so a message reads "Invalid edge-count value 1001: edge-count is the number of sides of a polygon: 3 triangle, 4 square, 6 hexagon, 100 or more looks like a circle; expected a number between 3 and 1000, for example layer1-edge-count=6" -- enough to fix the line without opening the reference. Structural errors (no shape, gaps in layer numbers, a text layer without text, an undefined effect name, keyframes out of order or past the duration) say what to add or change rather than what rule was broken. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ine's boundaries A keyframe with a position and nothing after it, and a trailing | (which String.split used to drop silently), are now errors that say what a keyframe needs. Tests cover every range's exact edges and one step outside, and the engine at its seams: a keyframe at 0% over the base, a single keyframe (base, interpolation, hold), the timeline end at speed 2, alternate at the cycle boundary, delay equal to the elapsed time, duration-millis=0, and a one-shot ending on its final value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…parsing EffectManagerTest drives the manager against a stub overlay: restarting an effect restarts its cycle and re-anchors it, the overlay is hidden exactly once when the last effect ends and not touched while idle, a mode change stops loops but lets one-shots finish, stopping a stopped effect and starting an unknown name are harmless, and one long step resolves the same frame as many short ones. EffectRecipesDocsTest parses every effect example in the configuration reference, so the docs cannot drift from the parser (the click-ripple recipe now declares the leftbutton alias it uses). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ModeListeners hear about mutations of the current mode (a property branch
such as _{isleftmousepressing} -> ... flipping) as well as mode switches.
EffectManager treated both as a switch and stopped every loop, so in a
configuration with mutation branches a loop started by a wheel key died on
the very press that started it. Loops are now stopped only when the mode
name changes; the mutated mode still replaces the current one so start-effect
sees its definitions. Found by running effects against the real configuration.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…one effect take the loop down The engine side is cheap (10 effects x 4 layers x 4 keyframes: ~90us per tick), so the pass goes after what the overlay is asked to do: setEffects is skipped when the resolved frames equal the last ones and no following effect saw the mouse move, so a hold keyframe, a delay or a hidden layer costs no repaint. A tick never advances an effect by more than 100ms: after a main-loop stall a one-shot is still on screen instead of gone and a loop slows instead of jumping. A player that throws is dropped with an error and the paint loop skips a layer it cannot draw (logged once), so an effect can fail without stopping mousemaster. Text fonts are cached per family, size, weight and style (bounded, since an animated size makes many), and each layer resolves only the properties its keyframes mention. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The section now opens with the three things that describe an effect (when it plays, what it draws, how it moves), builds a first effect line by line, adds loops and layers, and explains keyframes with a worked example read back in words before the reference tables, which are grouped by purpose (where and how big, rotation, look, shape-specific, text). Nine recipes each name the trick they use, the rules are collected in one place, and a "nothing appears?" checklist closes it. Every example block is parsed by EffectRecipesDocsTest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Like indicator.enabled: enabled=false keeps the definition (its combos still load and its lines stay for later) but start-effect does nothing, which is what one wants while trying effects out or tracking one down. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A Windows checkout gives docs/configuration-reference.md CRLF line endings, and the recipe test's pattern expected a bare newline after the code fence, so it found no recipes at all on the Windows CI job. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n the native image QtJambi analyzes a QWidget subclass reflectively when it is first instantiated (fields and methods, to build its meta-object). In the GraalVM native image that analysis hung at "new EffectWidget" once the widget carried the frame state (a List<double[]> field and an anonymous map subclass): the process sat in the pre-warm with hooks never installed. The widget is now an inner class with nothing but paintEvent, like the indicator's, and the frames, centers, font cache and paint counter live in the enclosing renderer, which Qt never looks at. Found with a stderr trace after a bisect showed the base was innocent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ursor The system draws the cursor, indicator included, above every window, so the "nothing appears?" list now says to leave the cursor clear: rings wider than the indicator, or shapes offset from it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…efore starting it A selection key that both picks a hint and switches mode runs its start-effect after the switch: the commands wait for the hint's mouse move while the SwitchMode runs at once. The effect was then looked up in the new mode and, not found there, ignored with a warning. EffectManager now remembers the mode before the last switch and falls back to it, so the combo's own effect plays, where the mouse landed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…w that stops growing Two things made an effect trail the cursor. The effect window's rectangle was the union of every area seen while showing, so a following effect dragged an ever-growing window behind it, resized and repainted whole every frame. The window now keeps its largest size (like the indicator's) and is placed over the current frames. And the window was only moved once per main-loop iteration, with the tick's frames: the platform now moves a following effect as soon as the mouse moves, inside its sleep, the way it repositions the indicator window, and only repaints when an anchored frame shares the window. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A text layer's text may contain {key}, replaced by the key that completed the
start-effect combo, and {keys}, the keys that started or restarted the effect
while it was running: restarting keeps the history, so a burst of typing
reads as one line beside the mouse. The combo's key was already handed to
every command; StartEffect now passes it on. Recipe and tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ove}/{moves} placeholders
A label beside the mouse was cut at a screen edge: a text layer is now moved
inwards by what would stick out (keep-on-screen, on by default; shapes are
left alone, a ripple at the edge is cut like anything drawn there), and the
effect window grows to cover it. A text that varies in length (a keycast)
wraps at its spaces past max-width. {move} and {moves} are {key} and {keys}
with the combo's + or -, so a press and its release both show; the command
runner is handed the key event rather than the key alone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tests that stub the command runner override run(Command, Key); routing the combo watcher through a new run(Command, KeyEvent) bypassed them. The key event overload now records whether the event is a press and delegates to run(Command, Key), which StartEffect reads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The area clip set per frame cut a text layer that keep-on-screen had moved outside the area at a screen edge; the clip is lifted for that layer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A filled star is what a small "done" or "placed" mark wants to be, and two crossed lines only approximate it. Drawn like the odd polygons (a point at the top), with the inner vertices at 0.4 of the radius. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
layer<n>-points lists the corners in pixels from the layer's center, so a diamond, a parallelogram, an arrow or a check mark are one line each; scale, rotation, filled, thickness and the colors apply as for any shape, the size is not used. A recipe shows a check mark over a spinning diamond. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pture A camera effect on the screenshot key (started with #printscreen, so the key still reaches Windows) must not end up in the screenshot. While an effect with exclude-from-capture=true runs, the effect window gets WDA_EXCLUDEFROMCAPTURE, as the zoom window always has; otherwise effects are captured like anything on screen, so a recording of them still works. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Effects were centered on the mouse position, the arrow's tip at its top left, while the indicator sits on the cursor's visual center some pixels down and right: an effect above the cursor looked farther than the same one below it, and one drawn just under the tip overlapped the indicator. The Windows overlay now hands the renderer the same offset the indicator uses, applied to following and anchored effects alike. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every frame of an animation re-placed the native window even when nothing had moved, and a SetWindowPos under the mouse makes Windows re-pick the cursor: over a window edge the resize cursor flickered against the arrow for as long as the effect played. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Skipping the window move was not enough: a Qt widget flush hands the window's position to UpdateLayeredWindow, which Windows treats as a move and re-picks the cursor for, so an animation over a window edge still flickered between the resize cursor and the arrow every frame. The Windows overlay now owns a plain layered popup (WindowsEffectWindow) and the renderer paints each frame into a QImage handed to it; the window is updated with UpdateLayeredWindow and no destination point, and only moved when the effects move. macOS keeps the Qt window. The capture exclusion of the effect window combines the zoom's rule with the effects' own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
QtJambi's QImage has bits(), not constBits(); the rows are copied one by one since Qt pads scanlines to four bytes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
That SetWindowPos(HWND_TOPMOST), issued every 200ms while nothing else animates, made DWM recompose the layered effect window (a frame without it) and Windows re-pick the cursor under it: over a window edge the resize cursor and the arrow alternated five times a second for as long as an effect played. The window is created topmost and, like the zoom window during a zoom, is not re-ordered while it shows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The offset from the mouse position to the cursor's visual center depends on the cursor shown. Read every frame over a window edge, it fed a loop: the resize cursor gave another offset, the effect window moved, Windows re-picked the cursor under it, the arrow came back with its own offset, the window moved again. The cursor flickered and the effect jittered for as long as the effect played. The offset is now read when the effects appear and kept until they are gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thank you for reading it so closely. The stray filesThey were mine: NamingI had not connected the # today
normal-mode.effect.click-ripple.duration-millis=250
normal-mode.effect.click-ripple.layer1-shape=circle
normal-mode.effect.click-ripple.layer1-keyframes=0 size=10 opacity=0.8 | 100 size=36 opacity=0
normal-mode.start-effect.click-ripple=-leftbutton
# proposed
normal-mode.click-ripple-effect.duration-millis=250
normal-mode.click-ripple-effect.layer1-shape=circle
normal-mode.click-ripple-effect.layer1-size=0% 10; 100% 36
normal-mode.click-ripple-effect.layer1-opacity=0% 0.8; 100% 0
normal-mode.click-ripple-effect.start=-leftbuttonThree levels everywhere, and I would keep the definition inside a mode rather than making it global like Your points 1, 5 and 6 are the same problemMutations are not missing by design. They are missing because
A bonus: size and opacity no longer have to share keyframe positions. The separator is the one thing I am unsure about. Your Nothing here breaks an existing configurationEffects have never been in a release, so renaming every effect property costs no one The merge is the opposite: every GradientsI do not use any of it yet - effect colors are plain hex. Having read the reference, I And if the indicator and effects end up on one path, effect colors go through NextThe naming and the timeline change are a parser, docs, tests and demo-config rewrite, and I No hurry - keep the comments coming as you read, and I will fold them all in at once. |
|
I think I need some time to think about this, I wouldn't want you to make changes and then change our mind later. I like this: 0% 10; 60% 30 smootherstep; 100% 36Like you, I'm not sure about the syntax but I like the idea. To be honest, I would like to leverage this work to replace my current indicator animation that relies on virtual keys. Virtual keys seem like a bad substitute for keyframes (too complex). I think that the keyframe approach would simplify it, and also make it more flexible. I don't like some of the properties that were recently introduced, like _indicator-mode.indicator.transition-animation-switch-at=startI also don't like indicator.inner-outline and outer-outline, which were introduced because I wanted to have 2 outlines. Now, I think having 2 outlines would just be implemented as 2 layers; and we can have a third outline if we want to. So this is what I'm wondering: is there anything missing in the current effect implementation that would make a migration of this animation not possible? Would it be possible to add the missing parts? 2026-09-13-Ql8Y3RWDij.mp4Off the top of my head, the things that could be challenging are:
For reference, here is a cleaned-up Claude description of the animation:
Resting: 26 px disk, fill 0.2, 0.5 px ring, Timeline
Three virtual keys drive it: The outline draws outward from the fill radius, so The expansion is one continuous decay — every 50 ms the remaining distance halves — running the InterruptsIf a button is still down at the 250 ms mark, or the mouse is moving then, Another click restarts from t=0. Over hints
|




What
Combo-driven effects: short, named animations drawn around the mouse, started and stopped by combos (
start-effect.<name>/stop-effect.<name>). An effect is a stack of simple shape layers (rect, polygon, star, line, cross, arc, text, apathof your own points, plus the aliases dot, circle, triangle) animated over one cycle with keyframes, in an area that follows the mouse or stays where the effect started.effect-demo.mp4
The idea was raised with the ripple-on-click use case; the indicator cannot do it (it is one persistent shape), and a mode's
to+ timeout chain is a heavy way to fake an animation.Design
The model fits in one sentence: an effect is layers of shapes; every layer value is written once, and any number or color among them can be animated with keyframes. To keep the surface small and easy to extend:
EffectPropertyis a single table (key, kind, default, range) that drives parsing, defaults, keyframe interpolation and the docs. Adding an animatable property is one enum line plus one use in the renderer;EffectPropertyDocsTestfails until the key is documented.EffectManagerturns time into fully-resolvedEffectFrames (no Qt, unit-testable);EffectRendereris a dumb draw loop over resolved numbers.TransparentWindow+ child widget pattern (on macOS; on Windows the overlay draws the same frames into a layered window of its own, updated in place withUpdateLayeredWindowand no destination position, because a Qt widget flush passes the position along and Windows re-picks the cursor on every frame: an animation over a window edge flickered between the resize cursor and the arrow), itspolygonPath(same edge-count convention), its fill-angle convention for arcs (0 = 12 o'clock, clockwise), hint gradients' OkLab color mixing (HintGradientColor.mix), andEasing(same values aszoom.animation-easing).Effect settings
duration-millis,repeat(once/loop/ a count),direction(forward/alternate),easing,area(48or64x32),follow-mouse,enabled(likeindicator.enabled: switch an effect off without removing it),exclude-from-capture(keeps the effect out of screenshots and recordings while it runs, the way the zoom window is: for a camera effect on the screenshot key itself, started with_{leftwin} #printscreenso the key still reaches Windows).Layer settings (hold for the layer's life)
shape,filled,points(the corners of apathlayer: a diamond, a parallelogram, an arrow, a check mark are one line each),speed(layer timeline relative to the cycle),delay(staggers layers: a sonar is three delayed rings with the same keyframes); fortextlayers,text,font-name,font-weight,font-italic,text-align. Intext,{key}is the key that completed thestart-effectcombo and{keys}the keys that (re)started the effect while it showed ({move}/{moves}with the combo's+or-): a keycast, to check what you typed or to show the keys in a recording (the combo's key event was already handed to every command;StartEffectnow carries it).max-widthwraps a text at its spaces;keep-on-screen(default) moves a text inwards at a screen edge so a label stays readable there.Layer values (each settable once and animatable by keyframes)
x,y,size(orarea),scale,rotationabout apivot(a pivot at the area center makes an orbit),rotation-x/rotation-y(3D-projected tilt and card flips),color(mixed in OkLab),opacity,thickness,dash(<on>,<off>lengths) with an animatabledash-offset(a travelling marquee),corner-radius,edge-count(polygon),arc-start/arc-length(arc, pie when filled),font-size/background-color/outline-color/outline-thickness/padding(text), plusshow/hideand a per-segmenteasing=. Keyframe positions are a percent of the cycle or a time (150ms), mixable.Behavior
stop-effectcombo may not exist in the new mode). A mutation of the current mode is not a mode change: loops keep running through it.|separator belongs to keyframes).true/false,#RRGGBB, the easing names), and for a keyframe token, which keyframe of which layer it was in; nothing surfaces as a NumberFormatException.Files
EffectProperty,EffectConfiguration,EffectLayer,EffectKeyframe,EffectShape,EffectText,EffectFrame,EffectManager,renderer/EffectRenderer,platform/windows/WindowsEffectWindow.ConfigurationParser(effect block),Mode(effects map),Overlay(setEffects/hideEffects),Mousemaster(wiring, mouse position),HintGradientColor(mixextracted),IndicatorRenderer(IndicatorWidgetpackage-private forpolygonPath), native-image configs.EffectTest(parsing, interpolation, loop wrap, speed, easing, hide, axis rotations, dashes, ms keyframes, text layers, error messages, range edges),EffectManagerTest(restart re-anchors, hide once, mode change vs loops/one-shots, resolution depends only on elapsed time),EffectPropertyDocsTest(every property documented),EffectRecipesDocsTest(every recipe in the reference parses).configuration-reference.md, written as a walk-through (first effect line by line, loops, layers, how keyframes work) followed by reference tables grouped by purpose, eleven recipes each naming its trick (a custom-shape one and a keycast among them), the rules, and a "nothing appears?" checklist. Every recipe is parsed byEffectRecipesDocsTest.Not in this PR
Testing
effects-camera-record.mp4
mousemaster-demo.properties, run with--configuration-file), where every effect answers one real event: F2 switches idle/normal with an "IDLE > NORMAL" / "NORMAL > IDLE" tag, space clicks with an anchored ripple, m / u wheel with arrows on a track, d drags with a crane claw seen from the side (pathfingers,starscatter on the release; the claw comes down on a 250ms hold, or closes on the spot when a move key is pressed with d held, and a plain click shows nothing), Win+Shift+S / Win+PrtScn shows a camera that stays out of the screenshot (exclude-from-capture), and F3 enters a record-mode that also shows every key pressed and released ({moves}). Five one-trick samples stay on F7-F11 in idle-mode. Together they use every property.mousemaster-demo.properties.txt
(The recording GIF was made with the camera's
exclude-from-captureswitched off, since a screen recorder is a capture too and would not see it otherwise.)mvnw clean package(JUnit) green on the fork's nightly workflow, on Windows and on both macOS jobs.MacosOverlay.setEffects, 29 lines that mirror the indicator's window setup) is untested on a real machine. Nothing happens on macOS unless a config usesstart-effect.