Editor/Prism/Nodes/ColorNodes.cs

Editor-side nodes for Prism color operations and helpers. Defines color-space enums, a collection of shader helper functions (HLSL snippets) for conversions and adjustments, and many PrismNode-derived node classes that emit shader IR calls to those helpers (conversion, adjustments, utility nodes, and a color ramp texture sampler).

Native Interop
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;

namespace Editor.Prism.Nodes;

// ---------------------------------------------------------------------------------------------------
// Colour.
//
// Two rules run through this whole family. First, everything is linear unless a node says otherwise:
// the engine shades in linear space, so a colour correction that assumes sRGB will look wrong in shadow
// and right in daylight, which is the worst way for a bug to present. Second, nothing here uses a
// vector ternary — HLSL 2021 and Slang both require a scalar condition, so every per-channel branch is
// written with step() and lerp().
// ---------------------------------------------------------------------------------------------------

/// <summary>Whether a hue offset is authored as a fraction of the wheel, in radians, or in degrees.</summary>
public enum PrismHueUnit
{
	/// <summary>0..1 around the wheel.</summary>
	Normalized,
	/// <summary>Radians.</summary>
	Radians,
	/// <summary>Degrees.</summary>
	Degrees
}

/// <summary>The colour models a conversion node can move between.</summary>
public enum PrismColorSpace
{
	/// <summary>Linear RGB, which is what the engine shades in.</summary>
	Linear,
	/// <summary>sRGB-encoded RGB, which is what a texture or a colour picker holds.</summary>
	Srgb,
	/// <summary>Hue, saturation, value.</summary>
	Hsv,
	/// <summary>Hue, saturation, lightness.</summary>
	Hsl
}

/// <summary>The colour bodies.</summary>
internal static class PrismColorHelpers
{
	/// <summary>Linear RGB to hue, saturation and value.</summary>
	internal static readonly HelperFunction RgbToHsv = new( "Prism_RgbToHsv", ShaderType.Float3,
		[new HelperParam( "c", ShaderType.Float3 )] )
	{
		Hlsl = """
			float3 Prism_RgbToHsv( float3 c )
			{
				const float4 K = float4( 0.0f, -1.0f / 3.0f, 2.0f / 3.0f, -1.0f );

				float4 p = lerp( float4( c.bg, K.wz ), float4( c.gb, K.xy ), step( c.b, c.g ) );
				float4 q = lerp( float4( p.xyw, c.r ), float4( c.r, p.yzx ), step( p.x, c.r ) );

				float d = q.x - min( q.w, q.y );
				const float e = 1.0e-10f;

				return float3( abs( q.z + ( q.w - q.y ) / ( 6.0f * d + e ) ), d / ( q.x + e ), q.x );
			}
			"""
	};

	/// <summary>Hue, saturation and value back to linear RGB.</summary>
	internal static readonly HelperFunction HsvToRgb = new( "Prism_HsvToRgb", ShaderType.Float3,
		[new HelperParam( "c", ShaderType.Float3 )] )
	{
		Hlsl = """
			float3 Prism_HsvToRgb( float3 c )
			{
				const float4 K = float4( 1.0f, 2.0f / 3.0f, 1.0f / 3.0f, 3.0f );

				float3 p = abs( frac( c.xxx + K.xyz ) * 6.0f - K.www );

				return c.z * lerp( K.xxx, clamp( p - K.xxx, 0.0f, 1.0f ), c.y );
			}
			"""
	};

	/// <summary>Linear RGB to hue, saturation and lightness.</summary>
	internal static readonly HelperFunction RgbToHsl = new( "Prism_RgbToHsl", ShaderType.Float3,
		[new HelperParam( "c", ShaderType.Float3 )] )
	{
		Hlsl = """
			float3 Prism_RgbToHsl( float3 c )
			{
				float flMax = max( c.r, max( c.g, c.b ) );
				float flMin = min( c.r, min( c.g, c.b ) );
				float d = flMax - flMin;

				float h = 0.0f;

				if ( d > 1e-6f )
				{
					if ( flMax == c.r )      h = frac( ( c.g - c.b ) / d / 6.0f );
					else if ( flMax == c.g ) h = ( ( c.b - c.r ) / d + 2.0f ) / 6.0f;
					else                     h = ( ( c.r - c.g ) / d + 4.0f ) / 6.0f;
				}

				float l = ( flMax + flMin ) * 0.5f;
				float s = ( d < 1e-6f ) ? 0.0f : d / ( 1.0f - abs( 2.0f * l - 1.0f ) + 1e-6f );

				return float3( h, s, l );
			}
			"""
	};

	/// <summary>Hue, saturation and lightness back to linear RGB.</summary>
	internal static readonly HelperFunction HslToRgb = new( "Prism_HslToRgb", ShaderType.Float3,
		[new HelperParam( "hsl", ShaderType.Float3 )] )
	{
		Hlsl = """
			float3 Prism_HslToRgb( float3 hsl )
			{
				float c = ( 1.0f - abs( 2.0f * hsl.z - 1.0f ) ) * hsl.y;
				float h = frac( hsl.x ) * 6.0f;
				float x = c * ( 1.0f - abs( fmod( h, 2.0f ) - 1.0f ) );
				float m = hsl.z - c * 0.5f;

				float3 rgb;

				if      ( h < 1.0f ) rgb = float3( c, x, 0.0f );
				else if ( h < 2.0f ) rgb = float3( x, c, 0.0f );
				else if ( h < 3.0f ) rgb = float3( 0.0f, c, x );
				else if ( h < 4.0f ) rgb = float3( 0.0f, x, c );
				else if ( h < 5.0f ) rgb = float3( x, 0.0f, c );
				else                 rgb = float3( c, 0.0f, x );

				return rgb + m;
			}
			"""
	};

	/// <summary>sRGB-encoded values to linear, with the exact piecewise transfer function.</summary>
	internal static readonly HelperFunction SrgbToLinear = new( "Prism_SrgbToLinear", ShaderType.Float3,
		[new HelperParam( "c", ShaderType.Float3 )] )
	{
		Hlsl = """
			float3 Prism_SrgbToLinear( float3 c )
			{
				float3 vLow = c / 12.92f;
				float3 vHigh = pow( max( c + 0.055f, 0.0f ) / 1.055f, 2.4f );

				return lerp( vLow, vHigh, step( 0.04045f, c ) );
			}
			"""
	};

	/// <summary>Linear values to sRGB encoding.</summary>
	internal static readonly HelperFunction LinearToSrgb = new( "Prism_LinearToSrgb", ShaderType.Float3,
		[new HelperParam( "c", ShaderType.Float3 )] )
	{
		Hlsl = """
			float3 Prism_LinearToSrgb( float3 c )
			{
				float3 vLow = c * 12.92f;
				float3 vHigh = 1.055f * pow( max( c, 0.0f ), 1.0f / 2.4f ) - 0.055f;

				return lerp( vLow, vHigh, step( 0.0031308f, c ) );
			}
			"""
	};

	/// <summary>Contrast about the perceptual mid-point.</summary>
	internal static readonly HelperFunction Contrast = new( "Prism_Contrast", ShaderType.Float3,
		[
			new HelperParam( "vColor", ShaderType.Float3 ),
			new HelperParam( "flContrast", ShaderType.Float )
		] )
	{
		Hlsl = """
			float3 Prism_Contrast( float3 vColor, float flContrast )
			{
				const float flMidpoint = 0.21763764f;

				return ( vColor - flMidpoint ) * flContrast + flMidpoint;
			}
			"""
	};

	/// <summary>Saturation about the Rec.709 luma of the colour.</summary>
	internal static readonly HelperFunction Saturation = new( "Prism_Saturation", ShaderType.Float3,
		[
			new HelperParam( "vColor", ShaderType.Float3 ),
			new HelperParam( "flSaturation", ShaderType.Float )
		] )
	{
		Requires = [PrismCommon.Luminance],
		Hlsl = """
			float3 Prism_Saturation( float3 vColor, float flSaturation )
			{
				float flLuma = Prism_Luminance( vColor );

				return flLuma.xxx + flSaturation.xxx * ( vColor - flLuma.xxx );
			}
			"""
	};

	/// <summary>Rotate the hue of a colour by a fraction of the wheel.</summary>
	internal static readonly HelperFunction HueShift = new( "Prism_HueShift", ShaderType.Float3,
		[
			new HelperParam( "vColor", ShaderType.Float3 ),
			new HelperParam( "flOffset", ShaderType.Float )
		] )
	{
		Requires = [RgbToHsv, HsvToRgb],
		Hlsl = """
			float3 Prism_HueShift( float3 vColor, float flOffset )
			{
				float3 vHsv = Prism_RgbToHsv( vColor );

				vHsv.x = frac( vHsv.x + flOffset + 1.0f );

				return Prism_HsvToRgb( vHsv );
			}
			"""
	};

	/// <summary>Blender-style combined hue, saturation and value adjustment with a blend factor.</summary>
	internal static readonly HelperFunction HsvAdjust = new( "Prism_HsvAdjust", ShaderType.Float3,
		[
			new HelperParam( "vColor", ShaderType.Float3 ),
			new HelperParam( "flHue", ShaderType.Float ),
			new HelperParam( "flSaturation", ShaderType.Float ),
			new HelperParam( "flValue", ShaderType.Float ),
			new HelperParam( "flFactor", ShaderType.Float )
		] )
	{
		Requires = [RgbToHsv, HsvToRgb],
		Hlsl = """
			float3 Prism_HsvAdjust( float3 vColor, float flHue, float flSaturation, float flValue, float flFactor )
			{
				float3 vHsv = Prism_RgbToHsv( vColor );

				vHsv.x = frac( vHsv.x + flHue + 1.0f );
				vHsv.y = saturate( vHsv.y * flSaturation );
				vHsv.z = vHsv.z * flValue;

				return lerp( vColor, max( Prism_HsvToRgb( vHsv ), 0.0f ), saturate( flFactor ) );
			}
			"""
	};

	/// <summary>Shift the white point of a linear colour along the temperature and tint axes.</summary>
	internal static readonly HelperFunction WhiteBalance = new( "Prism_WhiteBalance", ShaderType.Float3,
		[
			new HelperParam( "vColor", ShaderType.Float3 ),
			new HelperParam( "flTemperature", ShaderType.Float ),
			new HelperParam( "flTint", ShaderType.Float )
		] )
	{
		Hlsl = """
			float3 Prism_WhiteBalance( float3 vColor, float flTemperature, float flTint )
			{
				// Roughly -1.67 .. 1.67 is the useful range on both axes.
				float t1 = flTemperature * 10.0f / 6.0f;
				float t2 = flTint * 10.0f / 6.0f;

				// 0.31271 is the x of the D65 white point.
				float x = 0.31271f - t1 * ( t1 < 0.0f ? 0.1f : 0.05f );
				float flStandardY = 2.87f * x - 3.0f * x * x - 0.27509507f;
				float y = flStandardY + t2 * 0.05f;

				float3 w1 = float3( 0.949237f, 1.03542f, 1.08728f );

				float Y = 1.0f;
				float X = Y * x / y;
				float Z = Y * ( 1.0f - x - y ) / y;

				float L =  0.7328f * X + 0.4296f * Y - 0.1624f * Z;
				float M = -0.7036f * X + 1.6975f * Y + 0.0061f * Z;
				float S =  0.0030f * X + 0.0136f * Y + 0.9834f * Z;

				float3 w2 = float3( L, M, S );
				float3 vBalance = float3( w1.x / w2.x, w1.y / w2.y, w1.z / w2.z );

				const float3x3 mLinearToLms =
				{
					3.90405e-1f, 5.49941e-1f, 8.92632e-3f,
					7.08416e-2f, 9.63172e-1f, 1.35775e-3f,
					2.31082e-2f, 1.28021e-1f, 9.36245e-1f
				};

				const float3x3 mLmsToLinear =
				{
					 2.85847e+0f, -1.62879e+0f, -2.48910e-2f,
					-2.10182e-1f,  1.15820e+0f,  3.24281e-4f,
					-4.18120e-2f, -1.18169e-1f,  1.06867e+0f
				};

				float3 vLms = mul( mLinearToLms, vColor );
				vLms *= vBalance;

				return mul( mLmsToLinear, vLms );
			}
			"""
	};

	/// <summary>Black, white, gamma and output levels, the way an image editor spells them.</summary>
	internal static readonly HelperFunction Levels = new( "Prism_Levels", ShaderType.Float3,
		[
			new HelperParam( "vColor", ShaderType.Float3 ),
			new HelperParam( "flInBlack", ShaderType.Float ),
			new HelperParam( "flInWhite", ShaderType.Float ),
			new HelperParam( "flGamma", ShaderType.Float ),
			new HelperParam( "flOutBlack", ShaderType.Float ),
			new HelperParam( "flOutWhite", ShaderType.Float )
		] )
	{
		Hlsl = """
			float3 Prism_Levels( float3 vColor, float flInBlack, float flInWhite, float flGamma, float flOutBlack, float flOutWhite )
			{
				float3 v = saturate( ( vColor - flInBlack ) / max( flInWhite - flInBlack, 1e-5f ) );

				v = pow( max( v, 0.0f ), 1.0f / max( flGamma, 1e-5f ) );

				return v * ( flOutWhite - flOutBlack ) + flOutBlack;
			}
			"""
	};

	/// <summary>Replace one colour with another, feathered by a distance range.</summary>
	internal static readonly HelperFunction ReplaceColor = new( "Prism_ReplaceColor", ShaderType.Float3,
		[
			new HelperParam( "vColor", ShaderType.Float3 ),
			new HelperParam( "vFrom", ShaderType.Float3 ),
			new HelperParam( "vTo", ShaderType.Float3 ),
			new HelperParam( "flRange", ShaderType.Float ),
			new HelperParam( "flFuzziness", ShaderType.Float )
		] )
	{
		Hlsl = """
			float3 Prism_ReplaceColor( float3 vColor, float3 vFrom, float3 vTo, float flRange, float flFuzziness )
			{
				float flDistance = distance( vFrom, vColor );

				return lerp( vTo, vColor, saturate( ( flDistance - flRange ) / max( flFuzziness, 1e-5f ) ) );
			}
			"""
	};

	/// <summary>One where a colour is close to a key colour, feathered by a distance range.</summary>
	internal static readonly HelperFunction ColorMask = new( "Prism_ColorMask", ShaderType.Float,
		[
			new HelperParam( "vColor", ShaderType.Float3 ),
			new HelperParam( "vKey", ShaderType.Float3 ),
			new HelperParam( "flRange", ShaderType.Float ),
			new HelperParam( "flFuzziness", ShaderType.Float )
		] )
	{
		Hlsl = """
			float Prism_ColorMask( float3 vColor, float3 vKey, float flRange, float flFuzziness )
			{
				float flDistance = distance( vKey, vColor );

				return saturate( 1.0f - ( flDistance - flRange ) / max( flFuzziness, 1e-5f ) );
			}
			"""
	};

	/// <summary>The colour a black body radiates at a given temperature, in linear RGB.</summary>
	internal static readonly HelperFunction Blackbody = new( "Prism_Blackbody", ShaderType.Float3,
		[new HelperParam( "flTemperature", ShaderType.Float )] )
	{
		Requires = [SrgbToLinear],
		Hlsl = """
			float3 Prism_Blackbody( float flTemperature )
			{
				float t = clamp( flTemperature, 1000.0f, 40000.0f ) / 100.0f;

				float r, g, b;

				if ( t <= 66.0f )
				{
					r = 255.0f;
					g = 99.4708025861f * log( t ) - 161.1195681661f;
				}
				else
				{
					r = 329.698727446f * pow( t - 60.0f, -0.1332047592f );
					g = 288.1221695283f * pow( t - 60.0f, -0.0755148492f );
				}

				if      ( t >= 66.0f ) b = 255.0f;
				else if ( t <= 19.0f ) b = 0.0f;
				else                   b = 138.5177312231f * log( t - 10.0f ) - 305.0447927307f;

				return Prism_SrgbToLinear( saturate( float3( r, g, b ) / 255.0f ) );
			}
			"""
	};
}

// ---------------------------------------------------------------------------------------------------
// Conversion
// ---------------------------------------------------------------------------------------------------

/// <summary>Converts a colour between two colour models.</summary>
[NodeInfo( Id = "prism.color.convert", Title = "Colorspace Conversion", Category = "Color/Convert",
	Icon = "invert_colors", Keywords = new[] { "colorspace", "convert", "hsv", "hsl", "srgb", "linear" },
	Description = "Converts a colour between linear RGB, sRGB, HSV and HSL." )]
[NodeVersion( 1 )]
public sealed class ColorspaceConversionNode : PrismNode
{
	/// <summary>The colour to convert.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The model the input is in.</summary>
	public PrismColorSpace From { get; set; } = PrismColorSpace.Linear;

	/// <summary>The model to convert to.</summary>
	public PrismColorSpace To { get; set; } = PrismColorSpace.Hsv;

	/// <summary>The converted colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null || From != To ) return;

		ctx.Info( "The source and destination colour spaces are the same, so this node does nothing.", null );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.InAs( nameof( In ), ShaderType.Float3 );

		if ( !value.IsValid ) return;

		// Everything routes through linear RGB, so any pair of models is one or two helper calls.
		var linear = From switch
		{
			PrismColorSpace.Srgb => ctx.Helper( PrismColorHelpers.SrgbToLinear, value ),
			PrismColorSpace.Hsv => ctx.Helper( PrismColorHelpers.HsvToRgb, value ),
			PrismColorSpace.Hsl => ctx.Helper( PrismColorHelpers.HslToRgb, value ),
			_ => value
		};

		var result = To switch
		{
			PrismColorSpace.Srgb => ctx.Helper( PrismColorHelpers.LinearToSrgb, linear ),
			PrismColorSpace.Hsv => ctx.Helper( PrismColorHelpers.RgbToHsv, linear ),
			PrismColorSpace.Hsl => ctx.Helper( PrismColorHelpers.RgbToHsl, linear ),
			_ => linear
		};

		ctx.Out( nameof( Out ), result );
	}
}

/// <summary>Converts linear RGB to hue, saturation and value.</summary>
[NodeInfo( Id = "prism.color.rgbToHsv", Title = "RGB to HSV", Category = "Color/Convert",
	Icon = "invert_colors", Keywords = new[] { "rgb", "hsv", "hue", "convert" },
	Description = "Converts linear RGB to hue, saturation and value." )]
[NodeVersion( 1 )]
public sealed class RgbToHsvNode : PrismNode
{
	/// <summary>The colour to convert.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>Hue, saturation and value.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx ) =>
		ctx?.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.RgbToHsv, ctx.InAs( nameof( In ), ShaderType.Float3 ) ) );
}

/// <summary>Converts hue, saturation and value to linear RGB.</summary>
[NodeInfo( Id = "prism.color.hsvToRgb", Title = "HSV to RGB", Category = "Color/Convert",
	Icon = "invert_colors", Keywords = new[] { "hsv", "rgb", "hue", "convert" },
	Description = "Converts hue, saturation and value to linear RGB." )]
[NodeVersion( 1 )]
public sealed class HsvToRgbNode : PrismNode
{
	/// <summary>Hue, saturation and value.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The value used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Vector3 DefaultIn { get; set; } = new( 0f, 1f, 1f );

	/// <summary>The colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx ) =>
		ctx?.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.HsvToRgb, ctx.InAs( nameof( In ), ShaderType.Float3 ) ) );
}

/// <summary>Converts linear RGB to hue, saturation and lightness.</summary>
[NodeInfo( Id = "prism.color.rgbToHsl", Title = "RGB to HSL", Category = "Color/Convert",
	Icon = "invert_colors", Keywords = new[] { "rgb", "hsl", "lightness", "convert" },
	Description = "Converts linear RGB to hue, saturation and lightness." )]
[NodeVersion( 1 )]
public sealed class RgbToHslNode : PrismNode
{
	/// <summary>The colour to convert.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>Hue, saturation and lightness.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx ) =>
		ctx?.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.RgbToHsl, ctx.InAs( nameof( In ), ShaderType.Float3 ) ) );
}

/// <summary>Converts hue, saturation and lightness to linear RGB.</summary>
[NodeInfo( Id = "prism.color.hslToRgb", Title = "HSL to RGB", Category = "Color/Convert",
	Icon = "invert_colors", Keywords = new[] { "hsl", "rgb", "lightness", "convert" },
	Description = "Converts hue, saturation and lightness to linear RGB." )]
[NodeVersion( 1 )]
public sealed class HslToRgbNode : PrismNode
{
	/// <summary>Hue, saturation and lightness.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The value used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Vector3 DefaultIn { get; set; } = new( 0f, 1f, 0.5f );

	/// <summary>The colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx ) =>
		ctx?.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.HslToRgb, ctx.InAs( nameof( In ), ShaderType.Float3 ) ) );
}

/// <summary>Decodes an sRGB-encoded value into linear light.</summary>
[NodeInfo( Id = "prism.color.gammaToLinear", Title = "sRGB to Linear", Category = "Color/Convert",
	Icon = "brightness_6", Keywords = new[] { "srgb", "gamma", "linear", "decode" },
	Description = "Decodes sRGB-encoded values into linear light." )]
[NodeVersion( 1 )]
public sealed class SrgbToLinearNode : PrismNode
{
	/// <summary>The encoded colour.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The linear colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx ) =>
		ctx?.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.SrgbToLinear, ctx.InAs( nameof( In ), ShaderType.Float3 ) ) );
}

/// <summary>Encodes a linear value into sRGB.</summary>
[NodeInfo( Id = "prism.color.linearToGamma", Title = "Linear to sRGB", Category = "Color/Convert",
	Icon = "brightness_5", Keywords = new[] { "linear", "srgb", "gamma", "encode" },
	Description = "Encodes linear light into sRGB." )]
[NodeVersion( 1 )]
public sealed class LinearToSrgbNode : PrismNode
{
	/// <summary>The linear colour.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The encoded colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx ) =>
		ctx?.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.LinearToSrgb, ctx.InAs( nameof( In ), ShaderType.Float3 ) ) );
}

// ---------------------------------------------------------------------------------------------------
// Adjustment
// ---------------------------------------------------------------------------------------------------

/// <summary>Pushes a colour away from or towards the perceptual mid-grey.</summary>
[NodeInfo( Id = "prism.color.contrast", Title = "Contrast", Category = "Color/Adjust",
	Icon = "contrast", Keywords = new[] { "contrast", "adjust", "levels" },
	Description = "Scales a colour about a mid-point, which is what contrast means." )]
[NodeVersion( 1 )]
public sealed class ContrastNode : PrismNode
{
	/// <summary>The colour to adjust.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>How far to push. One leaves the colour alone.</summary>
	[In( "float", Name = "Contrast" )] public PortRef Contrast { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The contrast used when nothing is connected.</summary>
	[InlineValue( nameof( Contrast ) )] public float DefaultContrast { get; set; } = 1f;

	/// <summary>The adjusted colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.Contrast,
			ctx.InAs( nameof( In ), ShaderType.Float3 ),
			ctx.In( nameof( Contrast ), ctx.Const( DefaultContrast ) ) ) );
	}
}

/// <summary>Pushes a colour away from or towards its own luminance.</summary>
[NodeInfo( Id = "prism.color.saturation", Title = "Saturation", Category = "Color/Adjust",
	Icon = "opacity", Keywords = new[] { "saturation", "desaturate", "vibrance", "grey" },
	Description = "Scales a colour about its Rec.709 luminance. Zero gives greyscale." )]
[NodeVersion( 1 )]
public sealed class SaturationNode : PrismNode
{
	/// <summary>The colour to adjust.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>How far to push. One leaves the colour alone, zero removes all colour.</summary>
	[In( "float", Name = "Saturation" )] public PortRef Saturation { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The saturation used when nothing is connected.</summary>
	[InlineValue( nameof( Saturation ) )] public float DefaultSaturation { get; set; } = 1f;

	/// <summary>The adjusted colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.Saturation,
			ctx.InAs( nameof( In ), ShaderType.Float3 ),
			ctx.In( nameof( Saturation ), ctx.Const( DefaultSaturation ) ) ) );
	}
}

/// <summary>Rotates a colour around the hue wheel.</summary>
[NodeInfo( Id = "prism.color.hueShift", Title = "Hue", Category = "Color/Adjust",
	Icon = "color_lens", Keywords = new[] { "hue", "shift", "rotate", "colour" },
	Description = "Rotates the hue of a colour, leaving its saturation and value alone." )]
[NodeVersion( 1 )]
public sealed class HueShiftNode : PrismNode
{
	/// <summary>The colour to rotate.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>How far around the wheel to rotate.</summary>
	[In( "float", Name = "Offset" )] public PortRef Offset { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The offset used when nothing is connected.</summary>
	[InlineValue( nameof( Offset ) )] public float DefaultOffset { get; set; }

	/// <summary>How the offset is measured.</summary>
	public PrismHueUnit Unit { get; set; } = PrismHueUnit.Normalized;

	/// <summary>The rotated colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var offset = ctx.In( nameof( Offset ), ctx.Const( DefaultOffset ) );

		offset = Unit switch
		{
			PrismHueUnit.Degrees => ctx.Bin( BinaryOp.Div, offset, ctx.Const( 360f ) ),
			PrismHueUnit.Radians => ctx.Bin( BinaryOp.Div, offset, ctx.Const( 6.28318530718f ) ),
			_ => offset
		};

		ctx.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.HueShift,
			ctx.InAs( nameof( In ), ShaderType.Float3 ), offset ) );
	}
}

/// <summary>Adjusts hue, saturation and value together, with a blend back to the original.</summary>
[NodeInfo( Id = "prism.color.hsvAdjust", Title = "Hue Saturation Value", Category = "Color/Adjust",
	Icon = "tune", Keywords = new[] { "hsv", "hue", "saturation", "value", "adjust" },
	Description = "Rotates the hue, scales the saturation and scales the value in one pass." )]
[NodeVersion( 1 )]
public sealed class HsvAdjustNode : PrismNode
{
	/// <summary>The colour to adjust.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>How far around the wheel to rotate, as a fraction.</summary>
	[In( "float", Name = "Hue" )] public PortRef Hue { get; set; }

	/// <summary>How far to scale the saturation.</summary>
	[In( "float", Name = "Saturation" )] public PortRef Saturation { get; set; }

	/// <summary>How far to scale the value.</summary>
	[In( "float", Name = "Value" )] public PortRef Value { get; set; }

	/// <summary>How much of the adjustment to apply.</summary>
	[In( "float", Name = "Factor" )] public PortRef Factor { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The hue offset used when nothing is connected.</summary>
	[InlineValue( nameof( Hue ) )] public float DefaultHue { get; set; }

	/// <summary>The saturation scale used when nothing is connected.</summary>
	[InlineValue( nameof( Saturation ) )] public float DefaultSaturation { get; set; } = 1f;

	/// <summary>The value scale used when nothing is connected.</summary>
	[InlineValue( nameof( Value ) )] public float DefaultValue { get; set; } = 1f;

	/// <summary>The blend factor used when nothing is connected.</summary>
	[InlineValue( nameof( Factor ) )] public float DefaultFactor { get; set; } = 1f;

	/// <summary>The adjusted colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.HsvAdjust,
			ctx.InAs( nameof( In ), ShaderType.Float3 ),
			ctx.In( nameof( Hue ), ctx.Const( DefaultHue ) ),
			ctx.In( nameof( Saturation ), ctx.Const( DefaultSaturation ) ),
			ctx.In( nameof( Value ), ctx.Const( DefaultValue ) ),
			ctx.In( nameof( Factor ), ctx.Const( DefaultFactor ) ) ) );
	}
}

/// <summary>Shifts the white point of a colour along the temperature and tint axes.</summary>
[NodeInfo( Id = "prism.color.whiteBalance", Title = "White Balance", Category = "Color/Adjust",
	Icon = "wb_sunny", Keywords = new[] { "white balance", "temperature", "tint", "kelvin" },
	Description = "Warms or cools a colour, and shifts it towards green or magenta, in LMS space." )]
[NodeVersion( 1 )]
public sealed class WhiteBalanceNode : PrismNode
{
	/// <summary>The colour to balance.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>Negative cools towards blue, positive warms towards orange.</summary>
	[In( "float", Name = "Temperature" )] public PortRef Temperature { get; set; }

	/// <summary>Negative shifts towards green, positive towards magenta.</summary>
	[In( "float", Name = "Tint" )] public PortRef Tint { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The temperature used when nothing is connected.</summary>
	[InlineValue( nameof( Temperature ) )] public float DefaultTemperature { get; set; }

	/// <summary>The tint used when nothing is connected.</summary>
	[InlineValue( nameof( Tint ) )] public float DefaultTint { get; set; }

	/// <summary>The balanced colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.WhiteBalance,
			ctx.InAs( nameof( In ), ShaderType.Float3 ),
			ctx.In( nameof( Temperature ), ctx.Const( DefaultTemperature ) ),
			ctx.In( nameof( Tint ), ctx.Const( DefaultTint ) ) ) );
	}
}

/// <summary>Rebuilds each output channel as a weighted sum of the input channels.</summary>
[NodeInfo( Id = "prism.color.channelMixer", Title = "Channel Mixer", Category = "Color/Adjust",
	Icon = "tune", Keywords = new[] { "channel", "mixer", "matrix", "swap" },
	Description = "Each output channel is a dot product of the input with a row of weights." )]
[NodeVersion( 1 )]
public sealed class ChannelMixerNode : PrismNode
{
	/// <summary>The colour to mix.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>How much of each input channel lands in the red output.</summary>
	public Vector3 Red { get; set; } = new( 1f, 0f, 0f );

	/// <summary>How much of each input channel lands in the green output.</summary>
	public Vector3 Green { get; set; } = new( 0f, 1f, 0f );

	/// <summary>How much of each input channel lands in the blue output.</summary>
	public Vector3 Blue { get; set; } = new( 0f, 0f, 1f );

	/// <summary>The mixed colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.InAs( nameof( In ), ShaderType.Float3 );

		if ( !value.IsValid ) return;

		ctx.Out( nameof( Out ), ctx.Construct( ShaderType.Float3,
			ctx.Call( Intrinsic.Dot, value, ctx.Const( Red ) ),
			ctx.Call( Intrinsic.Dot, value, ctx.Const( Green ) ),
			ctx.Call( Intrinsic.Dot, value, ctx.Const( Blue ) ) ) );
	}
}

/// <summary>Remaps the black point, white point and gamma of a colour.</summary>
[NodeInfo( Id = "prism.color.levels", Title = "Levels", Category = "Color/Adjust",
	Icon = "equalizer", Keywords = new[] { "levels", "curves", "black", "white", "gamma" },
	Description = "Photoshop-style levels: input black and white, gamma, then output black and white." )]
[NodeVersion( 1 )]
public sealed class LevelsNode : PrismNode
{
	/// <summary>The colour to remap.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The input value that becomes black.</summary>
	[In( "float", Name = "In Black" )] public PortRef InBlack { get; set; }

	/// <summary>The input value that becomes white.</summary>
	[In( "float", Name = "In White" )] public PortRef InWhite { get; set; }

	/// <summary>The mid-tone curve.</summary>
	[In( "float", Name = "Gamma" )] public PortRef Gamma { get; set; }

	/// <summary>What black is mapped to.</summary>
	[In( "float", Name = "Out Black" )] public PortRef OutBlack { get; set; }

	/// <summary>What white is mapped to.</summary>
	[In( "float", Name = "Out White" )] public PortRef OutWhite { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The input black used when nothing is connected.</summary>
	[InlineValue( nameof( InBlack ) )] public float DefaultInBlack { get; set; }

	/// <summary>The input white used when nothing is connected.</summary>
	[InlineValue( nameof( InWhite ) )] public float DefaultInWhite { get; set; } = 1f;

	/// <summary>The gamma used when nothing is connected.</summary>
	[InlineValue( nameof( Gamma ) )] public float DefaultGamma { get; set; } = 1f;

	/// <summary>The output black used when nothing is connected.</summary>
	[InlineValue( nameof( OutBlack ) )] public float DefaultOutBlack { get; set; }

	/// <summary>The output white used when nothing is connected.</summary>
	[InlineValue( nameof( OutWhite ) )] public float DefaultOutWhite { get; set; } = 1f;

	/// <summary>The remapped colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.Levels,
			ctx.InAs( nameof( In ), ShaderType.Float3 ),
			ctx.In( nameof( InBlack ), ctx.Const( DefaultInBlack ) ),
			ctx.In( nameof( InWhite ), ctx.Const( DefaultInWhite ) ),
			ctx.In( nameof( Gamma ), ctx.Const( DefaultGamma ) ),
			ctx.In( nameof( OutBlack ), ctx.Const( DefaultOutBlack ) ),
			ctx.In( nameof( OutWhite ), ctx.Const( DefaultOutWhite ) ) ) );
	}
}

/// <summary>Applies a power curve to a value.</summary>
[NodeInfo( Id = "prism.color.gamma", Title = "Gamma", Category = "Color/Adjust",
	Icon = "brightness_medium", Keywords = new[] { "gamma", "power", "curve", "exponent" },
	Description = "Raises a value to a power. Values above one darken the mid-tones, below one lift them." )]
[NodeVersion( 1 )]
public sealed class GammaNode : PrismNode
{
	/// <summary>The value to curve.</summary>
	[In( "T", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The exponent.</summary>
	[In( "float", Name = "Gamma" )] public PortRef Gamma { get; set; }

	/// <summary>The value used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public float DefaultIn { get; set; } = 0.5f;

	/// <summary>The exponent used when nothing is connected.</summary>
	[InlineValue( nameof( Gamma ) )] public float DefaultGamma { get; set; } = 2.2f;

	/// <summary>The curved value.</summary>
	[Out( "T", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.In( nameof( In ), ctx.Const( DefaultIn ) );

		if ( !value.IsValid ) return;

		// A negative base is undefined for pow(), and a shader that hits it produces NaN rather than a
		// warning, so clamp here instead of leaving it to the driver.
		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Pow, ctx.Call( Intrinsic.Max, value, ctx.Const( 0f ) ),
			ctx.In( nameof( Gamma ), ctx.Const( DefaultGamma ) ) ) );
	}
}

/// <summary>Inverts selected channels of a colour.</summary>
[NodeInfo( Id = "prism.color.invert", Title = "Invert Colors", Category = "Color/Adjust",
	Icon = "flip", Keywords = new[] { "invert", "negate", "one minus", "negative" },
	Description = "Inverts a colour channel by channel, under a per-channel mask." )]
[NodeVersion( 1 )]
public sealed class InvertColorsNode : PrismNode
{
	/// <summary>The colour to invert.</summary>
	[In( "float4", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>True to invert the red channel.</summary>
	public bool Red { get; set; } = true;

	/// <summary>True to invert the green channel.</summary>
	public bool Green { get; set; } = true;

	/// <summary>True to invert the blue channel.</summary>
	public bool Blue { get; set; } = true;

	/// <summary>True to invert the alpha channel.</summary>
	public bool Alpha { get; set; }

	/// <summary>The inverted colour.</summary>
	[Out( "float4", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.InAs( nameof( In ), ShaderType.Float4 );

		if ( !value.IsValid ) return;

		var mask = ctx.Const( new Vector4( Red ? 1f : 0f, Green ? 1f : 0f, Blue ? 1f : 0f, Alpha ? 1f : 0f ) );

		ctx.Out( nameof( Out ), ctx.Call( Intrinsic.Lerp, value,
			ctx.Bin( BinaryOp.Sub, ctx.Const( 1f ), value ), mask ) );
	}
}

/// <summary>Quantises a colour to a fixed number of steps per channel.</summary>
[NodeInfo( Id = "prism.color.posterize", Title = "Posterize Color", Category = "Color/Adjust",
	Icon = "filter_b_and_w", Keywords = new[] { "posterize", "quantize", "steps", "banding", "toon" },
	Description = "Rounds each channel down to a fixed number of steps." )]
[NodeVersion( 1 )]
public sealed class PosterizeColorNode : PrismNode
{
	/// <summary>The colour to quantise.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>How many steps each channel is allowed.</summary>
	[In( "float", Name = "Steps" )] public PortRef Steps { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The step count used when nothing is connected.</summary>
	[InlineValue( nameof( Steps ) )] public float DefaultSteps { get; set; } = 4f;

	/// <summary>True to quantise in a perceptual space, which spaces the bands more evenly.</summary>
	public bool Perceptual { get; set; }

	/// <summary>The quantised colour.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = ctx.InAs( nameof( In ), ShaderType.Float3 );

		if ( !value.IsValid ) return;

		if ( Perceptual ) value = ctx.Helper( PrismColorHelpers.LinearToSrgb, value );

		var steps = ctx.Call( Intrinsic.Max, ctx.In( nameof( Steps ), ctx.Const( DefaultSteps ) ), ctx.Const( 1f ) );

		var quantised = ctx.Bin( BinaryOp.Div,
			ctx.Call( Intrinsic.Floor, ctx.Bin( BinaryOp.Mul, value, steps ) ), steps );

		if ( Perceptual ) quantised = ctx.Helper( PrismColorHelpers.SrgbToLinear, quantised );

		ctx.Out( nameof( Out ), quantised );
	}
}

/// <summary>Swaps one colour for another, wherever it appears.</summary>
[NodeInfo( Id = "prism.color.replace", Title = "Replace Color", Category = "Color/Utility",
	Icon = "colorize", Keywords = new[] { "replace", "swap", "recolour", "chroma key" },
	Description = "Replaces colours near a key colour with another, feathered by a range and a softness." )]
[NodeVersion( 1 )]
public sealed class ReplaceColorNode : PrismNode
{
	/// <summary>The colour to search.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour to look for.</summary>
	[In( "float3", Name = "From" )] public PortRef From { get; set; }

	/// <summary>The colour to put in its place.</summary>
	[In( "float3", Name = "To" )] public PortRef To { get; set; }

	/// <summary>How far from the key colour still counts as a match.</summary>
	[In( "float", Name = "Range" )] public PortRef Range { get; set; }

	/// <summary>How softly the match falls off past the range.</summary>
	[In( "float", Name = "Fuzziness" )] public PortRef Fuzziness { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The key colour used when nothing is connected.</summary>
	[InlineValue( nameof( From ) )] public Color DefaultFrom { get; set; } = Color.Green;

	/// <summary>The replacement used when nothing is connected.</summary>
	[InlineValue( nameof( To ) )] public Color DefaultTo { get; set; } = Color.Black;

	/// <summary>The range used when nothing is connected.</summary>
	[InlineValue( nameof( Range ) )] public float DefaultRange { get; set; } = 0.2f;

	/// <summary>The fuzziness used when nothing is connected.</summary>
	[InlineValue( nameof( Fuzziness ) )] public float DefaultFuzziness { get; set; } = 0.1f;

	/// <summary>The recoloured result.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.ReplaceColor,
			ctx.InAs( nameof( In ), ShaderType.Float3 ),
			ctx.InAs( nameof( From ), ShaderType.Float3 ),
			ctx.InAs( nameof( To ), ShaderType.Float3 ),
			ctx.In( nameof( Range ), ctx.Const( DefaultRange ) ),
			ctx.In( nameof( Fuzziness ), ctx.Const( DefaultFuzziness ) ) ) );
	}
}

/// <summary>Builds a mask from how close a colour is to a key colour.</summary>
[NodeInfo( Id = "prism.color.mask", Title = "Color Mask", Category = "Color/Utility",
	Icon = "filter_center_focus", Keywords = new[] { "mask", "key", "chroma", "select", "isolate" },
	Description = "One where the colour matches a key, feathered outwards by a range and a softness." )]
[NodeVersion( 1 )]
public sealed class ColorMaskNode : PrismNode
{
	/// <summary>The colour to test.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour to match against.</summary>
	[In( "float3", Name = "Key" )] public PortRef Key { get; set; }

	/// <summary>How far from the key still counts as a match.</summary>
	[In( "float", Name = "Range" )] public PortRef Range { get; set; }

	/// <summary>How softly the match falls off past the range.</summary>
	[In( "float", Name = "Fuzziness" )] public PortRef Fuzziness { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The key used when nothing is connected.</summary>
	[InlineValue( nameof( Key ) )] public Color DefaultKey { get; set; } = Color.Green;

	/// <summary>The range used when nothing is connected.</summary>
	[InlineValue( nameof( Range ) )] public float DefaultRange { get; set; } = 0.2f;

	/// <summary>The fuzziness used when nothing is connected.</summary>
	[InlineValue( nameof( Fuzziness ) )] public float DefaultFuzziness { get; set; } = 0.1f;

	/// <summary>The mask.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.ColorMask,
			ctx.InAs( nameof( In ), ShaderType.Float3 ),
			ctx.InAs( nameof( Key ), ShaderType.Float3 ),
			ctx.In( nameof( Range ), ctx.Const( DefaultRange ) ),
			ctx.In( nameof( Fuzziness ), ctx.Const( DefaultFuzziness ) ) ) );
	}
}

/// <summary>The perceived brightness of a colour.</summary>
[NodeInfo( Id = "prism.color.luminance", Title = "Luminance", Category = "Color/Utility",
	Icon = "brightness_low", Keywords = new[] { "luminance", "luma", "grey", "brightness", "desaturate" },
	Description = "Rec.709 relative luminance: how bright a colour looks, not how bright it is." )]
[NodeVersion( 1 )]
public sealed class LuminanceNode : PrismNode
{
	/// <summary>The colour to measure.</summary>
	[In( "float3", Name = "In" )] public PortRef In { get; set; }

	/// <summary>The colour used when nothing is connected.</summary>
	[InlineValue( nameof( In ) )] public Color DefaultIn { get; set; } = Color.White;

	/// <summary>The luminance.</summary>
	[Out( "float", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx ) =>
		ctx?.Out( nameof( Out ), ctx.Helper( PrismCommon.Luminance, ctx.InAs( nameof( In ), ShaderType.Float3 ) ) );
}

/// <summary>The colour a black body glows at a given temperature.</summary>
[NodeInfo( Id = "prism.color.blackbody", Title = "Blackbody", Category = "Color/Utility",
	Icon = "local_fire_department", Keywords = new[] { "blackbody", "temperature", "kelvin", "fire", "heat" },
	Description = "Planckian locus: the linear colour of a black body at a temperature in kelvin." )]
[NodeVersion( 1 )]
public sealed class BlackbodyNode : PrismNode
{
	/// <summary>The temperature, in kelvin. Roughly 1000 to 40000 is meaningful.</summary>
	[In( "float", Name = "Temperature" )] public PortRef Temperature { get; set; }

	/// <summary>The temperature used when nothing is connected.</summary>
	[InlineValue( nameof( Temperature ) )] public float DefaultTemperature { get; set; } = 6500f;

	/// <summary>The emitted colour, in linear RGB.</summary>
	[Out( "float3", Name = "Out" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx ) =>
		ctx?.Out( nameof( Out ), ctx.Helper( PrismColorHelpers.Blackbody,
			ctx.In( nameof( Temperature ), ctx.Const( DefaultTemperature ) ) ) );
}

/// <summary>Looks a scalar up in a gradient stored as a one-dimensional texture.</summary>
[NodeInfo( Id = "prism.color.ramp", Title = "Color Ramp", Category = "Color/Utility",
	Icon = "gradient", Keywords = new[] { "ramp", "gradient", "lut", "palette", "curve" },
	Description = "Samples a gradient texture at a position, clamped at both ends. The way to drive colour from a mask." )]
[NodeVersion( 1 )]
public sealed class ColorRampNode : TextureNode
{
	/// <summary>The gradient to sample.</summary>
	[In( "Texture2D", Name = "Gradient" )] public PortRef Texture { get; set; }

	/// <summary>Where along the gradient to read, 0..1.</summary>
	[In( "float", Name = "T" )] public PortRef T { get; set; }

	/// <summary>The position used when nothing is connected.</summary>
	[InlineValue( nameof( T ) )] public float DefaultT { get; set; } = 0.5f;

	/// <summary>Which row of the texture to read. Useful when several ramps share one image.</summary>
	public float Row { get; set; } = 0.5f;

	/// <summary>The sampled colour.</summary>
	[Out( "float4", Name = "RGBA" )] public PortRef RGBA { get; set; }

	/// <summary>The sampled colour without its alpha.</summary>
	[Out( "float3", Name = "RGB" )] public PortRef RGB { get; set; }

	/// <inheritdoc/>
	public ColorRampNode()
	{
		TextureGroup = "Gradients";
		Filter = PrismTextureFilter.Bilinear;
		AddressU = PrismTextureAddress.Clamp;
		AddressV = PrismTextureAddress.Clamp;
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var t = ctx.Call( Intrinsic.Saturate, ctx.In( nameof( T ), ctx.Const( DefaultT ) ) );
		var uv = ctx.Construct( ShaderType.Float2, t, ctx.Const( Row ) );

		var rgba = ctx.Call( Intrinsic.SampleLevel, ResolveTexture( ctx ), ResolveSampler( ctx ), uv, ctx.Const( 0f ) );

		ctx.Out( nameof( RGBA ), rgba );
		ctx.Out( nameof( RGB ), ctx.Swizzle( rgba, "xyz" ) );
	}
}