slint-ui / slint

Slint is a declarative GUI toolkit to build native user interfaces for Rust, C++, or JavaScript apps.
https://slint.dev
Other
17.38k stars 590 forks source link

How to do animation on condition and hide parts of sub element outside of its parent #1327

Closed cppdev123 closed 8 months ago

cppdev123 commented 2 years ago

I'm trying to implement an ink ripple like effect for buttons. It consists of a circular rectangle (the ripple) and a mouse area to received the mouse clicks to start animations on the radius of the circle and its opacity. what I want to do: when the mouse is pressed (not clicked !) the ripple will start from radius 0 until it reaches the furthest corner of the parent (cover the whole parent) and then it disappears when opacity reaches 1 then circle returns to size 0 again and during all of this the circle should not be visible outside of its parent

what I did so far: first try: circle is initially 0 in size and when the mouse is clicked (not pressed !) animation starts from the point of mouse until it covers the parent then it disappears when opacity reaches 0 and I can't restore it to its original state to start animation again !

second try: used states with transitions to run the animation as long as the TouchArea is pressed and hide it immediately when the mouse button is released, not what I want but animation can be restarted

third try: tried to create the ripple rectangle inside if block when the touch is pressed but I can't animate here !

in all tries the circle is visible outside its parent which is not desired

Any help how to overcome these issues ?

ogoffart commented 2 years ago

What you ask doesn't seem easy to do right now...

I've come up with a hack

preview

Wow := Rectangle {
    property <float> controler: 0;
    animate controler { duration: 1s; }
    background: #33d;
    touch := TouchArea {
       pointer-event(e) => {
          if (e.kind == PointerEventKind.down) {
             controler = controler + 2;
          }
       }
    }
    circle := Rectangle {
        width: 0;
        height: width;
        opacity: 1.;
        border-radius: width/2;
        background: #0005;
        x: touch.pressed-x - width/2;
        y: touch.pressed-y - width/2;

        states [
            pressed when mod(ceil(root.controler), 2) != 0 : {
                circle.width: root.width * 2 * 1.4142;
                circle.opacity: 0.;
            }
        ]
        transitions [
            in pressed : {
                animate circle.width { duration: 1s; easing: ease; }
                animate circle.opacity { delay: 0.5s; duration: 300ms; easing: ease; }
            }
            out pressed : {
            }
        ]

    }

}

export _ := Window {
    width: 300px;
    height: 300px;
    Wow {

    }
}

We definitievly should add some feature to our animation system so such things can be easier to do.

cppdev123 commented 2 years ago

Found that clip restricts the visible part to the parent and using your hack I tried to make many circles to display one on each press if the previous is still running (a hack too) but the macro proc panicked due to unreachable code !

export struct SPoint := {
        x: float,
        y: float,
    }

    export global RadiusCalc := {
        callback calc-radius(SPoint, float, float) -> float; // mouse pos, parent pos, width, height
    }

    export RipplCircle := Rectangle {
        property <length> radius: 0;
        property <length> mx: 0;
        property <length> my: 0;
        property <float> fwidth: 0;
        property <float> fheight: 0;
        property <bool> running: false;
        width: radius * 2;
        height: radius * 2;
        x: mx - width / 2;
        y: my - height / 2;
        background: rgba(0,0,0, 0.5);
        border-radius: width / 2;

        property <float> counter: 0;
        animate counter { duration: 800ms; }

        states [
            ripple when counter > 0 && counter < 0.1 : {
                radius: RadiusCalc.calc_radius({ x: mx / 1px, y: my / 1px }, fwidth, fheight) * 1px;
                opacity: 0.;
                running: true;
            }
        ]

        transitions [
            in ripple: {
                animate radius { duration: 800ms; }
                animate opacity { duration: 800ms; }
            }
        ]
    }

    export InkEffect := Rectangle {

        preferred-width: 200px;
        preferred-height: 200px;
        background: white;
        clip: true;

        circle1 := RipplCircle {}
        // un commenting the next line causes macro proc panick
        //circle2 := RipplCircle { background: green; }

        tch := TouchArea {
            width: parent.width;
            height: parent.height;
            pointer-event(ev) => {
                if (ev.kind == PointerEventKind.down && ev.button == PointerEventButton.left) {
                    if (!circle1.running) {
                        circle1.mx = mouse-x;
                        circle1.my = mouse-y;
                        circle1.fwidth = parent.width / 1px;
                        circle1.fheight = parent.height / 1px;
                        circle1.counter = circle1.counter == 0 ? 0.1 : 0;
                    }
                }
            }

        }       
    }

What is needed is a method to create objects dynamically and destroy them on animation end as qml does

ogoffart commented 8 months ago

I think sequence of state would solve this issue: https://github.com/slint-ui/slint/issues/2255 So closing as a duplicate