| /* |
| * 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); |
| } |