11import { describe , expect , it } from "vitest" ;
2+ import { COLOR_GRADING_ADVANCED_LIMITS } from "@hyperframes/parsers/color-grading-contract" ;
3+ import { unitFloatToByte } from "./colorLuts" ;
24import {
5+ calculateHfColorGradingSecondaryMask ,
36 HF_COLOR_GRADING_COLOR_SPACE ,
47 HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS ,
58 HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS ,
@@ -8,6 +11,7 @@ import {
811 HF_COLOR_GRADING_PALETTES ,
912 HF_COLOR_GRADING_PRESETS ,
1013 getHfColorGradingCapabilities ,
14+ hasHfColorGradingAuthoredValues ,
1115 isHfColorGradingActive ,
1216 normalizeHfColorGrading ,
1317 normalizeHfColorGradingWithVariables ,
@@ -123,6 +127,7 @@ describe("color grading", () => {
123127 it ( "publishes a complete capability catalog for agent-built treatments" , ( ) => {
124128 const capabilities = getHfColorGradingCapabilities ( ) ;
125129
130+ expect ( capabilities . version ) . toBe ( 2 ) ;
126131 expect ( capabilities . colorSpace ) . toBe ( "rec709" ) ;
127132 expect ( capabilities . adjustments . map ( ( { key } ) => key ) ) . toEqual ( [
128133 "exposure" ,
@@ -158,6 +163,72 @@ describe("color grading", () => {
158163 expect ( capabilities . effects . find ( ( { key } ) => key === "kuwahara" ) ?. renderLane ) . toBe (
159164 "multipass" ,
160165 ) ;
166+ expect ( capabilities . wheels . zones ) . toEqual ( [ "shadows" , "midtones" , "highlights" ] ) ;
167+ expect ( capabilities . curves ) . toMatchObject ( {
168+ channels : [ "master" , "red" , "green" , "blue" ] ,
169+ minPoints : 2 ,
170+ maxPoints : 16 ,
171+ implicitEndpoints : true ,
172+ } ) ;
173+ expect ( capabilities . hueCurves . channels ) . toEqual (
174+ expect . arrayContaining ( [
175+ expect . objectContaining ( {
176+ key : "hueVsHue" ,
177+ input : expect . objectContaining ( { unit : "degrees" , wrap : true } ) ,
178+ output : { min : - 180 , max : 180 } ,
179+ } ) ,
180+ ] ) ,
181+ ) ;
182+ expect ( capabilities . secondaries . max ) . toBe ( 4 ) ;
183+ expect ( capabilities . secondaries ) . toMatchObject ( {
184+ hue : {
185+ center : {
186+ min : COLOR_GRADING_ADVANCED_LIMITS . hueDegrees . min ,
187+ maxExclusive : COLOR_GRADING_ADVANCED_LIMITS . hueDegrees . max ,
188+ } ,
189+ range : COLOR_GRADING_ADVANCED_LIMITS . secondaryHueRange ,
190+ softness : COLOR_GRADING_ADVANCED_LIMITS . secondaryHueSoftness ,
191+ rangePlusSoftnessMax : COLOR_GRADING_ADVANCED_LIMITS . secondaryHueCombinedMax ,
192+ } ,
193+ saturation : {
194+ softness : COLOR_GRADING_ADVANCED_LIMITS . secondarySoftRangeSoftness ,
195+ } ,
196+ correction : {
197+ hueShift : COLOR_GRADING_ADVANCED_LIMITS . secondaryHueShift ,
198+ saturation : COLOR_GRADING_ADVANCED_LIMITS . signedUnit ,
199+ } ,
200+ } ) ;
201+ } ) ;
202+
203+ it ( "matches the shader's packed secondary-softness convention" , ( ) => {
204+ for ( const axis of [ "saturation" , "luma" ] as const ) {
205+ const grading = normalizeHfColorGrading ( {
206+ secondaries : [
207+ {
208+ key : {
209+ hue : { center : 0 , range : 180 , softness : 0 } ,
210+ saturation : { min : axis === "saturation" ? 0.5 : 0 , max : 1 , softness : 0.4 } ,
211+ luma : { min : axis === "luma" ? 0.5 : 0 , max : 1 , softness : 0.4 } ,
212+ } ,
213+ correction : { } ,
214+ } ,
215+ ] ,
216+ } ) ;
217+ const key = grading ?. secondaries [ 0 ] ?. key ;
218+ if ( ! key ) throw new Error ( "Expected a normalized secondary key" ) ;
219+
220+ const packedSoftness = unitFloatToByte ( key [ axis ] . softness / 0.5 ) ;
221+ const shaderDecodedSoftness = ( packedSoftness / 255 ) * 0.5 ;
222+ const decodedKey = {
223+ ...key ,
224+ [ axis ] : { ...key [ axis ] , softness : shaderDecodedSoftness } ,
225+ } ;
226+ const productMask = calculateHfColorGradingSecondaryMask ( 0 , 0.2 , 0.2 , key ) ;
227+ const shaderMask = calculateHfColorGradingSecondaryMask ( 0 , 0.2 , 0.2 , decodedKey ) ;
228+
229+ expect ( productMask , axis ) . toBeGreaterThan ( 0 ) ;
230+ expect ( productMask , axis ) . toBeCloseTo ( shaderMask , 2 ) ;
231+ }
161232 } ) ;
162233
163234 it ( "merges manual adjustments over preset values" , ( ) => {
@@ -617,4 +688,181 @@ describe("color grading", () => {
617688 expect ( grading ?. adjust . contrast ) . toBe ( 0.2 ) ;
618689 expect ( grading ?. lut ) . toEqual ( { src : "assets/luts/natural-boost.cube" , intensity : 0.75 } ) ;
619690 } ) ;
691+
692+ it ( "resolves scalar variable references inside secondary arrays" , ( ) => {
693+ const grading = normalizeHfColorGradingWithVariables (
694+ {
695+ secondaries : [
696+ {
697+ key : {
698+ hue : { center : "$skinHue" , range : 18 } ,
699+ saturation : { min : 0.1 , max : 1 } ,
700+ } ,
701+ correction : { saturation : "${skinSaturation}" } ,
702+ } ,
703+ ] ,
704+ } ,
705+ { skinHue : 24 , skinSaturation : - 0.12 } ,
706+ ) ;
707+
708+ expect ( grading ?. secondaries [ 0 ] ) . toMatchObject ( {
709+ key : { hue : { center : 24 , range : 18 } } ,
710+ correction : { saturation : - 0.12 } ,
711+ } ) ;
712+ } ) ;
713+
714+ it ( "normalizes advanced controls defensively" , ( ) => {
715+ const grading = normalizeHfColorGrading ( {
716+ colorSpace : "display-p3" ,
717+ wheels : {
718+ shadows : { hue : - 30 , amount : 2 , level : - 2 } ,
719+ highlights : { hue : 750 , amount : 0.15 , level : 0.05 } ,
720+ } ,
721+ curves : {
722+ master : [
723+ [ 0.75 , 0.82 ] ,
724+ [ 0.25 , 0.18 ] ,
725+ ] ,
726+ } ,
727+ hueCurves : {
728+ hueVsHue : [
729+ [ 330 , - 20 ] ,
730+ [ - 10 , 15 ] ,
731+ [ 120 , 0 ] ,
732+ ] ,
733+ } ,
734+ secondaries : [
735+ {
736+ enabled : false ,
737+ key : {
738+ hue : { center : - 10 , range : 30 , softness : 200 } ,
739+ saturation : { min : 0.8 , max : 0.2 , softness : 1 } ,
740+ } ,
741+ correction : { hueShift : 250 , saturation : 2 , luma : - 2 } ,
742+ } ,
743+ ] ,
744+ } ) ;
745+
746+ expect ( grading ?. colorSpace ) . toBe ( "display-p3" ) ;
747+ expect ( grading ?. wheels . shadows ) . toEqual ( { hue : 330 , amount : 1 , level : - 1 } ) ;
748+ expect ( grading ?. wheels . highlights ) . toEqual ( { hue : 30 , amount : 0.15 , level : 0.05 } ) ;
749+ expect ( grading ?. curves . master ) . toEqual ( [
750+ [ 0 , 0 ] ,
751+ [ 0.25 , 0.18 ] ,
752+ [ 0.75 , 0.82 ] ,
753+ [ 1 , 1 ] ,
754+ ] ) ;
755+ expect ( grading ?. hueCurves . hueVsHue ) . toEqual ( [
756+ [ 120 , 0 ] ,
757+ [ 330 , - 20 ] ,
758+ [ 350 , 15 ] ,
759+ ] ) ;
760+ expect ( grading ?. secondaries [ 0 ] ) . toMatchObject ( {
761+ enabled : false ,
762+ key : {
763+ hue : { center : 350 , range : 30 , softness : 150 } ,
764+ saturation : { min : 0.2 , max : 0.8 , softness : 0.5 } ,
765+ } ,
766+ correction : { hueShift : 180 , saturation : 1 , luma : - 1 } ,
767+ } ) ;
768+ } ) ;
769+
770+ it ( "keeps disabled secondaries authored but pixel-inactive" , ( ) => {
771+ const grading = normalizeHfColorGrading ( {
772+ secondaries : [
773+ {
774+ enabled : false ,
775+ key : { hue : { center : 24 , range : 18 } } ,
776+ correction : { saturation : 0.5 } ,
777+ } ,
778+ ] ,
779+ } ) ;
780+
781+ expect ( grading ?. secondaries [ 0 ] ?. enabled ) . toBe ( false ) ;
782+ expect ( isHfColorGradingActive ( grading ) ) . toBe ( false ) ;
783+ expect ( hasHfColorGradingAuthoredValues ( grading ) ) . toBe ( true ) ;
784+ expect ( serializeHfColorGrading ( grading ) ) . toContain ( '"enabled":false' ) ;
785+ } ) ;
786+
787+ it ( "omits identity advanced controls while retaining authored secondary selectors" , ( ) => {
788+ const grading = normalizeHfColorGrading ( {
789+ wheels : { shadows : { hue : 200 } } ,
790+ curves : {
791+ master : [
792+ [ 0 , 0 ] ,
793+ [ 0.5 , 0.5 ] ,
794+ [ 1 , 1 ] ,
795+ ] ,
796+ } ,
797+ hueCurves : {
798+ hueVsSaturation : [
799+ [ 0 , 0 ] ,
800+ [ 120 , 0 ] ,
801+ [ 240 , 0 ] ,
802+ ] ,
803+ } ,
804+ secondaries : [
805+ {
806+ key : { hue : { center : 20 , range : 10 , softness : 5 } } ,
807+ correction : { } ,
808+ } ,
809+ ] ,
810+ } ) ;
811+
812+ expect ( isHfColorGradingActive ( grading ) ) . toBe ( false ) ;
813+ expect ( hasHfColorGradingAuthoredValues ( grading ) ) . toBe ( true ) ;
814+ const serialized = serializeHfColorGrading ( grading ) ;
815+ expect ( serialized ) . not . toContain ( '"wheels"' ) ;
816+ expect ( serialized ) . not . toContain ( '"curves"' ) ;
817+ expect ( serialized ) . not . toContain ( '"hueCurves"' ) ;
818+ expect ( serialized ) . toContain ( '"secondaries"' ) ;
819+ } ) ;
820+
821+ it ( "round-trips advanced grading byte-identically" , ( ) => {
822+ const grading = normalizeHfColorGrading ( {
823+ wheels : { shadows : { hue : 205 , amount : 0.08 } } ,
824+ curves : {
825+ red : [
826+ [ 0 , 0 ] ,
827+ [ 0.5 , 0.55 ] ,
828+ [ 1 , 1 ] ,
829+ ] ,
830+ } ,
831+ hueCurves : {
832+ hueVsSaturation : [
833+ [ 180 , 0 ] ,
834+ [ 215 , 0.15 ] ,
835+ [ 250 , 0 ] ,
836+ ] ,
837+ } ,
838+ secondaries : [
839+ {
840+ key : { hue : { center : 215 , range : 25 , softness : 10 } } ,
841+ correction : { saturation : 0.15 } ,
842+ } ,
843+ ] ,
844+ } ) ;
845+ const first = serializeHfColorGrading ( grading ) ;
846+ expect ( serializeHfColorGrading ( normalizeHfColorGrading ( first ) ) ) . toBe ( first ) ;
847+ } ) ;
848+
849+ it ( "gates advanced grading with intensity while keeping effects independent" , ( ) => {
850+ expect (
851+ isHfColorGradingActive (
852+ normalizeHfColorGrading ( {
853+ intensity : 0 ,
854+ wheels : { shadows : { hue : 205 , amount : 0.2 } } ,
855+ } ) ,
856+ ) ,
857+ ) . toBe ( false ) ;
858+ expect (
859+ isHfColorGradingActive (
860+ normalizeHfColorGrading ( {
861+ intensity : 0 ,
862+ wheels : { shadows : { hue : 205 , amount : 0.2 } } ,
863+ effects : { blur : 0.2 } ,
864+ } ) ,
865+ ) ,
866+ ) . toBe ( true ) ;
867+ } ) ;
620868} ) ;
0 commit comments