|
8 | 8 | HF_COLOR_GRADING_PALETTES, |
9 | 9 | HF_COLOR_GRADING_PRESETS, |
10 | 10 | getHfColorGradingCapabilities, |
| 11 | + hasHfColorGradingAuthoredValues, |
11 | 12 | isHfColorGradingActive, |
12 | 13 | normalizeHfColorGrading, |
13 | 14 | normalizeHfColorGradingWithVariables, |
@@ -123,6 +124,7 @@ describe("color grading", () => { |
123 | 124 | it("publishes a complete capability catalog for agent-built treatments", () => { |
124 | 125 | const capabilities = getHfColorGradingCapabilities(); |
125 | 126 |
|
| 127 | + expect(capabilities.version).toBe(2); |
126 | 128 | expect(capabilities.colorSpace).toBe("rec709"); |
127 | 129 | expect(capabilities.adjustments.map(({ key }) => key)).toEqual([ |
128 | 130 | "exposure", |
@@ -158,6 +160,23 @@ describe("color grading", () => { |
158 | 160 | expect(capabilities.effects.find(({ key }) => key === "kuwahara")?.renderLane).toBe( |
159 | 161 | "multipass", |
160 | 162 | ); |
| 163 | + expect(capabilities.wheels.zones).toEqual(["shadows", "midtones", "highlights"]); |
| 164 | + expect(capabilities.curves).toMatchObject({ |
| 165 | + channels: ["master", "red", "green", "blue"], |
| 166 | + minPoints: 2, |
| 167 | + maxPoints: 16, |
| 168 | + implicitEndpoints: true, |
| 169 | + }); |
| 170 | + expect(capabilities.hueCurves.channels).toEqual( |
| 171 | + expect.arrayContaining([ |
| 172 | + expect.objectContaining({ |
| 173 | + key: "hueVsHue", |
| 174 | + input: expect.objectContaining({ unit: "degrees", wrap: true }), |
| 175 | + output: { min: -180, max: 180 }, |
| 176 | + }), |
| 177 | + ]), |
| 178 | + ); |
| 179 | + expect(capabilities.secondaries.max).toBe(4); |
161 | 180 | }); |
162 | 181 |
|
163 | 182 | it("merges manual adjustments over preset values", () => { |
@@ -617,4 +636,181 @@ describe("color grading", () => { |
617 | 636 | expect(grading?.adjust.contrast).toBe(0.2); |
618 | 637 | expect(grading?.lut).toEqual({ src: "assets/luts/natural-boost.cube", intensity: 0.75 }); |
619 | 638 | }); |
| 639 | + |
| 640 | + it("resolves scalar variable references inside secondary arrays", () => { |
| 641 | + const grading = normalizeHfColorGradingWithVariables( |
| 642 | + { |
| 643 | + secondaries: [ |
| 644 | + { |
| 645 | + key: { |
| 646 | + hue: { center: "$skinHue", range: 18 }, |
| 647 | + saturation: { min: 0.1, max: 1 }, |
| 648 | + }, |
| 649 | + correction: { saturation: "${skinSaturation}" }, |
| 650 | + }, |
| 651 | + ], |
| 652 | + }, |
| 653 | + { skinHue: 24, skinSaturation: -0.12 }, |
| 654 | + ); |
| 655 | + |
| 656 | + expect(grading?.secondaries[0]).toMatchObject({ |
| 657 | + key: { hue: { center: 24, range: 18 } }, |
| 658 | + correction: { saturation: -0.12 }, |
| 659 | + }); |
| 660 | + }); |
| 661 | + |
| 662 | + it("normalizes advanced controls defensively", () => { |
| 663 | + const grading = normalizeHfColorGrading({ |
| 664 | + colorSpace: "display-p3", |
| 665 | + wheels: { |
| 666 | + shadows: { hue: -30, amount: 2, level: -2 }, |
| 667 | + highlights: { hue: 750, amount: 0.15, level: 0.05 }, |
| 668 | + }, |
| 669 | + curves: { |
| 670 | + master: [ |
| 671 | + [0.75, 0.82], |
| 672 | + [0.25, 0.18], |
| 673 | + ], |
| 674 | + }, |
| 675 | + hueCurves: { |
| 676 | + hueVsHue: [ |
| 677 | + [330, -20], |
| 678 | + [-10, 15], |
| 679 | + [120, 0], |
| 680 | + ], |
| 681 | + }, |
| 682 | + secondaries: [ |
| 683 | + { |
| 684 | + enabled: false, |
| 685 | + key: { |
| 686 | + hue: { center: -10, range: 30, softness: 200 }, |
| 687 | + saturation: { min: 0.8, max: 0.2, softness: 1 }, |
| 688 | + }, |
| 689 | + correction: { hueShift: 250, saturation: 2, luma: -2 }, |
| 690 | + }, |
| 691 | + ], |
| 692 | + }); |
| 693 | + |
| 694 | + expect(grading?.colorSpace).toBe("display-p3"); |
| 695 | + expect(grading?.wheels.shadows).toEqual({ hue: 330, amount: 1, level: -1 }); |
| 696 | + expect(grading?.wheels.highlights).toEqual({ hue: 30, amount: 0.15, level: 0.05 }); |
| 697 | + expect(grading?.curves.master).toEqual([ |
| 698 | + [0, 0], |
| 699 | + [0.25, 0.18], |
| 700 | + [0.75, 0.82], |
| 701 | + [1, 1], |
| 702 | + ]); |
| 703 | + expect(grading?.hueCurves.hueVsHue).toEqual([ |
| 704 | + [120, 0], |
| 705 | + [330, -20], |
| 706 | + [350, 15], |
| 707 | + ]); |
| 708 | + expect(grading?.secondaries[0]).toMatchObject({ |
| 709 | + enabled: false, |
| 710 | + key: { |
| 711 | + hue: { center: 350, range: 30, softness: 150 }, |
| 712 | + saturation: { min: 0.2, max: 0.8, softness: 0.5 }, |
| 713 | + }, |
| 714 | + correction: { hueShift: 180, saturation: 1, luma: -1 }, |
| 715 | + }); |
| 716 | + }); |
| 717 | + |
| 718 | + it("keeps disabled secondaries authored but pixel-inactive", () => { |
| 719 | + const grading = normalizeHfColorGrading({ |
| 720 | + secondaries: [ |
| 721 | + { |
| 722 | + enabled: false, |
| 723 | + key: { hue: { center: 24, range: 18 } }, |
| 724 | + correction: { saturation: 0.5 }, |
| 725 | + }, |
| 726 | + ], |
| 727 | + }); |
| 728 | + |
| 729 | + expect(grading?.secondaries[0]?.enabled).toBe(false); |
| 730 | + expect(isHfColorGradingActive(grading)).toBe(false); |
| 731 | + expect(hasHfColorGradingAuthoredValues(grading)).toBe(true); |
| 732 | + expect(serializeHfColorGrading(grading)).toContain('"enabled":false'); |
| 733 | + }); |
| 734 | + |
| 735 | + it("omits identity advanced controls while retaining authored secondary selectors", () => { |
| 736 | + const grading = normalizeHfColorGrading({ |
| 737 | + wheels: { shadows: { hue: 200 } }, |
| 738 | + curves: { |
| 739 | + master: [ |
| 740 | + [0, 0], |
| 741 | + [0.5, 0.5], |
| 742 | + [1, 1], |
| 743 | + ], |
| 744 | + }, |
| 745 | + hueCurves: { |
| 746 | + hueVsSaturation: [ |
| 747 | + [0, 0], |
| 748 | + [120, 0], |
| 749 | + [240, 0], |
| 750 | + ], |
| 751 | + }, |
| 752 | + secondaries: [ |
| 753 | + { |
| 754 | + key: { hue: { center: 20, range: 10, softness: 5 } }, |
| 755 | + correction: {}, |
| 756 | + }, |
| 757 | + ], |
| 758 | + }); |
| 759 | + |
| 760 | + expect(isHfColorGradingActive(grading)).toBe(false); |
| 761 | + expect(hasHfColorGradingAuthoredValues(grading)).toBe(true); |
| 762 | + const serialized = serializeHfColorGrading(grading); |
| 763 | + expect(serialized).not.toContain('"wheels"'); |
| 764 | + expect(serialized).not.toContain('"curves"'); |
| 765 | + expect(serialized).not.toContain('"hueCurves"'); |
| 766 | + expect(serialized).toContain('"secondaries"'); |
| 767 | + }); |
| 768 | + |
| 769 | + it("round-trips advanced grading byte-identically", () => { |
| 770 | + const grading = normalizeHfColorGrading({ |
| 771 | + wheels: { shadows: { hue: 205, amount: 0.08 } }, |
| 772 | + curves: { |
| 773 | + red: [ |
| 774 | + [0, 0], |
| 775 | + [0.5, 0.55], |
| 776 | + [1, 1], |
| 777 | + ], |
| 778 | + }, |
| 779 | + hueCurves: { |
| 780 | + hueVsSaturation: [ |
| 781 | + [180, 0], |
| 782 | + [215, 0.15], |
| 783 | + [250, 0], |
| 784 | + ], |
| 785 | + }, |
| 786 | + secondaries: [ |
| 787 | + { |
| 788 | + key: { hue: { center: 215, range: 25, softness: 10 } }, |
| 789 | + correction: { saturation: 0.15 }, |
| 790 | + }, |
| 791 | + ], |
| 792 | + }); |
| 793 | + const first = serializeHfColorGrading(grading); |
| 794 | + expect(serializeHfColorGrading(normalizeHfColorGrading(first))).toBe(first); |
| 795 | + }); |
| 796 | + |
| 797 | + it("gates advanced grading with intensity while keeping effects independent", () => { |
| 798 | + expect( |
| 799 | + isHfColorGradingActive( |
| 800 | + normalizeHfColorGrading({ |
| 801 | + intensity: 0, |
| 802 | + wheels: { shadows: { hue: 205, amount: 0.2 } }, |
| 803 | + }), |
| 804 | + ), |
| 805 | + ).toBe(false); |
| 806 | + expect( |
| 807 | + isHfColorGradingActive( |
| 808 | + normalizeHfColorGrading({ |
| 809 | + intensity: 0, |
| 810 | + wheels: { shadows: { hue: 205, amount: 0.2 } }, |
| 811 | + effects: { blur: 0.2 }, |
| 812 | + }), |
| 813 | + ), |
| 814 | + ).toBe(true); |
| 815 | + }); |
620 | 816 | }); |
0 commit comments