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) + ) } } }