KivyMD
KivyMD copied to clipboard
✓ KivyMD. Development - Bounty program
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.
It's working now. But I still can't solve the problem when widgets with Elevation
behavior are in ScrollView
layout.
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:
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.
I want to help in solving this issue. I'll try if I can get it to work.
@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 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);
}
@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
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
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 got minimal code working now!
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 The good news! Thank you, I will test it a little later, after work, and I will write to you.
@inna-voig What is your nickname on Discord?
It's InnaVoiG#1862, I'll connect this github account there too
@inna-voig I wrote to you on Discord.