Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/docs/paint/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const OpacityDemo = () => {
Sets the blend mode that is, the mode used to combine source color with destination color.
The following values are available: `clear`, `src`, `dst`, `srcOver`, `dstOver`, `srcIn`, `dstIn`, `srcOut`, `dstOut`,
`srcATop`, `dstATop`, `xor`, `plus`, `modulate`, `screen`, `overlay`, `darken`, `lighten`, `colorDodge`, `colorBurn`, `hardLight`,
`softLight`, `difference`, `exclusion`, `multiply`, `hue`, `saturation`, `color`, `luminosity`.
`softLight`, `difference`, `exclusion`, `multiply`, `hue`, `saturation`, `color`, `luminosity`, `plusDarker`, and `plusLighter`.

## style

Expand Down
2 changes: 1 addition & 1 deletion apps/remotion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"flubber": "^0.4.2",
"geometry-extrude": "^0.2.1",
"jest": "^29.3.1",
"lodash": "^4.17.21",
"lodash": "^4.17.23",
"normalize-svg-path": "^1.1.0",
"opentype.js": "^1.3.4",
"parse-svg-path": "^0.1.2",
Expand Down
96 changes: 96 additions & 0 deletions packages/skia/cpp/api/CustomBlendModes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#pragma once

#include "include/core/SkBlender.h"
#include "include/core/SkString.h"
#include "include/effects/SkRuntimeEffect.h"

namespace RNSkia {

// Custom blend mode values (must match TypeScript BlendMode enum)
constexpr int kBlendModePlusDarker = 1001;
constexpr int kBlendModePlusLighter = 1002;

// SkSL for PlusDarker blend mode
// Formula: rc = max(0, 1 - ((1-dst) + (1-src))) = max(0, src + dst - 1)
// This darkens the image by subtracting from white
inline const char *kPlusDarkerSkSL = R"(
vec4 main(vec4 src, vec4 dst) {
float outAlpha = src.a + dst.a - src.a * dst.a;
vec3 srcUnpremul = src.a > 0.0 ? src.rgb / src.a : vec3(0.0);
vec3 dstUnpremul = dst.a > 0.0 ? dst.rgb / dst.a : vec3(0.0);
vec3 blended = max(vec3(0.0), srcUnpremul + dstUnpremul - vec3(1.0));
return vec4(blended * outAlpha, outAlpha);
}
)";

// SkSL for PlusLighter blend mode
// Formula: rc = min(1, dst + src)
// This lightens the image by adding colors and clamping
inline const char *kPlusLighterSkSL = R"(
vec4 main(vec4 src, vec4 dst) {
float outAlpha = src.a + dst.a - src.a * dst.a;
vec3 srcUnpremul = src.a > 0.0 ? src.rgb / src.a : vec3(0.0);
vec3 dstUnpremul = dst.a > 0.0 ? dst.rgb / dst.a : vec3(0.0);
vec3 blended = min(vec3(1.0), srcUnpremul + dstUnpremul);
return vec4(blended * outAlpha, outAlpha);
}
)";

// Singleton class to cache custom blenders
class CustomBlenders {
public:
static CustomBlenders &getInstance() {
static CustomBlenders instance;
return instance;
}

sk_sp<SkBlender> getPlusDarkerBlender() {
if (!_plusDarkerBlender) {
auto [effect, err] =
SkRuntimeEffect::MakeForBlender(SkString(kPlusDarkerSkSL));
if (effect) {
_plusDarkerBlender = effect->makeBlender(nullptr);
}
}
return _plusDarkerBlender;
}

sk_sp<SkBlender> getPlusLighterBlender() {
if (!_plusLighterBlender) {
auto [effect, err] =
SkRuntimeEffect::MakeForBlender(SkString(kPlusLighterSkSL));
if (effect) {
_plusLighterBlender = effect->makeBlender(nullptr);
}
}
return _plusLighterBlender;
}

private:
CustomBlenders() = default;
~CustomBlenders() = default;
CustomBlenders(const CustomBlenders &) = delete;
CustomBlenders &operator=(const CustomBlenders &) = delete;

sk_sp<SkBlender> _plusDarkerBlender;
sk_sp<SkBlender> _plusLighterBlender;
};

// Helper function to apply custom blend mode to a paint
inline void applyBlendMode(SkPaint &paint, int blendModeValue) {
if (blendModeValue == kBlendModePlusDarker) {
auto blender = CustomBlenders::getInstance().getPlusDarkerBlender();
if (blender) {
paint.setBlender(blender);
}
} else if (blendModeValue == kBlendModePlusLighter) {
auto blender = CustomBlenders::getInstance().getPlusLighterBlender();
if (blender) {
paint.setBlender(blender);
}
} else {
paint.setBlendMode(static_cast<SkBlendMode>(blendModeValue));
}
}

} // namespace RNSkia
5 changes: 3 additions & 2 deletions packages/skia/cpp/api/JsiSkPaint.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <jsi/jsi.h>

#include "CustomBlendModes.h"
#include "JsiSkColor.h"
#include "JsiSkColorFilter.h"
#include "JsiSkHostObjects.h"
Expand Down Expand Up @@ -126,8 +127,8 @@ class JsiSkPaint : public JsiSkWrappingSharedPtrHostObject<SkPaint> {
}

JSI_HOST_FUNCTION(setBlendMode) {
auto blendMode = (SkBlendMode)arguments[0].asNumber();
getObject()->setBlendMode(blendMode);
int blendModeValue = static_cast<int>(arguments[0].asNumber());
applyBlendMode(*getObject(), blendModeValue);
return jsi::Value::undefined();
}

Expand Down
86 changes: 86 additions & 0 deletions packages/skia/cpp/api/recorder/Convertor.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <modules/skparagraph/include/ParagraphBuilder.h>
#include <modules/skparagraph/include/ParagraphStyle.h>

#include "../CustomBlendModes.h"
#include "third_party/CSSColorParser.h"

#include "DataTypes.h"
Expand Down Expand Up @@ -666,6 +667,85 @@ SkPath1DPathEffect::Style getPropertyValue(jsi::Runtime &runtime,
throw std::runtime_error("Invalid prop value for Path1DEffectStyle received");
}

// Wrapper type for blend mode to avoid conflict with int template specialization
struct BlendModeValue {
int value;
BlendModeValue(int v = 0) : value(v) {}
operator int() const { return value; }
};

template <>
BlendModeValue getPropertyValue(jsi::Runtime &runtime, const jsi::Value &val) {
if (val.isString()) {
auto value = val.asString(runtime).utf8(runtime);
if (value == "clear") {
return BlendModeValue(static_cast<int>(SkBlendMode::kClear));
} else if (value == "src") {
return BlendModeValue(static_cast<int>(SkBlendMode::kSrc));
} else if (value == "dst") {
return BlendModeValue(static_cast<int>(SkBlendMode::kDst));
} else if (value == "srcOver") {
return BlendModeValue(static_cast<int>(SkBlendMode::kSrcOver));
} else if (value == "dstOver") {
return BlendModeValue(static_cast<int>(SkBlendMode::kDstOver));
} else if (value == "srcIn") {
return BlendModeValue(static_cast<int>(SkBlendMode::kSrcIn));
} else if (value == "dstIn") {
return BlendModeValue(static_cast<int>(SkBlendMode::kDstIn));
} else if (value == "srcOut") {
return BlendModeValue(static_cast<int>(SkBlendMode::kSrcOut));
} else if (value == "dstOut") {
return BlendModeValue(static_cast<int>(SkBlendMode::kDstOut));
} else if (value == "srcATop") {
return BlendModeValue(static_cast<int>(SkBlendMode::kSrcATop));
} else if (value == "dstATop") {
return BlendModeValue(static_cast<int>(SkBlendMode::kDstATop));
} else if (value == "xor") {
return BlendModeValue(static_cast<int>(SkBlendMode::kXor));
} else if (value == "plus") {
return BlendModeValue(static_cast<int>(SkBlendMode::kPlus));
} else if (value == "modulate") {
return BlendModeValue(static_cast<int>(SkBlendMode::kModulate));
} else if (value == "screen") {
return BlendModeValue(static_cast<int>(SkBlendMode::kScreen));
} else if (value == "overlay") {
return BlendModeValue(static_cast<int>(SkBlendMode::kOverlay));
} else if (value == "darken") {
return BlendModeValue(static_cast<int>(SkBlendMode::kDarken));
} else if (value == "lighten") {
return BlendModeValue(static_cast<int>(SkBlendMode::kLighten));
} else if (value == "colorDodge") {
return BlendModeValue(static_cast<int>(SkBlendMode::kColorDodge));
} else if (value == "colorBurn") {
return BlendModeValue(static_cast<int>(SkBlendMode::kColorBurn));
} else if (value == "hardLight") {
return BlendModeValue(static_cast<int>(SkBlendMode::kHardLight));
} else if (value == "softLight") {
return BlendModeValue(static_cast<int>(SkBlendMode::kSoftLight));
} else if (value == "difference") {
return BlendModeValue(static_cast<int>(SkBlendMode::kDifference));
} else if (value == "exclusion") {
return BlendModeValue(static_cast<int>(SkBlendMode::kExclusion));
} else if (value == "multiply") {
return BlendModeValue(static_cast<int>(SkBlendMode::kMultiply));
} else if (value == "hue") {
return BlendModeValue(static_cast<int>(SkBlendMode::kHue));
} else if (value == "saturation") {
return BlendModeValue(static_cast<int>(SkBlendMode::kSaturation));
} else if (value == "color") {
return BlendModeValue(static_cast<int>(SkBlendMode::kColor));
} else if (value == "luminosity") {
return BlendModeValue(static_cast<int>(SkBlendMode::kLuminosity));
} else if (value == "plusDarker") {
return BlendModeValue(kBlendModePlusDarker);
} else if (value == "plusLighter") {
return BlendModeValue(kBlendModePlusLighter);
}
}
throw std::runtime_error("Invalid prop value for BlendMode received");
}

// Keep SkBlendMode specialization for other usages (Shaders, ImageFilters, ColorFilters, Drawings)
template <>
SkBlendMode getPropertyValue(jsi::Runtime &runtime, const jsi::Value &val) {
if (val.isString()) {
Expand Down Expand Up @@ -1156,6 +1236,12 @@ std::optional<SkColor> getPropertyValue(jsi::Runtime &runtime,
return makeOptionalPropertyValue<SkColor>(runtime, value);
}

template <>
std::optional<BlendModeValue> getPropertyValue(jsi::Runtime &runtime,
const jsi::Value &value) {
return makeOptionalPropertyValue<BlendModeValue>(runtime, value);
}

template <>
std::optional<SkBlendMode> getPropertyValue(jsi::Runtime &runtime,
const jsi::Value &value) {
Expand Down
5 changes: 3 additions & 2 deletions packages/skia/cpp/api/recorder/Paint.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <string>
#include <variant>

#include "../CustomBlendModes.h"
#include "Command.h"
#include "Convertor.h"
#include "DrawingCtx.h"
Expand Down Expand Up @@ -110,7 +111,7 @@ class SaveCTMCmd : public Command {

struct PaintCmdProps {
std::optional<SkColor> color;
std::optional<SkBlendMode> blendMode;
std::optional<BlendModeValue> blendMode;
std::optional<SkPaint::Style> style;
std::optional<SkPaint::Join> strokeJoin;
std::optional<SkPaint::Cap> strokeCap;
Expand Down Expand Up @@ -169,7 +170,7 @@ class SavePaintCmd : public Command {
paint.setColor(props.color.value());
}
if (props.blendMode.has_value()) {
paint.setBlendMode(props.blendMode.value());
applyBlendMode(paint, props.blendMode.value().value);
}
if (props.style.has_value()) {
paint.setStyle(props.style.value());
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions packages/skia/src/renderer/__tests__/e2e/Paint.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,96 @@ describe("Paint", () => {
);
checkImage(result, docPath("paint/test-paint.png"));
});
it("should apply PlusDarker blend mode", async () => {
const { width, height } = surface;
const r = width / 4;
const result = await surface.draw(
<Group>
<Fill color="white" />
<Circle cx={width / 2 - r / 2} cy={height / 2} r={r} color="red" />
<Circle
cx={width / 2 + r / 2}
cy={height / 2}
r={r}
color="cyan"
blendMode="plusDarker"
/>
</Group>
);
checkImage(result, "snapshots/paint/plus-darker.png");
});
it("should apply PlusLighter blend mode", async () => {
const { width, height } = surface;
const r = width / 4;
const result = await surface.draw(
<Group>
<Fill color="black" />
<Circle cx={width / 2 - r / 2} cy={height / 2} r={r} color="red" />
<Circle
cx={width / 2 + r / 2}
cy={height / 2}
r={r}
color="cyan"
blendMode="plusLighter"
/>
</Group>
);
checkImage(result, "snapshots/paint/plus-lighter.png");
});
it("should apply PlusDarker blend mode with overlapping RGB circles", async () => {
const { width, height } = surface;
const r = width / 4;
const cx = width / 2;
const cy = height / 2;
const offset = r / 2;
const result = await surface.draw(
<Group>
<Fill color="white" />
<Circle cx={cx} cy={cy - offset} r={r} color="red" />
<Circle
cx={cx - offset}
cy={cy + offset}
r={r}
color="green"
blendMode="plusLighter"
/>
<Circle
cx={cx + offset}
cy={cy + offset}
r={r}
color="blue"
blendMode="plusLighter"
/>
</Group>
);
checkImage(result, "snapshots/paint/plus-darker-rgb.png");
});
it("should apply PlusLighter blend mode with overlapping RGB circles", async () => {
const { width, height } = surface;
const r = width / 4;
const cx = width / 2;
const cy = height / 2;
const offset = r / 2;
const result = await surface.draw(
<Group>
<Fill color="black" />
<Circle cx={cx} cy={cy - offset} r={r} color="red" />
<Circle
cx={cx - offset}
cy={cy + offset}
r={r}
color="green"
blendMode="plusLighter"
/>
<Circle
cx={cx + offset}
cy={cy + offset}
r={r}
color="blue"
blendMode="plusLighter"
/>
</Group>
);
checkImage(result, "snapshots/paint/plus-lighter-rgb.png");
});
});
4 changes: 4 additions & 0 deletions packages/skia/src/skia/types/Paint/BlendMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ export enum BlendMode {
//!< destination
Color, //!< hue and saturation of source with luminosity of destination
Luminosity, //!< luminosity of source with hue and saturation of

// Custom blend modes (implemented via SkRuntimeEffect blenders)
PlusDarker = 1001, //!< rc = max(0, 1 - ((1-dst) + (1-src))), similar to iOS kCGBlendModePlusDarker
PlusLighter = 1002, //!< rc = min(1, dst + src), similar to iOS kCGBlendModePlusLighter
}
Loading
Loading