diff --git a/src/scenegraph/shadowedborderrectangle.frag b/src/scenegraph/shadowedborderrectangle.frag index 21217517..440eed5e 100644 --- a/src/scenegraph/shadowedborderrectangle.frag +++ b/src/scenegraph/shadowedborderrectangle.frag @@ -1,94 +1,94 @@ /* * SPDX-FileCopyrightText: 2020 Arjen Hiemstra * * SPDX-License-Identifier: LGPL-2.0-or-later */ #line 7 // This is based on the 2D SDF functions provided by Inigo Quilez: // https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm // This shader renders a rectangle with rounded corners and a shadow below it. // In addition it renders a border around it. uniform lowp float opacity; uniform lowp float size; uniform lowp float radius; uniform lowp vec4 color; uniform lowp vec4 shadowColor; uniform lowp vec2 offset; uniform lowp vec2 aspect; uniform lowp float borderWidth; uniform lowp vec4 borderColor; varying lowp vec2 uv; const lowp float minimum_shadow_radius = 0.05; const lowp float smoothing = 0.001; // Calculate the distance to a rectangle with rounded corners. // \param point The point to calculate the distance of. // \param rect The rectangle to calculate the distance of. // \param translation The amount of translation to apply to the rectangle. // \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left. lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec2 translation, in lowp vec4 radius) { radius.xy = (point.x > 0.0) ? radius.xy : radius.zw; radius.x = (point.y > 0.0) ? radius.x : radius.y; lowp vec2 d = abs(point - translation) - rect + radius.x; return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x; } // Render an sdf value into a color. lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float sdfAlpha) { lowp float g = fwidth(sdf); return mix(sourceColor, sdfColor, sdfAlpha * (1.0 - smoothstep(smoothing - g, smoothing + g, sdf))); } void main() { // Scaling factor that is the inverse of the amount of scaling applied to the geometry. - lowp float inverse_scale = 1.0 / (1.0 + size + length(offset) * 2.0 + borderWidth * 2.0); + lowp float inverse_scale = 1.0 / (1.0 + size + length(offset) * 2.0); // Correction factor to round the corners of a larger shadow. // We want to account for size in regards to shadow radius, so that a larger shadow is // more rounded, but only if we are not already rounding the corners due to corner radius. lowp float size_factor = 0.5 * (minimum_shadow_radius / max(radius, minimum_shadow_radius)); lowp float shadow_radius = radius + size * size_factor; lowp vec4 col = vec4(0.0); // Calculate the shadow's distance field. - lowp float shadow = sdf_rounded_rectangle(uv, (aspect + borderWidth) * inverse_scale, offset * 2.0 * inverse_scale, vec4(shadow_radius * inverse_scale)); + lowp float shadow = sdf_rounded_rectangle(uv, aspect * inverse_scale, offset * 2.0 * inverse_scale, vec4(shadow_radius * inverse_scale)); // Render it, interpolating the color over the distance. col = mix(col, shadowColor * sign(size), 1.0 - smoothstep(-size * 0.5, size * 0.5, shadow)); // Scale corrected corner radius lowp vec4 corner_radius = vec4(radius * inverse_scale); // Calculate the outer rectangle distance field. - lowp float outer_rect = sdf_rounded_rectangle(uv, (aspect + borderWidth) * inverse_scale, vec2(0.0), corner_radius); + lowp float outer_rect = sdf_rounded_rectangle(uv, aspect * inverse_scale, vec2(0.0), corner_radius); // First, remove anything that was rendered by the shadow if it is inside the rectangle. // This allows us to use colors with alpha without rendering artifacts. col = sdf_render(outer_rect, col, vec4(0.0), 1.0); // Then, render it again but this time with the proper color and properly alpha blended. col = sdf_render(outer_rect, col, borderColor, 1.0); // Calculate the inner rectangle distance field. // This uses a reduced corner radius because the inner corners need to be smaller than the outer corners. - lowp vec4 inner_radius = vec4((radius - borderWidth) * inverse_scale); - lowp float inner_rect = sdf_rounded_rectangle(uv, (aspect - borderWidth) * inverse_scale, vec2(0.0), inner_radius); + lowp vec4 inner_radius = vec4((radius - borderWidth * 2.0) * inverse_scale); + lowp float inner_rect = sdf_rounded_rectangle(uv, (aspect - borderWidth * 2.0) * inverse_scale, vec2(0.0), inner_radius); // Like above, but this time cut out the inner rectangle. col = sdf_render(inner_rect, col, vec4(0.0), 1.0); // Finally, render the inner rectangle. col = sdf_render(inner_rect, col, color, 1.0); gl_FragColor = col * opacity; } diff --git a/src/scenegraph/shadowedborderrectangle_core.frag b/src/scenegraph/shadowedborderrectangle_core.frag index 46fe0daf..983bd130 100644 --- a/src/scenegraph/shadowedborderrectangle_core.frag +++ b/src/scenegraph/shadowedborderrectangle_core.frag @@ -1,96 +1,96 @@ /* * SPDX-FileCopyrightText: 2020 Arjen Hiemstra * * SPDX-License-Identifier: LGPL-2.0-or-later */ #line 7 // This is based on the 2D SDF functions provided by Inigo Quilez: // https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm // This shader renders a rectangle with rounded corners and a shadow below it. // In addition it renders a border around it. uniform lowp float opacity; uniform lowp float size; uniform lowp float radius; uniform lowp vec4 color; uniform lowp vec4 shadowColor; uniform lowp vec2 offset; uniform lowp vec2 aspect; uniform lowp float borderWidth; uniform lowp vec4 borderColor; in lowp vec2 uv; out lowp vec4 out_color; const lowp float minimum_shadow_radius = 0.05; const lowp float smoothing = 0.001; // Calculate the distance to a rectangle with rounded corners. // \param point The point to calculate the distance of. // \param rect The rectangle to calculate the distance of. // \param translation The amount of translation to apply to the rectangle. // \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left. lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec2 translation, in lowp vec4 radius) { radius.xy = (point.x > 0.0) ? radius.xy : radius.zw; radius.x = (point.y > 0.0) ? radius.x : radius.y; lowp vec2 d = abs(point - translation) - rect + radius.x; return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x; } // Render an sdf value into a color. lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float sdfAlpha) { lowp float g = fwidth(sdf); return mix(sourceColor, sdfColor, sdfAlpha * (1.0 - smoothstep(smoothing - g, smoothing + g, sdf))); } void main() { // Scaling factor that is the inverse of the amount of scaling applied to the geometry. - lowp float inverse_scale = 1.0 / (1.0 + size + length(offset) * 2.0 + borderWidth * 2.0); + lowp float inverse_scale = 1.0 / (1.0 + size + length(offset) * 2.0); // Correction factor to round the corners of a larger shadow. // We want to account for size in regards to shadow radius, so that a larger shadow is // more rounded, but only if we are not already rounding the corners due to corner radius. lowp float size_factor = 0.5 * (minimum_shadow_radius / max(radius, minimum_shadow_radius)); lowp float shadow_radius = radius + size * size_factor; lowp vec4 col = vec4(0.0); // Calculate the shadow's distance field. - lowp float shadow = sdf_rounded_rectangle(uv, (aspect + borderWidth) * inverse_scale, offset * 2.0 * inverse_scale, vec4(shadow_radius * inverse_scale)); + lowp float shadow = sdf_rounded_rectangle(uv, aspect * inverse_scale, offset * 2.0 * inverse_scale, vec4(shadow_radius * inverse_scale)); // Render it, interpolating the color over the distance. col = mix(col, shadowColor * sign(size), 1.0 - smoothstep(-size * 0.5, size * 0.5, shadow)); // Scale corrected corner radius lowp vec4 corner_radius = vec4(radius * inverse_scale); // Calculate the outer rectangle distance field. - lowp float outer_rect = sdf_rounded_rectangle(uv, (aspect + borderWidth) * inverse_scale, vec2(0.0), corner_radius); + lowp float outer_rect = sdf_rounded_rectangle(uv, aspect * inverse_scale, vec2(0.0), corner_radius); // First, remove anything that was rendered by the shadow if it is inside the rectangle. // This allows us to use colors with alpha without rendering artifacts. col = sdf_render(outer_rect, col, vec4(0.0), 1.0); // Then, render it again but this time with the proper color and properly alpha blended. col = sdf_render(outer_rect, col, borderColor, 1.0); // Calculate the inner rectangle distance field. // This uses a reduced corner radius because the inner corners need to be smaller than the outer corners. - lowp vec4 inner_radius = vec4((radius - borderWidth) * inverse_scale); - lowp float inner_rect = sdf_rounded_rectangle(uv, (aspect - borderWidth) * inverse_scale, vec2(0.0), inner_radius); + lowp vec4 inner_radius = vec4((radius - borderWidth * 2.0) * inverse_scale); + lowp float inner_rect = sdf_rounded_rectangle(uv, (aspect - borderWidth * 2.0) * inverse_scale, vec2(0.0), inner_radius); // Like above, but this time cut out the inner rectangle. col = sdf_render(inner_rect, col, vec4(0.0), 1.0); // Finally, render the inner rectangle. col = sdf_render(inner_rect, col, color, 1.0); out_color = col * opacity; } diff --git a/src/scenegraph/shadowedrectanglenode.cpp b/src/scenegraph/shadowedrectanglenode.cpp index ca726049..4a4f2238 100644 --- a/src/scenegraph/shadowedrectanglenode.cpp +++ b/src/scenegraph/shadowedrectanglenode.cpp @@ -1,175 +1,178 @@ /* * SPDX-FileCopyrightText: 2020 Arjen Hiemstra * * SPDX-License-Identifier: LGPL-2.0-or-later */ #include "shadowedrectanglenode.h" #include "shadowedrectanglematerial.h" #include "shadowedborderrectanglematerial.h" QColor premultiply(const QColor &color) { return QColor::fromRgbF( color.redF() * color.alphaF(), color.greenF() * color.alphaF(), color.blueF() * color.alphaF(), color.alphaF() ); } ShadowedRectangleNode::ShadowedRectangleNode() { m_geometry = new QSGGeometry{QSGGeometry::defaultAttributes_TexturedPoint2D(), 4}; setGeometry(m_geometry); m_material = new ShadowedRectangleMaterial{}; setMaterial(m_material); setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial); } +void ShadowedRectangleNode::setBorderEnabled(bool enabled) +{ + // We can achieve more performant shaders by splitting the two into separate + // shaders. This requires separating the materials as well. So when + // borderWidth is increased to something where the border should be visible, + // switch to the with-border material. Otherwise use the no-border version. + + if (enabled) { + if (m_material->type() == &ShadowedRectangleMaterial::staticType) { + auto newMaterial = new ShadowedBorderRectangleMaterial(); + setMaterial(newMaterial); + m_material = newMaterial; + m_rect = QRectF{}; + markDirty(QSGNode::DirtyMaterial); + } + } else { + if (m_material->type() == &ShadowedBorderRectangleMaterial::staticType) { + auto newMaterial = new ShadowedRectangleMaterial(); + setMaterial(newMaterial); + m_material = newMaterial; + m_material->aspect = m_aspect; + m_rect = QRectF{}; + markDirty(QSGNode::DirtyMaterial); + } + } +} + void ShadowedRectangleNode::setRect(const QRectF& rect) { if (rect == m_rect) { return; } m_rect = rect; QVector2D newAspect{1.0, 1.0}; if (m_rect.width() >= m_rect.height()) { newAspect.setX(m_rect.width() / m_rect.height()); } else { newAspect.setY(m_rect.height() / m_rect.width()); } if (m_material->aspect != newAspect) { m_material->aspect = newAspect; markDirty(QSGNode::DirtyMaterial); m_aspect = newAspect; } } void ShadowedRectangleNode::setSize(qreal size) { auto minDimension = std::min(m_rect.width(), m_rect.height()); float uniformSize = (size / minDimension) * 2.0; if (!qFuzzyCompare(m_material->size, uniformSize)) { m_material->size = uniformSize; markDirty(QSGNode::DirtyMaterial); m_size = size; } } void ShadowedRectangleNode::setRadius(qreal radius) { auto minDimension = std::min(m_rect.width(), m_rect.height()); float uniformRadius = radius * 2.0 / minDimension; if (!qFuzzyCompare(m_material->radius, uniformRadius)) { m_material->radius = std::min(uniformRadius, 1.0f); markDirty(QSGNode::DirtyMaterial); m_radius = radius; } } void ShadowedRectangleNode::setColor(const QColor &color) { auto premultiplied = premultiply(color); if (m_material->color != premultiplied) { m_material->color = premultiplied; markDirty(QSGNode::DirtyMaterial); } } void ShadowedRectangleNode::setShadowColor(const QColor& color) { auto premultiplied = premultiply(color); if (m_material->shadowColor != premultiplied) { m_material->shadowColor = premultiplied; markDirty(QSGNode::DirtyMaterial); } } void ShadowedRectangleNode::setOffset(const QVector2D& offset) { auto minDimension = std::min(m_rect.width(), m_rect.height()); auto uniformOffset = offset / minDimension; if (m_material->offset != uniformOffset) { m_material->offset = uniformOffset; markDirty(QSGNode::DirtyMaterial); m_offset = offset; } } void ShadowedRectangleNode::setBorderWidth(qreal width) { - // We can achieve more performant shaders by splitting the two into separate - // shaders. This requires separating the materials as well. So when - // borderWidth is increased to something where the border should be visible, - // switch to the with-border material. Otherwise use the no-border version. - - if (qFuzzyIsNull(width)) { - if (m_material->type() == &ShadowedBorderRectangleMaterial::staticType) { - auto newMaterial = new ShadowedRectangleMaterial(); - setMaterial(newMaterial); - m_material = newMaterial; - m_borderWidth = width; - m_rect = QRectF{}; - markDirty(QSGNode::DirtyMaterial); - } + if (m_material->type() != &ShadowedBorderRectangleMaterial::staticType) { return; - } else { - if (m_material->type() == &ShadowedRectangleMaterial::staticType) { - auto newMaterial = new ShadowedBorderRectangleMaterial(); - setMaterial(newMaterial); - m_material = newMaterial; - m_rect = QRectF{}; - markDirty(QSGNode::DirtyMaterial); - } } auto minDimension = std::min(m_rect.width(), m_rect.height()); float uniformBorderWidth = width / minDimension; auto borderMaterial = static_cast(m_material); if (!qFuzzyCompare(borderMaterial->borderWidth, uniformBorderWidth)) { borderMaterial->borderWidth = uniformBorderWidth; markDirty(QSGNode::DirtyMaterial); m_borderWidth = width; } } void ShadowedRectangleNode::setBorderColor(const QColor& color) { if (m_material->type() != &ShadowedBorderRectangleMaterial::staticType) { return; } auto borderMaterial = static_cast(m_material); auto premultiplied = premultiply(color); if (borderMaterial->borderColor != premultiplied) { borderMaterial->borderColor = premultiplied; markDirty(QSGNode::DirtyMaterial); } } void ShadowedRectangleNode::updateGeometry() { auto rect = m_rect.adjusted(-m_size * m_aspect.x(), -m_size * m_aspect.y(), m_size * m_aspect.x(), m_size * m_aspect.y()); auto offsetLength = m_offset.length(); rect = rect.adjusted(-offsetLength * m_aspect.x(), -offsetLength * m_aspect.y(), offsetLength * m_aspect.x(), offsetLength * m_aspect.y()); - rect = rect.adjusted(-m_borderWidth * m_aspect.x(), -m_borderWidth * m_aspect.y(), - m_borderWidth * m_aspect.x(), m_borderWidth * m_aspect.y()); - QSGGeometry::updateTexturedRectGeometry(m_geometry, rect, QRectF{0.0, 0.0, 1.0, 1.0}); markDirty(QSGNode::DirtyGeometry); } diff --git a/src/scenegraph/shadowedrectanglenode.h b/src/scenegraph/shadowedrectanglenode.h index 1c5ce7b6..a1d5a437 100644 --- a/src/scenegraph/shadowedrectanglenode.h +++ b/src/scenegraph/shadowedrectanglenode.h @@ -1,66 +1,67 @@ /* * SPDX-FileCopyrightText: 2020 Arjen Hiemstra * * SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once #include #include #include class ShadowedRectangleMaterial; /** * Scene graph node for a shadowed rectangle. * * This node will set up the geometry and materials for a shadowed rectangle, * optionally with rounded corners. * * \note You must call updateGeometry() after setting properties of this node, * otherwise the node's state will not correctly reflect all the properties. * * \sa ShadowedRectangle */ class ShadowedRectangleNode : public QSGGeometryNode { public: ShadowedRectangleNode(); /** - * Set the width of the border. + * Set whether to draw a border. * * Note that this will switch between a material with or without border. * This means this needs to be called before any other setters. */ - void setBorderWidth(qreal width); + void setBorderEnabled(bool enabled); void setRect(const QRectF &rect); void setSize(qreal size); void setRadius(qreal radius); void setColor(const QColor &color); void setShadowColor(const QColor &color); void setOffset(const QVector2D &offset); + void setBorderWidth(qreal width); void setBorderColor(const QColor &color); /** * Update the geometry for this node. * * This is done as an explicit step to avoid the geometry being recreated * multiple times while updating properties. */ void updateGeometry(); private: QSGGeometry *m_geometry; ShadowedRectangleMaterial *m_material; QRectF m_rect; qreal m_size = 0.0; qreal m_radius = 0.0; QVector2D m_offset = QVector2D{0.0, 0.0}; QVector2D m_aspect = QVector2D{1.0, 1.0}; qreal m_borderWidth = 0.0; QColor m_borderColor; }; diff --git a/src/shadowedrectangle.cpp b/src/shadowedrectangle.cpp index 93972fff..5fedb97f 100644 --- a/src/shadowedrectangle.cpp +++ b/src/shadowedrectangle.cpp @@ -1,233 +1,234 @@ /* * SPDX-FileCopyrightText: 2020 Arjen Hiemstra * * SPDX-License-Identifier: LGPL-2.0-or-later */ #include "shadowedrectangle.h" #include #include #include #include "scenegraph/shadowedrectanglenode.h" #include "scenegraph/paintedrectangleitem.h" BorderGroup::BorderGroup(QObject* parent) : QObject(parent) { } qreal BorderGroup::width() const { return m_width; } void BorderGroup::setWidth(qreal newWidth) { if (newWidth == m_width) { return; } m_width = newWidth; Q_EMIT changed(); } QColor BorderGroup::color() const { return m_color; } void BorderGroup::setColor(const QColor & newColor) { if (newColor == m_color) { return; } m_color = newColor; Q_EMIT changed(); } ShadowGroup::ShadowGroup(QObject *parent) : QObject(parent) { } qreal ShadowGroup::size() const { return m_size; } void ShadowGroup::setSize(qreal newSize) { if (newSize == m_size) { return; } m_size = newSize; Q_EMIT changed(); } qreal ShadowGroup::xOffset() const { return m_xOffset; } void ShadowGroup::setXOffset(qreal newXOffset) { if (newXOffset == m_xOffset) { return; } m_xOffset = newXOffset; Q_EMIT changed(); } qreal ShadowGroup::yOffset() const { return m_yOffset; } void ShadowGroup::setYOffset(qreal newYOffset) { if (newYOffset == m_yOffset) { return; } m_yOffset = newYOffset; Q_EMIT changed(); } QColor ShadowGroup::color() const { return m_color; } void ShadowGroup::setColor(const QColor & newColor) { if (newColor == m_color) { return; } m_color = newColor; Q_EMIT changed(); } ShadowedRectangle::ShadowedRectangle(QQuickItem *parentItem) : QQuickItem(parentItem), m_border(new BorderGroup), m_shadow(new ShadowGroup) { setFlag(QQuickItem::ItemHasContents, true); connect(m_border.get(), &BorderGroup::changed, this, &ShadowedRectangle::update); connect(m_shadow.get(), &ShadowGroup::changed, this, &ShadowedRectangle::update); } ShadowedRectangle::~ShadowedRectangle() { } BorderGroup *ShadowedRectangle::border() const { return m_border.get(); } ShadowGroup *ShadowedRectangle::shadow() const { return m_shadow.get(); } qreal ShadowedRectangle::radius() const { return m_radius; } void ShadowedRectangle::setRadius(qreal newRadius) { if (newRadius == m_radius) { return; } m_radius = newRadius; update(); Q_EMIT radiusChanged(); } QColor ShadowedRectangle::color() const { return m_color; } void ShadowedRectangle::setColor(const QColor & newColor) { if (newColor == m_color) { return; } m_color = newColor; update(); Q_EMIT colorChanged(); } void ShadowedRectangle::componentComplete() { QQuickItem::componentComplete(); checkSoftwareItem(); } void ShadowedRectangle::checkSoftwareItem() { if (!m_softwareItem && window() && window()->rendererInterface()->graphicsApi() == QSGRendererInterface::Software) { m_softwareItem = new PaintedRectangleItem{this}; auto updateItem = [this]() { auto borderWidth = m_border->width(); auto rect = boundingRect().adjusted(-borderWidth / 2, -borderWidth / 2, borderWidth / 2, borderWidth / 2); m_softwareItem->setX(-borderWidth / 2); m_softwareItem->setY(-borderWidth / 2); m_softwareItem->setSize(rect.size()); m_softwareItem->setColor(m_color); m_softwareItem->setRadius(m_radius); m_softwareItem->setBorderWidth(borderWidth); m_softwareItem->setBorderColor(m_border->color()); }; updateItem(); connect(this, &ShadowedRectangle::widthChanged, m_softwareItem, updateItem); connect(this, &ShadowedRectangle::heightChanged, m_softwareItem, updateItem); connect(this, &ShadowedRectangle::colorChanged, m_softwareItem, updateItem); connect(this, &ShadowedRectangle::radiusChanged, m_softwareItem, updateItem); connect(m_border.get(), &BorderGroup::changed, m_softwareItem, updateItem); setFlag(QQuickItem::ItemHasContents, false); } } void ShadowedRectangle::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) { if (change == QQuickItem::ItemSceneChange && value.window) { checkSoftwareItem(); } } QSGNode *ShadowedRectangle::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) { Q_UNUSED(data); if (!node) { node = new ShadowedRectangleNode; } auto elevatedNode = static_cast(node); - elevatedNode->setBorderWidth(m_border->width()); + elevatedNode->setBorderEnabled(!qFuzzyIsNull(m_border->width())); elevatedNode->setRect(boundingRect()); elevatedNode->setSize(m_shadow->size()); elevatedNode->setRadius(m_radius); elevatedNode->setOffset(QVector2D{float(m_shadow->xOffset()), float(m_shadow->yOffset())}); elevatedNode->setColor(m_color); elevatedNode->setShadowColor(m_shadow->color()); + elevatedNode->setBorderWidth(m_border->width()); elevatedNode->setBorderColor(m_border->color()); elevatedNode->updateGeometry(); return elevatedNode; }