KivyMD icon indicating copy to clipboard operation
KivyMD copied to clipboard

✓ KivyMD. Development - Bounty program

Open HeaTTheatR opened this issue 2 years ago • 7 comments

Hello everyone who helps the KivyMD library to develop!

If you have the desire and free time to fix bugs and introduce new functions to the KivyMD library, then you can do it for a monetary reward.

  • [ ] Solve the problem with scrolling widgets with the behavior of Elevation - $50

As you already know, the new shader-based implementation for Elevation is almost finished.

shadow-radius

It's working now. But I still can't solve the problem when widgets with Elevation behavior are in ScrollView layout.

ezgif-5-51ac04a05a

Obviously, the canvas position is incorrectly calculated in the shader code:

float roundedBoxSDF(vec2 centerPosition, vec2 size, vec4 radius) {
    radius.xy = (centerPosition.x > 0.0) ? radius.xy : radius.zw;
    radius.x = (centerPosition.y > 0.0) ? radius.x : radius.y;

    vec2 q = abs(centerPosition) - (size - shadow_softness) + radius.x;
    return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius.x;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    // Smooth the result (free antialiasing).
    float smoothedAlpha = 1.0 - smoothstep(0.0, 0.0, 1.0);
    // Get the resultant shape.
    vec4 quadColor = mix(
        vec4(shadow_color[0], shadow_color[1], shadow_color[2], 0.0),
        shadow_color,
        smoothedAlpha
    );
    // Apply a drop shadow effect.
    float shadowDistance = roundedBoxSDF(
        fragCoord.xy - mouse.xy - (size / 2.0), size / 2.0, shadow_radius
    );
    float shadowAlpha = 1.0 - smoothstep(
        -shadow_softness, shadow_softness, shadowDistance
    );
    fragColor = mix(quadColor, shadow_color, shadowAlpha - smoothedAlpha);
}

The whole problem lies in the calculation of the centerPosition variable for roundedBoxSDF method:

float roundedBoxSDF(vec2 centerPosition, vec2 size, vec4 radius) {
    [...]
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    [...]
    float shadowDistance = roundedBoxSDF(
        fragCoord.xy - mouse.xy - (size / 2.0), // Evil lurks here :)
        size / 2.0,
        shadow_radius
    );
    [...]
}

If we simplify the shader code to a minimum:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    fragColor = vec4(0.0, 1.0, 1.0, 1.0);
}

Then we will see that the canvas position is calculated correctly:

ezgif-1-d776ebc6e8

If you want to help solve this problem, write a comment in this issue.

I attachment the archive with the minimal example and the Elevation module in the archive attached to this issue.

ElevationBug.zip

HeaTTheatR avatar Jun 01 '22 17:06 HeaTTheatR

I want to help in solving this issue. I'll try if I can get it to work.

AM-ash-OR-AM-I avatar Jun 17 '22 15:06 AM-ash-OR-AM-I

@AM-ash-OR-AM-I OK! The whole problem lies in the values of the arguments for the roundedBoxSDF function:

    float shadowDistance = roundedBoxSDF(
        (fragCoord.xy - pos) - (size / 2.0), size / 2.0, shadow_radius
    );

HeaTTheatR avatar Jun 17 '22 16:06 HeaTTheatR

@HeaTTheatR Well I've not solved issue yet, rather I found this to be not working either, So basically I couldn't get minimum shader to work either.

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    fragColor = vec4(0.0, 1.0, 1.0, 1.0);
}

python_pZ0JpLonxI

AM-ash-OR-AM-I avatar Jun 17 '22 16:06 AM-ash-OR-AM-I

@AM-ash-OR-AM-I Work:

        with self.canvas.before:
            self.context = RenderContext(
                use_parent_projection=True,
                use_parent_modelview=True,
            )

https://user-images.githubusercontent.com/16930280/174342902-b0b50b35-4c9c-41ae-bba3-077d821e77eb.mov

HeaTTheatR avatar Jun 17 '22 16:06 HeaTTheatR

with self.canvas.before:
            self.context = RenderContext(
                use_parent_projection=True,
                use_parent_modelview=True,
            )

@HeaTTheatR After doing this same issue persists. Can't get it to move with canvas. (Using Windows 11)

AM-ash-OR-AM-I avatar Jun 17 '22 16:06 AM-ash-OR-AM-I

@AM-ash-OR-AM-I


import os

from kivy.clock import Clock
from kivy.graphics import RenderContext, RoundedRectangle
from kivy.properties import (
    BoundedNumericProperty,
    ColorProperty,
    ListProperty,
    NumericProperty,
    VariableListProperty,
)
from kivy.uix.widget import Widget


class CommonElevationBehavior(Widget):
    """Common base class for rectangular and circular elevation behavior."""

    elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
    shadow_radius = VariableListProperty([16], length=4)
    shadow_softness = NumericProperty(12)
    shadow_offset = ListProperty((0, 0))
    shadow_color = ColorProperty([0.4, 0.4, 0.4, 0.8])

    _elevation = 0
    _shadow_color = [0.0, 0.0, 0.0, 0.0]

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        with self.canvas.before:
            self.context = RenderContext(
                use_parent_projection=True,
                use_parent_modelview=True,
            )
        with self.context:
            self.rect = RoundedRectangle(pos=self.pos, size=self.size)

        Clock.schedule_once(self.set_shader_string, 0)

    def get_shader_string(self) -> str:
        shader_string = ""
        for name_file in ["header.frag", "elevation.frag", "main.frag"]:
            with open(
                os.path.join(os.path.dirname(__file__), "data", "glsl", "elevation", name_file),
                encoding="utf-8",
            ) as file:
                shader_string += f"{file.read()}\n\n"

        return shader_string

    def set_shader_string(self, *args) -> None:
        self.context["shadow_radius"] = list(map(float, self.shadow_radius))
        self.context["shadow_softness"] = float(self.shadow_softness)
        self.context["shadow_color"] = list(map(float, self.shadow_color))
        self.context["pos"] = list(map(float, self.rect.pos))
        self.context.shader.fs = self.get_shader_string()

    def update_resolution(self) -> None:
        self.context["resolution"] = (*self.rect.size, *self.rect.pos)

    def on_shadow_color(self, instance, value) -> None:
        self._shadow_color = list(map(float, value))
        self.context["shadow_color"] = self._shadow_color

    def on_shadow_radius(self, instance, value) -> None:
        self.context["shadow_radius"] = list(map(float, value))

    def on_shadow_softness(self, instance, value) -> None:
        self.context["shadow_softness"] = float(value)

    def on_shadow_offset(self, instance, value) -> None:
        self.on_size()
        self.on_pos()

    def on_elevation(self, instance, value) -> None:
        if hasattr(self, "context"):
            self._elevation = value
            self.hide_elevation(True if value <= 0 else False)

    def on_pos(self, *args) -> None:
        if not hasattr(self, "rect"):
            return

        self.rect.pos = [
            self.pos[0]
            - ((self.rect.size[0] - self.width) / 2)
            - self.shadow_offset[0],
            self.pos[1]
            - ((self.rect.size[1] - self.height) / 2)
            - self.shadow_offset[1],
        ]

        self.context["mouse"] = [self.rect.pos[0], 0.0, 0.0, 0.0]
        self.context["pos"] = list(map(float, self.rect.pos))
        self.update_resolution()

    def on_size(self, *args) -> None:
        if not hasattr(self, "rect"):
            return

        self.rect.size = (
            self.size[0] + (self._elevation * self.shadow_softness / 2),
            self.size[1] + (self._elevation * self.shadow_softness / 2),
        )
        self.context["mouse"] = [self.rect.pos[0], 0.0, 0.0, 0.0]
        self.context["size"] = list(map(float, self.rect.size))
        self.update_resolution()

    def on_radius(self, instance, value) -> None:
        self.shadow_radius = [value[1], value[2], value[0], value[3]]

    def on_disabled(self, instance, value):
        self.hide_elevation(value)

        try:
            super().on_disabled(instance, value)
        except Exception:
            pass

    def hide_elevation(self, hide: bool):
        if hide:
            self._elevation = -self.elevation
            self._shadow_color = [0.0, 0.0, 0.0, 0.0]
        else:
            self._elevation = self.elevation
            self._shadow_color = self.shadow_color

        self.on_shadow_color(self, self._shadow_color)
        self.on_size()
        self.on_pos()

HeaTTheatR avatar Jun 17 '22 17:06 HeaTTheatR

@HeaTTheatR got minimal code working now!

AM-ash-OR-AM-I avatar Jun 17 '22 17:06 AM-ash-OR-AM-I

Hi, I've been trying to change the shader code for a few days now and got no luck. But I did found a workaround by turning off use_parent_modelview and creating a window_position variable that is binded to Window.on_draw method, then when it changes values the on_pos function is called and the shadow position is updated.

I made this approach because I've noticed on_pos was never beeing called by widgets inside a relative layout, since its position inside the layout didn't change. Even though I thought it should not be so important when "use_parent_modelview" was enabled, because it supposedly would use the window matrix for positioning (I guess), but apparently it was using a wrong matrix.

In the case that's not a good solution, I hope that at least it helps to find a better one.

Here's the code and a video.

import os

from kivy.clock import Clock
from kivy.graphics import RenderContext, RoundedRectangle
from kivy.properties import (
    BoundedNumericProperty,
    ColorProperty,
    ListProperty,
    NumericProperty,
    VariableListProperty,
    BooleanProperty,
    AliasProperty
)
from kivy.uix.widget import Widget
from kivy.core.window import Window

class CommonElevationBehavior(Widget):
    """Common base class for rectangular and circular elevation behavior."""

    elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
    shadow_radius = VariableListProperty([16], length=4)
    shadow_softness = NumericProperty(12)
    shadow_offset = ListProperty([0, 0])
    shadow_color = ColorProperty([0.4, 0.4, 0.4, 0.8])

    def _get_window_pos(self,*args):
        window_pos = self.to_window(*self.pos)
        # to list so it can be compared to self.pos directly
        return [window_pos[0],window_pos[1]]

    def _set_window_pos(self,value):
        self.window_pos = value

    window_pos = AliasProperty(_get_window_pos,_set_window_pos)

    old_window_pos = ListProperty([0, 0])

    has_relative_position = BooleanProperty(defaultvalue=False)

    _elevation = 0
    _shadow_color = [0.0, 0.0, 0.0, 0.0]

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        with self.canvas.before:
            self.context = RenderContext(
                use_parent_projection=True,
                #use_parent_modelview=True,
            )
        with self.context:
            self.rect = RoundedRectangle(pos=self.pos, size=self.size)

        Clock.schedule_once(self.build, 0)

    def build(self,*args):
        Clock.schedule_once(self.check_for_relative_pos, 0)
        Clock.schedule_once(self.set_shader_string, 0)
        self.on_pos()

    def check_for_relative_pos(self,*args) -> None:
        """
        Checks if the widget has relative position properties and if necessary binds Window.on_draw
        call to update window position
        """

        if self.pos == self.window_pos:
            return
        else:
            self.has_relative_position = True

        # loops until parent is the window and binds to window on_draw method
        parent = self.parent
        while not isinstance(parent, type(Window)):
            parent = parent.parent

        parent.bind(on_draw=self.update_window_position)
        return

    def get_shader_string(self) -> str:
        shader_string = ""
        for name_file in ["header.frag", "elevation.frag", "main.frag"]:
            with open(
                os.path.join(os.path.dirname(__file__), "ElevationBug/data", "glsl", "elevation", name_file),
                encoding="utf-8",
            ) as file:
                shader_string += f"{file.read()}\n\n"

        return shader_string

    def set_shader_string(self, *args) -> None:
        self.context["shadow_radius"] = list(map(float, self.shadow_radius))
        self.context["shadow_softness"] = float(self.shadow_softness)
        self.context["shadow_color"] = list(map(float, self.shadow_color))
        self.context["pos"] = list(map(float, self.rect.pos))
        self.context.shader.fs = self.get_shader_string()

    def update_resolution(self) -> None:
        self.context["resolution"] = (*self.rect.size, *self.rect.pos)

    def on_shadow_color(self, instance, value) -> None:
        self._shadow_color = list(map(float, value))
        self.context["shadow_color"] = self._shadow_color

    def on_shadow_radius(self, instance, value) -> None:
        self.context["shadow_radius"] = list(map(float, value))

    def on_shadow_softness(self, instance, value) -> None:
        self.context["shadow_softness"] = float(value)

    def on_shadow_offset(self, instance, value) -> None:
        self.on_size()
        self.on_pos()

    def on_elevation(self, instance, value) -> None:
        if hasattr(self, "context"):
            self._elevation = value
            self.hide_elevation(True if value <= 0 else False)

    def update_window_position(self,*args):
        """This function is used only when the widget has relative position properties"""

        if self.old_window_pos == self.window_pos:
            return
        else:
            self.old_window_pos = self.window_pos
            self.on_pos()

    def on_pos(self, *args) -> None:
        if not hasattr(self, "rect"):
            return

        if self.has_relative_position:
            pos = self.window_pos
        else:
            pos = self.pos

        self.rect.pos = [
            pos[0]
            - ((self.rect.size[0] - self.width) / 2)
            - self.shadow_offset[0],
            pos[1]
            - ((self.rect.size[1] - self.height) / 2)
            - self.shadow_offset[1],
        ]

        self.context["mouse"] = [self.rect.pos[0], 0.0, 0.0, 0.0]
        self.context["pos"] = list(map(float, self.rect.pos))
        self.update_resolution()

    def on_size(self, *args) -> None:
        if not hasattr(self, "rect"):
            return

        self.rect.size = (
            self.size[0] + (self._elevation * self.shadow_softness / 2),
            self.size[1] + (self._elevation * self.shadow_softness / 2),
        )
        self.context["mouse"] = [self.rect.pos[0], 0.0, 0.0, 0.0]
        self.context["size"] = list(map(float, self.rect.size))
        self.update_resolution()

    def on_radius(self, instance, value) -> None:
        self.shadow_radius = [value[1], value[2], value[0], value[3]]

    def on_disabled(self, instance, value):
        self.hide_elevation(value)

        try:
            super().on_disabled(instance, value)
        except Exception:
            pass

    def hide_elevation(self, hide: bool):
        if hide:
            self._elevation = -self.elevation
            self._shadow_color = [0.0, 0.0, 0.0, 0.0]
        else:
            self._elevation = self.elevation
            self._shadow_color = self.shadow_color

        self.on_shadow_color(self, self._shadow_color)
        self.on_size()
        self.on_pos()

https://user-images.githubusercontent.com/111649971/186003859-659b5a77-03cf-4580-ad08-c1fdc4683b9c.mp4

inna-voig avatar Aug 23 '22 13:08 inna-voig

@inna-voig The good news! Thank you, I will test it a little later, after work, and I will write to you.

HeaTTheatR avatar Aug 23 '22 13:08 HeaTTheatR

@inna-voig What is your nickname on Discord?

HeaTTheatR avatar Aug 23 '22 14:08 HeaTTheatR

It's InnaVoiG#1862, I'll connect this github account there too

inna-voig avatar Aug 23 '22 14:08 inna-voig

@inna-voig I wrote to you on Discord.

HeaTTheatR avatar Aug 23 '22 14:08 HeaTTheatR