Files
MrLowPr--overdrive/ps5-react/patches/embeddedReact-border-gradient.patch
2026-10-14 04:20:24 +02:00

519 lines
24 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Embedded React: a conic gradient seen through a View's border ring.
Modified from Embedded React (Apache-2.0, Copyright 2026 Cory Lamming) at the
revision pinned in dependencies.lock.json, after the patches listed before this
one there. Changed files: engine/include/er_scene.h,
engine/scene/er_node_internal.h, engine/scene/compositor.c,
engine/animation/animation.c, engine/rendering/border_sweep.c,
engine/rendering/border_sweep.h, bridges/quickjs/native_ui_bridge.c,
bridges/quickjs/js/src/embedded-react/index.d.ts and
engine/tests/rendering/test_border_sweep.c. Proposed upstream, with the other
border ring patches, as pull request #325.
The rotating-gradient border (a conic gradient turning behind the card, masked
to its border) is the classic focus-ring shimmer. The View style
borderGradient {type: 'conic', width, angle, stops} draws a conic gradient,
CSS conic-gradient style (0 degrees up, clockwise, up to 6 straight-alpha
stops), only inside the anti-aliased rounded ring `width` px thick along the
box's edge, over the content. borderGradientAngle sets the start angle and
animates on the native driver; an angle-only change damages the ring's edge
bands, like the border sweep it shares its rasteriser with.
diff --git a/bridges/quickjs/js/src/embedded-react/index.d.ts b/bridges/quickjs/js/src/embedded-react/index.d.ts
index 92e2952..66d4c7d 100644
--- a/bridges/quickjs/js/src/embedded-react/index.d.ts
+++ b/bridges/quickjs/js/src/embedded-react/index.d.ts
@@ -189,6 +189,10 @@ export interface ViewStyle {
borderSweepLength?: number;
/** Head position, 0–1 clockwise from the top edge's left end; wraps. Animate it with the native driver. */
borderSweepPhase?: number | AnimatedValue;
+ /** A conic gradient seen through the border ring: the rotating-gradient border. Up to 6 stops. */
+ borderGradient?: {type: 'conic'; width: number; angle?: number; stops: {color: string; offset?: number}[]};
+ /** The conic border gradient's start angle in degrees (0 = up, clockwise); animate it to rotate the light. */
+ borderGradientAngle?: number | AnimatedValue;
// Border.
borderRadius?: number;
diff --git a/bridges/quickjs/native_ui_bridge.c b/bridges/quickjs/native_ui_bridge.c
index 95a9f65..ffc771a 100644
--- a/bridges/quickjs/native_ui_bridge.c
+++ b/bridges/quickjs/native_ui_bridge.c
@@ -1175,6 +1175,8 @@ typedef enum
PROP_BORDER_SWEEP_WIDTH,
PROP_BORDER_SWEEP_PHASE,
PROP_BORDER_SWEEP_LENGTH,
+ PROP_BORDER_GRADIENT,
+ PROP_BORDER_GRADIENT_ANGLE,
PROP_COUNT_,
} PropId;
@@ -1303,6 +1305,8 @@ static const char* const k_prop_names[PROP_COUNT_] = {
[PROP_BORDER_SWEEP_WIDTH] = "borderSweepWidth",
[PROP_BORDER_SWEEP_PHASE] = "borderSweepPhase",
[PROP_BORDER_SWEEP_LENGTH] = "borderSweepLength",
+ [PROP_BORDER_GRADIENT] = "borderGradient",
+ [PROP_BORDER_GRADIENT_ANGLE] = "borderGradientAngle",
};
/** @brief (atom, id) pair; s_prop_atoms is sorted by atom value once, for prop_id_from_atom()'s bsearch. */
@@ -2126,6 +2130,73 @@ static void apply_background_gradient(JSContext* ctx, JSValueConst v, ERProps* p
apply_gradient_stops(ctx, v, p);
}
+/**
+ * @brief Reads a View `borderGradient` ({type: 'conic', width, angle?, stops: [{color, offset?}]}): a conic
+ * gradient seen through the border ring. `angle` is the start in degrees (0 = up, clockwise);
+ * `borderGradientAngle` overrides it and can be animated.
+ *
+ * @param[in] ctx QuickJS context.
+ * @param[in] v The style value.
+ * @param[in,out] p Props being built.
+ */
+static void apply_border_gradient(JSContext* ctx, JSValueConst v, ERProps* p)
+{
+ if (!JS_IsObject(v))
+ return;
+ JSValue t = JS_GetPropertyStr(ctx, v, "type");
+ const char* ts = JS_ToCString(ctx, t);
+ JS_FreeValue(ctx, t);
+ const bool conic = ts && strcmp(ts, "conic") == 0;
+ if (ts)
+ JS_FreeCString(ctx, ts);
+ if (!conic)
+ return;
+ JSValue wv = JS_GetPropertyStr(ctx, v, "width");
+ int16_t width = 0;
+ if (to_dim(ctx, wv, &width))
+ p->border_gradient_width = width;
+ JS_FreeValue(ctx, wv);
+ JSValue av = JS_GetPropertyStr(ctx, v, "angle");
+ double a = 0.0;
+ if (JS_IsNumber(av) && JS_ToFloat64(ctx, &a, av) == 0)
+ p->border_gradient_angle = (float)(a - 360.0 * floor(a / 360.0));
+ JS_FreeValue(ctx, av);
+ JSValue stops = JS_GetPropertyStr(ctx, v, "stops");
+ if (JS_IsArray(stops))
+ {
+ JSValue lenv = JS_GetPropertyStr(ctx, stops, "length");
+ int32_t len = 0;
+ JS_ToInt32(ctx, &len, lenv);
+ JS_FreeValue(ctx, lenv);
+ if (len > ER_BORDER_GRADIENT_MAX_STOPS)
+ len = ER_BORDER_GRADIENT_MAX_STOPS;
+ uint8_t n = 0;
+ for (int32_t i = 0; i < len; i++)
+ {
+ JSValue st = JS_GetPropertyUint32(ctx, stops, (uint32_t)i);
+ JSValue cv = JS_GetPropertyStr(ctx, st, "color");
+ JSValue ov = JS_GetPropertyStr(ctx, st, "offset");
+ uint32_t c;
+ double o = (len > 1) ? (double)i / (double)(len - 1) : 0.0;
+ if (!JS_IsUndefined(ov))
+ JS_ToFloat64(ctx, &o, ov);
+ if (n > 0 && o < p->border_gradient_stops[n - 1].position)
+ o = p->border_gradient_stops[n - 1].position;
+ if (to_color(ctx, cv, &c))
+ {
+ p->border_gradient_stops[n].color = c;
+ p->border_gradient_stops[n].position = (float)o;
+ n++;
+ }
+ JS_FreeValue(ctx, ov);
+ JS_FreeValue(ctx, cv);
+ JS_FreeValue(ctx, st);
+ }
+ p->border_gradient_stop_count = n;
+ }
+ JS_FreeValue(ctx, stops);
+}
+
/**
* @brief Reads obj's own enumerable string keys into s_prop_slots.
*
@@ -2336,6 +2407,13 @@ static void apply_props(JSContext* ctx, ERNode* node, JSValueConst obj)
&& JS_ToFloat64(ctx, &d, s_prop_slots[PROP_BORDER_SWEEP_LENGTH]) == 0)
p.border_sweep_length = (float)d;
}
+ apply_border_gradient(ctx, s_prop_slots[PROP_BORDER_GRADIENT], &p);
+ {
+ double d = 0.0;
+ if (!JS_IsUndefined(s_prop_slots[PROP_BORDER_GRADIENT_ANGLE])
+ && JS_ToFloat64(ctx, &d, s_prop_slots[PROP_BORDER_GRADIENT_ANGLE]) == 0)
+ p.border_gradient_angle = (float)(d - 360.0 * floor(d / 360.0));
+ }
/* ActivityIndicator (RN uses `color` for the spinner tint). */
ER_COL(PROP_COLOR, indicator_color);
@@ -4238,6 +4316,7 @@ static bool anim_prop_from_name(const char* s, ERAnimProp* out)
{"value", ER_PROP_ARC_VALUE}, /* Dial value (ER_NODE_ARC) */
{"valueStart", ER_PROP_ARC_VALUE_START}, /* Dial RANGE low end */
{"borderSweepPhase", ER_PROP_BORDER_SWEEP_PHASE},
+ {"borderGradientAngle", ER_PROP_BORDER_GRADIENT_ANGLE},
};
for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++)
{
diff --git a/engine/animation/animation.c b/engine/animation/animation.c
index 12c6358..39542cc 100644
--- a/engine/animation/animation.c
+++ b/engine/animation/animation.c
@@ -302,6 +302,11 @@ static bool read_numeric_value(const ERNode* node, ERAnimProp prop, float* value
return false;
*value = node->props.view.border_sweep_phase;
return true;
+ case ER_PROP_BORDER_GRADIENT_ANGLE:
+ if (!is_view_node(node))
+ return false;
+ *value = node->props.view.border_gradient_angle;
+ return true;
case ER_PROP_SWITCH_THUMB:
if (node->type != ER_NODE_SWITCH)
return false;
@@ -481,6 +486,19 @@ static bool apply_numeric_value(ERNode* node, ERAnimProp prop, float value)
node->sweep_moved = true;
return true;
}
+ case ER_PROP_BORDER_GRADIENT_ANGLE:
+ {
+ if (!is_view_node(node))
+ return false;
+ value -= 360.0f * floorf(value / 360.0f);
+ if (node->props.view.border_gradient_angle == value)
+ return false;
+ node->props.view.border_gradient_angle = value;
+ /* Only the ring changed: the damage pre-pass repaints its bands, not the whole box. */
+ if (!node->source_dirty)
+ node->sweep_moved = true;
+ return true;
+ }
case ER_PROP_ARC_VALUE:
return er_arc_apply_value(node, value);
case ER_PROP_ARC_VALUE_START:
@@ -883,7 +901,7 @@ static void push_to_value_bindings(ERAnimValue* val)
/* An animated opacity value changes only the blend, not the subtree's rendered
* content — mark visual-only so the compositor's fade cache stays valid. Every
* other prop (position, scale, rotation, color) changes content. */
- if (bind->prop != ER_PROP_BORDER_SWEEP_PHASE)
+ if (bind->prop != ER_PROP_BORDER_SWEEP_PHASE && bind->prop != ER_PROP_BORDER_GRADIENT_ANGLE)
n->sweep_moved = false; /* anything else changed too: the whole box repaints */
if (bind->prop == ER_PROP_OPACITY)
er_mark_dirty_upward_visual(n);
diff --git a/engine/include/er_scene.h b/engine/include/er_scene.h
index e048243..757ef16 100644
--- a/engine/include/er_scene.h
+++ b/engine/include/er_scene.h
@@ -369,6 +369,9 @@ extern "C"
ER_ARC_CAP_ROUND = 1, /**< Semicircular ends extending half the width past each boundary ray. */
} ERArcCap;
+/** @brief Maximum number of color stops in a View's conic border gradient (stored in every node's props). */
+#define ER_BORDER_GRADIENT_MAX_STOPS 6
+
/** @brief Maximum number of color stops in a View-background gradient (stored in every node's props). */
#define ER_GRADIENT_MAX_STOPS 4
@@ -504,6 +507,7 @@ extern "C"
ER_PROP_ARC_VALUE, /**< Arc value in [arc_min, arc_max] units (ER_NODE_ARC only). */
ER_PROP_ARC_VALUE_START, /**< Arc range-mode low end, same units (ER_NODE_ARC with arc_range). */
ER_PROP_BORDER_SWEEP_PHASE, /**< Border sweep head position, 0–1 around the perimeter (wraps). */
+ ER_PROP_BORDER_GRADIENT_ANGLE, /**< Conic border gradient start angle in degrees. */
} ERAnimProp;
/**
@@ -768,6 +772,12 @@ extern "C"
float border_sweep_phase; /**< Head position, 0–1 around the perimeter clockwise from top-left. */
float border_sweep_length; /**< Tail as a fraction of the perimeter; 0 = 0.3. */
+ /* --- Border gradient (View-family): a conic gradient seen through the border ring --- */
+ int16_t border_gradient_width; /**< Ring thickness in pixels, inside the box; 0 = none. */
+ float border_gradient_angle; /**< Start angle in degrees: 0 = up, clockwise (CSS conic `from`). */
+ uint8_t border_gradient_stop_count; /**< Valid entries in border_gradient_stops. */
+ ERGradientStop border_gradient_stops[ER_BORDER_GRADIENT_MAX_STOPS]; /**< Ascending positions 0–1. */
+
/* --- ActivityIndicator --- */
uint32_t indicator_color; /**< Spinner dot color; 0 = 0xFFFFFFFF (white). */
uint8_t animating; /**< 1 = spinning (default when node created), 0 = stopped. */
diff --git a/engine/rendering/border_sweep.c b/engine/rendering/border_sweep.c
index 81ff851..6daa887 100644
--- a/engine/rendering/border_sweep.c
+++ b/engine/rendering/border_sweep.c
@@ -83,24 +83,70 @@ int er_border_sweep_reach(int radius, int width)
return (radius > width ? radius : width) + 2;
}
-void er_border_sweep_render(int x, int y, int w, int h, int radius, int width, uint32_t argb, float phase,
- float length)
+/** Colour of the ring at a pixel, from its position relative to the box centre: straight ARGB. */
+typedef uint32_t (*RingShader)(float px, float py, const void* ctx);
+
+typedef struct
+{
+ float a, b, r; /* the ring's centre line: straight half-lengths and corner radius */
+ float phase, length;
+ uint32_t argb;
+} SweepShader;
+
+static uint32_t sweep_shade(float px, float py, const void* ctx)
+{
+ const SweepShader* s = ctx;
+ const float k = sweep_alpha(perimeter_at(px, py, s->a, s->b, s->r), s->phase, s->length);
+ const uint32_t a8 = (uint32_t)((float)(s->argb >> 24) * k + 0.5f);
+ return (a8 << 24) | (s->argb & 0xFFFFFFU);
+}
+
+typedef struct
+{
+ float from; /* degrees, CSS conic `from`: 0 = up, clockwise */
+ const ERGradientStop* stops;
+ int count;
+} ConicShader;
+
+/** Straight-alpha lerp of two ARGB colours. */
+static uint32_t lerp_argb(uint32_t c0, uint32_t c1, float f)
+{
+ uint32_t out = 0U;
+ for (int shift = 0; shift < 32; shift += 8)
+ {
+ const float v0 = (float)((c0 >> shift) & 0xFFU), v1 = (float)((c1 >> shift) & 0xFFU);
+ out |= (uint32_t)(v0 + (v1 - v0) * f + 0.5f) << shift;
+ }
+ return out;
+}
+
+static uint32_t conic_shade(float px, float py, const void* ctx)
+{
+ const ConicShader* s = ctx;
+ float t = (atan2f(px, -py) * (180.0f / SWEEP_PI) - s->from) / 360.0f;
+ t -= floorf(t);
+ if (t <= s->stops[0].position)
+ return s->stops[0].color;
+ for (int i = 1; i < s->count; i++)
+ {
+ if (t <= s->stops[i].position)
+ {
+ const float span = s->stops[i].position - s->stops[i - 1].position;
+ return lerp_argb(s->stops[i - 1].color, s->stops[i].color,
+ span > 0.0f ? (t - s->stops[i - 1].position) / span : 1.0f);
+ }
+ }
+ return s->stops[s->count - 1].color;
+}
+
+/** Rasterises the anti-aliased ring `width` px inside the rounded box, coloured by `shade`. */
+static void ring_render(int x, int y, int w, int h, int radius, int width, RingShader shade, const void* ctx)
{
- if (w <= 0 || h <= 0 || width <= 0 || (argb >> 24) == 0U || length <= 0.0f)
- return;
const int max_r = (w < h ? w : h) / 2;
const int r = radius < 0 ? 0 : (radius > max_r ? max_r : radius);
const float hw = (float)w * 0.5f, hh = (float)h * 0.5f;
const float bw = (float)width;
const float inner_r = (float)r > bw ? (float)r - bw : 0.0f;
- /* The ring's centre line, which the sweep travels along. */
- const float mid = bw * 0.5f;
- float cr = (float)r - mid;
- if (cr < 0.0f)
- cr = 0.0f;
- const float ca = hw - mid - cr, cb = hh - mid - cr;
- const float head_a = (float)(argb >> 24) / 255.0f;
- const uint32_t cr8 = (argb >> 16) & 0xFFU, cg8 = (argb >> 8) & 0xFFU, cb8 = argb & 0xFFU;
int cx0 = x, cy0 = y, cx1 = x + w, cy1 = y + h;
int gx, gy, gw, gh;
@@ -150,12 +196,12 @@ void er_border_sweep_render(int x, int y, int w, int h, int radius, int width, u
uint32_t p = 0U;
if (cov > 0.0f)
{
- const float a = cov * head_a * sweep_alpha(perimeter_at(px, py, ca, cb, cr), phase, length);
- const uint32_t a8 = (uint32_t)(a * 255.0f + 0.5f);
+ const uint32_t c = shade(px, py, ctx);
+ const uint32_t a8 = (uint32_t)((float)(c >> 24) * cov + 0.5f);
if (a8 > 0U)
{
- p = (a8 << 24) | (((cr8 * a8 + 127U) / 255U) << 16) | (((cg8 * a8 + 127U) / 255U) << 8)
- | ((cb8 * a8 + 127U) / 255U);
+ p = (a8 << 24) | (((((c >> 16) & 0xFFU) * a8 + 127U) / 255U) << 16)
+ | (((((c >> 8) & 0xFFU) * a8 + 127U) / 255U) << 8) | (((c & 0xFFU) * a8 + 127U) / 255U);
any = true;
}
}
@@ -167,3 +213,27 @@ void er_border_sweep_render(int x, int y, int w, int h, int radius, int width, u
}
}
}
+
+void er_border_sweep_render(int x, int y, int w, int h, int radius, int width, uint32_t argb, float phase,
+ float length)
+{
+ if (w <= 0 || h <= 0 || width <= 0 || (argb >> 24) == 0U || length <= 0.0f)
+ return;
+ const int max_r = (w < h ? w : h) / 2;
+ const int r = radius < 0 ? 0 : (radius > max_r ? max_r : radius);
+ const float mid = (float)width * 0.5f;
+ float cr = (float)r - mid;
+ if (cr < 0.0f)
+ cr = 0.0f;
+ const SweepShader s = {(float)w * 0.5f - mid - cr, (float)h * 0.5f - mid - cr, cr, phase, length, argb};
+ ring_render(x, y, w, h, r, width, sweep_shade, &s);
+}
+
+void er_border_conic_render(int x, int y, int w, int h, int radius, int width, float from,
+ const ERGradientStop* stops, int count)
+{
+ if (w <= 0 || h <= 0 || width <= 0 || count < 1)
+ return;
+ const ConicShader s = {from, stops, count};
+ ring_render(x, y, w, h, radius, width, conic_shade, &s);
+}
diff --git a/engine/rendering/border_sweep.h b/engine/rendering/border_sweep.h
index 076d1bf..8995b75 100644
--- a/engine/rendering/border_sweep.h
+++ b/engine/rendering/border_sweep.h
@@ -17,6 +17,7 @@
#ifndef EMBEDDED_REACT_BORDER_SWEEP_H
#define EMBEDDED_REACT_BORDER_SWEEP_H
+#include "er_scene.h"
#include <stdint.h>
/**
@@ -34,6 +35,20 @@
void er_border_sweep_render(int x, int y, int w, int h, int radius, int width, uint32_t argb, float phase,
float length);
+/**
+ * @brief Draws a conic gradient seen only through a rounded-rect ring `width` px thick inside the box: the
+ * rotating-gradient border. Colours follow the angle around the box centre, CSS conic-gradient style.
+ *
+ * @param[in] x, y, w, h The node's box in screen pixels.
+ * @param[in] radius Corner radius of the box.
+ * @param[in] width Ring thickness in pixels.
+ * @param[in] from Start angle in degrees: 0 points up, positive turns clockwise.
+ * @param[in] stops Straight-alpha colour stops at positions 0–1 around the circle, ascending.
+ * @param[in] count Number of stops.
+ */
+void er_border_conic_render(int x, int y, int w, int h, int radius, int width, float from,
+ const ERGradientStop* stops, int count);
+
/**
* @brief How far the sweep reaches in from each edge of the box: the bands a phase change repaints.
*
diff --git a/engine/scene/compositor.c b/engine/scene/compositor.c
index 6133ff0..ead7df3 100644
--- a/engine/scene/compositor.c
+++ b/engine/scene/compositor.c
@@ -2887,14 +2887,18 @@ static void render_node_content(
render_tree(child, needs_paint, occluded || buried, child_tx, child_ty);
}
- /* The border sweep lights the edge over the node's content, like a focus ring. */
- if (should_render && (n->type == ER_NODE_VIEW || n->type == ER_NODE_PRESSABLE)
- && n->props.view.border_sweep_width > 0)
+ /* The border gradient and sweep light the edge over the node's content, like a focus ring. */
+ if (should_render && (n->type == ER_NODE_VIEW || n->type == ER_NODE_PRESSABLE))
{
const ERViewProps* vp = &n->props.view;
- er_border_sweep_render(px, py, w, h, vp->border_tl_radius > 0 ? vp->border_tl_radius : vp->border_radius,
- vp->border_sweep_width, vp->border_sweep_color, vp->border_sweep_phase,
- vp->border_sweep_length > 0.0f ? vp->border_sweep_length : 0.3f);
+ const int radius = vp->border_tl_radius > 0 ? vp->border_tl_radius : vp->border_radius;
+ if (vp->border_gradient_width > 0)
+ er_border_conic_render(px, py, w, h, radius, vp->border_gradient_width, vp->border_gradient_angle,
+ vp->border_gradient_stops, vp->border_gradient_stop_count);
+ if (vp->border_sweep_width > 0)
+ er_border_sweep_render(px, py, w, h, radius, vp->border_sweep_width, vp->border_sweep_color,
+ vp->border_sweep_phase,
+ vp->border_sweep_length > 0.0f ? vp->border_sweep_length : 0.3f);
}
if (clips)
@@ -3358,6 +3362,11 @@ static void copy_view_shadow_and_gradient(ERNode* node, const ERProps* props)
node->props.view.border_sweep_width = props->border_sweep_width;
node->props.view.border_sweep_phase = props->border_sweep_phase;
node->props.view.border_sweep_length = props->border_sweep_length;
+ node->props.view.border_gradient_width = props->border_gradient_width;
+ node->props.view.border_gradient_angle = props->border_gradient_angle;
+ node->props.view.border_gradient_stop_count = props->border_gradient_stop_count;
+ for (int bi = 0; bi < ER_BORDER_GRADIENT_MAX_STOPS; bi++)
+ node->props.view.border_gradient_stops[bi] = props->border_gradient_stops[bi];
node->props.view.gradient_type = props->gradient_type;
node->props.view.gradient_corner = props->gradient_corner;
node->props.view.gradient_angle = props->gradient_angle;
@@ -5596,9 +5605,12 @@ void er_commit(void)
* not the whole box (a focused card under a travelling light). */
if (!moved)
{
+ const int ring = n->props.view.border_sweep_width > n->props.view.border_gradient_width
+ ? n->props.view.border_sweep_width
+ : n->props.view.border_gradient_width;
int reach = er_border_sweep_reach(
n->props.view.border_tl_radius > 0 ? n->props.view.border_tl_radius : n->props.view.border_radius,
- n->props.view.border_sweep_width);
+ ring);
if (reach > rw / 2)
reach = (rw + 1) / 2;
if (reach > rh / 2)
diff --git a/engine/scene/er_node_internal.h b/engine/scene/er_node_internal.h
index 5e3992d..d47020a 100644
--- a/engine/scene/er_node_internal.h
+++ b/engine/scene/er_node_internal.h
@@ -227,6 +227,12 @@ typedef struct
float border_sweep_phase;
float border_sweep_length;
+ /* Conic border gradient */
+ int16_t border_gradient_width;
+ float border_gradient_angle;
+ uint8_t border_gradient_stop_count;
+ ERGradientStop border_gradient_stops[ER_BORDER_GRADIENT_MAX_STOPS];
+
/* Gradient (requires ERUI_GRADIENT) */
uint8_t gradient_type; /**< ERGradientType. */
uint8_t gradient_corner; /**< ERGradientCorner (overrides gradient_angle). */
diff --git a/engine/tests/rendering/test_border_sweep.c b/engine/tests/rendering/test_border_sweep.c
index d727369..2fa7f53 100644
--- a/engine/tests/rendering/test_border_sweep.c
+++ b/engine/tests/rendering/test_border_sweep.c
@@ -317,5 +317,42 @@ int main(void)
}
memcpy(fb, incremental, sizeof(incremental));
}
+
+ /* A conic gradient seen through a 4 px ring: clear, white at a quarter turn, clear again. From 0 the
+ * white points right (90 degrees), so the right edge is lit and the left one dark; turning the start
+ * angle by 180 swaps them, repainting only the ring's bands. */
+ er_anim_value_unbind_all(phase);
+ cp.border_sweep_width = 0;
+ cp.border_gradient_width = 4;
+ cp.border_gradient_angle = 0.0f;
+ cp.border_gradient_stop_count = 3;
+ cp.border_gradient_stops[0] = (ERGradientStop){0x00FFFFFFU, 0.0f};
+ cp.border_gradient_stops[1] = (ERGradientStop){0xFFFFFFFFU, 0.25f};
+ cp.border_gradient_stops[2] = (ERGradientStop){0x00FFFFFFU, 0.5f};
+ er_node_set_props(card, &cp);
+ er_commit();
+ const unsigned right = brightness(px_at(&t, 20 + 160 - 2, 80));
+ const unsigned left = brightness(px_at(&t, 20 + 1, 80));
+ if (right < 600 || left > card_bg + 10)
+ return fail("conic ring: the lit side is not where the stops put it");
+ if (brightness(px_at(&t, 100, 80)) != brightness(cp.background_color))
+ return fail("conic ring: drew inside the card");
+
+ ERAnimValueHandle turn = er_anim_value_create(0.0f);
+ er_anim_value_bind(turn, card, ER_PROP_BORDER_GRADIENT_ANGLE);
+ er_commit(); /* binding marks the card for a full repaint; the angle steps after it are what is measured */
+ er_anim_value_set(turn, 180.0f);
+ s_area = 0;
+ er_commit();
+ if (s_area * 5 >= whole_card * 3)
+ return fail("conic ring: an angle step cost more than 60% of repainting the whole card");
+ if (brightness(px_at(&t, 20 + 1, 80)) < 600 || brightness(px_at(&t, 20 + 160 - 2, 80)) > card_bg + 10)
+ return fail("conic ring: turning the start angle by 180 did not move the light across");
+ memcpy(incremental, fb, sizeof(incremental));
+ memset(fb, 0, sizeof(fb));
+ er_force_full_repaint();
+ er_commit();
+ if (memcmp(incremental, fb, sizeof(incremental)) != 0)
+ return fail("conic ring: the turned frame differs from a full repaint");
return EXIT_SUCCESS;
}