| /* | |
| * Copyright 2026 Rive | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "gm.hpp" | |
| #include "gmutils.hpp" | |
| #include "rive/renderer.hpp" | |
| #include "assets/nomoon.png.hpp" | |
| #include <array> | |
| using namespace rivegm; | |
| using namespace rive; | |
| // Exercises the additiveness paint value (a Rive extension), which | |
| // interpolates the srcOver composite toward additive: 0 is normal srcOver, | |
| // 1 is fully additive (result.rgb = src + dst, destination alpha preserved). | |
| // | |
| // Layout: 2 columns x 6 rows of 300x300 cells on a black background. | |
| // | |
| // Left column (srcOver references): | |
| // Row 0: opaque green square overlaid by a red square whose alpha ramps | |
| // from 1 at the top to 0 at the bottom. | |
| // Row 1: stroked variants of row 0. | |
| // Row 2: opaque gradient square overlaid by a gradient square whose alpha | |
| // ramps from 1 at the top to 0 at the bottom. | |
| // Row 3: image mesh overlaid by an image mesh whose opacity ramps from 1 | |
| // at the top to 0 at the bottom (drawn as horizontal strips). | |
| // Row 4: drawImage (image paint on a path): base image at full opacity | |
| // under an overlapping image at 60% opacity. | |
| // | |
| // Right column (additive): | |
| // Row 0: filled RGB venn circles; overlaps accumulate to | |
| // yellow/cyan/magenta/white. | |
| // Row 1: stroked rings (additive applies to strokes independently). | |
| // Rows 2 & 3: same geometry as the left column, with the overlay drawn | |
| // additively, so left vs right isolates srcOver vs additive. | |
| // | |
| // Row 5: the additiveness slider (0 = srcOver, 1 = fully additive). An opaque | |
| // green square under five red strips with additiveness 0, .25, .5, | |
| // .75, 1 (srcOver to fully additive, left to right). Left column solid | |
| // color, right column gradient. | |
| constexpr float CELL = 300.f; | |
| // Draws one quad (2 triangles) of an image as a mesh. | |
| static void draw_image_mesh_quad( | |
| Renderer* renderer, | |
| RenderImage* img, | |
| const AABB& dst, | |
| const AABB& uv, | |
| float additiveness, | |
| float opacity) | |
| { | |
| Factory* factory = TestingWindow::Get()->factory(); | |
| auto pts = factory->makeRenderBuffer( | |
| RenderBufferType::vertex, | |
| RenderBufferFlags::mappedOnceAtInitialization, | |
| 4 * sizeof(Vec2D)); | |
| memcpy( | |
| pts->map(), | |
| std::array<Vec2D, 4>{ | |
| Vec2D{dst.left(), dst.top()}, | |
| Vec2D{dst.right(), dst.top()}, | |
| Vec2D{dst.left(), dst.bottom()}, | |
| Vec2D{dst.right(), dst.bottom()}} | |
| .data(), | |
| pts->sizeInBytes()); | |
| pts->unmap(); | |
| auto uvs = factory->makeRenderBuffer( | |
| RenderBufferType::vertex, | |
| RenderBufferFlags::mappedOnceAtInitialization, | |
| 4 * sizeof(Vec2D)); | |
| memcpy( | |
| uvs->map(), | |
| std::array<Vec2D, 4>{ | |
| Vec2D{uv.left(), uv.top()}, | |
| Vec2D{uv.right(), uv.top()}, | |
| Vec2D{uv.left(), uv.bottom()}, | |
| Vec2D{uv.right(), uv.bottom()}} | |
| .data(), | |
| uvs->sizeInBytes()); | |
| uvs->unmap(); | |
| auto indices = factory->makeRenderBuffer( | |
| RenderBufferType::index, | |
| RenderBufferFlags::mappedOnceAtInitialization, | |
| 6 * sizeof(uint16_t)); | |
| memcpy( | |
| indices->map(), | |
| std::array<uint16_t, 6>{0, 1, 2, 1, 3, 2}.data(), | |
| indices->sizeInBytes()); | |
| indices->unmap(); | |
| renderer->drawImageMesh( | |
| img, | |
| ImageSampler::LinearClamp(), | |
| pts, | |
| uvs, | |
| indices, | |
| 4, | |
| 6, | |
| BlendMode::srcOver, | |
| opacity, | |
| additiveness); | |
| } | |
| // Approximates a vertical opacity ramp (1 at the top, 0 at the bottom) by | |
| // drawing the image as a stack of mesh strips with stepped opacity. | |
| static void draw_image_mesh_alpha_ramp( | |
| Renderer* renderer, | |
| RenderImage* img, | |
| const AABB& dst, | |
| float additiveness) | |
| { | |
| constexpr int strips = 256; | |
| for (int i = 0; i < strips; ++i) | |
| { | |
| float t0 = float(i) / strips; | |
| float t1 = float(i + 1) / strips; | |
| AABB stripDst = { | |
| dst.left(), | |
| dst.top() + t0 * dst.height(), | |
| dst.right(), | |
| dst.top() + t1 * dst.height()}; | |
| AABB stripUV = {0.f, t0, 1.f, t1}; | |
| draw_image_mesh_quad( | |
| renderer, | |
| img, | |
| stripDst, | |
| stripUV, | |
| additiveness, | |
| 1.f - (t0 + t1) * .5f); | |
| } | |
| } | |
| DEF_SIMPLE_GM_WITH_CLEAR_COLOR(additive_blend, 0xff000000, 600, 1800, renderer) | |
| { | |
| Factory* factory = TestingWindow::Get()->factory(); | |
| constexpr float stops[2] = {0.f, 1.f}; | |
| // Base square and overlapping overlay square within a cell at (ox, oy). | |
| auto baseRect = [](float ox, float oy) { | |
| return AABB{ox + 40.f, oy + 40.f, ox + 190.f, oy + 190.f}; | |
| }; | |
| auto overlayRect = [](float ox, float oy) { | |
| return AABB{ox + 110.f, oy + 110.f, ox + 260.f, oy + 260.f}; | |
| }; | |
| // Rows 0 & 1, left: opaque green square under a red square whose alpha | |
| // ramps 1 -> 0 top to bottom (via a gradient that only varies alpha). | |
| auto squaresCell = [&](float ox, | |
| float oy, | |
| RenderPaintStyle style, | |
| float thickness, | |
| BlendMode overlayBlend) { | |
| AABB base = baseRect(ox, oy); | |
| Paint basePaint; | |
| basePaint->style(style); | |
| if (style == RenderPaintStyle::stroke) | |
| { | |
| basePaint->thickness(thickness); | |
| } | |
| basePaint->color(0xff00ff00); | |
| renderer->drawPath(PathBuilder::Rect(base), basePaint); | |
| AABB overlay = overlayRect(ox, oy); | |
| ColorInt overlayColors[2] = {0xffff0000, 0x00ff0000}; | |
| Paint overlayPaint; | |
| overlayPaint->style(style); | |
| if (style == RenderPaintStyle::stroke) | |
| { | |
| overlayPaint->thickness(thickness); | |
| } | |
| overlayPaint->shader(factory->makeLinearGradient( | |
| 0, | |
| overlay.top(), | |
| 0, | |
| overlay.bottom(), | |
| overlayColors, | |
| stops, | |
| 2)); | |
| overlayPaint->blendMode(overlayBlend); | |
| renderer->drawPath(PathBuilder::Rect(overlay), overlayPaint); | |
| }; | |
| squaresCell(0, 0, RenderPaintStyle::fill, 0.f, BlendMode::srcOver); | |
| squaresCell(0, CELL, RenderPaintStyle::stroke, 20.f, BlendMode::srcOver); | |
| // Rows 0 & 1, right: classic RGB venn arrangement, all circles additive. | |
| // Two-circle overlaps accumulate to yellow/cyan/magenta, all three to | |
| // white. | |
| auto vennCell = | |
| [&](float ox, float oy, RenderPaintStyle style, float thickness) { | |
| struct Circle | |
| { | |
| float cx, cy; | |
| ColorInt color; | |
| }; | |
| constexpr float radius = 80.f; | |
| const Circle circles[] = { | |
| {ox + 150.f, oy + 110.f, 0xffff0000}, // red, top | |
| {ox + 110.f, oy + 185.f, 0xff00ff00}, // green, bottom-left | |
| {ox + 190.f, oy + 185.f, 0xff0000ff}, // blue, bottom-right | |
| }; | |
| for (const Circle& c : circles) | |
| { | |
| Paint paint; | |
| paint->style(style); | |
| if (style == RenderPaintStyle::stroke) | |
| { | |
| paint->thickness(thickness); | |
| } | |
| paint->color(c.color); | |
| paint->additiveness(1.f); | |
| renderer->drawPath(PathBuilder::Circle(c.cx, c.cy, radius), paint); | |
| } | |
| }; | |
| vennCell(CELL, 0, RenderPaintStyle::fill, 0.f); | |
| vennCell(CELL, CELL, RenderPaintStyle::stroke, 40.f); | |
| // Row 2: opaque horizontal gradient square under a vertical gradient | |
| // square that fades opaque red -> transparent yellow. Left srcOver, | |
| // right additive. | |
| auto gradientCell = [&](float ox, float overlayAdditiveness) { | |
| constexpr float oy = CELL * 2; | |
| AABB base = baseRect(ox, oy); | |
| ColorInt baseColors[2] = {0xff00ff00, 0xff0000ff}; | |
| Paint basePaint; | |
| basePaint->shader(factory->makeLinearGradient( | |
| base.left(), | |
| 0, | |
| base.right(), | |
| 0, | |
| baseColors, | |
| stops, | |
| 2)); | |
| renderer->drawPath(PathBuilder::Rect(base), basePaint); | |
| AABB overlay = overlayRect(ox, oy); | |
| ColorInt overlayColors[2] = {0xffff0000, 0x00ffff00}; | |
| Paint overlayPaint; | |
| overlayPaint->shader(factory->makeLinearGradient( | |
| 0, | |
| overlay.top(), | |
| 0, | |
| overlay.bottom(), | |
| overlayColors, | |
| stops, | |
| 2)); | |
| overlayPaint->additiveness(overlayAdditiveness); | |
| renderer->drawPath(PathBuilder::Rect(overlay), overlayPaint); | |
| }; | |
| gradientCell(0, /*overlayAdditiveness =*/0.f); | |
| gradientCell(CELL, /*overlayAdditiveness =*/1.f); | |
| // Row 3: image mesh under an overlapping image mesh whose opacity ramps | |
| // 1 -> 0 top to bottom. Left srcOver, right additive. | |
| auto img = LoadImage(assets::nomoon_png()); | |
| if (img != nullptr) | |
| { | |
| auto imageCell = [&](float ox, float overlayAdditiveness) { | |
| constexpr float oy = CELL * 3; | |
| draw_image_mesh_quad( | |
| renderer, | |
| img.get(), | |
| baseRect(ox, oy), | |
| {0.f, 0.f, 1.f, 1.f}, | |
| /*additiveness =*/0.f, | |
| 1.f); | |
| draw_image_mesh_alpha_ramp( | |
| renderer, | |
| img.get(), | |
| overlayRect(ox, oy), | |
| overlayAdditiveness); | |
| }; | |
| imageCell(0, /*overlayAdditiveness =*/0.f); | |
| imageCell(CELL, /*overlayAdditiveness =*/1.f); | |
| // Row 4: drawImage (image paint on a path): base image at full | |
| // opacity under an overlapping image at 60% opacity. Left srcOver, | |
| // right additive. | |
| auto drawImageRect = | |
| [&](const AABB& rect, float additiveness, float opacity) { | |
| renderer->save(); | |
| renderer->translate(rect.left(), rect.top()); | |
| renderer->scale( | |
| rect.width() / img->width(), | |
| rect.height() / img->height()); | |
| renderer->drawImage( | |
| img.get(), | |
| ImageSampler::LinearClamp(), | |
| BlendMode::srcOver, | |
| opacity, | |
| additiveness); | |
| renderer->restore(); | |
| }; | |
| auto drawImageCell = [&](float ox, float overlayAdditiveness) { | |
| constexpr float oy = CELL * 4; | |
| drawImageRect(baseRect(ox, oy), /*additiveness =*/0.f, 1.f); | |
| drawImageRect(overlayRect(ox, oy), overlayAdditiveness, .6f); | |
| }; | |
| drawImageCell(0, /*overlayAdditiveness =*/0.f); | |
| drawImageCell(CELL, /*overlayAdditiveness =*/1.f); | |
| } | |
| // Row 5: the additiveness slider. An opaque green square under five red | |
| // strips with additiveness 0, .25, .5, .75, 1 (srcOver to fully additive, | |
| // left to right). Left cell solid color, right cell gradient. | |
| auto additivenessSweepCell = [&](float ox, bool gradient) { | |
| constexpr float oy = CELL * 5; | |
| AABB base = baseRect(ox, oy); | |
| Paint basePaint; | |
| basePaint->color(0xff00ff00); | |
| renderer->drawPath(PathBuilder::Rect(base), basePaint); | |
| AABB overlay = overlayRect(ox, oy); | |
| constexpr int strips = 5; | |
| for (int i = 0; i < strips; ++i) | |
| { | |
| AABB strip = { | |
| overlay.left() + overlay.width() * i / strips, | |
| overlay.top(), | |
| overlay.left() + overlay.width() * (i + 1) / strips, | |
| overlay.bottom()}; | |
| Paint paint; | |
| if (gradient) | |
| { | |
| // Opaque red -> half-transparent red, top to bottom. | |
| ColorInt colors[2] = {0xffff0000, 0x80ff0000}; | |
| paint->shader(factory->makeLinearGradient( | |
| 0, | |
| strip.top(), | |
| 0, | |
| strip.bottom(), | |
| colors, | |
| stops, | |
| 2)); | |
| } | |
| else | |
| { | |
| paint->color(0xc0ff0000); | |
| } | |
| paint->additiveness(float(i) / (strips - 1)); | |
| renderer->drawPath(PathBuilder::Rect(strip), paint); | |
| } | |
| }; | |
| additivenessSweepCell(0, /*gradient =*/false); | |
| additivenessSweepCell(CELL, /*gradient =*/true); | |
| } |