diff --git a/app/src/main/java/com/example/cahier/developer/brushgraph/data/DisplayUtils.kt b/app/src/main/java/com/example/cahier/developer/brushgraph/data/DisplayUtils.kt
index 731d0e4a..53be4cfa 100644
--- a/app/src/main/java/com/example/cahier/developer/brushgraph/data/DisplayUtils.kt
+++ b/app/src/main/java/com/example/cahier/developer/brushgraph/data/DisplayUtils.kt
@@ -21,6 +21,7 @@ import com.example.cahier.developer.brushdesigner.ui.NumericLimits
import ink.proto.BrushBehavior as ProtoBrushBehavior
import ink.proto.BrushFamily as ProtoBrushFamily
import ink.proto.BrushPaint as ProtoBrushPaint
+import ink.proto.ColorFunction as ProtoColorFunction
import ink.proto.PredefinedEasingFunction as ProtoPredefinedEasingFunction
import ink.proto.StepPosition as ProtoStepPosition
@@ -517,3 +518,18 @@ fun ProtoBrushFamily.InputModel.displayStringRId(): Int =
hasPassthroughModel() -> R.string.bg_model_passthrough
else -> R.string.bg_unknown_model
}
+
+fun ProtoColorFunction.displayStringRId(): Int =
+ if (this.hasOpacityMultiplier()) {
+ R.string.bg_target_opacity_multiplier
+ } else if (this.hasReplaceColor()) {
+ R.string.bg_replace_color
+ } else if (this.hasHueOffsetRadians()) {
+ R.string.bg_target_hue_offset
+ } else if (this.hasSaturationMultiplier()) {
+ R.string.bg_target_saturation_multiplier
+ } else if (this.hasLuminosityOffset()) {
+ R.string.bg_target_luminosity_offset
+ } else {
+ R.string.bg_node_unknown
+ }
diff --git a/app/src/main/java/com/example/cahier/developer/brushgraph/data/GraphDataModel.kt b/app/src/main/java/com/example/cahier/developer/brushgraph/data/GraphDataModel.kt
index 61bedd23..9c326c9f 100644
--- a/app/src/main/java/com/example/cahier/developer/brushgraph/data/GraphDataModel.kt
+++ b/app/src/main/java/com/example/cahier/developer/brushgraph/data/GraphDataModel.kt
@@ -183,13 +183,7 @@ sealed interface NodeData {
override fun title() = R.string.bg_color_function
override fun subtitles() =
- listOf(
- if (function.hasOpacityMultiplier()) {
- DisplayText.Resource(R.string.bg_opacity_multiplier)
- } else {
- DisplayText.Resource(R.string.bg_replace_color)
- }
- )
+ listOf(DisplayText.Resource(function.displayStringRId()))
}
/** Wraps a [ProtoBrushBehavior.Node] */
diff --git a/app/src/main/java/com/example/cahier/developer/brushgraph/ui/Tooltips.kt b/app/src/main/java/com/example/cahier/developer/brushgraph/ui/Tooltips.kt
index 3c57ba80..6823f79c 100644
--- a/app/src/main/java/com/example/cahier/developer/brushgraph/ui/Tooltips.kt
+++ b/app/src/main/java/com/example/cahier/developer/brushgraph/ui/Tooltips.kt
@@ -220,7 +220,10 @@ fun getInputModelTooltip(resId: Int): Int = when (resId) {
}
fun getColorFunctionTooltip(resId: Int): Int = when (resId) {
- R.string.bg_opacity_multiplier -> R.string.bg_tooltip_color_func_opacity
+ R.string.bg_target_opacity_multiplier -> R.string.bg_tooltip_color_func_opacity
R.string.bg_replace_color -> R.string.bg_tooltip_color_func_replace
+ R.string.bg_target_luminosity_offset -> R.string.bg_tooltip_color_func_luminosity
+ R.string.bg_target_hue_offset -> R.string.bg_tooltip_color_func_hue
+ R.string.bg_target_saturation_multiplier -> R.string.bg_tooltip_color_func_saturation
else -> R.string.bg_tooltip_color_func_default
}
diff --git a/app/src/main/java/com/example/cahier/developer/brushgraph/ui/fields/ColorFunctionNodeFields.kt b/app/src/main/java/com/example/cahier/developer/brushgraph/ui/fields/ColorFunctionNodeFields.kt
index 80692eb3..8c34f2c3 100644
--- a/app/src/main/java/com/example/cahier/developer/brushgraph/ui/fields/ColorFunctionNodeFields.kt
+++ b/app/src/main/java/com/example/cahier/developer/brushgraph/ui/fields/ColorFunctionNodeFields.kt
@@ -39,6 +39,7 @@ import com.example.cahier.developer.brushdesigner.ui.EnumDropdown
import com.example.cahier.developer.brushdesigner.ui.NumericField
import com.example.cahier.developer.brushdesigner.ui.NumericLimits
import com.example.cahier.developer.brushgraph.data.NodeData
+import com.example.cahier.developer.brushgraph.data.displayStringRId
import com.example.cahier.developer.brushgraph.ui.FieldWithTooltip
import com.example.cahier.developer.brushgraph.ui.getColorFunctionTooltip
import ink.proto.Color as ProtoColor
@@ -46,18 +47,14 @@ import ink.proto.ColorFunction as ProtoColorFunction
@Composable
fun ColorFunctionNodeFields(
- function: ProtoColorFunction,
- onUpdate: (NodeData) -> Unit,
- onChooseColor: (Color, (Color) -> Unit) -> Unit,
- onDropdownEditComplete: () -> Unit,
- onFieldEditComplete: () -> Unit,
- modifier: Modifier = Modifier,
+ function: ProtoColorFunction,
+ onUpdate: (NodeData) -> Unit,
+ onChooseColor: (Color, (Color) -> Unit) -> Unit,
+ onDropdownEditComplete: () -> Unit,
+ onFieldEditComplete: () -> Unit,
+ modifier: Modifier = Modifier,
) {
- val currentTypeResId = if (function.hasOpacityMultiplier()) {
- R.string.bg_opacity_multiplier
- } else {
- R.string.bg_replace_color
- }
+ val currentTypeResId = function.displayStringRId()
Column(modifier = modifier) {
FieldWithTooltip(
@@ -70,28 +67,54 @@ fun ColorFunctionNodeFields(
EnumDropdown(
label = stringResource(R.string.bg_function_type),
currentValue = currentTypeResId,
- values = listOf(R.string.bg_opacity_multiplier, R.string.bg_replace_color),
+ values = listOf(
+ R.string.bg_target_opacity_multiplier,
+ R.string.bg_replace_color,
+ R.string.bg_target_hue_offset,
+ R.string.bg_target_saturation_multiplier,
+ R.string.bg_target_luminosity_offset
+ ),
displayName = { stringResource(it) },
onSelected = { resId ->
if (resId != currentTypeResId) {
onUpdate(
- if (resId == R.string.bg_opacity_multiplier) {
- NodeData.ColorFunction(
- ProtoColorFunction.newBuilder().setOpacityMultiplier(1f).build()
- )
- } else {
- NodeData.ColorFunction(
- ProtoColorFunction.newBuilder()
- .setReplaceColor(
- ProtoColor.newBuilder()
- .setRed(0f)
- .setGreen(0f)
- .setBlue(0f)
- .setAlpha(1f)
- .build()
- )
- .build()
- )
+ when (resId) {
+ R.string.bg_target_opacity_multiplier -> {
+ NodeData.ColorFunction(
+ ProtoColorFunction.newBuilder().setOpacityMultiplier(1f)
+ .build()
+ )
+ }
+
+ R.string.bg_replace_color -> {
+ NodeData.ColorFunction(
+ ProtoColorFunction.newBuilder().setReplaceColor(
+ ProtoColor.newBuilder().setRed(0f).setGreen(0f)
+ .setBlue(0f).setAlpha(1f).build()
+ ).build()
+ )
+ }
+
+ R.string.bg_target_hue_offset -> {
+ NodeData.ColorFunction(
+ ProtoColorFunction.newBuilder().setHueOffsetRadians(0f)
+ .build()
+ )
+ }
+
+ R.string.bg_target_saturation_multiplier -> {
+ NodeData.ColorFunction(
+ ProtoColorFunction.newBuilder().setSaturationMultiplier(1f)
+ .build()
+ )
+ }
+
+ else -> { // bg_target_luminosity_offset
+ NodeData.ColorFunction(
+ ProtoColorFunction.newBuilder().setLuminosityOffset(0f)
+ .build()
+ )
+ }
}
)
}
@@ -104,7 +127,7 @@ fun ColorFunctionNodeFields(
NumericField(
title = stringResource(R.string.bg_label_opacity_multiplier),
value = function.opacityMultiplier,
- limits = NumericLimits(0f, 2f, 0.01f),
+ limits = NumericLimits(0f, 2f, 0.01f, "x"),
onValueChanged = {
onUpdate(
NodeData.ColorFunction(
@@ -114,6 +137,48 @@ fun ColorFunctionNodeFields(
},
onValueChangeFinished = onFieldEditComplete
)
+ } else if (function.hasSaturationMultiplier()) {
+ NumericField(
+ title = stringResource(R.string.bg_label_saturation_multiplier),
+ value = function.saturationMultiplier,
+ limits = NumericLimits(0f, 2f, 0.01f, "x"),
+ onValueChanged = {
+ onUpdate(
+ NodeData.ColorFunction(
+ function.toBuilder().setSaturationMultiplier(it).build()
+ )
+ )
+ },
+ onValueChangeFinished = onFieldEditComplete
+ )
+ } else if (function.hasHueOffsetRadians()) {
+ NumericField(
+ title = stringResource(R.string.bg_label_hue_offset),
+ value = function.hueOffsetRadians,
+ limits = NumericLimits.radiansShownAsDegrees(-360f, 360f),
+ onValueChanged = {
+ onUpdate(
+ NodeData.ColorFunction(
+ function.toBuilder().setHueOffsetRadians(it).build()
+ )
+ )
+ },
+ onValueChangeFinished = onFieldEditComplete
+ )
+ } else if (function.hasLuminosityOffset()) {
+ NumericField(
+ title = stringResource(R.string.bg_label_luminosity_offset),
+ value = function.luminosityOffset,
+ limits = NumericLimits.floatShownAsPercent(-100f, 100f),
+ onValueChanged = {
+ onUpdate(
+ NodeData.ColorFunction(
+ function.toBuilder().setLuminosityOffset(it).build()
+ )
+ )
+ },
+ onValueChangeFinished = onFieldEditComplete
+ )
} else if (function.hasReplaceColor()) {
val color = function.replaceColor
val composeColor =
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1f97fe58..cf21cc73 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -497,7 +497,6 @@
Angle
Mag
- opacity multiplier
replace color
unknown
@@ -661,6 +660,10 @@
Out
Step Count
Opacity Multiplier
+ Hue Offset
+ Saturation Multiplier
+ Luminosity Offset
+
Defines the geometric shape and size of the brush tip. This shape acts as a cross-section that is repeated or extruded along the path to create the stroke mesh.
@@ -837,8 +840,11 @@
Selects the model used to smooth raw hardware inputs.
- Multiplies the opacity of the stroke by a calculated value, allowing for dynamic transparency effects.
+ Multiplies the opacity of the stroke by the specified value.
Replaces the brush color with a specified color, ignoring the baseline color for this function\'s output.
+ Offsets the hue of the stroke by the specified value.
+ Multiplies the saturation of the stroke by the specified value.
+ Offsets the luminosity of the stroke by the specified value.
Selects the type of color function to apply.
Welcome!
diff --git a/app/src/test/java/com/example/cahier/developer/brushgraph/ui/TooltipsTest.kt b/app/src/test/java/com/example/cahier/developer/brushgraph/ui/TooltipsTest.kt
index 41556a75..2ca13776 100644
--- a/app/src/test/java/com/example/cahier/developer/brushgraph/ui/TooltipsTest.kt
+++ b/app/src/test/java/com/example/cahier/developer/brushgraph/ui/TooltipsTest.kt
@@ -15,14 +15,14 @@
*/
package com.example.cahier.developer.brushgraph.ui
+import com.example.cahier.R
import com.example.cahier.developer.brushgraph.data.NodeData
import ink.proto.BrushBehavior
import ink.proto.BrushPaint
-import ink.proto.PredefinedEasingFunction as ProtoPredefinedEasingFunction
-import ink.proto.StepPosition as ProtoStepPosition
-import com.example.cahier.R
import org.junit.Assert.assertTrue
import org.junit.Test
+import ink.proto.PredefinedEasingFunction as ProtoPredefinedEasingFunction
+import ink.proto.StepPosition as ProtoStepPosition
class TooltipsTest {
@@ -38,19 +38,54 @@ class TooltipsTest {
NodeData.ColorFunction(ink.proto.ColorFunction.getDefaultInstance()),
NodeData.Coat(),
NodeData.Family(),
-
+
// Behavior nodes
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setSourceNode(BrushBehavior.SourceNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setConstantNode(BrushBehavior.ConstantNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setNoiseNode(BrushBehavior.NoiseNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setToolTypeFilterNode(BrushBehavior.ToolTypeFilterNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setDampingNode(BrushBehavior.DampingNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setResponseNode(BrushBehavior.ResponseNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setBinaryOpNode(BrushBehavior.BinaryOpNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setInterpolationNode(BrushBehavior.InterpolationNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setIntegralNode(BrushBehavior.IntegralNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setTargetNode(BrushBehavior.TargetNode.getDefaultInstance()).build()),
- NodeData.Behavior(BrushBehavior.Node.newBuilder().setPolarTargetNode(BrushBehavior.PolarTargetNode.getDefaultInstance()).build())
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setSourceNode(BrushBehavior.SourceNode.getDefaultInstance()).build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setConstantNode(BrushBehavior.ConstantNode.getDefaultInstance()).build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setNoiseNode(BrushBehavior.NoiseNode.getDefaultInstance()).build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setToolTypeFilterNode(BrushBehavior.ToolTypeFilterNode.getDefaultInstance())
+ .build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setDampingNode(BrushBehavior.DampingNode.getDefaultInstance()).build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setResponseNode(BrushBehavior.ResponseNode.getDefaultInstance()).build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setBinaryOpNode(BrushBehavior.BinaryOpNode.getDefaultInstance()).build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setInterpolationNode(BrushBehavior.InterpolationNode.getDefaultInstance())
+ .build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setIntegralNode(BrushBehavior.IntegralNode.getDefaultInstance()).build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setTargetNode(BrushBehavior.TargetNode.getDefaultInstance()).build()
+ ),
+ NodeData.Behavior(
+ BrushBehavior.Node.newBuilder()
+ .setPolarTargetNode(BrushBehavior.PolarTargetNode.getDefaultInstance()).build()
+ )
)
for (node in nodes) {
@@ -65,7 +100,10 @@ class TooltipsTest {
for (value in BrushBehavior.Source.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for Source.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for Source.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -75,7 +113,10 @@ class TooltipsTest {
for (value in BrushBehavior.Target.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for Target.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for Target.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -95,7 +136,10 @@ class TooltipsTest {
for (value in BrushPaint.TextureLayer.SizeUnit.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for SizeUnit.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for SizeUnit.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -105,7 +149,10 @@ class TooltipsTest {
for (value in BrushPaint.TextureLayer.Origin.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for Origin.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for Origin.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -115,7 +162,10 @@ class TooltipsTest {
for (value in BrushPaint.TextureLayer.Mapping.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for Mapping.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for Mapping.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -125,7 +175,10 @@ class TooltipsTest {
for (value in BrushPaint.TextureLayer.BlendMode.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for BlendMode.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for BlendMode.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -135,7 +188,10 @@ class TooltipsTest {
for (value in BrushPaint.SelfOverlap.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for SelfOverlap.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for SelfOverlap.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -145,7 +201,10 @@ class TooltipsTest {
for (value in BrushBehavior.PolarTarget.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for PolarTarget.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for PolarTarget.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -155,7 +214,10 @@ class TooltipsTest {
for (value in BrushBehavior.OutOfRange.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for OutOfRange.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for OutOfRange.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -165,7 +227,10 @@ class TooltipsTest {
for (value in BrushBehavior.BinaryOp.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for BinaryOp.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for BinaryOp.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -175,7 +240,10 @@ class TooltipsTest {
for (value in BrushBehavior.ProgressDomain.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for ProgressDomain.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for ProgressDomain.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -185,7 +253,10 @@ class TooltipsTest {
for (value in BrushBehavior.Interpolation.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for Interpolation.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for Interpolation.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -195,7 +266,10 @@ class TooltipsTest {
for (value in ProtoPredefinedEasingFunction.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for ProtoPredefinedEasingFunction.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for ProtoPredefinedEasingFunction.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -205,7 +279,10 @@ class TooltipsTest {
for (value in ProtoStepPosition.values()) {
if (value.name == "UNRECOGNIZED") continue
val tooltip = value.getTooltip()
- assertTrue("Tooltip should be unique for ProtoStepPosition.$value: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for ProtoStepPosition.$value: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
@@ -218,19 +295,29 @@ class TooltipsTest {
)
for (modelResId in models) {
val tooltip = getInputModelTooltip(modelResId)
- assertTrue("Tooltip should be unique for InputModel.$modelResId: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for InputModel.$modelResId: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
+
@Test
fun colorFunctionTooltips_checked_areUnique() {
val tooltips = mutableSetOf()
val options = arrayOf(
- R.string.bg_opacity_multiplier,
- R.string.bg_replace_color
+ R.string.bg_target_opacity_multiplier,
+ R.string.bg_replace_color,
+ R.string.bg_target_saturation_multiplier,
+ R.string.bg_target_hue_offset,
+ R.string.bg_target_luminosity_offset
)
for (optionResId in options) {
val tooltip = getColorFunctionTooltip(optionResId)
- assertTrue("Tooltip should be unique for ColorFunction.$optionResId: $tooltip", tooltips.add(tooltip))
+ assertTrue(
+ "Tooltip should be unique for ColorFunction.$optionResId: $tooltip",
+ tooltips.add(tooltip)
+ )
}
}
}